From 2e5d60b7db7020b726cd54ee4cad8f2afbd1479d Mon Sep 17 00:00:00 2001 From: Ismael Mejia Date: Thu, 21 Feb 2013 20:51:35 +0100 Subject: [PATCH 001/221] Removed conversion from youtube closed caption format to srt since youtube api supports the 'srt' format --- test/test_youtube_subtitles.py | 4 ++-- youtube_dl/InfoExtractors.py | 24 ++++-------------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/test/test_youtube_subtitles.py b/test/test_youtube_subtitles.py index 5d3566a35..ff09ea459 100644 --- a/test/test_youtube_subtitles.py +++ b/test/test_youtube_subtitles.py @@ -43,7 +43,7 @@ class TestYoutubeSubtitles(unittest.TestCase): DL.params['writesubtitles'] = True IE = YoutubeIE(DL) info_dict = IE.extract('QRS8MkLhQmM') - self.assertEqual(md5(info_dict[0]['subtitles']), 'c3228550d59116f3c29fba370b55d033') + self.assertEqual(md5(info_dict[0]['subtitles']), '4cd9278a35ba2305f47354ee13472260') def test_youtube_subtitles_it(self): DL = FakeDownloader() @@ -51,7 +51,7 @@ class TestYoutubeSubtitles(unittest.TestCase): DL.params['subtitleslang'] = 'it' IE = YoutubeIE(DL) info_dict = IE.extract('QRS8MkLhQmM') - self.assertEqual(md5(info_dict[0]['subtitles']), '132a88a0daf8e1520f393eb58f1f646a') + self.assertEqual(md5(info_dict[0]['subtitles']), '164a51f16f260476a05b50fe4c2f161d') if __name__ == '__main__': unittest.main() diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index d3c3ac264..e3998fbe8 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -228,23 +228,6 @@ class YoutubeIE(InfoExtractor): """Indicate the download will use the RTMP protocol.""" self._downloader.to_screen(u'[youtube] RTMP download detected') - def _closed_captions_xml_to_srt(self, xml_string): - srt = '' - texts = re.findall(r'([^<]+)', xml_string, re.MULTILINE) - # TODO parse xml instead of regex - for n, (start, dur_tag, dur, caption) in enumerate(texts): - if not dur: dur = '4' - start = float(start) - end = start + float(dur) - start = "%02i:%02i:%02i,%03i" %(start/(60*60), start/60%60, start%60, start%1*1000) - end = "%02i:%02i:%02i,%03i" %(end/(60*60), end/60%60, end%60, end%1*1000) - caption = unescapeHTML(caption) - caption = unescapeHTML(caption) # double cycle, intentional - srt += str(n+1) + '\n' - srt += start + ' --> ' + end + '\n' - srt += caption + '\n\n' - return srt - def _extract_subtitles(self, video_id): self.report_video_subtitles_download(video_id) request = compat_urllib_request.Request('http://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id) @@ -268,15 +251,16 @@ class YoutubeIE(InfoExtractor): 'lang': srt_lang, 'name': srt_lang_list[srt_lang].encode('utf-8'), 'v': video_id, + 'fmt': 'srt', }) url = 'http://www.youtube.com/api/timedtext?' + params try: - srt_xml = compat_urllib_request.urlopen(url).read().decode('utf-8') + srt = compat_urllib_request.urlopen(url).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: return (u'WARNING: unable to download video subtitles: %s' % compat_str(err), None) - if not srt_xml: + if not srt: return (u'WARNING: Did not fetch video subtitles', None) - return (None, self._closed_captions_xml_to_srt(srt_xml)) + return (None, srt) def _print_formats(self, formats): print('Available formats:') From cdb130b09a16865b81fd34d19b74fa634d45cad7 Mon Sep 17 00:00:00 2001 From: Ismael Mejia Date: Thu, 21 Feb 2013 22:12:36 +0100 Subject: [PATCH 002/221] Added new option '--only-srt' to download only the subtitles of a video Improved option '--srt-lang' - it shows the argument in case of missing subtitles - added language suffix for non-english languages (e.g. video.it.srt) --- test/test_youtube_subtitles.py | 7 +++++++ youtube_dl/FileDownloader.py | 5 +++++ youtube_dl/InfoExtractors.py | 7 ++++++- youtube_dl/__init__.py | 4 ++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/test/test_youtube_subtitles.py b/test/test_youtube_subtitles.py index ff09ea459..77c275b75 100644 --- a/test/test_youtube_subtitles.py +++ b/test/test_youtube_subtitles.py @@ -53,5 +53,12 @@ class TestYoutubeSubtitles(unittest.TestCase): info_dict = IE.extract('QRS8MkLhQmM') self.assertEqual(md5(info_dict[0]['subtitles']), '164a51f16f260476a05b50fe4c2f161d') + def test_youtube_onlysubtitles(self): + DL = FakeDownloader() + DL.params['onlysubtitles'] = True + IE = YoutubeIE(DL) + info_dict = IE.extract('QRS8MkLhQmM') + self.assertEqual(md5(info_dict[0]['subtitles']), '4cd9278a35ba2305f47354ee13472260') + if __name__ == '__main__': unittest.main() diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 53c2d1dce..487c9dadb 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -79,6 +79,7 @@ class FileDownloader(object): writedescription: Write the video description to a .description file writeinfojson: Write the video description to a .info.json file writesubtitles: Write the video subtitles to a .srt file + onlysubtitles: Downloads only the subtitles of the video subtitleslang: Language of the subtitles to download test: Download only first bytes to test the downloader. keepvideo: Keep the video file after post-processing @@ -443,9 +444,13 @@ class FileDownloader(object): # that way it will silently go on when used with unsupporting IE try: srtfn = filename.rsplit('.', 1)[0] + u'.srt' + if self.params.get('subtitleslang', False): + srtfn = filename.rsplit('.', 1)[0] + u'.' + self.params['subtitleslang'] + u'.srt' self.report_writesubtitles(srtfn) with io.open(encodeFilename(srtfn), 'w', encoding='utf-8') as srtfile: srtfile.write(info_dict['subtitles']) + if self.params.get('onlysubtitles', False): + return except (OSError, IOError): self.trouble(u'ERROR: Cannot write subtitles file ' + descfn) return diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index e3998fbe8..51b263383 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -228,6 +228,7 @@ class YoutubeIE(InfoExtractor): """Indicate the download will use the RTMP protocol.""" self._downloader.to_screen(u'[youtube] RTMP download detected') + def _extract_subtitles(self, video_id): self.report_video_subtitles_download(video_id) request = compat_urllib_request.Request('http://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id) @@ -246,7 +247,7 @@ class YoutubeIE(InfoExtractor): else: srt_lang = list(srt_lang_list.keys())[0] if not srt_lang in srt_lang_list: - return (u'WARNING: no closed captions found in the specified language', None) + return (u'WARNING: no closed captions found in the specified language "%s"' % srt_lang, None) params = compat_urllib_parse.urlencode({ 'lang': srt_lang, 'name': srt_lang_list[srt_lang].encode('utf-8'), @@ -483,6 +484,10 @@ class YoutubeIE(InfoExtractor): # closed captions video_subtitles = None + if self._downloader.params.get('subtitleslang', False): + self._downloader.params['writesubtitles'] = True + if self._downloader.params.get('onlysubtitles', False): + self._downloader.params['writesubtitles'] = True if self._downloader.params.get('writesubtitles', False): (srt_error, video_subtitles) = self._extract_subtitles(video_id) if srt_error: diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 23e3c2ac2..ababeac87 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -176,6 +176,9 @@ def parseOpts(): video_format.add_option('--write-srt', action='store_true', dest='writesubtitles', help='write video closed captions to a .srt file (currently youtube only)', default=False) + video_format.add_option('--only-srt', + action='store_true', dest='onlysubtitles', + help='downloads only the subtitles of the video (currently youtube only)', default=False) video_format.add_option('--srt-lang', action='store', dest='subtitleslang', metavar='LANG', help='language of the closed captions to download (optional) use IETF language tags like \'en\'') @@ -450,6 +453,7 @@ def _real_main(): 'writedescription': opts.writedescription, 'writeinfojson': opts.writeinfojson, 'writesubtitles': opts.writesubtitles, + 'onlysubtitles': opts.onlysubtitles, 'subtitleslang': opts.subtitleslang, 'matchtitle': decodeOption(opts.matchtitle), 'rejecttitle': decodeOption(opts.rejecttitle), From df8db1aa2107f204fa14c157d7a536e45ceb65c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Tue, 26 Feb 2013 23:33:58 +0100 Subject: [PATCH 003/221] Create extract_info method --- youtube_dl/FileDownloader.py | 85 +++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 192ad37d2..b26c34729 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -377,6 +377,44 @@ class FileDownloader(object): if re.search(rejecttitle, title, re.IGNORECASE): return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"' return None + + def extract_info(self, url): + ''' + Returns a list with a dictionary for each video we find. + ''' + suitable_found = False + for ie in self._ies: + # Go to next InfoExtractor if not suitable + if not ie.suitable(url): + continue + + # Warn if the _WORKING attribute is False + if not ie.working(): + self.to_stderr(u'WARNING: the program functionality for this site has been marked as broken, ' + u'and will probably not work. If you want to go on, use the -i option.') + + # Suitable InfoExtractor found + suitable_found = True + + # Extract information from URL and process it + try: + videos = ie.extract(url) + for video in videos or []: + if not 'extractor' in video: + #The extractor has already been set somewher else + video['extractor'] = ie.IE_NAME + return videos + except ExtractorError as de: # An error we somewhat expected + self.trouble(u'ERROR: ' + compat_str(de), de.format_traceback()) + break + except Exception as e: + if self.params.get('ignoreerrors', False): + self.trouble(u'ERROR: ' + compat_str(e), tb=compat_str(traceback.format_exc())) + break + else: + raise + if not suitable_found: + self.trouble(u'ERROR: no suitable InfoExtractor: %s' % url) def process_info(self, info_dict): """Process a single dictionary returned by an InfoExtractor.""" @@ -488,49 +526,14 @@ class FileDownloader(object): raise SameFileError(self.params['outtmpl']) for url in url_list: - suitable_found = False - for ie in self._ies: - # Go to next InfoExtractor if not suitable - if not ie.suitable(url): - continue + videos = self.extract_info(url) - # Warn if the _WORKING attribute is False - if not ie.working(): - self.to_stderr(u'WARNING: the program functionality for this site has been marked as broken, ' - u'and will probably not work. If you want to go on, use the -i option.') - - # Suitable InfoExtractor found - suitable_found = True - - # Extract information from URL and process it + for video in videos or []: try: - videos = ie.extract(url) - except ExtractorError as de: # An error we somewhat expected - self.trouble(u'ERROR: ' + compat_str(de), de.format_traceback()) - break - except Exception as e: - if self.params.get('ignoreerrors', False): - self.trouble(u'ERROR: ' + compat_str(e), tb=compat_str(traceback.format_exc())) - break - else: - raise - - if len(videos or []) > 1 and self.fixed_template(): - raise SameFileError(self.params['outtmpl']) - - for video in videos or []: - video['extractor'] = ie.IE_NAME - try: - self.increment_downloads() - self.process_info(video) - except UnavailableVideoError: - self.trouble(u'\nERROR: unable to download video') - - # Suitable InfoExtractor had been found; go to next URL - break - - if not suitable_found: - self.trouble(u'ERROR: no suitable InfoExtractor: %s' % url) + self.increment_downloads() + self.process_info(video) + except UnavailableVideoError: + self.trouble(u'\nERROR: unable to download video') return self._download_retcode From 4e1582f372d74d551e19d319e5b345002def480d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Mon, 4 Mar 2013 11:27:25 +0100 Subject: [PATCH 004/221] Use red color when printing error messages --- youtube_dl/FileDownloader.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 57f741c30..2f6c393a4 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -246,6 +246,18 @@ class FileDownloader(object): warning_message=u'%s %s' % (_msg_header,message) self.to_stderr(warning_message) + def report_error(self, message, tb=None): + ''' + Do the same as trouble, but prefixes the message with 'ERROR:', colored + in red if stderr is a tty file. + ''' + if sys.stderr.isatty(): + _msg_header = u'\033[0;31mERROR:\033[0m' + else: + _msg_header = u'ERROR:' + error_message = u'%s %s' % (_msg_header, message) + self.trouble(error_message, tb) + def slow_down(self, start_time, byte_counter): """Sleep if the download speed is over the rate limit.""" rate_limit = self.params.get('ratelimit', None) From 6622d22c79aa35ab1bd99c453afbdbecc0a9d61d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Mon, 4 Mar 2013 11:47:58 +0100 Subject: [PATCH 005/221] Use report_error in FileDownloader.py --- youtube_dl/FileDownloader.py | 40 ++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 2f6c393a4..8d21a79d5 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -289,7 +289,7 @@ class FileDownloader(object): return os.rename(encodeFilename(old_filename), encodeFilename(new_filename)) except (IOError, OSError) as err: - self.trouble(u'ERROR: unable to rename file') + self.report_error(u'unable to rename file') def try_utime(self, filename, last_modified_hdr): """Try to set the last-modified time of the given file.""" @@ -385,7 +385,7 @@ class FileDownloader(object): filename = self.params['outtmpl'] % template_dict return filename except (ValueError, KeyError) as err: - self.trouble(u'ERROR: invalid system charset or erroneous output template') + self.report_error(u'invalid system charset or erroneous output template') return None def _match_entry(self, info_dict): @@ -449,7 +449,7 @@ class FileDownloader(object): if dn != '' and not os.path.exists(dn): # dn is already encoded os.makedirs(dn) except (OSError, IOError) as err: - self.trouble(u'ERROR: unable to create directory ' + compat_str(err)) + self.report_error(u'unable to create directory ' + compat_str(err)) return if self.params.get('writedescription', False): @@ -459,7 +459,7 @@ class FileDownloader(object): with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile: descfile.write(info_dict['description']) except (OSError, IOError): - self.trouble(u'ERROR: Cannot write description file ' + descfn) + self.report_error(u'Cannot write description file ' + descfn) return if self.params.get('writesubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']: @@ -471,7 +471,7 @@ class FileDownloader(object): with io.open(encodeFilename(srtfn), 'w', encoding='utf-8') as srtfile: srtfile.write(info_dict['subtitles']) except (OSError, IOError): - self.trouble(u'ERROR: Cannot write subtitles file ' + descfn) + self.report_error(u'Cannot write subtitles file ' + descfn) return if self.params.get('writeinfojson', False): @@ -481,7 +481,7 @@ class FileDownloader(object): json_info_dict = dict((k, v) for k,v in info_dict.items() if not k in ['urlhandle']) write_json_file(json_info_dict, encodeFilename(infofn)) except (OSError, IOError): - self.trouble(u'ERROR: Cannot write metadata to JSON file ' + infofn) + self.report_error(u'Cannot write metadata to JSON file ' + infofn) return if not self.params.get('skip_download', False): @@ -493,17 +493,17 @@ class FileDownloader(object): except (OSError, IOError) as err: raise UnavailableVideoError() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self.trouble(u'ERROR: unable to download video data: %s' % str(err)) + self.report_error(u'unable to download video data: %s' % str(err)) return except (ContentTooShortError, ) as err: - self.trouble(u'ERROR: content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded)) + self.report_error(u'content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded)) return if success: try: self.post_process(filename, info_dict) except (PostProcessingError) as err: - self.trouble(u'ERROR: postprocessing: %s' % str(err)) + self.report_error(u'postprocessing: %s' % str(err)) return def download(self, url_list): @@ -534,7 +534,7 @@ class FileDownloader(object): break except Exception as e: if self.params.get('ignoreerrors', False): - self.trouble(u'ERROR: ' + compat_str(e), tb=compat_str(traceback.format_exc())) + self.report_error(u'' + compat_str(e), tb=compat_str(traceback.format_exc())) break else: raise @@ -548,13 +548,14 @@ class FileDownloader(object): self.increment_downloads() self.process_info(video) except UnavailableVideoError: - self.trouble(u'\nERROR: unable to download video') + self.to_stderr(u"\n") + self.report_error(u'unable to download video') # Suitable InfoExtractor had been found; go to next URL break if not suitable_found: - self.trouble(u'ERROR: no suitable InfoExtractor: %s' % url) + self.report_error(u'no suitable InfoExtractor: %s' % url) return self._download_retcode @@ -589,7 +590,7 @@ class FileDownloader(object): try: subprocess.call(['rtmpdump', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT) except (OSError, IOError): - self.trouble(u'ERROR: RTMP download detected but "rtmpdump" could not be run') + self.report_error(u'RTMP download detected but "rtmpdump" could not be run') return False # Download using rtmpdump. rtmpdump returns exit code 2 when @@ -634,7 +635,8 @@ class FileDownloader(object): }) return True else: - self.trouble(u'\nERROR: rtmpdump exited with code %d' % retval) + self.to_stderr(u"\n") + self.report_error(u'rtmpdump exited with code %d' % retval) return False def _do_download(self, filename, info_dict): @@ -734,7 +736,7 @@ class FileDownloader(object): self.report_retry(count, retries) if count > retries: - self.trouble(u'ERROR: giving up after %s retries' % retries) + self.report_error(u'giving up after %s retries' % retries) return False data_len = data.info().get('Content-length', None) @@ -770,12 +772,13 @@ class FileDownloader(object): filename = self.undo_temp_name(tmpfilename) self.report_destination(filename) except (OSError, IOError) as err: - self.trouble(u'ERROR: unable to open for writing: %s' % str(err)) + self.report_error(u'unable to open for writing: %s' % str(err)) return False try: stream.write(data_block) except (IOError, OSError) as err: - self.trouble(u'\nERROR: unable to write data: %s' % str(err)) + self.to_stderr(u"\n") + self.report_error(u'unable to write data: %s' % str(err)) return False if not self.params.get('noresizebuffer', False): block_size = self.best_block_size(after - before, len(data_block)) @@ -801,7 +804,8 @@ class FileDownloader(object): self.slow_down(start, byte_counter - resume_len) if stream is None: - self.trouble(u'\nERROR: Did not get any data blocks') + self.to_stderr(u"\n") + self.report_error(u'Did not get any data blocks') return False stream.close() self.report_finish() From e5f30ade100b33127f31dd8989585a87e6faa6e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Mon, 4 Mar 2013 15:56:14 +0100 Subject: [PATCH 006/221] Use report_error in InfoExtractors.py Some calls haven't been changed --- youtube_dl/InfoExtractors.py | 300 +++++++++++++++++------------------ 1 file changed, 150 insertions(+), 150 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 7ce84fe79..6328332a7 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -388,13 +388,13 @@ class YoutubeIE(InfoExtractor): self.report_age_confirmation() age_results = compat_urllib_request.urlopen(request).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to confirm age: %s' % compat_str(err)) + self._downloader.report_error(u'unable to confirm age: %s' % compat_str(err)) return def _extract_id(self, url): mobj = re.match(self._VALID_URL, url, re.VERBOSE) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return video_id = mobj.group(2) return video_id @@ -413,7 +413,7 @@ class YoutubeIE(InfoExtractor): try: video_webpage_bytes = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download video webpage: %s' % compat_str(err)) + self._downloader.report_error(u'unable to download video webpage: %s' % compat_str(err)) return video_webpage = video_webpage_bytes.decode('utf-8', 'ignore') @@ -438,18 +438,18 @@ class YoutubeIE(InfoExtractor): if 'token' in video_info: break except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download video info webpage: %s' % compat_str(err)) + self._downloader.report_error(u'unable to download video info webpage: %s' % compat_str(err)) return if 'token' not in video_info: if 'reason' in video_info: - self._downloader.trouble(u'ERROR: YouTube said: %s' % video_info['reason'][0]) + self._downloader.report_error(u'YouTube said: %s' % video_info['reason'][0]) else: - self._downloader.trouble(u'ERROR: "token" parameter not in video info for unknown reason') + self._downloader.report_error(u'"token" parameter not in video info for unknown reason') return # Check for "rental" videos if 'ypc_video_rental_bar_text' in video_info and 'author' not in video_info: - self._downloader.trouble(u'ERROR: "rental" videos not supported') + self._downloader.report_error(u'"rental" videos not supported') return # Start extracting information @@ -457,7 +457,7 @@ class YoutubeIE(InfoExtractor): # uploader if 'author' not in video_info: - self._downloader.trouble(u'ERROR: unable to extract uploader name') + self._downloader.report_error(u'unable to extract uploader name') return video_uploader = compat_urllib_parse.unquote_plus(video_info['author'][0]) @@ -471,7 +471,7 @@ class YoutubeIE(InfoExtractor): # title if 'title' not in video_info: - self._downloader.trouble(u'ERROR: unable to extract video title') + self._downloader.report_error(u'unable to extract video title') return video_title = compat_urllib_parse.unquote_plus(video_info['title'][0]) @@ -537,7 +537,7 @@ class YoutubeIE(InfoExtractor): format_list = available_formats existing_formats = [x for x in format_list if x in url_map] if len(existing_formats) == 0: - self._downloader.trouble(u'ERROR: no known formats available for video') + self._downloader.report_error(u'no known formats available for video') return if self._downloader.params.get('listformats', None): self._print_formats(existing_formats) @@ -558,10 +558,10 @@ class YoutubeIE(InfoExtractor): video_url_list = [(rf, url_map[rf])] break if video_url_list is None: - self._downloader.trouble(u'ERROR: requested format not available') + self._downloader.report_error(u'requested format not available') return else: - self._downloader.trouble(u'ERROR: no conn or url_encoded_fmt_stream_map information found in video info') + self._downloader.report_error(u'no conn or url_encoded_fmt_stream_map information found in video info') return results = [] @@ -624,7 +624,7 @@ class MetacafeIE(InfoExtractor): self.report_disclaimer() disclaimer = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to retrieve disclaimer: %s' % compat_str(err)) + self._downloader.report_error(u'unable to retrieve disclaimer: %s' % compat_str(err)) return # Confirm age @@ -637,14 +637,14 @@ class MetacafeIE(InfoExtractor): self.report_age_confirmation() disclaimer = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to confirm age: %s' % compat_str(err)) + self._downloader.report_error(u'unable to confirm age: %s' % compat_str(err)) return def _real_extract(self, url): # Extract id and simplified title from URL mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return video_id = mobj.group(1) @@ -661,7 +661,7 @@ class MetacafeIE(InfoExtractor): self.report_download_webpage(video_id) webpage = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable retrieve video webpage: %s' % compat_str(err)) + self._downloader.report_error(u'unable retrieve video webpage: %s' % compat_str(err)) return # Extract URL, uploader and title from webpage @@ -681,15 +681,15 @@ class MetacafeIE(InfoExtractor): else: mobj = re.search(r' name="flashvars" value="(.*?)"', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract media URL') + self._downloader.report_error(u'unable to extract media URL') return vardict = compat_parse_qs(mobj.group(1)) if 'mediaData' not in vardict: - self._downloader.trouble(u'ERROR: unable to extract media URL') + self._downloader.report_error(u'unable to extract media URL') return mobj = re.search(r'"mediaURL":"(http.*?)","key":"(.*?)"', vardict['mediaData'][0]) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract media URL') + self._downloader.report_error(u'unable to extract media URL') return mediaURL = mobj.group(1).replace('\\/', '/') video_extension = mediaURL[-3:] @@ -697,13 +697,13 @@ class MetacafeIE(InfoExtractor): mobj = re.search(r'(?im)(.*) - Video', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract title') + self._downloader.report_error(u'unable to extract title') return video_title = mobj.group(1).decode('utf-8') mobj = re.search(r'submitter=(.*?);', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract uploader nickname') + self._downloader.report_error(u'unable to extract uploader nickname') return video_uploader = mobj.group(1) @@ -735,7 +735,7 @@ class DailymotionIE(InfoExtractor): # Extract id and simplified title from URL mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return video_id = mobj.group(1).split('_')[0].split('?')[0] @@ -751,7 +751,7 @@ class DailymotionIE(InfoExtractor): self.report_extraction(video_id) mobj = re.search(r'\s*var flashvars = (.*)', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract media URL') + self._downloader.report_error(u'unable to extract media URL') return flashvars = compat_urllib_parse.unquote(mobj.group(1)) @@ -761,12 +761,12 @@ class DailymotionIE(InfoExtractor): self._downloader.to_screen(u'[dailymotion] Using %s' % key) break else: - self._downloader.trouble(u'ERROR: unable to extract video URL') + self._downloader.report_error(u'unable to extract video URL') return mobj = re.search(r'"' + max_quality + r'":"(.+?)"', flashvars) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video URL') + self._downloader.report_error(u'unable to extract video URL') return video_url = compat_urllib_parse.unquote(mobj.group(1)).replace('\\/', '/') @@ -775,7 +775,7 @@ class DailymotionIE(InfoExtractor): mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract title') + self._downloader.report_error(u'unable to extract title') return video_title = unescapeHTML(mobj.group('title')) @@ -827,7 +827,7 @@ class PhotobucketIE(InfoExtractor): # Extract id from URL mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: Invalid URL: %s' % url) + self._downloader.report_error(u'Invalid URL: %s' % url) return video_id = mobj.group(1) @@ -840,14 +840,14 @@ class PhotobucketIE(InfoExtractor): self.report_download_webpage(video_id) webpage = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) + self._downloader.report_error(u'Unable to retrieve video webpage: %s' % compat_str(err)) return # Extract URL, uploader, and title from webpage self.report_extraction(video_id) mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract media URL') + self._downloader.report_error(u'unable to extract media URL') return mediaURL = compat_urllib_parse.unquote(mobj.group(1)) @@ -855,7 +855,7 @@ class PhotobucketIE(InfoExtractor): mobj = re.search(r'(.*) video by (.*) - Photobucket', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract title') + self._downloader.report_error(u'unable to extract title') return video_title = mobj.group(1).decode('utf-8') @@ -896,7 +896,7 @@ class YahooIE(InfoExtractor): # Extract ID from URL mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: Invalid URL: %s' % url) + self._downloader.report_error(u'Invalid URL: %s' % url) return video_id = mobj.group(2) @@ -909,18 +909,18 @@ class YahooIE(InfoExtractor): try: webpage = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) + self._downloader.report_error(u'Unable to retrieve video webpage: %s' % compat_str(err)) return mobj = re.search(r'\("id", "([0-9]+)"\);', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: Unable to extract id field') + self._downloader.report_error(u'Unable to extract id field') return yahoo_id = mobj.group(1) mobj = re.search(r'\("vid", "([0-9]+)"\);', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: Unable to extract vid field') + self._downloader.report_error(u'Unable to extract vid field') return yahoo_vid = mobj.group(1) @@ -933,34 +933,34 @@ class YahooIE(InfoExtractor): self.report_download_webpage(video_id) webpage = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) + self._downloader.report_error(u'Unable to retrieve video webpage: %s' % compat_str(err)) return # Extract uploader and title from webpage self.report_extraction(video_id) mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video title') + self._downloader.report_error(u'unable to extract video title') return video_title = mobj.group(1).decode('utf-8') mobj = re.search(r'

(.*)

', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video uploader') + self._downloader.report_error(u'unable to extract video uploader') return video_uploader = mobj.group(1).decode('utf-8') # Extract video thumbnail mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video thumbnail') + self._downloader.report_error(u'unable to extract video thumbnail') return video_thumbnail = mobj.group(1).decode('utf-8') # Extract video description mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video description') + self._downloader.report_error(u'unable to extract video description') return video_description = mobj.group(1).decode('utf-8') if not video_description: @@ -969,13 +969,13 @@ class YahooIE(InfoExtractor): # Extract video height and width mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video height') + self._downloader.report_error(u'unable to extract video height') return yv_video_height = mobj.group(1) mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video width') + self._downloader.report_error(u'unable to extract video width') return yv_video_width = mobj.group(1) @@ -991,13 +991,13 @@ class YahooIE(InfoExtractor): self.report_download_webpage(video_id) webpage = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) + self._downloader.report_error(u'Unable to retrieve video webpage: %s' % compat_str(err)) return # Extract media URL from playlist XML mobj = re.search(r'(.*)', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract title') + self._downloader.report_error(u'unable to extract title') return video_title = mobj.group(1) # video uploader is domain name mobj = re.match(r'(?:https?://)?([^/]*)/.*', url) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract title') + self._downloader.report_error(u'unable to extract title') return video_uploader = mobj.group(1) @@ -1437,7 +1437,7 @@ class YoutubeSearchIE(InfoExtractor): def _real_extract(self, query): mobj = re.match(self._VALID_URL, query) if mobj is None: - self._downloader.trouble(u'ERROR: invalid search query "%s"' % query) + self._downloader.report_error(u'invalid search query "%s"' % query) return prefix, query = query.split(':') @@ -1453,7 +1453,7 @@ class YoutubeSearchIE(InfoExtractor): try: n = int(prefix) if n <= 0: - self._downloader.trouble(u'ERROR: invalid download number %s for query "%s"' % (n, query)) + self._downloader.report_error(u'invalid download number %s for query "%s"' % (n, query)) return elif n > self._max_youtube_results: self._downloader.report_warning(u'ytsearch returns max %i results (you requested %i)' % (self._max_youtube_results, n)) @@ -1478,7 +1478,7 @@ class YoutubeSearchIE(InfoExtractor): try: data = compat_urllib_request.urlopen(request).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download API page: %s' % compat_str(err)) + self._downloader.report_error(u'unable to download API page: %s' % compat_str(err)) return api_response = json.loads(data)['data'] @@ -1519,7 +1519,7 @@ class GoogleSearchIE(InfoExtractor): def _real_extract(self, query): mobj = re.match(self._VALID_URL, query) if mobj is None: - self._downloader.trouble(u'ERROR: invalid search query "%s"' % query) + self._downloader.report_error(u'invalid search query "%s"' % query) return prefix, query = query.split(':') @@ -1535,7 +1535,7 @@ class GoogleSearchIE(InfoExtractor): try: n = int(prefix) if n <= 0: - self._downloader.trouble(u'ERROR: invalid download number %s for query "%s"' % (n, query)) + self._downloader.report_error(u'invalid download number %s for query "%s"' % (n, query)) return elif n > self._max_google_results: self._downloader.report_warning(u'gvsearch returns max %i results (you requested %i)' % (self._max_google_results, n)) @@ -1559,7 +1559,7 @@ class GoogleSearchIE(InfoExtractor): try: page = compat_urllib_request.urlopen(request).read() 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)) + self._downloader.report_error(u'unable to download webpage: %s' % compat_str(err)) return # Extract video identifiers @@ -1603,7 +1603,7 @@ class YahooSearchIE(InfoExtractor): def _real_extract(self, query): mobj = re.match(self._VALID_URL, query) if mobj is None: - self._downloader.trouble(u'ERROR: invalid search query "%s"' % query) + self._downloader.report_error(u'invalid search query "%s"' % query) return prefix, query = query.split(':') @@ -1619,7 +1619,7 @@ class YahooSearchIE(InfoExtractor): try: n = int(prefix) if n <= 0: - self._downloader.trouble(u'ERROR: invalid download number %s for query "%s"' % (n, query)) + self._downloader.report_error(u'invalid download number %s for query "%s"' % (n, query)) return elif n > self._max_yahoo_results: self._downloader.report_warning(u'yvsearch returns max %i results (you requested %i)' % (self._max_yahoo_results, n)) @@ -1644,7 +1644,7 @@ class YahooSearchIE(InfoExtractor): try: page = compat_urllib_request.urlopen(request).read() 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)) + self._downloader.report_error(u'unable to download webpage: %s' % compat_str(err)) return # Extract video identifiers @@ -1706,7 +1706,7 @@ class YoutubePlaylistIE(InfoExtractor): # Extract playlist id mobj = re.match(self._VALID_URL, url, re.VERBOSE) if mobj is None: - self._downloader.trouble(u'ERROR: invalid url: %s' % url) + self._downloader.report_error(u'invalid url: %s' % url) return # Download playlist videos from API @@ -1721,17 +1721,17 @@ class YoutubePlaylistIE(InfoExtractor): try: page = compat_urllib_request.urlopen(url).read().decode('utf8') 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)) + self._downloader.report_error(u'unable to download webpage: %s' % compat_str(err)) return try: response = json.loads(page) except ValueError as err: - self._downloader.trouble(u'ERROR: Invalid JSON in API response: ' + compat_str(err)) + self._downloader.report_error(u'Invalid JSON in API response: ' + compat_str(err)) return if not 'feed' in response or not 'entry' in response['feed']: - self._downloader.trouble(u'ERROR: Got a malformed response from YouTube API') + self._downloader.report_error(u'Got a malformed response from YouTube API') return videos += [ (entry['yt$position']['$t'], entry['content']['src']) for entry in response['feed']['entry'] @@ -1777,7 +1777,7 @@ class YoutubeChannelIE(InfoExtractor): # Extract channel id mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid url: %s' % url) + self._downloader.report_error(u'invalid url: %s' % url) return # Download channel pages @@ -1792,7 +1792,7 @@ class YoutubeChannelIE(InfoExtractor): try: page = compat_urllib_request.urlopen(request).read().decode('utf8') 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)) + self._downloader.report_error(u'unable to download webpage: %s' % compat_str(err)) return # Extract video identifiers @@ -1835,7 +1835,7 @@ class YoutubeUserIE(InfoExtractor): # Extract username mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid url: %s' % url) + self._downloader.report_error(u'invalid url: %s' % url) return username = mobj.group(1) @@ -1857,7 +1857,7 @@ class YoutubeUserIE(InfoExtractor): try: page = compat_urllib_request.urlopen(request).read().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)) + self._downloader.report_error(u'unable to download webpage: %s' % compat_str(err)) return # Extract video identifiers @@ -1915,7 +1915,7 @@ class BlipTVUserIE(InfoExtractor): # Extract username mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid url: %s' % url) + self._downloader.report_error(u'invalid url: %s' % url) return username = mobj.group(1) @@ -1929,7 +1929,7 @@ class BlipTVUserIE(InfoExtractor): mobj = re.search(r'data-users-id="([^"]+)"', page) page_base = page_base % mobj.group(1) 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)) + self._downloader.report_error(u'unable to download webpage: %s' % compat_str(err)) return @@ -1948,7 +1948,7 @@ class BlipTVUserIE(InfoExtractor): try: page = compat_urllib_request.urlopen(request).read().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' % str(err)) + self._downloader.report_error(u'unable to download webpage: %s' % str(err)) return # Extract video identifiers @@ -2012,7 +2012,7 @@ class DepositFilesIE(InfoExtractor): self.report_download_webpage(file_id) webpage = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve file webpage: %s' % compat_str(err)) + self._downloader.report_error(u'Unable to retrieve file webpage: %s' % compat_str(err)) return # Search for the real file URL @@ -2022,9 +2022,9 @@ class DepositFilesIE(InfoExtractor): mobj = re.search(r'(Attention.*?)', webpage, re.DOTALL) if (mobj is not None) and (mobj.group(1) is not None): restriction_message = re.sub('\s+', ' ', mobj.group(1)).strip() - self._downloader.trouble(u'ERROR: %s' % restriction_message) + self._downloader.report_error(u'%s' % restriction_message) else: - self._downloader.trouble(u'ERROR: unable to extract download URL from: %s' % url) + self._downloader.report_error(u'unable to extract download URL from: %s' % url) return file_url = mobj.group(1) @@ -2033,7 +2033,7 @@ class DepositFilesIE(InfoExtractor): # Search for file title mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract title') + self._downloader.report_error(u'unable to extract title') return file_title = mobj.group(1).decode('utf-8') @@ -2106,7 +2106,7 @@ class FacebookIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return video_id = mobj.group('ID') @@ -2162,7 +2162,7 @@ class BlipTVIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return urlp = compat_urllib_parse_urlparse(url) @@ -2209,7 +2209,7 @@ class BlipTVIE(InfoExtractor): json_code_bytes = urlh.read() json_code = json_code_bytes.decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to read video info webpage: %s' % compat_str(err)) + self._downloader.report_error(u'unable to read video info webpage: %s' % compat_str(err)) return try: @@ -2240,7 +2240,7 @@ class BlipTVIE(InfoExtractor): 'user_agent': 'iTunes/10.6.1', } except (ValueError,KeyError) as err: - self._downloader.trouble(u'ERROR: unable to parse video information: %s' % repr(err)) + self._downloader.report_error(u'unable to parse video information: %s' % repr(err)) return return [info] @@ -2262,7 +2262,7 @@ class MyVideoIE(InfoExtractor): def _real_extract(self,url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._download.trouble(u'ERROR: invalid URL: %s' % url) + self._download.report_error(u'invalid URL: %s' % url) return video_id = mobj.group(1) @@ -2275,13 +2275,13 @@ class MyVideoIE(InfoExtractor): mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract media URL') + self._downloader.report_error(u'unable to extract media URL') return video_url = mobj.group(1) + ('/%s.flv' % video_id) mobj = re.search('([^<]+)', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract title') + self._downloader.report_error(u'unable to extract title') return video_title = mobj.group(1) @@ -2354,7 +2354,7 @@ class ComedyCentralIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url, re.VERBOSE) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return if mobj.group('shortname'): @@ -2385,16 +2385,16 @@ class ComedyCentralIE(InfoExtractor): 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)) + self._downloader.report_error(u'unable to download webpage: %s' % compat_str(err)) return if dlNewest: url = htmlHandle.geturl() mobj = re.match(self._VALID_URL, url, re.VERBOSE) if mobj is None: - self._downloader.trouble(u'ERROR: Invalid redirected URL: ' + url) + self._downloader.report_error(u'Invalid redirected URL: ' + url) return if mobj.group('episode') == '': - self._downloader.trouble(u'ERROR: Redirected URL is still not specific: ' + url) + self._downloader.report_error(u'Redirected URL is still not specific: ' + url) return epTitle = mobj.group('episode') @@ -2407,7 +2407,7 @@ class ComedyCentralIE(InfoExtractor): 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) + self._downloader.report_error(u'unable to find Flash URL in webpage ' + url) return else: mMovieParams = [("http://media.mtvnservices.com/" + altMovieParams[0], altMovieParams[0])] @@ -2418,7 +2418,7 @@ class ComedyCentralIE(InfoExtractor): try: indexXml = compat_urllib_request.urlopen(indexUrl).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download episode index: ' + compat_str(err)) + self._downloader.report_error(u'unable to download episode index: ' + compat_str(err)) return results = [] @@ -2439,7 +2439,7 @@ class ComedyCentralIE(InfoExtractor): try: configXml = compat_urllib_request.urlopen(configReq).read() 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)) + self._downloader.report_error(u'unable to download webpage: %s' % compat_str(err)) return cdoc = xml.etree.ElementTree.fromstring(configXml) @@ -2506,7 +2506,7 @@ class EscapistIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return showName = mobj.group('showname') videoId = mobj.group('episode') @@ -2518,7 +2518,7 @@ class EscapistIE(InfoExtractor): m = re.match(r'text/html; charset="?([^"]+)"?', webPage.headers['Content-Type']) webPage = webPageBytes.decode(m.group(1) if m else 'utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download webpage: ' + compat_str(err)) + self._downloader.report_error(u'unable to download webpage: ' + compat_str(err)) return descMatch = re.search('(.*?)\s+-\s+XVID', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video title') + self._downloader.report_error(u'unable to extract video title') return video_title = mobj.group(1) @@ -2678,7 +2678,7 @@ class XVideosIE(InfoExtractor): # Extract video thumbnail mobj = re.search(r'http://(?:img.*?\.)xvideos.com/videos/thumbs/[a-fA-F0-9]+/[a-fA-F0-9]+/[a-fA-F0-9]+/[a-fA-F0-9]+/([a-fA-F0-9.]+jpg)', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video thumbnail') + self._downloader.report_error(u'unable to extract video thumbnail') return video_thumbnail = mobj.group(0) @@ -2722,7 +2722,7 @@ class SoundcloudIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return # extract uploader (which is in the url) @@ -2740,7 +2740,7 @@ class SoundcloudIE(InfoExtractor): info_json_bytes = compat_urllib_request.urlopen(request).read() info_json = info_json_bytes.decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download video webpage: %s' % compat_str(err)) + self._downloader.report_error(u'unable to download video webpage: %s' % compat_str(err)) return info = json.loads(info_json) @@ -2753,7 +2753,7 @@ class SoundcloudIE(InfoExtractor): stream_json_bytes = compat_urllib_request.urlopen(request).read() stream_json = stream_json_bytes.decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download stream definitions: %s' % compat_str(err)) + self._downloader.report_error(u'unable to download stream definitions: %s' % compat_str(err)) return streams = json.loads(stream_json) @@ -2781,7 +2781,7 @@ class InfoQIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return webpage = self._download_webpage(url, video_id=url) @@ -2790,7 +2790,7 @@ class InfoQIE(InfoExtractor): # Extract video URL mobj = re.search(r"jsclassref='([^']*)'", webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video url') + self._downloader.report_error(u'unable to extract video url') return real_id = compat_urllib_parse.unquote(base64.b64decode(mobj.group(1).encode('ascii')).decode('utf-8')) video_url = 'rtmpe://video.infoq.com/cfx/st/' + real_id @@ -2798,7 +2798,7 @@ class InfoQIE(InfoExtractor): # Extract title mobj = re.search(r'contentTitle = "(.*?)";', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video title') + self._downloader.report_error(u'unable to extract video title') return video_title = mobj.group(1) @@ -2881,7 +2881,7 @@ class MixcloudIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return # extract uploader & filename from url uploader = mobj.group(1).decode('utf-8') @@ -2895,7 +2895,7 @@ class MixcloudIE(InfoExtractor): self.report_download_json(file_url) jsonData = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve file: %s' % compat_str(err)) + self._downloader.report_error(u'Unable to retrieve file: %s' % compat_str(err)) return # parse JSON @@ -2919,7 +2919,7 @@ class MixcloudIE(InfoExtractor): break # got it! else: if req_format not in formats: - self._downloader.trouble(u'ERROR: format is not available') + self._downloader.report_error(u'format is not available') return url_list = self.get_urls(formats, req_format) @@ -2973,7 +2973,7 @@ class StanfordOpenClassroomIE(InfoExtractor): try: metaXml = compat_urllib_request.urlopen(xmlUrl).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download video info XML: %s' % compat_str(err)) + self._downloader.report_error(u'unable to download video info XML: %s' % compat_str(err)) return mdoc = xml.etree.ElementTree.fromstring(metaXml) try: @@ -3032,7 +3032,7 @@ class StanfordOpenClassroomIE(InfoExtractor): try: rootpage = compat_urllib_request.urlopen(rootURL).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download course info page: ' + compat_str(err)) + self._downloader.report_error(u'unable to download course info page: ' + compat_str(err)) return info['title'] = info['id'] @@ -3064,7 +3064,7 @@ class MTVIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return if not mobj.group('proto'): url = 'http://' + url @@ -3074,25 +3074,25 @@ class MTVIE(InfoExtractor): mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract song name') + self._downloader.report_error(u'unable to extract song name') return song_name = unescapeHTML(mobj.group(1).decode('iso-8859-1')) mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract performer') + self._downloader.report_error(u'unable to extract performer') return performer = unescapeHTML(mobj.group(1).decode('iso-8859-1')) video_title = performer + ' - ' + song_name mobj = re.search(r'', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to mtvn_uri') + self._downloader.report_error(u'unable to mtvn_uri') return mtvn_uri = mobj.group(1) mobj = re.search(r'MTVN.Player.defaultPlaylistId = ([0-9]+);', webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract content id') + self._downloader.report_error(u'unable to extract content id') return content_id = mobj.group(1) @@ -3102,7 +3102,7 @@ class MTVIE(InfoExtractor): try: metadataXml = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download video metadata: %s' % compat_str(err)) + self._downloader.report_error(u'unable to download video metadata: %s' % compat_str(err)) return mdoc = xml.etree.ElementTree.fromstring(metadataXml) @@ -3174,7 +3174,7 @@ class YoukuIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return video_id = mobj.group('ID') @@ -3185,7 +3185,7 @@ class YoukuIE(InfoExtractor): self.report_download_webpage(video_id) jsondata = compat_urllib_request.urlopen(request).read() except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) + self._downloader.report_error(u'Unable to retrieve video webpage: %s' % compat_str(err)) return self.report_extraction(video_id) @@ -3216,7 +3216,7 @@ class YoukuIE(InfoExtractor): fileid = config['data'][0]['streamfileids'][format] keys = [s['k'] for s in config['data'][0]['segs'][format]] except (UnicodeDecodeError, ValueError, KeyError): - self._downloader.trouble(u'ERROR: unable to extract info section') + self._downloader.report_error(u'unable to extract info section') return files_info=[] @@ -3263,7 +3263,7 @@ class XNXXIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return video_id = mobj.group(1) @@ -3274,24 +3274,24 @@ class XNXXIE(InfoExtractor): webpage_bytes = compat_urllib_request.urlopen(url).read() webpage = webpage_bytes.decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download video webpage: %s' % err) + self._downloader.report_error(u'unable to download video webpage: %s' % err) return result = re.search(self.VIDEO_URL_RE, webpage) if result is None: - self._downloader.trouble(u'ERROR: unable to extract video url') + self._downloader.report_error(u'unable to extract video url') return video_url = compat_urllib_parse.unquote(result.group(1)) result = re.search(self.VIDEO_TITLE_RE, webpage) if result is None: - self._downloader.trouble(u'ERROR: unable to extract video title') + self._downloader.report_error(u'unable to extract video title') return video_title = result.group(1) result = re.search(self.VIDEO_THUMB_RE, webpage) if result is None: - self._downloader.trouble(u'ERROR: unable to extract video thumbnail') + self._downloader.report_error(u'unable to extract video thumbnail') return video_thumbnail = result.group(1) @@ -3340,7 +3340,7 @@ class GooglePlusIE(InfoExtractor): # Extract id from URL mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: Invalid URL: %s' % url) + self._downloader.report_error(u'Invalid URL: %s' % url) return post_url = mobj.group(0) @@ -3354,7 +3354,7 @@ class GooglePlusIE(InfoExtractor): try: webpage = compat_urllib_request.urlopen(request).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve entry webpage: %s' % compat_str(err)) + self._downloader.report_error(u'Unable to retrieve entry webpage: %s' % compat_str(err)) return # Extract update date @@ -3389,14 +3389,14 @@ class GooglePlusIE(InfoExtractor): pattern = '"(https\://plus\.google\.com/photos/.*?)",,"image/jpeg","video"\]' mobj = re.search(pattern, webpage) if mobj is None: - self._downloader.trouble(u'ERROR: unable to extract video page URL') + self._downloader.report_error(u'unable to extract video page URL') video_page = mobj.group(1) request = compat_urllib_request.Request(video_page) try: webpage = compat_urllib_request.urlopen(request).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: Unable to retrieve video webpage: %s' % compat_str(err)) + self._downloader.report_error(u'Unable to retrieve video webpage: %s' % compat_str(err)) return self.report_extract_vid_page(video_page) @@ -3406,7 +3406,7 @@ class GooglePlusIE(InfoExtractor): pattern = '\d+,\d+,(\d+),"(http\://redirector\.googlevideo\.com.*?)"' mobj = re.findall(pattern, webpage) if len(mobj) == 0: - self._downloader.trouble(u'ERROR: unable to extract video links') + self._downloader.report_error(u'unable to extract video links') # Sort in resolution links = sorted(mobj) @@ -3438,7 +3438,7 @@ class NBAIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return video_id = mobj.group(1) @@ -3494,13 +3494,13 @@ class JustinTVIE(InfoExtractor): webpage_bytes = urlh.read() webpage = webpage_bytes.decode('utf-8', 'ignore') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.trouble(u'ERROR: unable to download video info JSON: %s' % compat_str(err)) + self._downloader.report_error(u'unable to download video info JSON: %s' % compat_str(err)) 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) + self._downloader.report_error(u'Justin.tv API: %s' % error_text) return info = [] for clip in response: @@ -3525,7 +3525,7 @@ class JustinTVIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return api = 'http://api.justin.tv' @@ -3560,7 +3560,7 @@ class FunnyOrDieIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return video_id = mobj.group('id') @@ -3568,7 +3568,7 @@ class FunnyOrDieIE(InfoExtractor): m = re.search(r']*>\s*]*>\s*\s+(?P.*?)</a>", webpage) @@ -3621,7 +3621,7 @@ class SteamIE(InfoExtractor): video_url = vid.group('videoURL') video_thumb = thumb.group('thumbnail') if not video_url: - self._downloader.trouble(u'ERROR: Cannot find video url for %s' % video_id) + self._downloader.report_error(u'Cannot find video url for %s' % video_id) info = { 'id':video_id, 'url':video_url, @@ -3711,7 +3711,7 @@ class YouPornIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return video_id = mobj.group('videoid') @@ -3803,7 +3803,7 @@ class YouPornIE(InfoExtractor): else: format = self._specific( req_format, formats ) if result is None: - self._downloader.trouble(u'ERROR: requested format not available') + self._downloader.report_error(u'requested format not available') return return [format] @@ -3816,7 +3816,7 @@ class PornotubeIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return video_id = mobj.group('videoid') @@ -3829,7 +3829,7 @@ class PornotubeIE(InfoExtractor): VIDEO_URL_RE = r'url: "(?P<url>http://video[0-9].pornotube.com/.+\.flv)",' result = re.search(VIDEO_URL_RE, webpage) if result is None: - self._downloader.trouble(u'ERROR: unable to extract video url') + self._downloader.report_error(u'unable to extract video url') return video_url = compat_urllib_parse.unquote(result.group('url')) @@ -3837,7 +3837,7 @@ class PornotubeIE(InfoExtractor): VIDEO_UPLOADED_RE = r'<div class="video_added_by">Added (?P<date>[0-9\/]+) by' result = re.search(VIDEO_UPLOADED_RE, webpage) if result is None: - self._downloader.trouble(u'ERROR: unable to extract video title') + self._downloader.report_error(u'unable to extract video title') return upload_date = result.group('date') @@ -3858,7 +3858,7 @@ class YouJizzIE(InfoExtractor): def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: - self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + self._downloader.report_error(u'invalid URL: %s' % url) return video_id = mobj.group('videoid') @@ -4059,13 +4059,13 @@ class MySpassIE(InfoExtractor): # extract values from metadata url_flv_el = metadata.find('url_flv') if url_flv_el is None: - self._downloader.trouble(u'ERROR: unable to extract download url') + self._downloader.report_error(u'unable to extract download url') return video_url = url_flv_el.text extension = os.path.splitext(video_url)[1][1:] title_el = metadata.find('title') if title_el is None: - self._downloader.trouble(u'ERROR: unable to extract title') + self._downloader.report_error(u'unable to extract title') return title = title_el.text format_id_el = metadata.find('format_id') From 631f73978c0ee851950ac697dfd73f9092abd3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Mon, 4 Mar 2013 22:16:42 +0100 Subject: [PATCH 007/221] Add a method for extracting info from a list of urls --- youtube_dl/FileDownloader.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index b26c34729..f668b362b 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -415,6 +415,14 @@ class FileDownloader(object): raise if not suitable_found: self.trouble(u'ERROR: no suitable InfoExtractor: %s' % url) + def extract_info_iterable(self, urls): + ''' + Return the videos founded for the urls + ''' + results = [] + for url in urls: + results.extend(self.extract_info(url)) + return results def process_info(self, info_dict): """Process a single dictionary returned by an InfoExtractor.""" From 597cc8a45536aa4207c5ffc3e421fcebf2e08fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Tue, 5 Mar 2013 11:58:01 +0100 Subject: [PATCH 008/221] Use extract_info in YoutubePlaylist and YoutubeSearch --- test/test_youtube_lists.py | 16 +++++++++------- youtube_dl/InfoExtractors.py | 8 +++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/test/test_youtube_lists.py b/test/test_youtube_lists.py index f4705bc5b..055bf69c8 100644 --- a/test/test_youtube_lists.py +++ b/test/test_youtube_lists.py @@ -10,6 +10,7 @@ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from youtube_dl.InfoExtractors import YoutubeUserIE, YoutubePlaylistIE, YoutubeIE from youtube_dl.utils import * +from youtube_dl.FileDownloader import FileDownloader PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json") with io.open(PARAMETERS_FILE, encoding='utf-8') as pf: @@ -22,7 +23,7 @@ proxy_handler = compat_urllib_request.ProxyHandler() opener = compat_urllib_request.build_opener(proxy_handler, cookie_processor, YoutubeDLHandler()) compat_urllib_request.install_opener(opener) -class FakeDownloader(object): +class FakeDownloader(FileDownloader): def __init__(self): self.result = [] self.params = parameters @@ -30,15 +31,16 @@ class FakeDownloader(object): print(s) def trouble(self, s): raise Exception(s) - def download(self, x): - self.result.append(x) + def extract_info(self, url): + self.result.append(url) + return url class TestYoutubeLists(unittest.TestCase): def test_youtube_playlist(self): dl = FakeDownloader() ie = YoutubePlaylistIE(dl) ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re') - ytie_results = [YoutubeIE()._extract_id(r[0]) for r in dl.result] + ytie_results = [YoutubeIE()._extract_id(url) for url in dl.result] self.assertEqual(ytie_results, [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE']) def test_issue_673(self): @@ -58,7 +60,7 @@ class TestYoutubeLists(unittest.TestCase): dl = FakeDownloader() ie = YoutubePlaylistIE(dl) ie.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC') - ytie_results = [YoutubeIE()._extract_id(r[0]) for r in dl.result] + ytie_results = [YoutubeIE()._extract_id(url) for url in dl.result] self.assertFalse('pElCt5oNDuI' in ytie_results) self.assertFalse('KdPEApIVdWM' in ytie_results) @@ -67,9 +69,9 @@ class TestYoutubeLists(unittest.TestCase): ie = YoutubePlaylistIE(dl) # TODO find a > 100 (paginating?) videos course ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8') - self.assertEqual(YoutubeIE()._extract_id(dl.result[0][0]), 'j9WZyLZCBzs') + self.assertEqual(YoutubeIE()._extract_id(dl.result[0]), 'j9WZyLZCBzs') self.assertEqual(len(dl.result), 25) - self.assertEqual(YoutubeIE()._extract_id(dl.result[-1][0]), 'rYefUsYuEp0') + self.assertEqual(YoutubeIE()._extract_id(dl.result[-1]), 'rYefUsYuEp0') def test_youtube_channel(self): # I give up, please find a channel that does paginate and test this like test_youtube_playlist_long diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 7ce84fe79..8a7694a76 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1756,9 +1756,7 @@ class YoutubePlaylistIE(InfoExtractor): else: self._downloader.to_screen(u'[youtube] PL %s: Found %i videos, downloading %i' % (playlist_id, total, len(videos))) - for video in videos: - self._downloader.download([video]) - return + return self._downloader.extract_info_iterable(videos) class YoutubeChannelIE(InfoExtractor): @@ -1892,8 +1890,8 @@ class YoutubeUserIE(InfoExtractor): self._downloader.to_screen(u"[youtube] user %s: Collected %d video ids (downloading %d of them)" % (username, all_ids_count, len(video_ids))) - for video_id in video_ids: - self._downloader.download(['http://www.youtube.com/watch?v=%s' % video_id]) + urls = ['http://www.youtube.com/watch?v=%s' % video_id for video_id in video_ids] + return self._downloader.extract_info_iterable(urls) class BlipTVUserIE(InfoExtractor): From f6e6da9525150487476d4990693eedf73acffab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Tue, 5 Mar 2013 12:26:18 +0100 Subject: [PATCH 009/221] Use extract_info in BlipTV User and Youtube Channel --- youtube_dl/InfoExtractors.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 8a7694a76..d79f6068f 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1806,9 +1806,8 @@ class YoutubeChannelIE(InfoExtractor): self._downloader.to_screen(u'[youtube] Channel %s: Found %i videos' % (channel_id, len(video_ids))) - for id in video_ids: - self._downloader.download(['http://www.youtube.com/watch?v=%s' % id]) - return + urls = ['http://www.youtube.com/watch?v=%s' % id for id in video_ids] + return self._downloader.extract_info_iterable(urls) class YoutubeUserIE(InfoExtractor): @@ -1981,8 +1980,8 @@ class BlipTVUserIE(InfoExtractor): self._downloader.to_screen(u"[%s] user %s: Collected %d video ids (downloading %d of them)" % (self.IE_NAME, username, all_ids_count, len(video_ids))) - for video_id in video_ids: - self._downloader.download([u'http://blip.tv/'+video_id]) + urls = [u'http://blip.tv/%s' % video_id for video_id in video_ids] + return self._downloader.extract_info_iterable(urls) class DepositFilesIE(InfoExtractor): From 6ac7f082c469b3b2153735ae8475e1d0fc8b5439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Tue, 5 Mar 2013 20:14:32 +0100 Subject: [PATCH 010/221] `extract_info` now expects `ie.extract` to return a list in the format proposed in issue 608. Each element should have a '_type' key specifying if it's a video, an url or a playlist. `extract_info` will process each element to get the full info --- youtube_dl/FileDownloader.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 9b630c123..68fad11bc 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -410,12 +410,9 @@ class FileDownloader(object): # Extract information from URL and process it try: - videos = ie.extract(url) - for video in videos or []: - if not 'extractor' in video: - #The extractor has already been set somewher else - video['extractor'] = ie.IE_NAME - return videos + ie_results = ie.extract(url) + results = self.process_ie_results(ie_results, ie) + return results except ExtractorError as de: # An error we somewhat expected self.trouble(u'ERROR: ' + compat_str(de), de.format_traceback()) break @@ -435,6 +432,29 @@ class FileDownloader(object): for url in urls: results.extend(self.extract_info(url)) return results + + def process_ie_results(self, ie_results, ie): + """ + Take the results of the ie and return a list of videos. + For url elements it will seartch the suitable ie and get the videos + For playlist elements it will process each of the elements of the 'entries' key + """ + results = [] + for result in ie_results or []: + result_type = result.get('_type', 'video') #If not given we suppose it's a video, support the dafault old system + if result_type == 'video': + if not 'extractor' in result: + #The extractor has already been set somewhere else + result['extractor'] = ie.IE_NAME + results.append(result) + elif result_type == 'url': + #We get the videos pointed by the url + results.extend(self.extract_info(result['url'])) + elif result_type == 'playlist': + #We process each entry in the playlist + entries_result = self.process_ie_results(result['entries'], ie) + results.extend(entries_result) + return results def process_info(self, info_dict): """Process a single dictionary returned by an InfoExtractor.""" From 8a38a194fb08a253986cdbafa02cf699ef76c9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Tue, 5 Mar 2013 20:55:48 +0100 Subject: [PATCH 011/221] Add auxiliary methods to InfoExtractor to set the '_type' key and use them for some playlist IEs --- test/test_youtube_lists.py | 35 +++++++++++++++++++++-------------- youtube_dl/InfoExtractors.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/test/test_youtube_lists.py b/test/test_youtube_lists.py index 055bf69c8..9e91484f8 100644 --- a/test/test_youtube_lists.py +++ b/test/test_youtube_lists.py @@ -36,31 +36,37 @@ class FakeDownloader(FileDownloader): return url class TestYoutubeLists(unittest.TestCase): + def assertIsPlaylist(self,info): + """Make sure the info has '_type' set to 'playlist'""" + self.assertEqual(info['_type'], 'playlist') + def test_youtube_playlist(self): dl = FakeDownloader() ie = YoutubePlaylistIE(dl) - ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re') - ytie_results = [YoutubeIE()._extract_id(url) for url in dl.result] + result = ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')[0] + self.assertIsPlaylist(result) + ytie_results = [YoutubeIE()._extract_id(url['url']) for url in result['entries']] self.assertEqual(ytie_results, [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE']) def test_issue_673(self): dl = FakeDownloader() ie = YoutubePlaylistIE(dl) - ie.extract('PLBB231211A4F62143') - self.assertTrue(len(dl.result) > 40) + result = ie.extract('PLBB231211A4F62143')[0] + self.assertTrue(len(result['entries']) > 40) def test_youtube_playlist_long(self): dl = FakeDownloader() ie = YoutubePlaylistIE(dl) - ie.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q') - self.assertTrue(len(dl.result) >= 799) + result = ie.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')[0] + self.assertIsPlaylist(result) + self.assertTrue(len(result['entries']) >= 799) def test_youtube_playlist_with_deleted(self): #651 dl = FakeDownloader() ie = YoutubePlaylistIE(dl) - ie.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC') - ytie_results = [YoutubeIE()._extract_id(url) for url in dl.result] + result = ie.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')[0] + ytie_results = [YoutubeIE()._extract_id(url['url']) for url in result['entries']] self.assertFalse('pElCt5oNDuI' in ytie_results) self.assertFalse('KdPEApIVdWM' in ytie_results) @@ -68,10 +74,11 @@ class TestYoutubeLists(unittest.TestCase): dl = FakeDownloader() ie = YoutubePlaylistIE(dl) # TODO find a > 100 (paginating?) videos course - ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8') - self.assertEqual(YoutubeIE()._extract_id(dl.result[0]), 'j9WZyLZCBzs') - self.assertEqual(len(dl.result), 25) - self.assertEqual(YoutubeIE()._extract_id(dl.result[-1]), 'rYefUsYuEp0') + result = ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')[0] + entries = result['entries'] + self.assertEqual(YoutubeIE()._extract_id(entries[0]['url']), 'j9WZyLZCBzs') + self.assertEqual(len(entries), 25) + self.assertEqual(YoutubeIE()._extract_id(entries[-1]['url']), 'rYefUsYuEp0') def test_youtube_channel(self): # I give up, please find a channel that does paginate and test this like test_youtube_playlist_long @@ -80,8 +87,8 @@ class TestYoutubeLists(unittest.TestCase): def test_youtube_user(self): dl = FakeDownloader() ie = YoutubeUserIE(dl) - ie.extract('https://www.youtube.com/user/TheLinuxFoundation') - self.assertTrue(len(dl.result) >= 320) + result = ie.extract('https://www.youtube.com/user/TheLinuxFoundation')[0] + self.assertTrue(len(result['entries']) >= 320) if __name__ == '__main__': unittest.main() diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index d79f6068f..895658f49 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -128,6 +128,24 @@ class InfoExtractor(object): urlh = self._request_webpage(url_or_request, video_id, note, errnote) webpage_bytes = urlh.read() return webpage_bytes.decode('utf-8', 'replace') + + #Methods for following #608 + #They set the correct value of the '_type' key + def video_result(self, video_info): + """Returns a video""" + video_info['_type'] = 'video' + return video_info + def url_result(self, url, ie=None): + """Returns a url that points to a page that should be processed""" + #TODO: ie should be the class used for getting the info + video_info = {'_type': 'url', + 'url': url} + return video_info + def playlist_result(self, entries): + """Returns a playlist""" + video_info = {'_type': 'playlist', + 'entries': entries} + return video_info class YoutubeIE(InfoExtractor): @@ -1756,7 +1774,8 @@ class YoutubePlaylistIE(InfoExtractor): else: self._downloader.to_screen(u'[youtube] PL %s: Found %i videos, downloading %i' % (playlist_id, total, len(videos))) - return self._downloader.extract_info_iterable(videos) + url_results = [self.url_result(url) for url in videos] + return [self.playlist_result(url_results)] class YoutubeChannelIE(InfoExtractor): @@ -1807,7 +1826,8 @@ class YoutubeChannelIE(InfoExtractor): self._downloader.to_screen(u'[youtube] Channel %s: Found %i videos' % (channel_id, len(video_ids))) urls = ['http://www.youtube.com/watch?v=%s' % id for id in video_ids] - return self._downloader.extract_info_iterable(urls) + url_entries = [self.url_result(url) for url in urls] + return [self.playlist_result(url_entries)] class YoutubeUserIE(InfoExtractor): @@ -1890,7 +1910,8 @@ class YoutubeUserIE(InfoExtractor): (username, all_ids_count, len(video_ids))) urls = ['http://www.youtube.com/watch?v=%s' % video_id for video_id in video_ids] - return self._downloader.extract_info_iterable(urls) + url_results = [self.url_result(url) for url in urls] + return [self.playlist_result(url_results)] class BlipTVUserIE(InfoExtractor): @@ -1981,7 +2002,8 @@ class BlipTVUserIE(InfoExtractor): (self.IE_NAME, username, all_ids_count, len(video_ids))) urls = [u'http://blip.tv/%s' % video_id for video_id in video_ids] - return self._downloader.extract_info_iterable(urls) + url_entries = [self.url_result(url) for url in urls] + return [self.playlist_result(url_entries)] class DepositFilesIE(InfoExtractor): From c9fa1cbab6b24f48449aca3b0eddabee6d95a7d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Tue, 5 Mar 2013 21:13:17 +0100 Subject: [PATCH 012/221] More trouble calls changed in InfoExtractors.py The calls with the message starting with 'WARNING' have been changed to report_warning instead of report_error --- youtube_dl/InfoExtractors.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 6328332a7..83bf5b8f6 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -467,7 +467,7 @@ class YoutubeIE(InfoExtractor): if mobj is not None: video_uploader_id = mobj.group(1) else: - self._downloader.trouble(u'WARNING: unable to extract uploader nickname') + self._downloader.report_warning(u'unable to extract uploader nickname') # title if 'title' not in video_info: @@ -477,7 +477,7 @@ class YoutubeIE(InfoExtractor): # thumbnail image if 'thumbnail_url' not in video_info: - self._downloader.trouble(u'WARNING: unable to extract video thumbnail') + self._downloader.report_warning(u'unable to extract video thumbnail') video_thumbnail = '' else: # don't panic if we can't find it video_thumbnail = compat_urllib_parse.unquote_plus(video_info['thumbnail_url'][0]) @@ -509,7 +509,7 @@ class YoutubeIE(InfoExtractor): self._downloader.trouble(srt_error) if 'length_seconds' not in video_info: - self._downloader.trouble(u'WARNING: unable to extract video duration') + self._downloader.report_warning(u'unable to extract video duration') video_duration = '' else: video_duration = compat_urllib_parse.unquote_plus(video_info['length_seconds'][0]) @@ -785,7 +785,7 @@ class DailymotionIE(InfoExtractor): # lookin for official user mobj_official = re.search(r'<span rel="author"[^>]+?>([^<]+?)</span>', webpage) if mobj_official is None: - self._downloader.trouble(u'WARNING: unable to extract uploader nickname') + self._downloader.report_warning(u'unable to extract uploader nickname') else: video_uploader = mobj_official.group(1) else: @@ -2449,7 +2449,7 @@ class ComedyCentralIE(InfoExtractor): turls.append(finfo) if len(turls) == 0: - self._downloader.trouble(u'\nERROR: unable to download ' + mediaId + ': No videos found') + self._downloader.report_error(u'unable to download ' + mediaId + ': No videos found') continue if self._downloader.params.get('listformats', None): @@ -2609,7 +2609,7 @@ class CollegeHumorIE(InfoExtractor): info['thumbnail'] = videoNode.findall('./thumbnail')[0].text manifest_url = videoNode.findall('./file')[0].text except IndexError: - self._downloader.trouble(u'\nERROR: Invalid metadata XML file') + self._downloader.report_error(u'Invalid metadata XML file') return manifest_url += '?hdcore=2.10.3' @@ -2626,7 +2626,7 @@ class CollegeHumorIE(InfoExtractor): node_id = media_node.attrib['url'] video_id = adoc.findall('./{http://ns.adobe.com/f4m/1.0}id')[0].text except IndexError as err: - self._downloader.trouble(u'\nERROR: Invalid manifest file') + self._downloader.report_error(u'Invalid manifest file') return url_pr = compat_urllib_parse_urlparse(manifest_url) @@ -2980,7 +2980,7 @@ class StanfordOpenClassroomIE(InfoExtractor): info['title'] = mdoc.findall('./title')[0].text info['url'] = baseUrl + mdoc.findall('./videoFile')[0].text except IndexError: - self._downloader.trouble(u'\nERROR: Invalid metadata XML file') + self._downloader.report_error(u'Invalid metadata XML file') return info['ext'] = info['url'].rpartition('.')[2] return [info] From a0d6fe7b924697c089ed7ae37df0ca590ac38a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Tue, 5 Mar 2013 22:33:32 +0100 Subject: [PATCH 013/221] When a redirect is found return the new url using the new style --- youtube_dl/InfoExtractors.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 895658f49..e714fa6b0 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1311,7 +1311,7 @@ class GenericIE(InfoExtractor): self._downloader.to_screen(u'[redirect] Following redirect to %s' % new_url) def _test_redirect(self, url): - """Check if it is a redirect, like url shorteners, in case restart chain.""" + """Check if it is a redirect, like url shorteners, in case return the new url.""" class HeadRequest(compat_urllib_request.Request): def get_method(self): return "HEAD" @@ -1362,11 +1362,11 @@ class GenericIE(InfoExtractor): return False self.report_following_redirect(new_url) - self._downloader.download([new_url]) - return True + return new_url def _real_extract(self, url): - if self._test_redirect(url): return + new_url = self._test_redirect(url) + if new_url: return [self.url_result(new_url)] video_id = url.split('/')[-1] request = compat_urllib_request.Request(url) From 40634747f74d2c85b28ee33f11672378c9b30949 Mon Sep 17 00:00:00 2001 From: Johny Mo Swag <johnymo@me.com> Date: Wed, 6 Mar 2013 21:09:55 -0800 Subject: [PATCH 014/221] Support for WorldStarHipHop.com --- youtube_dl/InfoExtractors.py | 63 +++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 6b03bf307..8be2f160c 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -2557,7 +2557,7 @@ class EscapistIE(InfoExtractor): 'uploader': showName, 'upload_date': None, 'title': showName, - 'ext': 'mp4', + 'ext': 'flv', 'thumbnail': imgUrl, 'description': description, 'player_url': playerUrl, @@ -3654,6 +3654,66 @@ class UstreamIE(InfoExtractor): } return [info] +class WorldStarHipHopIE(InfoExtractor): + _VALID_URL = r"""(http://(?:www|m).worldstar(?:candy|hiphop)\.com.*)""" + IE_NAME = u'WorldStarHipHop' + + def _real_extract(self, url): + results = [] + + _src_url = r"""(http://hw-videos.*(?:mp4|flv))""" + + webpage_src = compat_urllib_request.urlopen(str(url)).read() + + mobj = re.search(_src_url, webpage_src) + + if mobj is not None: + video_url = mobj.group() + if 'mp4' in video_url: + ext = '.mp4' + else: + ext = '.flv' + else: + video_url = None + ext = None + + _title = r"""<title>(.*)""" + + mobj = re.search(_title, webpage_src) + + if mobj is not None: + title = mobj.group(1) + title = title.replace("'", "") + title = title.replace("'", "") + title = title.replace('Video: ', '') + title = title.replace('"', '"') + title = title.replace('&', 'n') + else: + title = None + + _thumbnail = r"""rel="image_src" href="(.*)" />""" + + mobj = re.search(_thumbnail, webpage_src) + + # Getting thumbnail and if not thumbnail sets correct title for WSHH candy video. + if mobj is not None: + thumbnail = mobj.group(1) + else: + _title = r"""candytitles.*>(.*)""" + mobj = re.search(_title, webpage_src) + if mobj is not None: + title = mobj.group(1) + thumbnail = None + + results.append({ + 'url' : video_url, + 'title' : title, + 'thumbnail' : thumbnail, + 'ext' : ext + }) + + return results + class RBMARadioIE(InfoExtractor): _VALID_URL = r'https?://(?:www\.)?rbmaradio\.com/shows/(?P[^/]+)$' @@ -4133,6 +4193,7 @@ def gen_extractors(): GooglePlusIE(), ArteTvIE(), NBAIE(), + WorldStarHipHopIE(), JustinTVIE(), FunnyOrDieIE(), SteamIE(), From 61e40c88a989d31b6f06d7001f614d62f06941a5 Mon Sep 17 00:00:00 2001 From: Johny Mo Swag Date: Wed, 6 Mar 2013 21:14:46 -0800 Subject: [PATCH 015/221] fixed typo --- youtube_dl/InfoExtractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 8be2f160c..58803c48a 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -2557,7 +2557,7 @@ class EscapistIE(InfoExtractor): 'uploader': showName, 'upload_date': None, 'title': showName, - 'ext': 'flv', + 'ext': 'mp4', 'thumbnail': imgUrl, 'description': description, 'player_url': playerUrl, From b3bcca0844cc8197cbb5e1e8127b1b8164304940 Mon Sep 17 00:00:00 2001 From: Johny Mo Swag Date: Thu, 7 Mar 2013 15:39:17 -0800 Subject: [PATCH 016/221] clean up --- youtube_dl/InfoExtractors.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 58803c48a..178b0beed 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3659,20 +3659,19 @@ class WorldStarHipHopIE(InfoExtractor): IE_NAME = u'WorldStarHipHop' def _real_extract(self, url): - results = [] - _src_url = r"""(http://hw-videos.*(?:mp4|flv))""" webpage_src = compat_urllib_request.urlopen(str(url)).read() + webpage_src = webpage_src.decode('utf-8') mobj = re.search(_src_url, webpage_src) if mobj is not None: video_url = mobj.group() if 'mp4' in video_url: - ext = '.mp4' + ext = 'mp4' else: - ext = '.flv' + ext = 'flv' else: video_url = None ext = None @@ -3683,16 +3682,12 @@ class WorldStarHipHopIE(InfoExtractor): if mobj is not None: title = mobj.group(1) - title = title.replace("'", "") - title = title.replace("'", "") - title = title.replace('Video: ', '') - title = title.replace('"', '"') - title = title.replace('&', 'n') else: - title = None + title = 'World Start Hip Hop - %s' % time.ctime() _thumbnail = r"""rel="image_src" href="(.*)" />""" + print title mobj = re.search(_thumbnail, webpage_src) # Getting thumbnail and if not thumbnail sets correct title for WSHH candy video. @@ -3705,13 +3700,12 @@ class WorldStarHipHopIE(InfoExtractor): title = mobj.group(1) thumbnail = None - results.append({ - 'url' : video_url, - 'title' : title, - 'thumbnail' : thumbnail, - 'ext' : ext - }) - + results = [{ + 'url' : video_url, + 'title' : title, + 'thumbnail' : thumbnail, + 'ext' : ext, + }] return results class RBMARadioIE(InfoExtractor): From 64c78d50ccf05f34e27b652530fc8b702aa54122 Mon Sep 17 00:00:00 2001 From: Johny Mo Swag Date: Thu, 7 Mar 2013 16:27:21 -0800 Subject: [PATCH 017/221] working - worldstarhiphop IE Support for WorldStarHipHop --- .gitignore | 2 ++ youtube_dl/InfoExtractors.py | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 77469b8a7..328fed8bd 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ youtube-dl.tar.gz cover/ updates_key.pem *.egg-info + +*.flv diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 178b0beed..f69bad4f3 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3655,7 +3655,7 @@ class UstreamIE(InfoExtractor): return [info] class WorldStarHipHopIE(InfoExtractor): - _VALID_URL = r"""(http://(?:www|m).worldstar(?:candy|hiphop)\.com.*)""" + _VALID_URL = r'http://(?:www|m)\.worldstar(?:candy|hiphop)\.com/videos/video\.php\?v=(?P.*)' IE_NAME = u'WorldStarHipHop' def _real_extract(self, url): @@ -3686,8 +3686,6 @@ class WorldStarHipHopIE(InfoExtractor): title = 'World Start Hip Hop - %s' % time.ctime() _thumbnail = r"""rel="image_src" href="(.*)" />""" - - print title mobj = re.search(_thumbnail, webpage_src) # Getting thumbnail and if not thumbnail sets correct title for WSHH candy video. @@ -3700,7 +3698,11 @@ class WorldStarHipHopIE(InfoExtractor): title = mobj.group(1) thumbnail = None + m = re.match(self._VALID_URL, url) + video_id = m.group('id') + results = [{ + 'id': video_id, 'url' : video_url, 'title' : title, 'thumbnail' : thumbnail, From 3b221c540640f7df9e4dc453a736dd25fe2505c4 Mon Sep 17 00:00:00 2001 From: Johny Mo Swag Date: Fri, 8 Mar 2013 22:39:45 -0800 Subject: [PATCH 018/221] removed str used for other project. --- youtube_dl/InfoExtractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index f69bad4f3..c2e3c8983 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3661,7 +3661,7 @@ class WorldStarHipHopIE(InfoExtractor): def _real_extract(self, url): _src_url = r"""(http://hw-videos.*(?:mp4|flv))""" - webpage_src = compat_urllib_request.urlopen(str(url)).read() + webpage_src = compat_urllib_request.urlopen(url).read() webpage_src = webpage_src.decode('utf-8') mobj = re.search(_src_url, webpage_src) From 08ec0af7c69f5da0f8c75c84886694877b9b08bf Mon Sep 17 00:00:00 2001 From: Johny Mo Swag Date: Fri, 8 Mar 2013 22:48:05 -0800 Subject: [PATCH 019/221] catch fatal error --- youtube_dl/InfoExtractors.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index c2e3c8983..a31aa759e 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3666,6 +3666,9 @@ class WorldStarHipHopIE(InfoExtractor): mobj = re.search(_src_url, webpage_src) + m = re.match(self._VALID_URL, url) + video_id = m.group('id') + if mobj is not None: video_url = mobj.group() if 'mp4' in video_url: @@ -3673,8 +3676,8 @@ class WorldStarHipHopIE(InfoExtractor): else: ext = 'flv' else: - video_url = None - ext = None + self._downloader.trouble(u'ERROR: Cannot find video url for %s' % video_id) + return _title = r"""(.*)""" @@ -3697,9 +3700,6 @@ class WorldStarHipHopIE(InfoExtractor): if mobj is not None: title = mobj.group(1) thumbnail = None - - m = re.match(self._VALID_URL, url) - video_id = m.group('id') results = [{ 'id': video_id, From 51af426d89f9a9e720d70f3cac1ce24b3b8e4d8f Mon Sep 17 00:00:00 2001 From: Johny Mo Swag Date: Fri, 8 Mar 2013 22:52:17 -0800 Subject: [PATCH 020/221] forgot to fix this. --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 328fed8bd..ca4e8f353 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,4 @@ youtube-dl.tar.gz .coverage cover/ updates_key.pem -*.egg-info - -*.flv +*.egg-info \ No newline at end of file From 8cc83b8dbea6e4f34f483c4a209158307df566f0 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Sat, 9 Mar 2013 10:05:43 +0100 Subject: [PATCH 021/221] Bubble up all the stack of exceptions and retry download tests on timeout errors --- test/test_download.py | 16 +++++++++++++++- youtube_dl/FileDownloader.py | 16 +++++++++++++--- youtube_dl/utils.py | 6 +++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/test/test_download.py b/test/test_download.py index f1bccf58c..a8de1d002 100644 --- a/test/test_download.py +++ b/test/test_download.py @@ -20,6 +20,8 @@ from youtube_dl.utils import * DEF_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tests.json') PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json") +RETRIES = 3 + # General configuration (from __init__, not very elegant...) jar = compat_cookiejar.CookieJar() cookie_processor = compat_urllib_request.HTTPCookieProcessor(jar) @@ -94,7 +96,19 @@ def generator(test_case): _try_rm(tc['file'] + '.part') _try_rm(tc['file'] + '.info.json') try: - fd.download([test_case['url']]) + for retry in range(1, RETRIES + 1): + try: + fd.download([test_case['url']]) + except (DownloadError, ExtractorError) as err: + if retry == RETRIES: raise + + # Check if the exception is not a network related one + if not err.exc_info[0] in (ZeroDivisionError, compat_urllib_error.URLError, socket.timeout): + raise + + print('Retrying: {0} failed tries\n\n##########\n\n'.format(retry)) + else: + break for tc in test_cases: if not test_case.get('params', {}).get('skip_download', False): diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 3b2adf84b..a13a5f9d7 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -227,11 +227,21 @@ class FileDownloader(object): self.to_stderr(message) if self.params.get('verbose'): if tb is None: - tb_data = traceback.format_list(traceback.extract_stack()) - tb = u''.join(tb_data) + if sys.exc_info()[0]: # if .trouble has been called from an except block + tb = u'' + if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]: + tb += u''.join(traceback.format_exception(*sys.exc_info()[1].exc_info)) + tb += compat_str(traceback.format_exc()) + else: + tb_data = traceback.format_list(traceback.extract_stack()) + tb = u''.join(tb_data) self.to_stderr(tb) if not self.params.get('ignoreerrors', False): - raise DownloadError(message) + if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]: + exc_info = sys.exc_info()[1].exc_info + else: + exc_info = sys.exc_info() + raise DownloadError(message, exc_info) self._download_retcode = 1 def report_warning(self, message): diff --git a/youtube_dl/utils.py b/youtube_dl/utils.py index 95bd94843..88d4ece13 100644 --- a/youtube_dl/utils.py +++ b/youtube_dl/utils.py @@ -435,6 +435,7 @@ class ExtractorError(Exception): """ tb, if given, is the original traceback (so that it can be printed out). """ super(ExtractorError, self).__init__(msg) self.traceback = tb + self.exc_info = sys.exc_info() # preserve original exception def format_traceback(self): if self.traceback is None: @@ -449,7 +450,10 @@ class DownloadError(Exception): configured to continue on errors. They will contain the appropriate error message. """ - pass + def __init__(self, msg, exc_info=None): + """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """ + super(DownloadError, self).__init__(msg) + self.exc_info = exc_info class SameFileError(Exception): From e32b06e977447f6be78c02c66f35f609f81331ce Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Tue, 12 Mar 2013 01:08:54 +0100 Subject: [PATCH 022/221] Spiegel IE --- test/tests.json | 9 ++++++++ youtube_dl/InfoExtractors.py | 43 +++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/test/tests.json b/test/tests.json index e4ea0b41e..fd9d33332 100644 --- a/test/tests.json +++ b/test/tests.json @@ -299,5 +299,14 @@ "url": "http://www.hodiho.fr/2013/02/regis-plante-sa-jeep.html", "file": "13601338388002.mp4", "md5": "85b90ccc9d73b4acd9138d3af4c27f89" + }, + { + "name": "Spiegel", + "url": "http://www.spiegel.de/video/vulkan-tungurahua-in-ecuador-ist-wieder-aktiv-video-1259285.html", + "file": "1259285.mp4", + "md5": "2c2754212136f35fb4b19767d242f66e", + "info_dict": { + "title": "Vulkanausbruch in Ecuador: Der \"Feuerschlund\" ist wieder aktiv" + } } ] diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index baba4a9a2..44b4c4376 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -126,8 +126,14 @@ class InfoExtractor(object): def _download_webpage(self, url_or_request, video_id, note=None, errnote=None): """ Returns the data of the page as a string """ urlh = self._request_webpage(url_or_request, video_id, note, errnote) + content_type = urlh.headers.get('Content-Type', '') + m = re.match(r'[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+\s*;\s*charset=(.+)', content_type) + if m: + encoding = m.group(1) + else: + encoding = 'utf-8' webpage_bytes = urlh.read() - return webpage_bytes.decode('utf-8', 'replace') + return webpage_bytes.decode(encoding, 'replace') class YoutubeIE(InfoExtractor): @@ -4090,6 +4096,40 @@ class MySpassIE(InfoExtractor): } return [info] +class SpiegelIE(InfoExtractor): + _VALID_URL = r'https?://(?:www\.)?spiegel\.de/video/[^/]*-(?P[0-9]+)(?:\.html)?$' + + def _real_extract(self, url): + m = re.match(self._VALID_URL, url) + video_id = m.group('videoID') + + webpage = self._download_webpage(url, video_id) + m = re.search(r'
(.*?)
', webpage) + if not m: + raise ExtractorError(u'Cannot find title') + video_title = unescapeHTML(m.group(1)) + + xml_url = u'http://video2.spiegel.de/flash/' + video_id + u'.xml' + xml_code = self._download_webpage(xml_url, video_id, + note=u'Downloading XML', errnote=u'Failed to download XML') + + idoc = xml.etree.ElementTree.fromstring(xml_code) + last_type = idoc[-1] + filename = last_type.findall('./filename')[0].text + duration = float(last_type.findall('./duration')[0].text) + + video_url = 'http://video2.spiegel.de/flash/' + filename + video_ext = filename.rpartition('.')[2] + info = { + 'id': video_id, + 'url': video_url, + 'ext': video_ext, + 'title': video_title, + 'duration': duration, + } + return [info] + + def gen_extractors(): """ Return a list of an instance of every supported extractor. The order does matter; the first extractor matched is the one handling the URL. @@ -4138,6 +4178,7 @@ def gen_extractors(): KeekIE(), TEDIE(), MySpassIE(), + SpiegelIE(), GenericIE() ] From c3971870616fb24c298b8f6f1bf1ec7c16c75470 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Sat, 16 Mar 2013 23:52:17 +0100 Subject: [PATCH 023/221] Spiegel: Support hash at end of URL --- youtube_dl/InfoExtractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 44b4c4376..5339bc0cd 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -4097,7 +4097,7 @@ class MySpassIE(InfoExtractor): return [info] class SpiegelIE(InfoExtractor): - _VALID_URL = r'https?://(?:www\.)?spiegel\.de/video/[^/]*-(?P[0-9]+)(?:\.html)?$' + _VALID_URL = r'https?://(?:www\.)?spiegel\.de/video/[^/]*-(?P[0-9]+)(?:\.html)?(?:#.*)$' def _real_extract(self, url): m = re.match(self._VALID_URL, url) From ae608b8076497d70e2a95e5e939c1fb31e2dde53 Mon Sep 17 00:00:00 2001 From: Ismael Mejia Date: Fri, 22 Feb 2013 02:52:55 +0100 Subject: [PATCH 024/221] Added new option '--all-srt' to download all the subtitles of a video. Only works in youtube for the moment. --- test/parameters.json | 6 ++- test/test_youtube_subtitles.py | 31 ++++++++++++--- youtube_dl/FileDownloader.py | 28 ++++++++++--- youtube_dl/InfoExtractors.py | 73 ++++++++++++++++++++++++---------- youtube_dl/__init__.py | 4 ++ 5 files changed, 107 insertions(+), 35 deletions(-) diff --git a/test/parameters.json b/test/parameters.json index 8215d25c5..0d4bd644c 100644 --- a/test/parameters.json +++ b/test/parameters.json @@ -36,5 +36,7 @@ "verbose": true, "writedescription": false, "writeinfojson": true, - "writesubtitles": false -} \ No newline at end of file + "writesubtitles": false, + "onlysubtitles": false, + "allsubtitles": false +} diff --git a/test/test_youtube_subtitles.py b/test/test_youtube_subtitles.py index 77c275b75..3b5a53fca 100644 --- a/test/test_youtube_subtitles.py +++ b/test/test_youtube_subtitles.py @@ -38,27 +38,48 @@ class FakeDownloader(object): md5 = lambda s: hashlib.md5(s.encode('utf-8')).hexdigest() class TestYoutubeSubtitles(unittest.TestCase): + def setUp(self): + DL = FakeDownloader() + DL.params['allsubtitles'] = False + DL.params['writesubtitles'] = False + + def test_youtube_no_subtitles(self): + DL = FakeDownloader() + DL.params['writesubtitles'] = False + IE = YoutubeIE(DL) + info_dict = IE.extract('QRS8MkLhQmM') + subtitles = info_dict[0]['subtitles'] + self.assertEqual(subtitles, None) def test_youtube_subtitles(self): DL = FakeDownloader() DL.params['writesubtitles'] = True IE = YoutubeIE(DL) info_dict = IE.extract('QRS8MkLhQmM') - self.assertEqual(md5(info_dict[0]['subtitles']), '4cd9278a35ba2305f47354ee13472260') - + sub = info_dict[0]['subtitles'][0] + self.assertEqual(md5(sub[2]), '4cd9278a35ba2305f47354ee13472260') def test_youtube_subtitles_it(self): DL = FakeDownloader() DL.params['writesubtitles'] = True DL.params['subtitleslang'] = 'it' IE = YoutubeIE(DL) info_dict = IE.extract('QRS8MkLhQmM') - self.assertEqual(md5(info_dict[0]['subtitles']), '164a51f16f260476a05b50fe4c2f161d') - + sub = info_dict[0]['subtitles'][0] + self.assertEqual(md5(sub[2]), '164a51f16f260476a05b50fe4c2f161d') def test_youtube_onlysubtitles(self): DL = FakeDownloader() + DL.params['writesubtitles'] = True DL.params['onlysubtitles'] = True IE = YoutubeIE(DL) info_dict = IE.extract('QRS8MkLhQmM') - self.assertEqual(md5(info_dict[0]['subtitles']), '4cd9278a35ba2305f47354ee13472260') + sub = info_dict[0]['subtitles'][0] + self.assertEqual(md5(sub[2]), '4cd9278a35ba2305f47354ee13472260') + def test_youtube_allsubtitles(self): + DL = FakeDownloader() + DL.params['allsubtitles'] = True + IE = YoutubeIE(DL) + info_dict = IE.extract('QRS8MkLhQmM') + subtitles = info_dict[0]['subtitles'] + self.assertEqual(len(subtitles), 12) if __name__ == '__main__': unittest.main() diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 487c9dadb..e496b8a8d 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -80,6 +80,7 @@ class FileDownloader(object): writeinfojson: Write the video description to a .info.json file writesubtitles: Write the video subtitles to a .srt file onlysubtitles: Downloads only the subtitles of the video + allsubtitles: Downloads all the subtitles of the video subtitleslang: Language of the subtitles to download test: Download only first bytes to test the downloader. keepvideo: Keep the video file after post-processing @@ -442,18 +443,33 @@ class FileDownloader(object): if self.params.get('writesubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']: # subtitles download errors are already managed as troubles in relevant IE # that way it will silently go on when used with unsupporting IE + subtitle = info_dict['subtitles'][0] + (srt_error, srt_lang, srt) = subtitle try: - srtfn = filename.rsplit('.', 1)[0] + u'.srt' - if self.params.get('subtitleslang', False): - srtfn = filename.rsplit('.', 1)[0] + u'.' + self.params['subtitleslang'] + u'.srt' + srtfn = filename.rsplit('.', 1)[0] + u'.' + srt_lang + u'.srt' self.report_writesubtitles(srtfn) with io.open(encodeFilename(srtfn), 'w', encoding='utf-8') as srtfile: - srtfile.write(info_dict['subtitles']) - if self.params.get('onlysubtitles', False): - return + srtfile.write(srt) except (OSError, IOError): self.trouble(u'ERROR: Cannot write subtitles file ' + descfn) return + if self.params.get('onlysubtitles', False): + return + + if self.params.get('allsubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']: + subtitles = info_dict['subtitles'] + for subtitle in subtitles: + (srt_error, srt_lang, srt) = subtitle + try: + srtfn = filename.rsplit('.', 1)[0] + u'.' + srt_lang + u'.srt' + self.report_writesubtitles(srtfn) + with io.open(encodeFilename(srtfn), 'w', encoding='utf-8') as srtfile: + srtfile.write(srt) + except (OSError, IOError): + self.trouble(u'ERROR: Cannot write subtitles file ' + descfn) + return + if self.params.get('onlysubtitles', False): + return if self.params.get('writeinfojson', False): infofn = filename + u'.info.json' diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 51b263383..a220de80a 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -216,6 +216,10 @@ class YoutubeIE(InfoExtractor): """Report attempt to download video info webpage.""" self._downloader.to_screen(u'[youtube] %s: Downloading video subtitles' % video_id) + def report_video_subtitles_request(self, video_id, lang): + """Report attempt to download video info webpage.""" + self._downloader.to_screen(u'[youtube] %s: Downloading video subtitles for lang: %s' % (video_id,lang)) + def report_information_extraction(self, video_id): """Report attempt to extract video information.""" self._downloader.to_screen(u'[youtube] %s: Extracting video information' % video_id) @@ -228,9 +232,7 @@ class YoutubeIE(InfoExtractor): """Indicate the download will use the RTMP protocol.""" self._downloader.to_screen(u'[youtube] RTMP download detected') - - def _extract_subtitles(self, video_id): - self.report_video_subtitles_download(video_id) + def _get_available_subtitles(self, video_id): request = compat_urllib_request.Request('http://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id) try: srt_list = compat_urllib_request.urlopen(request).read().decode('utf-8') @@ -240,19 +242,15 @@ class YoutubeIE(InfoExtractor): srt_lang_list = dict((l[1], l[0]) for l in srt_lang_list) if not srt_lang_list: return (u'WARNING: video has no closed captions', None) - if self._downloader.params.get('subtitleslang', False): - srt_lang = self._downloader.params.get('subtitleslang') - elif 'en' in srt_lang_list: - srt_lang = 'en' - else: - srt_lang = list(srt_lang_list.keys())[0] - if not srt_lang in srt_lang_list: - return (u'WARNING: no closed captions found in the specified language "%s"' % srt_lang, None) + return srt_lang_list + + def _request_subtitle(self, str_lang, str_name, video_id, format = 'srt'): + self.report_video_subtitles_request(video_id, str_lang) params = compat_urllib_parse.urlencode({ - 'lang': srt_lang, - 'name': srt_lang_list[srt_lang].encode('utf-8'), + 'lang': str_lang, + 'name': str_name, 'v': video_id, - 'fmt': 'srt', + 'fmt': format, }) url = 'http://www.youtube.com/api/timedtext?' + params try: @@ -261,7 +259,32 @@ class YoutubeIE(InfoExtractor): return (u'WARNING: unable to download video subtitles: %s' % compat_str(err), None) if not srt: return (u'WARNING: Did not fetch video subtitles', None) - return (None, srt) + return (None, str_lang, srt) + + def _extract_subtitle(self, video_id): + self.report_video_subtitles_download(video_id) + srt_lang_list = self._get_available_subtitles(video_id) + + if self._downloader.params.get('subtitleslang', False): + srt_lang = self._downloader.params.get('subtitleslang') + elif 'en' in srt_lang_list: + srt_lang = 'en' + else: + srt_lang = list(srt_lang_list.keys())[0] + if not srt_lang in srt_lang_list: + return (u'WARNING: no closed captions found in the specified language "%s"' % srt_lang, None) + + sub = self._request_subtitle(srt_lang, srt_lang_list[srt_lang].encode('utf-8'), video_id) + return [sub] + + def _extract_all_subtitles(self, video_id): + self.report_video_subtitles_download(video_id) + srt_lang_list = self._get_available_subtitles(video_id) + subs = [] + for srt_lang in srt_lang_list: + sub = self._request_subtitle(srt_lang, srt_lang_list[srt_lang].encode('utf-8'), video_id) + subs.append(sub) + return subs def _print_formats(self, formats): print('Available formats:') @@ -484,14 +507,20 @@ class YoutubeIE(InfoExtractor): # closed captions video_subtitles = None - if self._downloader.params.get('subtitleslang', False): - self._downloader.params['writesubtitles'] = True - if self._downloader.params.get('onlysubtitles', False): - self._downloader.params['writesubtitles'] = True + if self._downloader.params.get('writesubtitles', False): - (srt_error, video_subtitles) = self._extract_subtitles(video_id) - if srt_error: - self._downloader.trouble(srt_error) + video_subtitles = self._extract_subtitle(video_id) + if video_subtitles: + (srt_error, srt_lang, srt) = video_subtitles[0] + if srt_error: + self._downloader.trouble(srt_error) + + if self._downloader.params.get('allsubtitles', False): + video_subtitles = self._extract_all_subtitles(video_id) + for video_subtitle in video_subtitles: + (srt_error, srt_lang, srt) = video_subtitle + if srt_error: + self._downloader.trouble(srt_error) if 'length_seconds' not in video_info: self._downloader.trouble(u'WARNING: unable to extract video duration') diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index ababeac87..20a22a4d1 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -179,6 +179,9 @@ def parseOpts(): video_format.add_option('--only-srt', action='store_true', dest='onlysubtitles', help='downloads only the subtitles of the video (currently youtube only)', default=False) + video_format.add_option('--all-srt', + action='store_true', dest='allsubtitles', + help='downloads all the available subtitles of the video (currently youtube only)', default=False) video_format.add_option('--srt-lang', action='store', dest='subtitleslang', metavar='LANG', help='language of the closed captions to download (optional) use IETF language tags like \'en\'') @@ -454,6 +457,7 @@ def _real_main(): 'writeinfojson': opts.writeinfojson, 'writesubtitles': opts.writesubtitles, 'onlysubtitles': opts.onlysubtitles, + 'allsubtitles': opts.allsubtitles, 'subtitleslang': opts.subtitleslang, 'matchtitle': decodeOption(opts.matchtitle), 'rejecttitle': decodeOption(opts.rejecttitle), From 553d097442ad5ee62d227de2e2703a2377dcf40f Mon Sep 17 00:00:00 2001 From: Ismael Mejia Date: Fri, 22 Feb 2013 03:13:28 +0100 Subject: [PATCH 025/221] Refactor subtitle options from srt to the more generic 'sub'. In order to be more consistent with different subtitle formats. From: * --write-srt to --write-sub * --only-srt to --only-sub * --all-srt to --all-subs * --srt-lang to --sub-lang' Refactored also all the mentions of srt for sub in all the source code. --- youtube_dl/FileDownloader.py | 26 +++++++------- youtube_dl/InfoExtractors.py | 68 ++++++++++++++++++------------------ youtube_dl/__init__.py | 14 ++++---- 3 files changed, 54 insertions(+), 54 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index e496b8a8d..4549dd464 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -78,7 +78,7 @@ class FileDownloader(object): updatetime: Use the Last-modified header to set output file timestamps. writedescription: Write the video description to a .description file writeinfojson: Write the video description to a .info.json file - writesubtitles: Write the video subtitles to a .srt file + writesubtitles: Write the video subtitles to a file (default=srt) onlysubtitles: Downloads only the subtitles of the video allsubtitles: Downloads all the subtitles of the video subtitleslang: Language of the subtitles to download @@ -291,9 +291,9 @@ class FileDownloader(object): """ Report that the description file is being written """ self.to_screen(u'[info] Writing video description to: ' + descfn) - def report_writesubtitles(self, srtfn): + def report_writesubtitles(self, sub_filename): """ Report that the subtitles file is being written """ - self.to_screen(u'[info] Writing video subtitles to: ' + srtfn) + self.to_screen(u'[info] Writing video subtitles to: ' + sub_filename) def report_writeinfojson(self, infofn): """ Report that the metadata file has been written """ @@ -444,12 +444,12 @@ class FileDownloader(object): # subtitles download errors are already managed as troubles in relevant IE # that way it will silently go on when used with unsupporting IE subtitle = info_dict['subtitles'][0] - (srt_error, srt_lang, srt) = subtitle + (sub_error, sub_lang, sub) = subtitle try: - srtfn = filename.rsplit('.', 1)[0] + u'.' + srt_lang + u'.srt' - self.report_writesubtitles(srtfn) - with io.open(encodeFilename(srtfn), 'w', encoding='utf-8') as srtfile: - srtfile.write(srt) + sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.srt' + self.report_writesubtitles(sub_filename) + with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile: + subfile.write(sub) except (OSError, IOError): self.trouble(u'ERROR: Cannot write subtitles file ' + descfn) return @@ -459,12 +459,12 @@ class FileDownloader(object): if self.params.get('allsubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']: subtitles = info_dict['subtitles'] for subtitle in subtitles: - (srt_error, srt_lang, srt) = subtitle + (sub_error, sub_lang, sub) = subtitle try: - srtfn = filename.rsplit('.', 1)[0] + u'.' + srt_lang + u'.srt' - self.report_writesubtitles(srtfn) - with io.open(encodeFilename(srtfn), 'w', encoding='utf-8') as srtfile: - srtfile.write(srt) + sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.srt' + self.report_writesubtitles(sub_filename) + with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile: + subfile.write(sub) except (OSError, IOError): self.trouble(u'ERROR: Cannot write subtitles file ' + descfn) return diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index a220de80a..e078bb083 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -47,7 +47,7 @@ class InfoExtractor(object): uploader_id: Nickname or id of the video uploader. location: Physical location of the video. player_url: SWF Player URL (used for rtmpdump). - subtitles: The .srt file contents. + subtitles: The subtitle file contents. urlhandle: [internal] The urlHandle to be used to download the file, like returned by urllib.request.urlopen @@ -235,56 +235,56 @@ class YoutubeIE(InfoExtractor): def _get_available_subtitles(self, video_id): request = compat_urllib_request.Request('http://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id) try: - srt_list = compat_urllib_request.urlopen(request).read().decode('utf-8') + sub_list = compat_urllib_request.urlopen(request).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: return (u'WARNING: unable to download video subtitles: %s' % compat_str(err), None) - srt_lang_list = re.findall(r'name="([^"]*)"[^>]+lang_code="([\w\-]+)"', srt_list) - srt_lang_list = dict((l[1], l[0]) for l in srt_lang_list) - if not srt_lang_list: + sub_lang_list = re.findall(r'name="([^"]*)"[^>]+lang_code="([\w\-]+)"', sub_list) + sub_lang_list = dict((l[1], l[0]) for l in sub_lang_list) + if not sub_lang_list: return (u'WARNING: video has no closed captions', None) - return srt_lang_list + return sub_lang_list - def _request_subtitle(self, str_lang, str_name, video_id, format = 'srt'): - self.report_video_subtitles_request(video_id, str_lang) + def _request_subtitle(self, sub_lang, sub_name, video_id, format = 'srt'): + self.report_video_subtitles_request(video_id, sub_lang) params = compat_urllib_parse.urlencode({ - 'lang': str_lang, - 'name': str_name, + 'lang': sub_lang, + 'name': sub_name, 'v': video_id, 'fmt': format, }) url = 'http://www.youtube.com/api/timedtext?' + params try: - srt = compat_urllib_request.urlopen(url).read().decode('utf-8') + sub = compat_urllib_request.urlopen(url).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: return (u'WARNING: unable to download video subtitles: %s' % compat_str(err), None) - if not srt: + if not sub: return (u'WARNING: Did not fetch video subtitles', None) - return (None, str_lang, srt) + return (None, sub_lang, sub) def _extract_subtitle(self, video_id): self.report_video_subtitles_download(video_id) - srt_lang_list = self._get_available_subtitles(video_id) + sub_lang_list = self._get_available_subtitles(video_id) if self._downloader.params.get('subtitleslang', False): - srt_lang = self._downloader.params.get('subtitleslang') - elif 'en' in srt_lang_list: - srt_lang = 'en' + sub_lang = self._downloader.params.get('subtitleslang') + elif 'en' in sub_lang_list: + sub_lang = 'en' else: - srt_lang = list(srt_lang_list.keys())[0] - if not srt_lang in srt_lang_list: - return (u'WARNING: no closed captions found in the specified language "%s"' % srt_lang, None) + sub_lang = list(sub_lang_list.keys())[0] + if not sub_lang in sub_lang_list: + return (u'WARNING: no closed captions found in the specified language "%s"' % sub_lang, None) - sub = self._request_subtitle(srt_lang, srt_lang_list[srt_lang].encode('utf-8'), video_id) - return [sub] + subtitle = self._request_subtitle(sub_lang, sub_lang_list[sub_lang].encode('utf-8'), video_id) + return [subtitle] def _extract_all_subtitles(self, video_id): self.report_video_subtitles_download(video_id) - srt_lang_list = self._get_available_subtitles(video_id) - subs = [] - for srt_lang in srt_lang_list: - sub = self._request_subtitle(srt_lang, srt_lang_list[srt_lang].encode('utf-8'), video_id) - subs.append(sub) - return subs + sub_lang_list = self._get_available_subtitles(video_id) + subtitles = [] + for sub_lang in sub_lang_list: + subtitle = self._request_subtitle(sub_lang, sub_lang_list[sub_lang].encode('utf-8'), video_id) + subtitles.append(subtitle) + return subtitles def _print_formats(self, formats): print('Available formats:') @@ -511,16 +511,16 @@ class YoutubeIE(InfoExtractor): if self._downloader.params.get('writesubtitles', False): video_subtitles = self._extract_subtitle(video_id) if video_subtitles: - (srt_error, srt_lang, srt) = video_subtitles[0] - if srt_error: - self._downloader.trouble(srt_error) + (sub_error, sub_lang, sub) = video_subtitles[0] + if sub_error: + self._downloader.trouble(sub_error) if self._downloader.params.get('allsubtitles', False): video_subtitles = self._extract_all_subtitles(video_id) for video_subtitle in video_subtitles: - (srt_error, srt_lang, srt) = video_subtitle - if srt_error: - self._downloader.trouble(srt_error) + (sub_error, sub_lang, sub) = video_subtitle + if sub_error: + self._downloader.trouble(sub_error) if 'length_seconds' not in video_info: self._downloader.trouble(u'WARNING: unable to extract video duration') diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 20a22a4d1..495b5ac41 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -173,18 +173,18 @@ def parseOpts(): action='store', dest='format_limit', metavar='FORMAT', help='highest quality format to download') video_format.add_option('-F', '--list-formats', action='store_true', dest='listformats', help='list all available formats (currently youtube only)') - video_format.add_option('--write-srt', + video_format.add_option('--write-sub', action='store_true', dest='writesubtitles', - help='write video closed captions to a .srt file (currently youtube only)', default=False) - video_format.add_option('--only-srt', + help='write subtitle file (currently youtube only)', default=False) + video_format.add_option('--only-sub', action='store_true', dest='onlysubtitles', - help='downloads only the subtitles of the video (currently youtube only)', default=False) - video_format.add_option('--all-srt', + help='downloads only the subtitles (no video)', default=False) + video_format.add_option('--all-subs', action='store_true', dest='allsubtitles', help='downloads all the available subtitles of the video (currently youtube only)', default=False) - video_format.add_option('--srt-lang', + video_format.add_option('--sub-lang', action='store', dest='subtitleslang', metavar='LANG', - help='language of the closed captions to download (optional) use IETF language tags like \'en\'') + help='language of the subtitles 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) From 9e62bc443996c1950de0841997c76d110cb77c6e Mon Sep 17 00:00:00 2001 From: Ismael Mejia Date: Fri, 22 Feb 2013 03:53:54 +0100 Subject: [PATCH 026/221] Added new option '--sub-format' to choose the format of the subtitles to downloade (defaut=srt) --- test/parameters.json | 1 + test/test_youtube_subtitles.py | 10 +++++++++- youtube_dl/FileDownloader.py | 9 ++++++--- youtube_dl/InfoExtractors.py | 11 ++++++----- youtube_dl/__init__.py | 4 ++++ 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/test/parameters.json b/test/parameters.json index 0d4bd644c..750b1c96e 100644 --- a/test/parameters.json +++ b/test/parameters.json @@ -29,6 +29,7 @@ "simulate": false, "skip_download": false, "subtitleslang": null, + "subtitlesformat": "srt", "test": true, "updatetime": true, "usenetrc": false, diff --git a/test/test_youtube_subtitles.py b/test/test_youtube_subtitles.py index 3b5a53fca..94adc4555 100644 --- a/test/test_youtube_subtitles.py +++ b/test/test_youtube_subtitles.py @@ -42,7 +42,7 @@ class TestYoutubeSubtitles(unittest.TestCase): DL = FakeDownloader() DL.params['allsubtitles'] = False DL.params['writesubtitles'] = False - + DL.params['subtitlesformat'] = 'srt' def test_youtube_no_subtitles(self): DL = FakeDownloader() DL.params['writesubtitles'] = False @@ -80,6 +80,14 @@ class TestYoutubeSubtitles(unittest.TestCase): info_dict = IE.extract('QRS8MkLhQmM') subtitles = info_dict[0]['subtitles'] self.assertEqual(len(subtitles), 12) + def test_youtube_subtitles_format(self): + DL = FakeDownloader() + DL.params['writesubtitles'] = True + DL.params['subtitlesformat'] = 'sbv' + IE = YoutubeIE(DL) + info_dict = IE.extract('QRS8MkLhQmM') + sub = info_dict[0]['subtitles'][0] + self.assertEqual(md5(sub[2]), '13aeaa0c245a8bed9a451cb643e3ad8b') if __name__ == '__main__': unittest.main() diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 4549dd464..a041e1219 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -78,9 +78,10 @@ class FileDownloader(object): updatetime: Use the Last-modified header to set output file timestamps. writedescription: Write the video description to a .description file writeinfojson: Write the video description to a .info.json file - writesubtitles: Write the video subtitles to a file (default=srt) + writesubtitles: Write the video subtitles to a file onlysubtitles: Downloads only the subtitles of the video allsubtitles: Downloads all the subtitles of the video + subtitlesformat: Subtitle format [sbv/srt] (default=srt) subtitleslang: Language of the subtitles to download test: Download only first bytes to test the downloader. keepvideo: Keep the video file after post-processing @@ -445,8 +446,9 @@ class FileDownloader(object): # that way it will silently go on when used with unsupporting IE subtitle = info_dict['subtitles'][0] (sub_error, sub_lang, sub) = subtitle + sub_format = self.params.get('subtitlesformat') try: - sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.srt' + sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format self.report_writesubtitles(sub_filename) with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile: subfile.write(sub) @@ -458,10 +460,11 @@ class FileDownloader(object): if self.params.get('allsubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']: subtitles = info_dict['subtitles'] + sub_format = self.params.get('subtitlesformat') for subtitle in subtitles: (sub_error, sub_lang, sub) = subtitle try: - sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.srt' + sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format self.report_writesubtitles(sub_filename) with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile: subfile.write(sub) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index e078bb083..62522bb6c 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -244,7 +244,7 @@ class YoutubeIE(InfoExtractor): return (u'WARNING: video has no closed captions', None) return sub_lang_list - def _request_subtitle(self, sub_lang, sub_name, video_id, format = 'srt'): + def _request_subtitle(self, sub_lang, sub_name, video_id, format): self.report_video_subtitles_request(video_id, sub_lang) params = compat_urllib_parse.urlencode({ 'lang': sub_lang, @@ -264,7 +264,7 @@ class YoutubeIE(InfoExtractor): def _extract_subtitle(self, video_id): self.report_video_subtitles_download(video_id) sub_lang_list = self._get_available_subtitles(video_id) - + sub_format = self._downloader.params.get('subtitlesformat') if self._downloader.params.get('subtitleslang', False): sub_lang = self._downloader.params.get('subtitleslang') elif 'en' in sub_lang_list: @@ -274,15 +274,16 @@ class YoutubeIE(InfoExtractor): if not sub_lang in sub_lang_list: return (u'WARNING: no closed captions found in the specified language "%s"' % sub_lang, None) - subtitle = self._request_subtitle(sub_lang, sub_lang_list[sub_lang].encode('utf-8'), video_id) + subtitle = self._request_subtitle(sub_lang, sub_lang_list[sub_lang].encode('utf-8'), video_id, sub_format) return [subtitle] def _extract_all_subtitles(self, video_id): self.report_video_subtitles_download(video_id) sub_lang_list = self._get_available_subtitles(video_id) + sub_format = self._downloader.params.get('subtitlesformat') subtitles = [] for sub_lang in sub_lang_list: - subtitle = self._request_subtitle(sub_lang, sub_lang_list[sub_lang].encode('utf-8'), video_id) + subtitle = self._request_subtitle(sub_lang, sub_lang_list[sub_lang].encode('utf-8'), video_id, sub_format) subtitles.append(subtitle) return subtitles @@ -505,7 +506,7 @@ class YoutubeIE(InfoExtractor): else: video_description = '' - # closed captions + # subtitles video_subtitles = None if self._downloader.params.get('writesubtitles', False): diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 495b5ac41..914d030a3 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -182,6 +182,9 @@ def parseOpts(): video_format.add_option('--all-subs', action='store_true', dest='allsubtitles', help='downloads all the available subtitles of the video (currently youtube only)', default=False) + video_format.add_option('--sub-format', + action='store', dest='subtitlesformat', metavar='LANG', + help='subtitle format [srt/sbv] (default=srt) (currently youtube only)', default='srt') video_format.add_option('--sub-lang', action='store', dest='subtitleslang', metavar='LANG', help='language of the subtitles to download (optional) use IETF language tags like \'en\'') @@ -458,6 +461,7 @@ def _real_main(): 'writesubtitles': opts.writesubtitles, 'onlysubtitles': opts.onlysubtitles, 'allsubtitles': opts.allsubtitles, + 'subtitlesformat': opts.subtitlesformat, 'subtitleslang': opts.subtitleslang, 'matchtitle': decodeOption(opts.matchtitle), 'rejecttitle': decodeOption(opts.rejecttitle), From 2a4093eaf3af07fa0a74926ce09cb49aba73017e Mon Sep 17 00:00:00 2001 From: Ismael Mejia Date: Fri, 22 Feb 2013 04:50:05 +0100 Subject: [PATCH 027/221] Added new option '--list-subs' to show the available subtitle languages --- test/parameters.json | 3 ++- test/test_youtube_subtitles.py | 7 +++++++ youtube_dl/FileDownloader.py | 1 + youtube_dl/InfoExtractors.py | 26 +++++++++++++++++++------- youtube_dl/__init__.py | 4 ++++ 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/test/parameters.json b/test/parameters.json index 750b1c96e..96998b5c3 100644 --- a/test/parameters.json +++ b/test/parameters.json @@ -39,5 +39,6 @@ "writeinfojson": true, "writesubtitles": false, "onlysubtitles": false, - "allsubtitles": false + "allsubtitles": false, + "listssubtitles": false } diff --git a/test/test_youtube_subtitles.py b/test/test_youtube_subtitles.py index 94adc4555..30f2246dd 100644 --- a/test/test_youtube_subtitles.py +++ b/test/test_youtube_subtitles.py @@ -43,6 +43,7 @@ class TestYoutubeSubtitles(unittest.TestCase): DL.params['allsubtitles'] = False DL.params['writesubtitles'] = False DL.params['subtitlesformat'] = 'srt' + DL.params['listsubtitles'] = False def test_youtube_no_subtitles(self): DL = FakeDownloader() DL.params['writesubtitles'] = False @@ -88,6 +89,12 @@ class TestYoutubeSubtitles(unittest.TestCase): info_dict = IE.extract('QRS8MkLhQmM') sub = info_dict[0]['subtitles'][0] self.assertEqual(md5(sub[2]), '13aeaa0c245a8bed9a451cb643e3ad8b') + def test_youtube_list_subtitles(self): + DL = FakeDownloader() + DL.params['listsubtitles'] = True + IE = YoutubeIE(DL) + info_dict = IE.extract('QRS8MkLhQmM') + self.assertEqual(info_dict, None) if __name__ == '__main__': unittest.main() diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index a041e1219..164d25e54 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 file onlysubtitles: Downloads only the subtitles of the video allsubtitles: Downloads all the subtitles of the video + listsubtitles: Lists all available subtitles for the video subtitlesformat: Subtitle format [sbv/srt] (default=srt) subtitleslang: Language of the subtitles to download test: Download only first bytes to test the downloader. diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 62522bb6c..ff1fab773 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -214,11 +214,16 @@ class YoutubeIE(InfoExtractor): def report_video_subtitles_download(self, video_id): """Report attempt to download video info webpage.""" - self._downloader.to_screen(u'[youtube] %s: Downloading video subtitles' % video_id) + self._downloader.to_screen(u'[youtube] %s: Checking available subtitles' % video_id) - def report_video_subtitles_request(self, video_id, lang): + def report_video_subtitles_request(self, video_id, sub_lang, format): """Report attempt to download video info webpage.""" - self._downloader.to_screen(u'[youtube] %s: Downloading video subtitles for lang: %s' % (video_id,lang)) + self._downloader.to_screen(u'[youtube] %s: Downloading video subtitles for %s.%s' % (video_id, sub_lang, format)) + + def report_video_subtitles_available(self, video_id, sub_lang_list): + """Report available subtitles.""" + sub_lang = ",".join(list(sub_lang_list.keys())) + self._downloader.to_screen(u'[youtube] %s: Available subtitles for video: %s' % (video_id, sub_lang)) def report_information_extraction(self, video_id): """Report attempt to extract video information.""" @@ -233,6 +238,7 @@ class YoutubeIE(InfoExtractor): self._downloader.to_screen(u'[youtube] RTMP download detected') def _get_available_subtitles(self, video_id): + self.report_video_subtitles_download(video_id) request = compat_urllib_request.Request('http://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id) try: sub_list = compat_urllib_request.urlopen(request).read().decode('utf-8') @@ -241,11 +247,15 @@ class YoutubeIE(InfoExtractor): sub_lang_list = re.findall(r'name="([^"]*)"[^>]+lang_code="([\w\-]+)"', sub_list) sub_lang_list = dict((l[1], l[0]) for l in sub_lang_list) if not sub_lang_list: - return (u'WARNING: video has no closed captions', None) + return (u'WARNING: video doesn\'t have download', None) return sub_lang_list + def _list_available_subtitles(self, video_id): + sub_lang_list = self._get_available_subtitles(video_id) + self.report_video_subtitles_available(video_id, sub_lang_list) + def _request_subtitle(self, sub_lang, sub_name, video_id, format): - self.report_video_subtitles_request(video_id, sub_lang) + self.report_video_subtitles_request(video_id, sub_lang, format) params = compat_urllib_parse.urlencode({ 'lang': sub_lang, 'name': sub_name, @@ -262,7 +272,6 @@ class YoutubeIE(InfoExtractor): return (None, sub_lang, sub) def _extract_subtitle(self, video_id): - self.report_video_subtitles_download(video_id) sub_lang_list = self._get_available_subtitles(video_id) sub_format = self._downloader.params.get('subtitlesformat') if self._downloader.params.get('subtitleslang', False): @@ -278,7 +287,6 @@ class YoutubeIE(InfoExtractor): return [subtitle] def _extract_all_subtitles(self, video_id): - self.report_video_subtitles_download(video_id) sub_lang_list = self._get_available_subtitles(video_id) sub_format = self._downloader.params.get('subtitlesformat') subtitles = [] @@ -523,6 +531,10 @@ class YoutubeIE(InfoExtractor): if sub_error: self._downloader.trouble(sub_error) + if self._downloader.params.get('listsubtitles', False): + sub_lang_list = self._list_available_subtitles(video_id) + return + if 'length_seconds' not in video_info: self._downloader.trouble(u'WARNING: unable to extract video duration') video_duration = '' diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 914d030a3..e5a7469af 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -182,6 +182,9 @@ def parseOpts(): video_format.add_option('--all-subs', action='store_true', dest='allsubtitles', help='downloads all the available subtitles of the video (currently youtube only)', default=False) + video_format.add_option('--list-subs', + action='store_true', dest='listsubtitles', + help='lists all available subtitles for the video (currently youtube only)', default=False) video_format.add_option('--sub-format', action='store', dest='subtitlesformat', metavar='LANG', help='subtitle format [srt/sbv] (default=srt) (currently youtube only)', default='srt') @@ -461,6 +464,7 @@ def _real_main(): 'writesubtitles': opts.writesubtitles, 'onlysubtitles': opts.onlysubtitles, 'allsubtitles': opts.allsubtitles, + 'listsubtitles': opts.listsubtitles, 'subtitlesformat': opts.subtitlesformat, 'subtitleslang': opts.subtitleslang, 'matchtitle': decodeOption(opts.matchtitle), From c0ba10467457a58e7198b58793f3c4683b1c3ec7 Mon Sep 17 00:00:00 2001 From: Ismael Mejia Date: Sat, 23 Feb 2013 16:24:59 +0100 Subject: [PATCH 028/221] Fixed typo in error message when no subtitles were available. --- youtube_dl/InfoExtractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index ff1fab773..ab8bd2104 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -247,7 +247,7 @@ class YoutubeIE(InfoExtractor): sub_lang_list = re.findall(r'name="([^"]*)"[^>]+lang_code="([\w\-]+)"', sub_list) sub_lang_list = dict((l[1], l[0]) for l in sub_lang_list) if not sub_lang_list: - return (u'WARNING: video doesn\'t have download', None) + return (u'WARNING: video doesn\'t have subtitles', None) return sub_lang_list def _list_available_subtitles(self, video_id): From b9fc428494b22623529d364387b8693cc3cb1503 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Wed, 20 Mar 2013 11:29:07 +0100 Subject: [PATCH 029/221] add '--write-srt' and '--srt-lang' aliases for backwards compatibility --- youtube_dl/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index e5a7469af..c4f64893d 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -173,7 +173,7 @@ def parseOpts(): action='store', dest='format_limit', metavar='FORMAT', help='highest quality format to download') video_format.add_option('-F', '--list-formats', action='store_true', dest='listformats', help='list all available formats (currently youtube only)') - video_format.add_option('--write-sub', + video_format.add_option('--write-sub', '--write-srt', action='store_true', dest='writesubtitles', help='write subtitle file (currently youtube only)', default=False) video_format.add_option('--only-sub', @@ -188,7 +188,7 @@ def parseOpts(): video_format.add_option('--sub-format', action='store', dest='subtitlesformat', metavar='LANG', help='subtitle format [srt/sbv] (default=srt) (currently youtube only)', default='srt') - video_format.add_option('--sub-lang', + video_format.add_option('--sub-lang', '--srt-lang', action='store', dest='subtitleslang', metavar='LANG', help='language of the subtitles to download (optional) use IETF language tags like \'en\'') From f10b2a9c14db686e7f9b7d050f41b26d5cc35e01 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Wed, 20 Mar 2013 12:13:52 +0100 Subject: [PATCH 030/221] fix KeekIE --- youtube_dl/InfoExtractors.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 14fd644a2..835428f32 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3986,11 +3986,11 @@ class KeekIE(InfoExtractor): webpage = self._download_webpage(url, video_id) m = re.search(r'[\s\n]+

(?P\w+)

', webpage) - uploader = unescapeHTML(m.group('uploader')) + m = re.search(r'
[\S\s]+?

(?P.+?)

', webpage) + uploader = clean_html(m.group('uploader')) info = { - 'id':video_id, - 'url':video_url, + 'id': video_id, + 'url': video_url, 'ext': 'mp4', 'title': title, 'thumbnail': thumbnail, From 5011cded16d15bb03c2f172ddae81499d764e28a Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 24 Mar 2013 02:24:07 +0100 Subject: [PATCH 031/221] SoundcloudSetIE info extractor for soundcloud sets --- youtube_dl/InfoExtractors.py | 82 ++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 835428f32..87a926068 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -2802,6 +2802,87 @@ class SoundcloudIE(InfoExtractor): 'description': info['description'], }] +class SoundcloudSetIE(InfoExtractor): + """Information extractor for soundcloud.com sets + To access the media, the uid of the song and a stream token + must be extracted from the page source and the script must make + a request to media.soundcloud.com/crossdomain.xml. Then + the media can be grabbed by requesting from an url composed + of the stream token and uid + """ + + _VALID_URL = r'^(?:https?://)?(?:www\.)?soundcloud\.com/([\w\d-]+)/sets/([\w\d-]+)' + IE_NAME = u'soundcloud' + + def __init__(self, downloader=None): + InfoExtractor.__init__(self, downloader) + + def report_resolve(self, video_id): + """Report information extraction.""" + self._downloader.to_screen(u'[%s] %s: Resolving id' % (self.IE_NAME, video_id)) + + def report_extraction(self, video_id): + """Report information extraction.""" + self._downloader.to_screen(u'[%s] %s: Retrieving stream' % (self.IE_NAME, video_id)) + + def _real_extract(self, url): + mobj = re.match(self._VALID_URL, url) + if mobj is None: + self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + return + + # extract uploader (which is in the url) + uploader = mobj.group(1) + # extract simple title (uploader + slug of song title) + slug_title = mobj.group(2) + simple_title = uploader + u'-' + slug_title + + self.report_resolve('%s/sets/%s' % (uploader, slug_title)) + + url = 'http://soundcloud.com/%s/sets/%s' % (uploader, slug_title) + resolv_url = 'http://api.soundcloud.com/resolve.json?url=' + url + '&client_id=b45b1aa10f1ac2941910a7f0d10f8e28' + request = compat_urllib_request.Request(resolv_url) + try: + info_json_bytes = compat_urllib_request.urlopen(request).read() + info_json = info_json_bytes.decode('utf-8') + except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: + self._downloader.trouble(u'ERROR: unable to download video webpage: %s' % compat_str(err)) + return + + videos = [] + info = json.loads(info_json) + if 'errors' in info: + for err in info['errors']: + self._downloader.trouble(u'ERROR: unable to download video webpage: %s' % compat_str(err['error_message'])) + return + + for track in info['tracks']: + video_id = track['id'] + self.report_extraction('%s/sets/%s' % (uploader, slug_title)) + + streams_url = 'https://api.sndcdn.com/i1/tracks/' + str(video_id) + '/streams?client_id=b45b1aa10f1ac2941910a7f0d10f8e28' + request = compat_urllib_request.Request(streams_url) + try: + stream_json_bytes = compat_urllib_request.urlopen(request).read() + stream_json = stream_json_bytes.decode('utf-8') + except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: + self._downloader.trouble(u'ERROR: unable to download stream definitions: %s' % compat_str(err)) + return + + streams = json.loads(stream_json) + mediaURL = streams['http_mp3_128_url'] + + videos.append({ + 'id': video_id, + 'url': mediaURL, + 'uploader': track['user']['username'], + 'upload_date': track['created_at'], + 'title': track['title'], + 'ext': u'mp3', + 'description': track['description'], + }) + return videos + class InfoQIE(InfoExtractor): """Information extractor for infoq.com""" @@ -4187,6 +4268,7 @@ def gen_extractors(): EscapistIE(), CollegeHumorIE(), XVideosIE(), + SoundcloudSetIE(), SoundcloudIE(), InfoQIE(), MixcloudIE(), From db74c11d2b8ceea7ba04ef9cc3086d0209de10d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Tue, 26 Mar 2013 18:13:52 +0100 Subject: [PATCH 032/221] Add an Atom feed generator in devscripts --- devscripts/gh-pages/update-feed.py | 83 ++++++++++++++++++++++++++++++ devscripts/release.sh | 1 + 2 files changed, 84 insertions(+) create mode 100755 devscripts/gh-pages/update-feed.py diff --git a/devscripts/gh-pages/update-feed.py b/devscripts/gh-pages/update-feed.py new file mode 100755 index 000000000..7158d7a0b --- /dev/null +++ b/devscripts/gh-pages/update-feed.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +import sys + +import xml.etree.ElementTree as ET +import xml.dom.minidom as minidom + +import datetime + +if len(sys.argv) <= 1: + print('Specify the version number as parameter') + sys.exit() +version = sys.argv[1] + +out_file = "atom.atom" +in_file = out_file + +now = datetime.datetime.now() +now_iso = now.isoformat() + +atom_url = "http://www.w3.org/2005/Atom" + +#Some utilities functions +def atom_tag(tag): + #Return a tag in the atom namespace + return "{{{}}}{}".format(atom_url,tag) + +def atom_SubElement(parent,tag): + return ET.SubElement(parent,atom_tag(tag)) + +class YDLUpdateAtomEntry(object): + def __init__(self,parent,title,id ,link, downloads_link): + self.entry = entry = atom_SubElement(parent, "entry") + #We set the values: + atom_id = atom_SubElement(entry, "id") + atom_id.text = id + atom_title = atom_SubElement(entry, "title") + atom_title.text = title + atom_link = atom_SubElement(entry, "link") + atom_link.set("href", link) + atom_content = atom_SubElement(entry, "content") + atom_content.set("type", "xhtml") + #Here we go: + div = ET.SubElement(atom_content,"div") + div.set("xmlns", "http://www.w3.org/1999/xhtml") + div.text = "Downloads available at " + a = ET.SubElement(div, "a") + a.set("href", downloads_link) + a.text = downloads_link + + #Author info + atom_author = atom_SubElement(entry, "author") + author_name = atom_SubElement(atom_author, "name") + author_name.text = "The youtube-dl maintainers" + #If someone wants to put an email adress: + #author_email = atom_SubElement(atom_author, "email") + #author_email.text = the_email + + atom_updated = atom_SubElement(entry,"updated") + up = parent.find(atom_tag("updated")) + atom_updated.text = up.text = now_iso + + @classmethod + def entry(cls,parent, version): + update_id = "youtube-dl-{}".format(version) + update_title = "New version {}".format(version) + downloads_link = "http://youtube-dl.org/downloads/{}/".format(version) + #There's probably no better link + link = "http://rg3.github.com/youtube-dl" + return cls(parent, update_title, update_id, link, downloads_link) + + +atom = ET.parse(in_file) + +root = atom.getroot() + +#Otherwise when saving all tags will be prefixed with a 'ns0:' +ET.register_namespace("atom",atom_url) + +update_entry = YDLUpdateAtomEntry.entry(root, version) + +#Find some way of pretty printing +atom.write(out_file,encoding="utf-8",xml_declaration=True) diff --git a/devscripts/release.sh b/devscripts/release.sh index ee650f221..6e89d55b3 100755 --- a/devscripts/release.sh +++ b/devscripts/release.sh @@ -69,6 +69,7 @@ ROOT=$(pwd) ORIGIN_URL=$(git config --get remote.origin.url) cd build/gh-pages "$ROOT/devscripts/gh-pages/add-version.py" $version + "$ROOT/devscripts/gh-pages/update-feed.py" $version "$ROOT/devscripts/gh-pages/sign-versions.py" < "$ROOT/updates_key.pem" "$ROOT/devscripts/gh-pages/generate-download.py" "$ROOT/devscripts/gh-pages/update-copyright.py" From 1ee97784052d9f57ec618164a2a4c502186d93b2 Mon Sep 17 00:00:00 2001 From: Chirantan Ekbote Date: Wed, 27 Mar 2013 15:57:11 -0400 Subject: [PATCH 033/221] Use sys.stdout.buffer instead of sys.stdout sys.stdout defaults to text mode, we need to use the underlying buffer instead when writing binary data. Signed-off-by: Chirantan Ekbote --- youtube_dl/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/utils.py b/youtube_dl/utils.py index 95bd94843..901b5b5ad 100644 --- a/youtube_dl/utils.py +++ b/youtube_dl/utils.py @@ -329,7 +329,7 @@ def sanitize_open(filename, open_mode): if sys.platform == 'win32': import msvcrt msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) - return (sys.stdout, filename) + return (sys.stdout.buffer, filename) stream = open(encodeFilename(filename), open_mode) return (stream, filename) except (IOError, OSError) as err: From 898280a056b577c64005647cae68caf8f16ca059 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Thu, 28 Mar 2013 13:13:03 +0100 Subject: [PATCH 034/221] use sys.stdout.buffer only on Python3 --- youtube_dl/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/utils.py b/youtube_dl/utils.py index 901b5b5ad..49af7d7c0 100644 --- a/youtube_dl/utils.py +++ b/youtube_dl/utils.py @@ -329,7 +329,7 @@ def sanitize_open(filename, open_mode): if sys.platform == 'win32': import msvcrt msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) - return (sys.stdout.buffer, filename) + return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename) stream = open(encodeFilename(filename), open_mode) return (stream, filename) except (IOError, OSError) as err: From d2c690828a8297c014d8053fbdee4e26fe11586a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Thu, 28 Mar 2013 13:39:00 +0100 Subject: [PATCH 035/221] Add title and id to playlist results Not all IE give both. They are not used yet. --- youtube_dl/InfoExtractors.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index dd4a776e4..6053d14ec 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -147,10 +147,14 @@ class InfoExtractor(object): video_info = {'_type': 'url', 'url': url} return video_info - def playlist_result(self, entries): + def playlist_result(self, entries, playlist_id=None, playlist_title=None): """Returns a playlist""" video_info = {'_type': 'playlist', 'entries': entries} + if playlist_id: + video_info['id'] = playlist_id + if playlist_title: + video_info['title'] = playlist_title return video_info @@ -1808,7 +1812,7 @@ class YoutubePlaylistIE(InfoExtractor): self._downloader.to_screen(u'[youtube] PL %s: Found %i videos, downloading %i' % (playlist_id, total, len(videos))) url_results = [self.url_result(url) for url in videos] - return [self.playlist_result(url_results)] + return [self.playlist_result(url_results, playlist_id)] class YoutubeChannelIE(InfoExtractor): @@ -1860,7 +1864,7 @@ class YoutubeChannelIE(InfoExtractor): urls = ['http://www.youtube.com/watch?v=%s' % id for id in video_ids] url_entries = [self.url_result(url) for url in urls] - return [self.playlist_result(url_entries)] + return [self.playlist_result(url_entries, channel_id)] class YoutubeUserIE(InfoExtractor): @@ -1944,7 +1948,7 @@ class YoutubeUserIE(InfoExtractor): urls = ['http://www.youtube.com/watch?v=%s' % video_id for video_id in video_ids] url_results = [self.url_result(url) for url in urls] - return [self.playlist_result(url_results)] + return [self.playlist_result(url_results, playlist_title = username)] class BlipTVUserIE(InfoExtractor): @@ -2036,7 +2040,7 @@ class BlipTVUserIE(InfoExtractor): urls = [u'http://blip.tv/%s' % video_id for video_id in video_ids] url_entries = [self.url_result(url) for url in urls] - return [self.playlist_result(url_entries)] + return [self.playlist_result(url_entries, playlist_title = username)] class DepositFilesIE(InfoExtractor): From a91556fd74adf8ccfa4f923e21a0150e97d38bde Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Fri, 29 Mar 2013 00:19:58 +0100 Subject: [PATCH 036/221] Add a note on MaxDownloadsReached (#732, thanks to CBGoodBuddy) --- youtube_dl/FileDownloader.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 725d4a016..96130152d 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -548,6 +548,9 @@ class FileDownloader(object): except ExtractorError as de: # An error we somewhat expected self.trouble(u'ERROR: ' + compat_str(de), de.format_traceback()) break + except MaxDownloadsReached: + self.to_screen(u'[info] Maximum number of downloaded files reached.') + raise except Exception as e: if self.params.get('ignoreerrors', False): self.trouble(u'ERROR: ' + compat_str(e), tb=compat_str(traceback.format_exc())) From 44e939514ebb37f002bc9a2663e8669c3a201da8 Mon Sep 17 00:00:00 2001 From: Johny Mo Swag Date: Thu, 28 Mar 2013 20:05:28 -0700 Subject: [PATCH 037/221] Added test for WorldStarHipHop --- test/tests.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/tests.json b/test/tests.json index 7af3c2892..4190c5387 100644 --- a/test/tests.json +++ b/test/tests.json @@ -293,5 +293,14 @@ "info_dict": { "title": "Absolute Mehrheit vom 17.02.2013 - Die Highlights, Teil 2" } + }, + { + "name": "WorldStarHipHop", + "url": "http://www.worldstarhiphop.com/videos/video.php?v=wshh6a7q1ny0G34ZwuIO", + "file": "wshh6a7q1ny0G34ZwuIO.mp4", + "md5": "9d04de741161603bf7071bbf4e883186", + "info_dict": { + "title": "Video: KO Of The Week: MMA Fighter Gets Knocked Out By Swift Head Kick! " + } } ] From 7eab8dc7504cf1f5f1dd03eb62e266ce24948b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Fri, 29 Mar 2013 12:32:42 +0100 Subject: [PATCH 038/221] Pass the playlist info_dict to process_info the playlist value can be used in the output template --- README.md | 1 + youtube_dl/FileDownloader.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c09d0c0d..1f3422ef8 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ The `-o` option allows users to indicate a template for the output file names. T - `ext`: The sequence will be replaced by the appropriate extension (like flv or mp4). - `epoch`: The sequence will be replaced by the Unix epoch when creating the file. - `autonumber`: The sequence will be replaced by a five-digit number that will be increased with each download, starting at zero. + - `playlist`: The name or the id of the playlist that contains the video. The current default template is `%(id)s.%(ext)s`, but that will be switchted to `%(title)s-%(id)s.%(ext)s` (which can be requested with `-t` at the moment). diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 6af2acbee..d2b9be9ef 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -460,12 +460,21 @@ class FileDownloader(object): elif result_type == 'playlist': #We process each entry in the playlist entries_result = self.process_ie_results(result['entries'], ie) - results.extend(entries_result) + result['entries'] = entries_result + results.extend([result]) return results def process_info(self, info_dict): """Process a single dictionary returned by an InfoExtractor.""" + if info_dict.get('_type','video') == 'playlist': + playlist = info_dict.get('title', None) or info_dict.get('id', None) + self.to_screen(u'[download] Downloading playlist: %s' % playlist) + for video in info_dict['entries']: + video['playlist'] = playlist + self.process_info(video) + return + # Keep for backwards compatibility info_dict['stitle'] = info_dict['title'] From 43113d92cc89cb6c9ff98a1b45512a92c71abb23 Mon Sep 17 00:00:00 2001 From: kkalpakloglou Date: Tue, 26 Mar 2013 22:37:08 +0200 Subject: [PATCH 039/221] Update InfoExtractors.py --- youtube_dl/InfoExtractors.py | 43 ++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 8e164760b..eb1f32480 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -4160,6 +4160,46 @@ class SpiegelIE(InfoExtractor): } return [info] +class liveleakIE(InfoExtractor): + + _VALID_URL = r'^(?:http?://)?(?:\w+\.)?liveleak\.com/view\?(?:.*?)i=(?P\d+)(?:.*)' + IE_NAME = u'liveleak' + + def _real_extract(self, url): + mobj = re.match(self._VALID_URL, url) + if mobj is None: + self._downloader.trouble(u'ERROR: invalid URL: %s' % url) + return + + video_id = mobj.group(1) + if video_id.endswith('/index.html'): + video_id = video_id[:-len('/index.html')] + + webpage = self._download_webpage(url, video_id) + + video_url = u'http://edge.liveleak.com/80281E/u/u/ll2_player_files/mp55/player.swf?config=http://www.liveleak.com/player?a=config%26item_token=' + video_id + m = re.search(r' Date: Fri, 29 Mar 2013 15:13:24 +0100 Subject: [PATCH 040/221] Rebased, fixed and extended LiveLeak.com support close #757 - close #761 --- test/tests.json | 11 +++++++++++ youtube_dl/InfoExtractors.py | 27 ++++++++++++++++++--------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/test/tests.json b/test/tests.json index fd9d33332..929d454ff 100644 --- a/test/tests.json +++ b/test/tests.json @@ -308,5 +308,16 @@ "info_dict": { "title": "Vulkanausbruch in Ecuador: Der \"Feuerschlund\" ist wieder aktiv" } + }, + { + "name": "LiveLeak", + "md5": "0813c2430bea7a46bf13acf3406992f4", + "url": "http://www.liveleak.com/view?i=757_1364311680", + "file": "757_1364311680.mp4", + "info_dict": { + "title": "Most unlucky car accident", + "description": "extremely bad day for this guy..!", + "uploader": "ljfriel2" + } } ] diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index eb1f32480..45a23989a 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -4160,9 +4160,9 @@ class SpiegelIE(InfoExtractor): } return [info] -class liveleakIE(InfoExtractor): +class LiveLeakIE(InfoExtractor): - _VALID_URL = r'^(?:http?://)?(?:\w+\.)?liveleak\.com/view\?(?:.*?)i=(?P\d+)(?:.*)' + _VALID_URL = r'^(?:http?://)?(?:\w+\.)?liveleak\.com/view\?(?:.*?)i=(?P[\w_]+)(?:.*)' IE_NAME = u'liveleak' def _real_extract(self, url): @@ -4171,17 +4171,20 @@ class liveleakIE(InfoExtractor): self._downloader.trouble(u'ERROR: invalid URL: %s' % url) return - video_id = mobj.group(1) - if video_id.endswith('/index.html'): - video_id = video_id[:-len('/index.html')] + video_id = mobj.group('video_id') webpage = self._download_webpage(url, video_id) - video_url = u'http://edge.liveleak.com/80281E/u/u/ll2_player_files/mp55/player.swf?config=http://www.liveleak.com/player?a=config%26item_token=' + video_id + m = re.search(r'file: "(.*?)",', webpage) + if not m: + self._downloader.report_error(u'unable to find video url') + return + video_url = m.group(1) + m = re.search(r'', webpage) + if m: + uploader = clean_html(m.group(1)) + else: + uploader = None info = { 'id': video_id, 'url': video_url, 'ext': 'mp4', 'title': title, - 'description': desc + 'description': desc, + 'uploader': uploader } return [info] @@ -4250,6 +4259,6 @@ def gen_extractors(): TEDIE(), MySpassIE(), SpiegelIE(), - liveleakIE(), + LiveLeakIE(), GenericIE() ] From 1f46c152628bdd6b97212ced758b9f83063b5820 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Fri, 29 Mar 2013 15:31:38 +0100 Subject: [PATCH 041/221] fix SpiegelIE --- youtube_dl/InfoExtractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 45a23989a..83cb32196 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -4128,7 +4128,7 @@ class MySpassIE(InfoExtractor): return [info] class SpiegelIE(InfoExtractor): - _VALID_URL = r'https?://(?:www\.)?spiegel\.de/video/[^/]*-(?P[0-9]+)(?:\.html)?(?:#.*)$' + _VALID_URL = r'https?://(?:www\.)?spiegel\.de/video/[^/]*-(?P[0-9]+)(?:\.html)?(?:#.*)?$' def _real_extract(self, url): m = re.match(self._VALID_URL, url) From 7decf8951cd500acc6ed7c9ad049996957e26d73 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Fri, 29 Mar 2013 15:59:13 +0100 Subject: [PATCH 042/221] fix FunnyOrDieIE, MyVideoIE, TEDIE --- youtube_dl/InfoExtractors.py | 8 ++++---- youtube_dl/utils.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 83cb32196..b3c3dbb43 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -2305,7 +2305,7 @@ class MyVideoIE(InfoExtractor): webpage = self._download_webpage(webpage_url, video_id) self.report_extraction(video_id) - mobj = re.search(r'', + mobj = re.search(r'\s+(?P.*?)</a>", webpage) + m = re.search(r"<h1 class='player_page_h1'.*?>(?P<title>.*?)</h1>", webpage, flags=re.DOTALL) if not m: self._downloader.trouble(u'Cannot find video title') - title = unescapeHTML(m.group('title')) + title = clean_html(m.group('title')) m = re.search(r'<meta property="og:description" content="(?P<desc>.*?)"', webpage) if m: @@ -4051,7 +4051,7 @@ class TEDIE(InfoExtractor): videoName=m.group('name') webpage=self._download_webpage(url, video_id, 'Downloading \"%s\" page' % videoName) # If the url includes the language we get the title translated - title_RE=r'<h1><span id="altHeadline" >(?P<title>.*)</span></h1>' + title_RE=r'<span id="altHeadline" >(?P<title>.*)</span>' title=re.search(title_RE, webpage).group('title') info_RE=r'''<script\ type="text/javascript">var\ talkDetails\ =(.*?) "id":(?P<videoID>[\d]+).*? diff --git a/youtube_dl/utils.py b/youtube_dl/utils.py index 49af7d7c0..d366c4173 100644 --- a/youtube_dl/utils.py +++ b/youtube_dl/utils.py @@ -311,7 +311,7 @@ def clean_html(html): html = re.sub('<.*?>', '', html) # Replace html entities html = unescapeHTML(html) - return html + return html.strip() def sanitize_open(filename, open_mode): From 6060788083df366dae1cd75d4c1eac8e46918765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Fri, 29 Mar 2013 19:42:33 +0100 Subject: [PATCH 043/221] Write a new feed each time, reading from versions.json --- devscripts/gh-pages/update-feed.py | 110 +++++++++++------------------ devscripts/release.sh | 2 +- 2 files changed, 43 insertions(+), 69 deletions(-) diff --git a/devscripts/gh-pages/update-feed.py b/devscripts/gh-pages/update-feed.py index 7158d7a0b..f8b9fb594 100755 --- a/devscripts/gh-pages/update-feed.py +++ b/devscripts/gh-pages/update-feed.py @@ -1,83 +1,57 @@ #!/usr/bin/env python3 -import sys - -import xml.etree.ElementTree as ET -import xml.dom.minidom as minidom - import datetime -if len(sys.argv) <= 1: - print('Specify the version number as parameter') - sys.exit() -version = sys.argv[1] +import textwrap -out_file = "atom.atom" -in_file = out_file +import json + +atom_template=textwrap.dedent("""\ + <?xml version='1.0' encoding='utf-8'?> + <atom:feed xmlns:atom="http://www.w3.org/2005/Atom"> + <atom:subtitle>Updates feed.</atom:subtitle> + <atom:id>youtube-dl-updates-feed</atom:id> + <atom:updated>@TIMESTAMP@</atom:updated> + @ENTRIES@ + </atom:feed>""") + +entry_template=textwrap.dedent(""" + <atom:entry> + <atom:id>youtube-dl-@VERSION@</atom:id> + <atom:title>New version @VERSION@</atom:title> + <atom:link href="http://rg3.github.com/youtube-dl" /> + <atom:content type="xhtml"> + <div xmlns="http://www.w3.org/1999/xhtml"> + Downloads available at <a href="http://youtube-dl.org/downloads/@VERSION@/">http://youtube-dl.org/downloads/@VERSION@/</a> + </div> + </atom:content> + <atom:author> + <atom:name>The youtube-dl maintainers</atom:name> + </atom:author> + <atom:updated>@TIMESTAMP@</atom:updated> + </atom:entry> + """) now = datetime.datetime.now() now_iso = now.isoformat() -atom_url = "http://www.w3.org/2005/Atom" +atom_template = atom_template.replace('@TIMESTAMP@',now_iso) -#Some utilities functions -def atom_tag(tag): - #Return a tag in the atom namespace - return "{{{}}}{}".format(atom_url,tag) - -def atom_SubElement(parent,tag): - return ET.SubElement(parent,atom_tag(tag)) - -class YDLUpdateAtomEntry(object): - def __init__(self,parent,title,id ,link, downloads_link): - self.entry = entry = atom_SubElement(parent, "entry") - #We set the values: - atom_id = atom_SubElement(entry, "id") - atom_id.text = id - atom_title = atom_SubElement(entry, "title") - atom_title.text = title - atom_link = atom_SubElement(entry, "link") - atom_link.set("href", link) - atom_content = atom_SubElement(entry, "content") - atom_content.set("type", "xhtml") - #Here we go: - div = ET.SubElement(atom_content,"div") - div.set("xmlns", "http://www.w3.org/1999/xhtml") - div.text = "Downloads available at " - a = ET.SubElement(div, "a") - a.set("href", downloads_link) - a.text = downloads_link - - #Author info - atom_author = atom_SubElement(entry, "author") - author_name = atom_SubElement(atom_author, "name") - author_name.text = "The youtube-dl maintainers" - #If someone wants to put an email adress: - #author_email = atom_SubElement(atom_author, "email") - #author_email.text = the_email - - atom_updated = atom_SubElement(entry,"updated") - up = parent.find(atom_tag("updated")) - atom_updated.text = up.text = now_iso - - @classmethod - def entry(cls,parent, version): - update_id = "youtube-dl-{}".format(version) - update_title = "New version {}".format(version) - downloads_link = "http://youtube-dl.org/downloads/{}/".format(version) - #There's probably no better link - link = "http://rg3.github.com/youtube-dl" - return cls(parent, update_title, update_id, link, downloads_link) - +entries=[] -atom = ET.parse(in_file) +versions_info = json.load(open('update/versions.json')) +versions = list(versions_info['versions'].keys()) +versions.sort() -root = atom.getroot() +for v in versions: + entry = entry_template.replace('@TIMESTAMP@',v.replace('.','-')) + entry = entry.replace('@VERSION@',v) + entries.append(entry) -#Otherwise when saving all tags will be prefixed with a 'ns0:' -ET.register_namespace("atom",atom_url) +entries_str = textwrap.indent(''.join(entries), '\t') +atom_template = atom_template.replace('@ENTRIES@', entries_str) + +with open('update/atom.atom','w',encoding='utf-8') as atom_file: + atom_file.write(atom_template) -update_entry = YDLUpdateAtomEntry.entry(root, version) -#Find some way of pretty printing -atom.write(out_file,encoding="utf-8",xml_declaration=True) diff --git a/devscripts/release.sh b/devscripts/release.sh index 6e89d55b3..b2a91f817 100755 --- a/devscripts/release.sh +++ b/devscripts/release.sh @@ -69,7 +69,7 @@ ROOT=$(pwd) ORIGIN_URL=$(git config --get remote.origin.url) cd build/gh-pages "$ROOT/devscripts/gh-pages/add-version.py" $version - "$ROOT/devscripts/gh-pages/update-feed.py" $version + "$ROOT/devscripts/gh-pages/update-feed.py" "$ROOT/devscripts/gh-pages/sign-versions.py" < "$ROOT/updates_key.pem" "$ROOT/devscripts/gh-pages/generate-download.py" "$ROOT/devscripts/gh-pages/update-copyright.py" From 1bf2801e6a6b76976de6651478893ea1619cf869 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Fri, 29 Mar 2013 21:22:57 +0100 Subject: [PATCH 044/221] release 2013.03.29 --- README.md | 14 ++++++++++---- youtube_dl/version.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7c09d0c0d..338b6133f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ which means you can modify it, redistribute it or use it however you like. --version print program version and exit -U, --update update this program to latest version -i, --ignore-errors continue on download errors - -r, --rate-limit LIMIT download rate limit (e.g. 50k or 44.6m) + -r, --rate-limit LIMIT maximum download rate (e.g. 50k or 44.6m) -R, --retries RETRIES number of retries (default is 10) --buffer-size SIZE size of download buffer (e.g. 1024 or 16k) (default is 1024) @@ -97,10 +97,16 @@ which means you can modify it, redistribute it or use it however you like. requested --max-quality FORMAT highest quality format to download -F, --list-formats list all available formats (currently youtube only) - --write-srt write video closed captions to a .srt file + --write-sub write subtitle file (currently youtube only) + --only-sub downloads only the subtitles (no video) + --all-subs downloads all the available subtitles of the video (currently youtube only) - --srt-lang LANG language of the closed captions to download - (optional) use IETF language tags like 'en' + --list-subs lists all available subtitles for the video + (currently youtube only) + --sub-format LANG subtitle format [srt/sbv] (default=srt) (currently + youtube only) + --sub-lang LANG language of the subtitles to download (optional) + use IETF language tags like 'en' ## Authentication Options: -u, --username USERNAME account username diff --git a/youtube_dl/version.py b/youtube_dl/version.py index ce8f6ca23..cb2270001 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.02.25' +__version__ = '2013.03.29' From c238be3e3a5f9511f7ec43f6244e109113490a0a Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Fri, 29 Mar 2013 21:41:20 +0100 Subject: [PATCH 045/221] Correct feed title --- devscripts/gh-pages/update-feed.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devscripts/gh-pages/update-feed.py b/devscripts/gh-pages/update-feed.py index f8b9fb594..79e94a098 100755 --- a/devscripts/gh-pages/update-feed.py +++ b/devscripts/gh-pages/update-feed.py @@ -8,8 +8,8 @@ import json atom_template=textwrap.dedent("""\ <?xml version='1.0' encoding='utf-8'?> - <atom:feed xmlns:atom="http://www.w3.org/2005/Atom"> - <atom:subtitle>Updates feed.</atom:subtitle> + <atom:feed xmlns:atom="http://www.w3.org/2005/Atom"> + <atom:title>youtube-dl releases</atom:title> <atom:id>youtube-dl-updates-feed</atom:id> <atom:updated>@TIMESTAMP@</atom:updated> @ENTRIES@ From fbbdf475b1a534389585d696db5e6c8b3fd212fb Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Fri, 29 Mar 2013 21:44:06 +0100 Subject: [PATCH 046/221] Different feed file name --- devscripts/gh-pages/update-feed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devscripts/gh-pages/update-feed.py b/devscripts/gh-pages/update-feed.py index 79e94a098..e299429c1 100755 --- a/devscripts/gh-pages/update-feed.py +++ b/devscripts/gh-pages/update-feed.py @@ -51,7 +51,7 @@ for v in versions: entries_str = textwrap.indent(''.join(entries), '\t') atom_template = atom_template.replace('@ENTRIES@', entries_str) -with open('update/atom.atom','w',encoding='utf-8') as atom_file: +with open('update/releases.atom','w',encoding='utf-8') as atom_file: atom_file.write(atom_template) From 0fb375640990d5f1038000dc7937cd6cba6dfeb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Sat, 30 Mar 2013 14:11:33 +0100 Subject: [PATCH 047/221] Fix crash when subtitles are not found --- youtube_dl/InfoExtractors.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 2881ae67c..71f57b7c9 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -282,8 +282,14 @@ class YoutubeIE(InfoExtractor): return (None, sub_lang, sub) def _extract_subtitle(self, video_id): + """ + Return a list with a tuple: + [(error_message, sub_lang, sub)] + """ sub_lang_list = self._get_available_subtitles(video_id) sub_format = self._downloader.params.get('subtitlesformat') + if isinstance(sub_lang_list,tuple): #There was some error, it didn't get the available subtitles + return [(sub_lang_list[0], None, None)] if self._downloader.params.get('subtitleslang', False): sub_lang = self._downloader.params.get('subtitleslang') elif 'en' in sub_lang_list: @@ -291,7 +297,7 @@ class YoutubeIE(InfoExtractor): else: sub_lang = list(sub_lang_list.keys())[0] if not sub_lang in sub_lang_list: - return (u'WARNING: no closed captions found in the specified language "%s"' % sub_lang, None) + return [(u'WARNING: no closed captions found in the specified language "%s"' % sub_lang, None, None)] subtitle = self._request_subtitle(sub_lang, sub_lang_list[sub_lang].encode('utf-8'), video_id, sub_format) return [subtitle] From 6a205c8876eda3b34bd3b1f1f875bbd1b4ebdcbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Sat, 30 Mar 2013 14:17:12 +0100 Subject: [PATCH 048/221] More fixes on subtitles errors handling --- youtube_dl/InfoExtractors.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 71f57b7c9..8caace3af 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -265,6 +265,10 @@ class YoutubeIE(InfoExtractor): self.report_video_subtitles_available(video_id, sub_lang_list) def _request_subtitle(self, sub_lang, sub_name, video_id, format): + """ + Return tuple: + (error_message, sub_lang, sub) + """ self.report_video_subtitles_request(video_id, sub_lang, format) params = compat_urllib_parse.urlencode({ 'lang': sub_lang, @@ -276,9 +280,9 @@ class YoutubeIE(InfoExtractor): try: sub = compat_urllib_request.urlopen(url).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - return (u'WARNING: unable to download video subtitles: %s' % compat_str(err), None) + return (u'WARNING: unable to download video subtitles: %s' % compat_str(err), None, None) if not sub: - return (u'WARNING: Did not fetch video subtitles', None) + return (u'WARNING: Did not fetch video subtitles', None, None) return (None, sub_lang, sub) def _extract_subtitle(self, video_id): From fa41fbd3189b36300a4558b722dea5857a7e4214 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda <filippo.valsorda@gmail.com> Date: Sun, 31 Mar 2013 03:02:05 +0200 Subject: [PATCH 049/221] don't catch YT user URLs in YoutubePlaylistIE (fix #754, fix #763) --- youtube_dl/InfoExtractors.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 2881ae67c..cca7e1b54 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1710,9 +1710,7 @@ class YoutubePlaylistIE(InfoExtractor): (?: (?:course|view_play_list|my_playlists|artist|playlist|watch) \? (?:.*?&)*? (?:p|a|list)= - | user/.*?/user/ | p/ - | user/.*?#[pg]/c/ ) ((?:PL|EC|UU)?[0-9A-Za-z-_]{10,}) .* @@ -3796,7 +3794,7 @@ class WorldStarHipHopIE(InfoExtractor): _title = r"""<title>(.*)""" mobj = re.search(_title, webpage_src) - + if mobj is not None: title = mobj.group(1) else: @@ -3814,7 +3812,7 @@ class WorldStarHipHopIE(InfoExtractor): if mobj is not None: title = mobj.group(1) thumbnail = None - + results = [{ 'id': video_id, 'url' : video_url, From f375d4b7de17b1ac3d8fda9d4f071e1e55be1963 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Sun, 31 Mar 2013 03:12:28 +0200 Subject: [PATCH 050/221] import all IEs when testing to resemble more closely the real env --- test/test_download.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/test_download.py b/test/test_download.py index a8de1d002..e29092c45 100644 --- a/test/test_download.py +++ b/test/test_download.py @@ -81,9 +81,8 @@ def generator(test_case): params.update(test_case.get('params', {})) fd = FileDownloader(params) - fd.add_info_extractor(ie()) - for ien in test_case.get('add_ie', []): - fd.add_info_extractor(getattr(youtube_dl.InfoExtractors, ien + 'IE')()) + for ie in youtube_dl.InfoExtractors.gen_extractors(): + fd.add_info_extractor(ie) finished_hook_called = set() def _hook(status): if status['status'] == 'finished': From 90a99c1b5e01b86c793fb9ecb80a2521d8ae0f79 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Sun, 31 Mar 2013 03:29:34 +0200 Subject: [PATCH 051/221] retry on UnavailableVideoError --- test/test_download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_download.py b/test/test_download.py index e29092c45..59a6e1498 100644 --- a/test/test_download.py +++ b/test/test_download.py @@ -102,7 +102,7 @@ def generator(test_case): if retry == RETRIES: raise # Check if the exception is not a network related one - if not err.exc_info[0] in (ZeroDivisionError, compat_urllib_error.URLError, socket.timeout): + if not err.exc_info[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError): raise print('Retrying: {0} failed tries\n\n##########\n\n'.format(retry)) From bc97f6d60ceacdaffe6a6dbfd403a08ce06229eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sun, 31 Mar 2013 12:10:12 +0200 Subject: [PATCH 052/221] Use report_error in subtitles error handling --- youtube_dl/InfoExtractors.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 8caace3af..13b1f99b5 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -253,11 +253,11 @@ class YoutubeIE(InfoExtractor): try: sub_list = compat_urllib_request.urlopen(request).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - return (u'WARNING: unable to download video subtitles: %s' % compat_str(err), None) + return (u'unable to download video subtitles: %s' % compat_str(err), None) sub_lang_list = re.findall(r'name="([^"]*)"[^>]+lang_code="([\w\-]+)"', sub_list) sub_lang_list = dict((l[1], l[0]) for l in sub_lang_list) if not sub_lang_list: - return (u'WARNING: video doesn\'t have subtitles', None) + return (u'video doesn\'t have subtitles', None) return sub_lang_list def _list_available_subtitles(self, video_id): @@ -280,9 +280,9 @@ class YoutubeIE(InfoExtractor): try: sub = compat_urllib_request.urlopen(url).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - return (u'WARNING: unable to download video subtitles: %s' % compat_str(err), None, None) + return (u'unable to download video subtitles: %s' % compat_str(err), None, None) if not sub: - return (u'WARNING: Did not fetch video subtitles', None, None) + return (u'Did not fetch video subtitles', None, None) return (None, sub_lang, sub) def _extract_subtitle(self, video_id): @@ -301,7 +301,7 @@ class YoutubeIE(InfoExtractor): else: sub_lang = list(sub_lang_list.keys())[0] if not sub_lang in sub_lang_list: - return [(u'WARNING: no closed captions found in the specified language "%s"' % sub_lang, None, None)] + return [(u'no closed captions found in the specified language "%s"' % sub_lang, None, None)] subtitle = self._request_subtitle(sub_lang, sub_lang_list[sub_lang].encode('utf-8'), video_id, sub_format) return [subtitle] @@ -542,14 +542,14 @@ class YoutubeIE(InfoExtractor): if video_subtitles: (sub_error, sub_lang, sub) = video_subtitles[0] if sub_error: - self._downloader.trouble(sub_error) + self._downloader.report_error(sub_error) if self._downloader.params.get('allsubtitles', False): video_subtitles = self._extract_all_subtitles(video_id) for video_subtitle in video_subtitles: (sub_error, sub_lang, sub) = video_subtitle if sub_error: - self._downloader.trouble(sub_error) + self._downloader.report_error(sub_error) if self._downloader.params.get('listsubtitles', False): sub_lang_list = self._list_available_subtitles(video_id) From ef767f9fd5e852940de999da4962657bca452c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sun, 31 Mar 2013 12:19:13 +0200 Subject: [PATCH 053/221] Fix crash when subtitles are not found and the option --all-subs is given --- youtube_dl/InfoExtractors.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 13b1f99b5..1bd9e25c4 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -309,6 +309,8 @@ class YoutubeIE(InfoExtractor): def _extract_all_subtitles(self, video_id): sub_lang_list = self._get_available_subtitles(video_id) sub_format = self._downloader.params.get('subtitlesformat') + if isinstance(sub_lang_list,tuple): #There was some error, it didn't get the available subtitles + return [(sub_lang_list[0], None, None)] subtitles = [] for sub_lang in sub_lang_list: subtitle = self._request_subtitle(sub_lang, sub_lang_list[sub_lang].encode('utf-8'), video_id, sub_format) From bafeed9f5dd4613c6b0597f1328968658abb7cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sun, 31 Mar 2013 12:21:35 +0200 Subject: [PATCH 054/221] Don't crash in FileDownloader if subtitles couldn't be found and errors are ignored --- youtube_dl/FileDownloader.py | 38 +++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index d82aa2d83..7c5a52be1 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -485,14 +485,17 @@ class FileDownloader(object): subtitle = info_dict['subtitles'][0] (sub_error, sub_lang, sub) = subtitle sub_format = self.params.get('subtitlesformat') - try: - sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format - self.report_writesubtitles(sub_filename) - with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile: - subfile.write(sub) - except (OSError, IOError): - self.report_error(u'Cannot write subtitles file ' + descfn) - return + if sub_error: + self.report_warning("Some error while getting the subtitles") + else: + try: + sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format + self.report_writesubtitles(sub_filename) + with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile: + subfile.write(sub) + except (OSError, IOError): + self.report_error(u'Cannot write subtitles file ' + descfn) + return if self.params.get('onlysubtitles', False): return @@ -501,14 +504,17 @@ class FileDownloader(object): sub_format = self.params.get('subtitlesformat') for subtitle in subtitles: (sub_error, sub_lang, sub) = subtitle - try: - sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format - self.report_writesubtitles(sub_filename) - with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile: - subfile.write(sub) - except (OSError, IOError): - self.trouble(u'ERROR: Cannot write subtitles file ' + descfn) - return + if sub_error: + self.report_warning("Some error while getting the subtitles") + else: + try: + sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format + self.report_writesubtitles(sub_filename) + with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile: + subfile.write(sub) + except (OSError, IOError): + self.trouble(u'ERROR: Cannot write subtitles file ' + descfn) + return if self.params.get('onlysubtitles', False): return From 37cd9f522f4da057f098b584e46e44e2b512a254 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Mon, 1 Apr 2013 23:43:20 +0200 Subject: [PATCH 055/221] Restore youtube-dl (update) binary (#770) --- youtube-dl | Bin 59506 -> 3447 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/youtube-dl b/youtube-dl index ef9f332410cdea77f0bc2f47432a1a0a29c66786..e6f05c17327ed58f8db66e6dc7d2a38380355d61 100755 GIT binary patch literal 3447 zcmbVOdvDt|5dVAr6qgpAq}5eK$&%#&DbS%Q)@^8tq+Ky2fk8qkP9-o{xN}eW#I7RUp>b+i9VlpWt%Pb>@Oy`OR?7Qy47(ZWhKwxF}!V6ga5Te z|6x&+40Di*FF&I!^1Z!5rT7Y`po*2?x{*siG3gLgZCTU<^s@U9YK)N)E?VH{KnbpM!q}63H+DyOz&TVyWW2AKp(R%Am!6e9j^z);ml){hT?Owg={WT7ZvK}zlKJxGxlql>` zmE*uqlsM~l=XsU`PtDqL+>cdpOw@*;$PbEcyfgm}PR<8bVy8@vNqogArC-HuH1ozk z>q0>yMWqKJq`wdc9sgO{D3{`v;m#i2ah|d-6&)y*=2EL|q1PRG;N9*5Mj!opFX@Zu zTios<+Q+n>#l}V2hx8lJH>1z*&OCEw_~^{1DE{|2u*Jc?(KZv-?&*z{?qvSick}@BtxRbFsn(9{{%2!&Qy=1>WQO(hX@u|fA%(Chq$%6IER!L}$%qA&M^n01YU z)+xGhO=wAHwIrwl6dI+`va0BsE-NYbkXLlJCLOYBk`hlIQuE5-nX%iz@(Oj!L{QFU zLGP_YG_C8i(^I($+!f`zNnFC@(}SOAL%vF`mv4H;BPmb8`kf?RzgzYs*5udg2MBwNR<7x8R&UGo^6L7< z^0$jW$spU?Ewac%oMmwydtt!*JWE)}BR`Ml$s$a$C}MM3u`FlvXdZ`EJ7Z80YetDERKS7vGC^@ zc(ZUGC28zM3!i?_Tg>7`l&3jkNfhM)`U~`C81Om61!pV_WBTVQau&}#j%hH@(||aV zC?N~em~ocQ*ldPgkYp_N7|Ss7G04gIc@p^Z%!_A!l;a|pLKKF<+@Zms1FVX&m!?#~ z_K;IEC%7M$m%sHL&?Fdlg|@AWtE7U3#5bo2mxhaqUY9*2*r5T0XSU;J*tO9{=g;E4i{2M$a_*Y)SN0cILrHhNY4oKT zoJqO3wKP#vt{V-Wz8*kS$=xJ@fUd7(*y5!9DwI~=t8 zw5${!K-7MH*<$DvF0b2#W5!=Jg@g3{c+I8x7#SMN|TlgB43N7I+Q8j4cF=V%Yr=w z-i#;EzU~1ZreJLonv*Cb)T{GOuNeW&u5bBAz6_n|{|B`qUpw4N8!1g1kHY$m*8h?w VgcEqv4HG~(0kRQCgGDzp`~&H2U`7A{ literal 59506 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 GxBmd(Qkxk7 From c2b293ba3021d323a3d8ccbabeb3ebb993b276aa Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Wed, 3 Apr 2013 19:43:53 +0200 Subject: [PATCH 056/221] release 2013.04.03 --- youtube_dl/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/version.py b/youtube_dl/version.py index cb2270001..c433e2eaa 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.03.29' +__version__ = '2013.04.03' From 43ff1a347d766bbaa7116f627680e8e74a8760d1 Mon Sep 17 00:00:00 2001 From: Ricardo Garcia Date: Sat, 6 Apr 2013 10:46:17 +0200 Subject: [PATCH 057/221] Change rg3.github.com to rg3.github.io almost everywhere --- devscripts/gh-pages/update-feed.py | 2 +- devscripts/transition_helper_exe/youtube-dl.py | 2 +- youtube-dl | 4 ++-- youtube_dl/update.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/devscripts/gh-pages/update-feed.py b/devscripts/gh-pages/update-feed.py index e299429c1..cfff05fc8 100755 --- a/devscripts/gh-pages/update-feed.py +++ b/devscripts/gh-pages/update-feed.py @@ -19,7 +19,7 @@ entry_template=textwrap.dedent(""" youtube-dl-@VERSION@ New version @VERSION@ - +
Downloads available at http://youtube-dl.org/downloads/@VERSION@/ diff --git a/devscripts/transition_helper_exe/youtube-dl.py b/devscripts/transition_helper_exe/youtube-dl.py index dbb4c99e1..6297dfd40 100644 --- a/devscripts/transition_helper_exe/youtube-dl.py +++ b/devscripts/transition_helper_exe/youtube-dl.py @@ -40,7 +40,7 @@ raw_input() filename = sys.argv[0] -UPDATE_URL = "http://rg3.github.com/youtube-dl/update/" +UPDATE_URL = "http://rg3.github.io/youtube-dl/update/" VERSION_URL = UPDATE_URL + 'LATEST_VERSION' JSON_URL = UPDATE_URL + 'versions.json' UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537) diff --git a/youtube-dl b/youtube-dl index e6f05c173..e3eb8774c 100755 --- a/youtube-dl +++ b/youtube-dl @@ -38,7 +38,7 @@ def rsa_verify(message, signature, key): sys.stderr.write(u'Hi! We changed distribution method and now youtube-dl needs to update itself one more time.\n') sys.stderr.write(u'This will only happen once. Simply press enter to go on. Sorry for the trouble!\n') -sys.stderr.write(u'From now on, get the binaries from http://rg3.github.com/youtube-dl/download.html, not from the git repository.\n\n') +sys.stderr.write(u'From now on, get the binaries from http://rg3.github.io/youtube-dl/download.html, not from the git repository.\n\n') try: raw_input() @@ -47,7 +47,7 @@ except NameError: # Python 3 filename = sys.argv[0] -UPDATE_URL = "http://rg3.github.com/youtube-dl/update/" +UPDATE_URL = "http://rg3.github.io/youtube-dl/update/" VERSION_URL = UPDATE_URL + 'LATEST_VERSION' JSON_URL = UPDATE_URL + 'versions.json' UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537) diff --git a/youtube_dl/update.py b/youtube_dl/update.py index b446dd94c..d6e293875 100644 --- a/youtube_dl/update.py +++ b/youtube_dl/update.py @@ -37,7 +37,7 @@ def rsa_verify(message, signature, key): def update_self(to_screen, verbose, filename): """Update the program file with the latest version from the repository""" - UPDATE_URL = "http://rg3.github.com/youtube-dl/update/" + UPDATE_URL = "http://rg3.github.io/youtube-dl/update/" VERSION_URL = UPDATE_URL + 'LATEST_VERSION' JSON_URL = UPDATE_URL + 'versions.json' UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537) From adb029ed813dd29463fa04d827f89f37457c713c Mon Sep 17 00:00:00 2001 From: Michael Walter Date: Sun, 7 Apr 2013 15:17:36 +0200 Subject: [PATCH 058/221] added --playpath/-y support to RTMP downloads (via 'play_path' entry in 'info_dict') --- youtube_dl/FileDownloader.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 7c5a52be1..e801db00a 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -629,7 +629,7 @@ class FileDownloader(object): except (IOError, OSError): self.report_warning(u'Unable to remove downloaded video file') - def _download_with_rtmpdump(self, filename, url, player_url, page_url): + def _download_with_rtmpdump(self, filename, url, player_url, page_url, play_path): self.report_destination(filename) tmpfilename = self.temp_name(filename) @@ -648,6 +648,8 @@ class FileDownloader(object): basic_args += ['-W', player_url] if page_url is not None: basic_args += ['--pageUrl', page_url] + if play_path is not None: + basic_args += ['-y', play_path] args = basic_args + [[], ['-e', '-k', '1']][self.params.get('continuedl', False)] if self.params.get('verbose', False): try: @@ -702,7 +704,8 @@ class FileDownloader(object): if url.startswith('rtmp'): return self._download_with_rtmpdump(filename, url, info_dict.get('player_url', None), - info_dict.get('page_url', None)) + info_dict.get('page_url', None), + info_dict.get('play_path', None)) tmpfilename = self.temp_name(filename) stream = None From df2dedeefba281e37bbdae5534a3360320259b22 Mon Sep 17 00:00:00 2001 From: Michael Walter Date: Sun, 7 Apr 2013 15:23:48 +0200 Subject: [PATCH 059/221] added ARD InfoExtractor (german state television) --- youtube_dl/InfoExtractors.py | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index aa8074a9e..3d8145e16 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -4356,6 +4356,42 @@ class LiveLeakIE(InfoExtractor): return [info] +class ARDIE(InfoExtractor): + IE_NAME = 'ard' + _VALID_URL = r'^(?:http?://)?mediathek\.daserste\.de/(?:.*/)(?P[^/\?]+)(?:\?.*)?' + _TITLE = r'

(?P.*)</h1>' + _MEDIA_STREAM = r'mediaCollection\.addMediaStream\((?P<media_type>\d+), (?P<quality>\d+), "(?P<rtmp_url>[^"]*)", "(?P<video_url>[^"]*)", "[^"]*"\)' + + def _real_extract(self, url): + # determine video id from url + m = re.match(self._VALID_URL, url) + video_id = m.group('video_id') + + # determine title and media streams from webpage + html = self._download_webpage(url, video_id) + title = re.search(self._TITLE, html).group('title') + streams = [m.groupdict() for m in re.finditer(self._MEDIA_STREAM, html)] + if not streams: + assert '"fsk"' in html + self._downloader.report_error(u'this video is only available after 8:00 pm') + return + + # choose default media type and highest quality for now + stream = max([s for s in streams if int(s["media_type"]) == 0], key=lambda s: int(s["quality"])) + #stream = streams[-1] + + # there's two possibilities: RTMP stream or HTTP download + info = {'id': video_id, 'title': title, 'ext': 'mp4'} + if stream['rtmp_url']: + self._downloader.to_screen(u'[%s] RTMP download detected' % self.IE_NAME) + assert stream['video_url'].startswith('mp4:') + info["url"] = stream["rtmp_url"] + info["play_path"] = stream['video_url'] + else: + assert stream["video_url"].endswith('.mp4') + info["url"] = stream["video_url"] + return [info] + def gen_extractors(): """ Return a list of an instance of every supported extractor. @@ -4409,5 +4445,6 @@ def gen_extractors(): MySpassIE(), SpiegelIE(), LiveLeakIE(), + ARDIE(), GenericIE() ] From d39919c03e45b3e8f804c23f78fae33cb4adc7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaime.marquinez.ferrandiz@gmail.com> Date: Fri, 5 Apr 2013 13:01:59 +0200 Subject: [PATCH 060/221] Add progress counter for playlists Closes #276 --- youtube_dl/FileDownloader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 2237d355d..ba3277577 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -492,8 +492,10 @@ class FileDownloader(object): if info_dict.get('_type','video') == 'playlist': playlist = info_dict.get('title', None) or info_dict.get('id', None) self.to_screen(u'[download] Downloading playlist: %s' % playlist) - for video in info_dict['entries']: + n_videos = len(info_dict['entries']) + for i,video in enumerate(info_dict['entries'],1): video['playlist'] = playlist + self.to_screen(u'[download] Downloading video #%s of %s' %(i, n_videos)) self.process_info(video) return From 146c12a2dafdb9ff0e5138aa0f9da38bddca6c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaime.marquinez.ferrandiz@gmail.com> Date: Wed, 10 Apr 2013 00:05:04 +0200 Subject: [PATCH 061/221] Change the order for extracting/downloading Now it gets a video info and directly downloads it, the it pass to the next video founded. --- youtube_dl/FileDownloader.py | 103 ++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index ba3277577..58be5caee 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -419,9 +419,10 @@ class FileDownloader(object): return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"' return None - def extract_info(self, url): + def extract_info(self, url, download = True): ''' Returns a list with a dictionary for each video we find. + If 'download', also downloads the videos. ''' suitable_found = False for ie in self._ies: @@ -440,7 +441,12 @@ class FileDownloader(object): # Extract information from URL and process it try: ie_results = ie.extract(url) - results = self.process_ie_results(ie_results, ie) + results = [] + for ie_result in ie_results: + if not 'extractor' in ie_result: + #The extractor has already been set somewhere else + ie_result['extractor'] = ie.IE_NAME + results.append(self.process_ie_result(ie_result, download)) return results except ExtractorError as de: # An error we somewhat expected self.trouble(u'ERROR: ' + compat_str(de), de.format_traceback()) @@ -453,51 +459,51 @@ class FileDownloader(object): raise if not suitable_found: self.trouble(u'ERROR: no suitable InfoExtractor: %s' % url) - def extract_info_iterable(self, urls): - ''' - Return the videos founded for the urls - ''' - results = [] - for url in urls: - results.extend(self.extract_info(url)) - return results - def process_ie_results(self, ie_results, ie): + def process_ie_result(self, ie_result, download = True): """ - Take the results of the ie and return a list of videos. - For url elements it will seartch the suitable ie and get the videos + Take the result of the ie and return a list of videos. + For url elements it will search the suitable ie and get the videos For playlist elements it will process each of the elements of the 'entries' key + + It will also download the videos if 'download'. """ - results = [] - for result in ie_results or []: - result_type = result.get('_type', 'video') #If not given we suppose it's a video, support the dafault old system - if result_type == 'video': - if not 'extractor' in result: - #The extractor has already been set somewhere else - result['extractor'] = ie.IE_NAME - results.append(result) - elif result_type == 'url': - #We get the videos pointed by the url - results.extend(self.extract_info(result['url'])) - elif result_type == 'playlist': - #We process each entry in the playlist - entries_result = self.process_ie_results(result['entries'], ie) - result['entries'] = entries_result - results.extend([result]) - return results + result_type = ie_result.get('_type', 'video') #If not given we suppose it's a video, support the dafault old system + if result_type == 'video': + if 'playlist' not in ie_result: + #It isn't part of a playlist + ie_result['playlist'] = None + if download: + #Do the download: + self.process_info(ie_result) + return ie_result + elif result_type == 'url': + #We get the video pointed by the url + result = self.extract_info(ie_result['url'], download)[0] + return result + elif result_type == 'playlist': + #We process each entry in the playlist + playlist = ie_result.get('title', None) or ie_result.get('id', None) + self.to_screen(u'[download] Downloading playlist: %s' % playlist) + n_videos = len(ie_result['entries']) + playlist_results = [] + for i,entry in enumerate(ie_result['entries'],1): + self.to_screen(u'[download] Downloading video #%s of %s' %(i, n_videos)) + entry_result = self.process_ie_result(entry, False) + entry_result['playlist'] = playlist + #We must do the download here to correctly set the 'playlist' key + if download: + self.process_info(entry_result) + playlist_results.append(entry_result) + result = ie_result.copy() + result['entries'] = playlist_results + return result def process_info(self, info_dict): """Process a single dictionary returned by an InfoExtractor.""" - if info_dict.get('_type','video') == 'playlist': - playlist = info_dict.get('title', None) or info_dict.get('id', None) - self.to_screen(u'[download] Downloading playlist: %s' % playlist) - n_videos = len(info_dict['entries']) - for i,video in enumerate(info_dict['entries'],1): - video['playlist'] = playlist - self.to_screen(u'[download] Downloading video #%s of %s' %(i, n_videos)) - self.process_info(video) - return + #We increment the download the download count here to match the previous behaviour. + self.increment_downloads() # Keep for backwards compatibility info_dict['stitle'] = info_dict['title'] @@ -633,17 +639,14 @@ class FileDownloader(object): raise SameFileError(self.params['outtmpl']) for url in url_list: - videos = self.extract_info(url) - - for video in videos or []: - try: - self.increment_downloads() - self.process_info(video) - except UnavailableVideoError: - self.trouble(u'\nERROR: unable to download video') - except MaxDownloadsReached: - self.to_screen(u'[info] Maximum number of downloaded files reached.') - raise + try: + #It also downloads the videos + videos = self.extract_info(url) + except UnavailableVideoError: + self.trouble(u'\nERROR: unable to download video') + except MaxDownloadsReached: + self.to_screen(u'[info] Maximum number of downloaded files reached.') + raise return self._download_retcode From 532d797824a1ec48480f1d10075e66a90aa53449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaime.marquinez.ferrandiz@gmail.com> Date: Wed, 10 Apr 2013 00:06:03 +0200 Subject: [PATCH 062/221] In MetacafeIE return a url if YoutubeIE should do the job --- youtube_dl/InfoExtractors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 81eaddc72..b7371365a 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -723,8 +723,7 @@ class MetacafeIE(InfoExtractor): # Check if video comes from YouTube mobj2 = re.match(r'^yt-(.*)$', video_id) if mobj2 is not None: - self._downloader.download(['http://www.youtube.com/watch?v=%s' % mobj2.group(1)]) - return + return [self.url_result('http://www.youtube.com/watch?v=%s' % mobj2.group(1))] # Retrieve video webpage to extract further information request = compat_urllib_request.Request('http://www.metacafe.com/watch/%s/' % video_id) From bce878a7c1678ac698ecd556b2c77a1e2f2306df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaime.marquinez.ferrandiz@gmail.com> Date: Wed, 10 Apr 2013 14:32:03 +0200 Subject: [PATCH 063/221] Implement the playlist/start options in FileDownloader It makes it available for all the InfoExtractors --- youtube_dl/FileDownloader.py | 21 ++++++++++++++++++--- youtube_dl/InfoExtractors.py | 34 ---------------------------------- 2 files changed, 18 insertions(+), 37 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 58be5caee..5a5141ba5 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -485,10 +485,25 @@ class FileDownloader(object): #We process each entry in the playlist playlist = ie_result.get('title', None) or ie_result.get('id', None) self.to_screen(u'[download] Downloading playlist: %s' % playlist) - n_videos = len(ie_result['entries']) + playlist_results = [] - for i,entry in enumerate(ie_result['entries'],1): - self.to_screen(u'[download] Downloading video #%s of %s' %(i, n_videos)) + + n_all_entries = len(ie_result['entries']) + playliststart = self.params.get('playliststart', 1) - 1 + playlistend = self.params.get('playlistend', -1) + + if playlistend == -1: + entries = ie_result['entries'][playliststart:] + else: + entries = ie_result['entries'][playliststart:playlistend] + + n_entries = len(entries) + + self.to_screen(u"[%s] playlist '%s': Collected %d video ids (downloading %d of them)" % + (ie_result['extractor'], playlist, n_all_entries, n_entries)) + + for i,entry in enumerate(entries,1): + self.to_screen(u'[download] Downloading video #%s of %s' %(i, n_entries)) entry_result = self.process_ie_result(entry, False) entry_result['playlist'] = playlist #We must do the download here to correctly set the 'playlist' key diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index b7371365a..a7fdf1607 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1806,19 +1806,6 @@ class YoutubePlaylistIE(InfoExtractor): page_num += 1 videos = [v[1] for v in sorted(videos)] - total = len(videos) - - playliststart = self._downloader.params.get('playliststart', 1) - 1 - playlistend = self._downloader.params.get('playlistend', -1) - if playlistend == -1: - videos = videos[playliststart:] - else: - videos = videos[playliststart:playlistend] - - if len(videos) == total: - self._downloader.to_screen(u'[youtube] PL %s: Found %i videos' % (playlist_id, total)) - else: - self._downloader.to_screen(u'[youtube] PL %s: Found %i videos, downloading %i' % (playlist_id, total, len(videos))) url_results = [self.url_result(url) for url in videos] return [self.playlist_result(url_results, playlist_id)] @@ -1943,18 +1930,6 @@ class YoutubeUserIE(InfoExtractor): pagenum += 1 - all_ids_count = len(video_ids) - playliststart = self._downloader.params.get('playliststart', 1) - 1 - playlistend = self._downloader.params.get('playlistend', -1) - - if playlistend == -1: - video_ids = video_ids[playliststart:] - else: - video_ids = video_ids[playliststart:playlistend] - - self._downloader.to_screen(u"[youtube] user %s: Collected %d video ids (downloading %d of them)" % - (username, all_ids_count, len(video_ids))) - urls = ['http://www.youtube.com/watch?v=%s' % video_id for video_id in video_ids] url_results = [self.url_result(url) for url in urls] return [self.playlist_result(url_results, playlist_title = username)] @@ -2035,15 +2010,6 @@ class BlipTVUserIE(InfoExtractor): pagenum += 1 - all_ids_count = len(video_ids) - playliststart = self._downloader.params.get('playliststart', 1) - 1 - playlistend = self._downloader.params.get('playlistend', -1) - - if playlistend == -1: - video_ids = video_ids[playliststart:] - else: - video_ids = video_ids[playliststart:playlistend] - self._downloader.to_screen(u"[%s] user %s: Collected %d video ids (downloading %d of them)" % (self.IE_NAME, username, all_ids_count, len(video_ids))) From dc36bc943499e42fac7873bc55ab573eb573e25d Mon Sep 17 00:00:00 2001 From: Adam Mesha <adam@mesha.org> Date: Thu, 11 Apr 2013 07:27:04 +0300 Subject: [PATCH 064/221] Fix bug when the vimeo description is empty on Python 2.x. --- youtube_dl/InfoExtractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index aa8074a9e..82ba63449 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1130,7 +1130,7 @@ class VimeoIE(InfoExtractor): # Extract video description video_description = get_element_by_attribute("itemprop", "description", webpage) if video_description: video_description = clean_html(video_description) - else: video_description = '' + else: video_description = u'' # Extract upload date video_upload_date = None From 87439741894443fcf6e10fce221b4e6d6911b6b8 Mon Sep 17 00:00:00 2001 From: Adam Mesha <adam@mesha.org> Date: Thu, 11 Apr 2013 07:33:18 +0300 Subject: [PATCH 065/221] Resolve the symlink if __main__.py is invoke as a symlink. --- youtube_dl/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/youtube_dl/__main__.py b/youtube_dl/__main__.py index 7022ea4be..3fe29c91f 100755 --- a/youtube_dl/__main__.py +++ b/youtube_dl/__main__.py @@ -9,7 +9,8 @@ import sys if __package__ is None and not hasattr(sys, "frozen"): # direct call of __main__.py import os.path - sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + path = os.path.realpath(os.path.abspath(__file__)) + sys.path.append(os.path.dirname(os.path.dirname(path))) import youtube_dl From b03d65c2370d812f4bb141e6d9599553267c07ac Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 11 Apr 2013 10:47:21 +0200 Subject: [PATCH 066/221] Minor improvements for ARD IE --- test/tests.json | 10 ++++++++++ youtube_dl/InfoExtractors.py | 16 ++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/test/tests.json b/test/tests.json index 0c94c65bd..0c3b24054 100644 --- a/test/tests.json +++ b/test/tests.json @@ -328,5 +328,15 @@ "info_dict": { "title": "Video: KO Of The Week: MMA Fighter Gets Knocked Out By Swift Head Kick! " } + }, + { + "name": "ARD", + "url": "http://www.ardmediathek.de/das-erste/tagesschau-in-100-sek?documentId=14077640", + "file": "14077640.mp4", + "md5": "6ca8824255460c787376353f9e20bbd8", + "info_dict": { + "title": "11.04.2013 09:23 Uhr - Tagesschau in 100 Sekunden" + } } + ] diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 3d8145e16..51aca5497 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -4357,15 +4357,19 @@ class LiveLeakIE(InfoExtractor): return [info] class ARDIE(InfoExtractor): - IE_NAME = 'ard' - _VALID_URL = r'^(?:http?://)?mediathek\.daserste\.de/(?:.*/)(?P<video_id>[^/\?]+)(?:\?.*)?' - _TITLE = r'<h1 class="boxTopHeadline">(?P<title>.*)</h1>' + _VALID_URL = r'^(?:https?://)?(?:(?:www\.)?ardmediathek\.de|mediathek\.daserste\.de)/(?:.*/)(?P<video_id>[^/\?]+)(?:\?.*)?' + _TITLE = r'<h1(?: class="boxTopHeadline")?>(?P<title>.*)</h1>' _MEDIA_STREAM = r'mediaCollection\.addMediaStream\((?P<media_type>\d+), (?P<quality>\d+), "(?P<rtmp_url>[^"]*)", "(?P<video_url>[^"]*)", "[^"]*"\)' def _real_extract(self, url): # determine video id from url m = re.match(self._VALID_URL, url) - video_id = m.group('video_id') + + numid = re.search(r'documentId=([0-9]+)', url) + if numid: + video_id = numid.group(1) + else: + video_id = m.group('video_id') # determine title and media streams from webpage html = self._download_webpage(url, video_id) @@ -4377,8 +4381,8 @@ class ARDIE(InfoExtractor): return # choose default media type and highest quality for now - stream = max([s for s in streams if int(s["media_type"]) == 0], key=lambda s: int(s["quality"])) - #stream = streams[-1] + stream = max([s for s in streams if int(s["media_type"]) == 0], + key=lambda s: int(s["quality"])) # there's two possibilities: RTMP stream or HTTP download info = {'id': video_id, 'title': title, 'ext': 'mp4'} From e8600d69fdab95668acc443d639c5cd460d45b91 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 11 Apr 2013 10:48:37 +0200 Subject: [PATCH 067/221] Credit @catch22 for ARD IE --- youtube_dl/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 807b73541..6a1a9bb52 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -24,6 +24,7 @@ __authors__ = ( 'Jaime Marquínez Ferrándiz', 'Jeff Crouse', 'Osama Khalid', + 'Michael Walter', ) __license__ = 'Public Domain' From 213c31ae16641ed2df61f0abbd7ea032267b2222 Mon Sep 17 00:00:00 2001 From: Stanislav Kupryakhin <st.kupr@gmail.com> Date: Tue, 2 Apr 2013 11:40:07 +0700 Subject: [PATCH 068/221] Added option --autonumber-size: Specifies the number of digits in %(autonumber)s when it is present in output filename template or --autonumber option is given --- youtube_dl/FileDownloader.py | 4 +++- youtube_dl/__init__.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index e801db00a..7731db17f 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -388,7 +388,9 @@ class FileDownloader(object): template_dict = dict(info_dict) template_dict['epoch'] = int(time.time()) - template_dict['autonumber'] = u'%05d' % self._num_downloads + autonumber_size = self.params.get('autonumber_size', 5) + autonumber_templ = u'%0' + str(autonumber_size) + u'd' + template_dict['autonumber'] = autonumber_templ % self._num_downloads sanitize = lambda k,v: sanitize_filename( u'NA' if v is None else compat_str(v), diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 6a1a9bb52..6ca0ab674 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -236,6 +236,9 @@ def parseOpts(): help='number downloaded files starting from 00000', default=False) filesystem.add_option('-o', '--output', dest='outtmpl', metavar='TEMPLATE', help='output filename template. Use %(title)s to get the title, %(uploader)s for the uploader name, %(uploader_id)s for the uploader nickname if different, %(autonumber)s to get an automatically incremented number, %(ext)s for the filename extension, %(upload_date)s for the upload date (YYYYMMDD), %(extractor)s for the provider (youtube, metacafe, etc), %(id)s for the video id and %% for a literal percent. Use - to output to stdout. Can also be used to download to a different directory, for example with -o \'/my/downloads/%(uploader)s/%(title)s-%(id)s.%(ext)s\' .') + filesystem.add_option('--autonumber-size', + dest='autonumber_size', metavar='NUMBER', + help='Specifies the number of digits in %(autonumber)s when it is present in output filename template or --autonumber option is given') filesystem.add_option('--restrict-filenames', action='store_true', dest='restrictfilenames', help='Restrict filenames to only ASCII characters, and avoid "&" and spaces in filenames', default=False) @@ -300,6 +303,8 @@ def parseOpts(): print(u'[debug] System config: ' + repr(systemConf)) print(u'[debug] User config: ' + repr(userConf)) print(u'[debug] Command-line args: ' + repr(commandLineConf)) + print(u'[debug] opts: ' + repr(opts)) + print(u'[debug] args: ' + repr(args)) return parser, opts, args @@ -452,6 +457,7 @@ def _real_main(): 'format_limit': opts.format_limit, 'listformats': opts.listformats, 'outtmpl': outtmpl, + 'autonumber_size': opts.autonumber_size, 'restrictfilenames': opts.restrictfilenames, 'ignoreerrors': opts.ignoreerrors, 'ratelimit': opts.ratelimit, From 844d1f9fa199395b6ae6abd50c50859c9fd524e3 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 11 Apr 2013 10:54:37 +0200 Subject: [PATCH 069/221] Removed overly verbose options and arguments (Should be obvious from the previous lines) --- youtube_dl/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 6ca0ab674..489f8948a 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -303,8 +303,6 @@ def parseOpts(): print(u'[debug] System config: ' + repr(systemConf)) print(u'[debug] User config: ' + repr(userConf)) print(u'[debug] Command-line args: ' + repr(commandLineConf)) - print(u'[debug] opts: ' + repr(opts)) - print(u'[debug] args: ' + repr(args)) return parser, opts, args From e0fee250c30969b73685bbf9d3e6ad0d7917a0a6 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 11 Apr 2013 17:07:55 +0200 Subject: [PATCH 070/221] Fix default for variable-size autonumbering --- youtube_dl/FileDownloader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 7731db17f..d4f9cc621 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -388,7 +388,9 @@ class FileDownloader(object): template_dict = dict(info_dict) template_dict['epoch'] = int(time.time()) - autonumber_size = self.params.get('autonumber_size', 5) + autonumber_size = self.params.get('autonumber_size') + if autonumber_size is None: + autonumber_size = 5 autonumber_templ = u'%0' + str(autonumber_size) + u'd' template_dict['autonumber'] = autonumber_templ % self._num_downloads From af9ad45cd43c67f6110c449de00c7d11237add78 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 11 Apr 2013 17:20:05 +0200 Subject: [PATCH 071/221] Re-enable Stanford OC test --- test/tests.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/tests.json b/test/tests.json index 0c3b24054..23cccb7d8 100644 --- a/test/tests.json +++ b/test/tests.json @@ -76,8 +76,7 @@ "name": "StanfordOpenClassroom", "md5": "544a9468546059d4e80d76265b0443b8", "url": "http://openclassroom.stanford.edu/MainFolder/VideoPage.php?course=PracticalUnix&video=intro-environment&speed=100", - "file": "PracticalUnix_intro-environment.mp4", - "skip": "Currently offline" + "file": "PracticalUnix_intro-environment.mp4" }, { "name": "XNXX", From 0ba994e9e323f4162ed12542beb079df1f4b2825 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 11 Apr 2013 17:20:17 +0200 Subject: [PATCH 072/221] Skip ARD test as it requires rtmpdump --- test/tests.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/tests.json b/test/tests.json index 23cccb7d8..7808a07de 100644 --- a/test/tests.json +++ b/test/tests.json @@ -335,7 +335,8 @@ "md5": "6ca8824255460c787376353f9e20bbd8", "info_dict": { "title": "11.04.2013 09:23 Uhr - Tagesschau in 100 Sekunden" - } + }, + "skip": "Requires rtmpdump" } ] From 927c8c49246bb7ae98596b3cf518425d46f39100 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 11 Apr 2013 18:18:15 +0200 Subject: [PATCH 073/221] Use download_webpage in youtube IE --- youtube_dl/InfoExtractors.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 080730660..c1c206a8a 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -115,7 +115,8 @@ class InfoExtractor(object): """ Returns the response handle """ if note is None: note = u'Downloading video webpage' - self._downloader.to_screen(u'[%s] %s: %s' % (self.IE_NAME, video_id, note)) + if note is not False: + self._downloader.to_screen(u'[%s] %s: %s' % (self.IE_NAME, video_id, note)) try: return compat_urllib_request.urlopen(url_or_request) except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: @@ -463,18 +464,14 @@ class YoutubeIE(InfoExtractor): # Get video info self.report_video_info_webpage_download(video_id) for el_type in ['&el=embedded', '&el=detailpage', '&el=vevo', '']: - video_info_url = ('http://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en' + video_info_url = ('https://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en' % (video_id, el_type)) - request = compat_urllib_request.Request(video_info_url) - try: - video_info_webpage_bytes = compat_urllib_request.urlopen(request).read() - video_info_webpage = video_info_webpage_bytes.decode('utf-8', 'ignore') - video_info = compat_parse_qs(video_info_webpage) - if 'token' in video_info: - break - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.report_error(u'unable to download video info webpage: %s' % compat_str(err)) - return + video_info_webpage = self._download_webpage(video_info_url, video_id, + note=False, + errnote='unable to download video info webpage') + video_info = compat_parse_qs(video_info_webpage) + if 'token' in video_info: + break if 'token' not in video_info: if 'reason' in video_info: self._downloader.report_error(u'YouTube said: %s' % video_info['reason'][0]) From 855703e55e46f3e070d9837a2eda1e6eb8a27301 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 11 Apr 2013 18:31:35 +0200 Subject: [PATCH 074/221] Option to dump intermediate pages --- youtube_dl/InfoExtractors.py | 8 ++++++++ youtube_dl/__init__.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index c1c206a8a..94e4451ed 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -134,6 +134,14 @@ class InfoExtractor(object): else: encoding = 'utf-8' webpage_bytes = urlh.read() + if self._downloader.params.get('dump_intermediate_pages', False): + try: + url = url_or_request.get_full_url() + except AttributeError: + url = url_or_request + self._downloader.to_screen(u'Dumping request to ' + url) + dump = base64.b64encode(webpage_bytes).decode('ascii') + self._downloader.to_screen(dump) return webpage_bytes.decode(encoding, 'replace') diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 489f8948a..f46143e01 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -224,6 +224,9 @@ def parseOpts(): help='display progress in console titlebar', default=False) verbosity.add_option('-v', '--verbose', action='store_true', dest='verbose', help='print various debugging information', default=False) + verbosity.add_option('--dump-intermediate-pages', + action='store_true', dest='dump_intermediate_pages', default=False, + help='print downloaded pages to debug problems(very verbose)') filesystem.add_option('-t', '--title', action='store_true', dest='usetitle', help='use title in file name', default=False) @@ -485,6 +488,7 @@ def _real_main(): 'max_downloads': opts.max_downloads, 'prefer_free_formats': opts.prefer_free_formats, 'verbose': opts.verbose, + 'dump_intermediate_pages': opts.dump_intermediate_pages, 'test': opts.test, 'keepvideo': opts.keepvideo, 'min_filesize': opts.min_filesize, From 744435f2a420c2d734b70e302ca881a176a447ed Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 11 Apr 2013 18:38:43 +0200 Subject: [PATCH 075/221] Show whole diff in error cases --- test/test_download.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_download.py b/test/test_download.py index 59a6e1498..e3513efba 100644 --- a/test/test_download.py +++ b/test/test_download.py @@ -58,6 +58,7 @@ with io.open(PARAMETERS_FILE, encoding='utf-8') as pf: class TestDownload(unittest.TestCase): + maxDiff = None def setUp(self): self.parameters = parameters self.defs = defs From f4381ab88a5bebc241d6f089719720049014ced9 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 11 Apr 2013 18:39:13 +0200 Subject: [PATCH 076/221] Fix keek title extraction --- youtube_dl/InfoExtractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 94e4451ed..7deb488a9 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -4136,7 +4136,7 @@ class KeekIE(InfoExtractor): video_url = u'http://cdn.keek.com/keek/video/%s' % video_id thumbnail = u'http://cdn.keek.com/keek/thumbnail/%s/w100/h75' % video_id webpage = self._download_webpage(url, video_id) - m = re.search(r'<meta property="og:title" content="(?P<title>.+)"', webpage) + m = re.search(r'<meta property="og:title" content="(?P<title>.*?)"', webpage) title = unescapeHTML(m.group('title')) m = re.search(r'<div class="user-name-and-bio">[\S\s]+?<h2>(?P<uploader>.+?)</h2>', webpage) uploader = clean_html(m.group('uploader')) From b625bc2c31e9434135e077c00d96eb8f05f80a3e Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 11 Apr 2013 18:42:57 +0200 Subject: [PATCH 077/221] release 2013.04.11 --- README.md | 208 ++++++++++++++++++++++-------------------- youtube_dl/version.py | 2 +- 2 files changed, 108 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index 338b6133f..cf95187e6 100644 --- a/README.md +++ b/README.md @@ -14,119 +14,125 @@ 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 - -h, --help print this help text and exit - --version print program version and exit - -U, --update update this program to latest version - -i, --ignore-errors continue on download errors - -r, --rate-limit LIMIT maximum download rate (e.g. 50k or 44.6m) - -R, --retries RETRIES number of retries (default is 10) - --buffer-size SIZE size of download buffer (e.g. 1024 or 16k) (default - is 1024) - --no-resize-buffer do not automatically adjust the buffer size. By - default, the buffer size is automatically resized - from an initial value of SIZE. - --dump-user-agent display the current browser identification - --user-agent UA specify a custom user agent - --list-extractors List all supported extractors and the URLs they - would handle + -h, --help print this help text and exit + --version print program version and exit + -U, --update update this program to latest version + -i, --ignore-errors continue on download errors + -r, --rate-limit LIMIT maximum download rate (e.g. 50k or 44.6m) + -R, --retries RETRIES number of retries (default is 10) + --buffer-size SIZE size of download buffer (e.g. 1024 or 16k) + (default is 1024) + --no-resize-buffer do not automatically adjust the buffer size. By + default, the buffer size is automatically resized + from an initial value of SIZE. + --dump-user-agent display the current browser identification + --user-agent UA specify a custom user agent + --list-extractors List all supported extractors and the URLs they + would handle ## Video Selection: - --playlist-start NUMBER playlist video to start at (default is 1) - --playlist-end NUMBER playlist video to end at (default is last) - --match-title REGEX download only matching titles (regex or caseless - sub-string) - --reject-title REGEX skip download for matching titles (regex or - caseless sub-string) - --max-downloads NUMBER Abort after downloading NUMBER files - --min-filesize SIZE Do not download any videos smaller than SIZE (e.g. - 50k or 44.6m) - --max-filesize SIZE Do not download any videos larger than SIZE (e.g. - 50k or 44.6m) + --playlist-start NUMBER playlist video to start at (default is 1) + --playlist-end NUMBER playlist video to end at (default is last) + --match-title REGEX download only matching titles (regex or caseless + sub-string) + --reject-title REGEX skip download for matching titles (regex or + caseless sub-string) + --max-downloads NUMBER Abort after downloading NUMBER files + --min-filesize SIZE Do not download any videos smaller than SIZE + (e.g. 50k or 44.6m) + --max-filesize SIZE Do not download any videos larger than SIZE (e.g. + 50k or 44.6m) ## Filesystem Options: - -t, --title use title in file name - --id use video ID in file name - -l, --literal [deprecated] alias of --title - -A, --auto-number number downloaded files starting from 00000 - -o, --output TEMPLATE output filename template. Use %(title)s to get the - title, %(uploader)s for the uploader name, - %(uploader_id)s for the uploader nickname if - different, %(autonumber)s to get an automatically - incremented number, %(ext)s for the filename - extension, %(upload_date)s for the upload date - (YYYYMMDD), %(extractor)s for the provider - (youtube, metacafe, etc), %(id)s for the video id - and %% for a literal percent. Use - to output to - stdout. Can also be used to download to a different - directory, for example with -o '/my/downloads/%(upl - oader)s/%(title)s-%(id)s.%(ext)s' . - --restrict-filenames Restrict filenames to only ASCII characters, and - avoid "&" and spaces in filenames - -a, --batch-file FILE file containing URLs to download ('-' for stdin) - -w, --no-overwrites do not overwrite files - -c, --continue resume partially downloaded files - --no-continue do not resume partially downloaded files (restart - from beginning) - --cookies FILE file to read cookies from and dump cookie jar in - --no-part do not use .part files - --no-mtime do not use the Last-modified header to set the file - modification time - --write-description write video description to a .description file - --write-info-json write video metadata to a .info.json file + -t, --title use title in file name + --id use video ID in file name + -l, --literal [deprecated] alias of --title + -A, --auto-number number downloaded files starting from 00000 + -o, --output TEMPLATE output filename template. Use %(title)s to get + the title, %(uploader)s for the uploader name, + %(uploader_id)s for the uploader nickname if + different, %(autonumber)s to get an automatically + incremented number, %(ext)s for the filename + extension, %(upload_date)s for the upload date + (YYYYMMDD), %(extractor)s for the provider + (youtube, metacafe, etc), %(id)s for the video id + and %% for a literal percent. Use - to output to + stdout. Can also be used to download to a + different directory, for example with -o '/my/dow + nloads/%(uploader)s/%(title)s-%(id)s.%(ext)s' . + --autonumber-size NUMBER Specifies the number of digits in %(autonumber)s + when it is present in output filename template or + --autonumber option is given + --restrict-filenames Restrict filenames to only ASCII characters, and + avoid "&" and spaces in filenames + -a, --batch-file FILE file containing URLs to download ('-' for stdin) + -w, --no-overwrites do not overwrite files + -c, --continue resume partially downloaded files + --no-continue do not resume partially downloaded files (restart + from beginning) + --cookies FILE file to read cookies from and dump cookie jar in + --no-part do not use .part files + --no-mtime do not use the Last-modified header to set the + file modification time + --write-description write video description to a .description file + --write-info-json write video metadata to a .info.json file ## Verbosity / Simulation Options: - -q, --quiet activates quiet mode - -s, --simulate do not download the video and do not write anything - to disk - --skip-download do not download the video - -g, --get-url simulate, quiet but print URL - -e, --get-title simulate, quiet but print title - --get-thumbnail simulate, quiet but print thumbnail URL - --get-description simulate, quiet but print video description - --get-filename simulate, quiet but print output filename - --get-format simulate, quiet but print output format - --newline output progress bar as new lines - --no-progress do not print progress bar - --console-title display progress in console titlebar - -v, --verbose print various debugging information + -q, --quiet activates quiet mode + -s, --simulate do not download the video and do not write + anything to disk + --skip-download do not download the video + -g, --get-url simulate, quiet but print URL + -e, --get-title simulate, quiet but print title + --get-thumbnail simulate, quiet but print thumbnail URL + --get-description simulate, quiet but print video description + --get-filename simulate, quiet but print output filename + --get-format simulate, quiet but print output format + --newline output progress bar as new lines + --no-progress do not print progress bar + --console-title display progress in console titlebar + -v, --verbose print various debugging information + --dump-intermediate-pages print downloaded pages to debug problems(very + verbose) ## Video Format Options: - -f, --format FORMAT video format code - --all-formats download all available video formats - --prefer-free-formats prefer free video formats unless a specific one is - requested - --max-quality FORMAT highest quality format to download - -F, --list-formats list all available formats (currently youtube only) - --write-sub write subtitle file (currently youtube only) - --only-sub downloads only the subtitles (no video) - --all-subs downloads all the available subtitles of the video - (currently youtube only) - --list-subs lists all available subtitles for the video - (currently youtube only) - --sub-format LANG subtitle format [srt/sbv] (default=srt) (currently - youtube only) - --sub-lang LANG language of the subtitles to download (optional) - use IETF language tags like 'en' + -f, --format FORMAT video format code + --all-formats download all available video formats + --prefer-free-formats prefer free video formats unless a specific one + is requested + --max-quality FORMAT highest quality format to download + -F, --list-formats list all available formats (currently youtube + only) + --write-sub write subtitle file (currently youtube only) + --only-sub downloads only the subtitles (no video) + --all-subs downloads all the available subtitles of the + video (currently youtube only) + --list-subs lists all available subtitles for the video + (currently youtube only) + --sub-format LANG subtitle format [srt/sbv] (default=srt) + (currently youtube only) + --sub-lang LANG language of the subtitles to download (optional) + use IETF language tags like 'en' ## Authentication Options: - -u, --username USERNAME account username - -p, --password PASSWORD account password - -n, --netrc use .netrc authentication data + -u, --username USERNAME account username + -p, --password PASSWORD account password + -n, --netrc use .netrc authentication data ## 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", "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) - --recode-video FORMAT Encode the video to another format if necessary - (currently supported: mp4|flv|ogg|webm) - -k, --keep-video keeps the video file on disk after the post- - processing; the video is erased by default - --no-post-overwrites do not overwrite post-processed files; the post- - processed files are overwritten by default + -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", "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) + --recode-video FORMAT Encode the video to another format if necessary + (currently supported: mp4|flv|ogg|webm) + -k, --keep-video keeps the video file on disk after the post- + processing; the video is erased by default + --no-post-overwrites do not overwrite post-processed files; the post- + processed files are overwritten by default # CONFIGURATION diff --git a/youtube_dl/version.py b/youtube_dl/version.py index c433e2eaa..3535e61d4 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.04.03' +__version__ = '2013.04.11' From d281274bf250065f876bb4f75fb6f711e1a26eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaime.marquinez.ferrandiz@gmail.com> Date: Tue, 16 Apr 2013 15:13:29 +0200 Subject: [PATCH 078/221] Add a playlist_index key to the info_dict, can be used in the output template --- README.md | 1 + youtube_dl/FileDownloader.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/README.md b/README.md index c8d28db3c..e2958a9b0 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ The `-o` option allows users to indicate a template for the output file names. T - `epoch`: The sequence will be replaced by the Unix epoch when creating the file. - `autonumber`: The sequence will be replaced by a five-digit number that will be increased with each download, starting at zero. - `playlist`: The name or the id of the playlist that contains the video. + - `playlist_index`: The index of the video in the playlist, a five-digit number. The current default template is `%(id)s.%(ext)s`, but that will be switchted to `%(title)s-%(id)s.%(ext)s` (which can be requested with `-t` at the moment). diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 5a5141ba5..4dabbb440 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -389,6 +389,8 @@ class FileDownloader(object): template_dict['epoch'] = int(time.time()) template_dict['autonumber'] = u'%05d' % self._num_downloads + if template_dict['playlist_index'] is not None: + template_dict['playlist_index'] = u'%05d' % template_dict['playlist_index'] sanitize = lambda k,v: sanitize_filename( u'NA' if v is None else compat_str(v), @@ -473,6 +475,7 @@ class FileDownloader(object): if 'playlist' not in ie_result: #It isn't part of a playlist ie_result['playlist'] = None + ie_result['playlist_index'] = None if download: #Do the download: self.process_info(ie_result) @@ -506,6 +509,7 @@ class FileDownloader(object): self.to_screen(u'[download] Downloading video #%s of %s' %(i, n_entries)) entry_result = self.process_ie_result(entry, False) entry_result['playlist'] = playlist + entry_result['playlist_index'] = i + playliststart #We must do the download here to correctly set the 'playlist' key if download: self.process_info(entry_result) From b8ad4f02a2124c9e08570bfb2ab05f2024cb2fb7 Mon Sep 17 00:00:00 2001 From: Finn Petersen <4peterse@googlemail.com> Date: Tue, 16 Apr 2013 19:26:48 +0200 Subject: [PATCH 079/221] Arguments as parameter to function _real_main so it can be used programmatically --- youtube_dl/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index f46143e01..b339427e8 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -47,7 +47,7 @@ from .FileDownloader import * from .InfoExtractors import gen_extractors from .PostProcessor import * -def parseOpts(): +def parseOpts(arguments): def _readOptions(filename_bytes): try: optionf = open(filename_bytes) @@ -298,8 +298,8 @@ def parseOpts(): userConfFile = os.path.join(os.path.expanduser('~'), '.config', 'youtube-dl.conf') systemConf = _readOptions('/etc/youtube-dl.conf') userConf = _readOptions(userConfFile) - commandLineConf = sys.argv[1:] - argv = systemConf + userConf + commandLineConf + commandLineConf = sys.argv[1:] + argv = systemConf + userConf + commandLineConf if not arguments else arguments opts, args = parser.parse_args(argv) if opts.verbose: @@ -309,8 +309,8 @@ def parseOpts(): return parser, opts, args -def _real_main(): - parser, opts, args = parseOpts() +def _real_main(argv=None): + parser, opts, args = parseOpts(argv) # Open appropriate CookieJar if opts.cookiefile is None: @@ -544,9 +544,9 @@ def _real_main(): sys.exit(retcode) -def main(): +def main(argv=None): try: - _real_main() + _real_main(argv) except DownloadError: sys.exit(1) except SameFileError: From a60b854d9099e5a286accf8065d1dc9e00a4bfe6 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda <filippo.valsorda@gmail.com> Date: Wed, 17 Apr 2013 19:48:35 +0200 Subject: [PATCH 080/221] disable YT ratelimit; this should enable to max out the connection bandwidth --- youtube_dl/InfoExtractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 7deb488a9..e1d71ccb7 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -581,7 +581,7 @@ class YoutubeIE(InfoExtractor): url_data_strs = video_info['url_encoded_fmt_stream_map'][0].split(',') url_data = [compat_parse_qs(uds) for uds in url_data_strs] url_data = [ud for ud in url_data if 'itag' in ud and 'url' in ud] - url_map = dict((ud['itag'][0], ud['url'][0] + '&signature=' + ud['sig'][0]) for ud in url_data) + url_map = dict((ud['itag'][0], ud['url'][0] + '&signature=' + ud['sig'][0] + '&ratebypass=yes') for ud in url_data) format_limit = self._downloader.params.get('format_limit', None) available_formats = self._available_formats_prefer_free if self._downloader.params.get('prefer_free_formats', False) else self._available_formats From c8c5443bb50644bfd784044860092f95cb4616b5 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Wed, 17 Apr 2013 23:21:58 +0200 Subject: [PATCH 081/221] Revert "disable YT ratelimit; this should enable to max out the connection bandwidth" Although cool, that seems to break a lot of youtube videos. This reverts commit a60b854d9099e5a286accf8065d1dc9e00a4bfe6. --- youtube_dl/InfoExtractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index e1d71ccb7..7deb488a9 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -581,7 +581,7 @@ class YoutubeIE(InfoExtractor): url_data_strs = video_info['url_encoded_fmt_stream_map'][0].split(',') url_data = [compat_parse_qs(uds) for uds in url_data_strs] url_data = [ud for ud in url_data if 'itag' in ud and 'url' in ud] - url_map = dict((ud['itag'][0], ud['url'][0] + '&signature=' + ud['sig'][0] + '&ratebypass=yes') for ud in url_data) + url_map = dict((ud['itag'][0], ud['url'][0] + '&signature=' + ud['sig'][0]) for ud in url_data) format_limit = self._downloader.params.get('format_limit', None) available_formats = self._available_formats_prefer_free if self._downloader.params.get('prefer_free_formats', False) else self._available_formats From 32c96387c1837103b5cdde4b4c39b9653f2e0181 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 18 Apr 2013 04:41:48 +0200 Subject: [PATCH 082/221] Fix facebook IE --- youtube_dl/InfoExtractors.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 7deb488a9..306d96774 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -2161,7 +2161,7 @@ class FacebookIE(InfoExtractor): url = 'https://www.facebook.com/video/video.php?v=%s' % video_id webpage = self._download_webpage(url, video_id) - BEFORE = '[["allowFullScreen","true"],["allowScriptAccess","always"],["salign","tl"],["scale","noscale"],["wmode","opaque"]].forEach(function(param) {swf.addParam(param[0], param[1]);});\n' + BEFORE = '{swf.addParam(param[0], param[1]);});\n' AFTER = '.forEach(function(variable) {swf.addVariable(variable[0], variable[1]);});' m = re.search(re.escape(BEFORE) + '(.*?)' + re.escape(AFTER), webpage) if not m: @@ -2169,12 +2169,14 @@ class FacebookIE(InfoExtractor): data = dict(json.loads(m.group(1))) params_raw = compat_urllib_parse.unquote(data['params']) params = json.loads(params_raw) - video_url = params['hd_src'] + video_data = params['video_data'][0] + video_url = video_data.get('hd_src') if not video_url: - video_url = params['sd_src'] + video_url = video_data['sd_src'] if not video_url: raise ExtractorError(u'Cannot find video URL') - video_duration = int(params['video_duration']) + video_duration = int(video_data['video_duration']) + thumbnail = video_data['thumbnail_src'] m = re.search('<h2 class="uiHeaderTitle">([^<]+)</h2>', webpage) if not m: @@ -2187,7 +2189,7 @@ class FacebookIE(InfoExtractor): 'url': video_url, 'ext': 'mp4', 'duration': video_duration, - 'thumbnail': params['thumbnail_src'], + 'thumbnail': thumbnail, } return [info] From bfdf4692955b3ea300b8f6ae30b8b69127922f7e Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 18 Apr 2013 06:21:46 +0200 Subject: [PATCH 083/221] Fix FunnyOrDie extraction for a special video (#789) --- youtube_dl/InfoExtractors.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 306d96774..cf31970ef 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3704,7 +3704,9 @@ class FunnyOrDieIE(InfoExtractor): m = re.search(r"<h1 class='player_page_h1'.*?>(?P<title>.*?)</h1>", webpage, flags=re.DOTALL) if not m: - self._downloader.trouble(u'Cannot find video title') + m = re.search(r'<title>(?P<title>[^<]+?)', webpage) + if not m: + self._downloader.trouble(u'Cannot find video title') title = clean_html(m.group('title')) m = re.search(r' Date: Thu, 18 Apr 2013 06:27:11 +0200 Subject: [PATCH 084/221] Limit titles to 200 characters (Closes #789) --- youtube_dl/FileDownloader.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index d4f9cc621..96da754fb 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -426,6 +426,10 @@ class FileDownloader(object): def process_info(self, info_dict): """Process a single dictionary returned by an InfoExtractor.""" + info_dict['fulltitle'] = info_dict['title'] + if len(info_dict['title']) > 200: + info_dict['title'] = info_dict['title'][:197] + u'...' + # Keep for backwards compatibility info_dict['stitle'] = info_dict['title'] From d22f65413af51b24166de20d85d69f7525a70e25 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Thu, 18 Apr 2013 06:29:32 +0200 Subject: [PATCH 085/221] release 2013.04.18 --- youtube_dl/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/version.py b/youtube_dl/version.py index 3535e61d4..2fd5f40c8 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.04.11' +__version__ = '2013.04.18' From feba604e9256b48972dfe4b5658ada4b300581cd Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Thu, 18 Apr 2013 07:28:24 +0200 Subject: [PATCH 086/221] =?UTF-8?q?Fix=20playlists=20with=20size=2050i=20?= =?UTF-8?q?=E2=88=80=20i=E2=88=89=E2=84=95=20(Closes=20#782)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- youtube_dl/InfoExtractors.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index cf31970ef..bac3a747d 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1778,9 +1778,13 @@ class YoutubePlaylistIE(InfoExtractor): self._downloader.report_error(u'Invalid JSON in API response: ' + compat_str(err)) return - if not 'feed' in response or not 'entry' in response['feed']: + if 'feed' not in response: self._downloader.report_error(u'Got a malformed response from YouTube API') return + if 'entry' not in response['feed']: + # Number of videos is a multiple of self._MAX_RESULTS + break + videos += [ (entry['yt$position']['$t'], entry['content']['src']) for entry in response['feed']['entry'] if 'content' in entry ] From 5a8d13199cd6bd73d7ace023a528c02e12fd2954 Mon Sep 17 00:00:00 2001 From: ispedals Date: Fri, 19 Apr 2013 18:05:35 -0400 Subject: [PATCH 087/221] Fix YoutubeChannelIE - urls with query parameters now match - fixes regex for identifying videos - fixes pagination --- youtube_dl/InfoExtractors.py | 64 ++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index ae36558d7..e0a26eb58 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1823,15 +1823,23 @@ class YoutubePlaylistIE(InfoExtractor): class YoutubeChannelIE(InfoExtractor): """Information Extractor for YouTube channels.""" - _VALID_URL = r"^(?:https?://)?(?:youtu\.be|(?:\w+\.)?youtube(?:-nocookie)?\.com)/channel/([0-9A-Za-z_-]+)(?:/.*)?$" + _VALID_URL = r"^(?:https?://)?(?:youtu\.be|(?:\w+\.)?youtube(?:-nocookie)?\.com)/channel/([0-9A-Za-z_-]+)" _TEMPLATE_URL = 'http://www.youtube.com/channel/%s/videos?sort=da&flow=list&view=0&page=%s&gl=US&hl=en' - _MORE_PAGES_INDICATOR = u"Next \N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}" + _MORE_PAGES_INDICATOR = 'yt-uix-load-more' + _MORE_PAGES_URL = 'http://www.youtube.com/channel_ajax?action_load_more_videos=1&flow=list&paging=%s&view=0&sort=da&channel_id=%s' IE_NAME = u'youtube:channel' def report_download_page(self, channel_id, pagenum): """Report attempt to download channel page with given number.""" self._downloader.to_screen(u'[youtube] Channel %s: Downloading page #%s' % (channel_id, pagenum)) + def extract_videos_from_page(self, page): + ids_in_page = [] + for mobj in re.finditer(r'href="/watch\?v=([0-9A-Za-z_-]+)&?', page): + if mobj.group(1) not in ids_in_page: + ids_in_page.append(mobj.group(1)) + return ids_in_page + def _real_extract(self, url): # Extract channel id mobj = re.match(self._VALID_URL, url) @@ -1839,31 +1847,45 @@ class YoutubeChannelIE(InfoExtractor): self._downloader.report_error(u'invalid url: %s' % url) return - # Download channel pages + # Download channel page channel_id = mobj.group(1) video_ids = [] pagenum = 1 - while True: - self.report_download_page(channel_id, pagenum) - url = self._TEMPLATE_URL % (channel_id, pagenum) - request = compat_urllib_request.Request(url) - try: - page = compat_urllib_request.urlopen(request).read().decode('utf8') - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.report_error(u'unable to download webpage: %s' % compat_str(err)) - return + self.report_download_page(channel_id, pagenum) + url = self._TEMPLATE_URL % (channel_id, pagenum) + request = compat_urllib_request.Request(url) + try: + page = compat_urllib_request.urlopen(request).read().decode('utf8') + except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: + self._downloader.report_error(u'unable to download webpage: %s' % compat_str(err)) + return - # Extract video identifiers - ids_in_page = [] - for mobj in re.finditer(r'href="/watch\?v=([0-9A-Za-z_-]+)&', page): - if mobj.group(1) not in ids_in_page: - ids_in_page.append(mobj.group(1)) - video_ids.extend(ids_in_page) + # Extract video identifiers + ids_in_page = self.extract_videos_from_page(page) + video_ids.extend(ids_in_page) - if self._MORE_PAGES_INDICATOR not in page: - break - pagenum = pagenum + 1 + # Download any subsequent channel pages using the json-based channel_ajax query + if self._MORE_PAGES_INDICATOR in page: + while True: + pagenum = pagenum + 1 + + self.report_download_page(channel_id, pagenum) + url = self._MORE_PAGES_URL % (pagenum, channel_id) + request = compat_urllib_request.Request(url) + try: + page = compat_urllib_request.urlopen(request).read().decode('utf8') + except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: + self._downloader.report_error(u'unable to download webpage: %s' % compat_str(err)) + return + + page = json.loads(page) + + ids_in_page = self.extract_videos_from_page(page['content_html']) + video_ids.extend(ids_in_page) + + if self._MORE_PAGES_INDICATOR not in page['load_more_widget_html']: + break self._downloader.to_screen(u'[youtube] Channel %s: Found %i videos' % (channel_id, len(video_ids))) From fb6c319904b2bd626af534d3d8fb0726b91081c6 Mon Sep 17 00:00:00 2001 From: ispedals Date: Fri, 19 Apr 2013 18:06:28 -0400 Subject: [PATCH 088/221] Add tests for YoutubeChannelIE - tests for identifying channel urls - test retrieval of paginated channel - test retrieval of autogenerated channel --- test/test_all_urls.py | 7 ++++++- test/test_youtube_lists.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/test/test_all_urls.py b/test/test_all_urls.py index 69717b3fc..a40360122 100644 --- a/test/test_all_urls.py +++ b/test/test_all_urls.py @@ -7,7 +7,7 @@ import unittest import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from youtube_dl.InfoExtractors import YoutubeIE, YoutubePlaylistIE +from youtube_dl.InfoExtractors import YoutubeIE, YoutubePlaylistIE, YoutubeChannelIE class TestAllURLsMatching(unittest.TestCase): def test_youtube_playlist_matching(self): @@ -24,6 +24,11 @@ class TestAllURLsMatching(unittest.TestCase): self.assertTrue(YoutubeIE.suitable(u'PLtS2H6bU1M')) self.assertFalse(YoutubeIE.suitable(u'https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012')) #668 + def test_youtube_channel_matching(self): + self.assertTrue(YoutubeChannelIE.suitable('https://www.youtube.com/channel/HCtnHdj3df7iM')) + self.assertTrue(YoutubeChannelIE.suitable('https://www.youtube.com/channel/HCtnHdj3df7iM?feature=gb_ch_rec')) + self.assertTrue(YoutubeChannelIE.suitable('https://www.youtube.com/channel/HCtnHdj3df7iM/videos')) + def test_youtube_extract(self): self.assertEqual(YoutubeIE()._extract_id('http://www.youtube.com/watch?&v=BaW_jenozKc'), 'BaW_jenozKc') self.assertEqual(YoutubeIE()._extract_id('https://www.youtube.com/watch?&v=BaW_jenozKc'), 'BaW_jenozKc') diff --git a/test/test_youtube_lists.py b/test/test_youtube_lists.py index 9e91484f8..c5b22a5c4 100644 --- a/test/test_youtube_lists.py +++ b/test/test_youtube_lists.py @@ -8,7 +8,7 @@ import json import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from youtube_dl.InfoExtractors import YoutubeUserIE, YoutubePlaylistIE, YoutubeIE +from youtube_dl.InfoExtractors import YoutubeUserIE, YoutubePlaylistIE, YoutubeIE, YoutubeChannelIE from youtube_dl.utils import * from youtube_dl.FileDownloader import FileDownloader @@ -81,8 +81,14 @@ class TestYoutubeLists(unittest.TestCase): self.assertEqual(YoutubeIE()._extract_id(entries[-1]['url']), 'rYefUsYuEp0') def test_youtube_channel(self): - # I give up, please find a channel that does paginate and test this like test_youtube_playlist_long - pass # TODO + dl = FakeDownloader() + ie = YoutubeChannelIE(dl) + #test paginated channel + result = ie.extract('https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w')[0] + self.assertTrue(len(result['entries']) > 90) + #test autogenerated channel + result = ie.extract('https://www.youtube.com/channel/HCtnHdj3df7iM/videos')[0] + self.assertTrue(len(result['entries']) > 20) def test_youtube_user(self): dl = FakeDownloader() From 49da66e459e116e896625765d0b579de77acb218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sat, 20 Apr 2013 10:39:02 +0200 Subject: [PATCH 089/221] The test video for subtitles has added a new language --- test/test_youtube_subtitles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_youtube_subtitles.py b/test/test_youtube_subtitles.py index 30f2246dd..b4909091b 100644 --- a/test/test_youtube_subtitles.py +++ b/test/test_youtube_subtitles.py @@ -80,7 +80,7 @@ class TestYoutubeSubtitles(unittest.TestCase): IE = YoutubeIE(DL) info_dict = IE.extract('QRS8MkLhQmM') subtitles = info_dict[0]['subtitles'] - self.assertEqual(len(subtitles), 12) + self.assertEqual(len(subtitles), 13) def test_youtube_subtitles_format(self): DL = FakeDownloader() DL.params['writesubtitles'] = True From c8056d866aa5522f8caf4a8661c3112888e28ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sat, 20 Apr 2013 11:17:03 +0200 Subject: [PATCH 090/221] Add myself to travis notifications --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 0687c8957..7f1fa8a3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ notifications: email: - filippo.valsorda@gmail.com - phihag@phihag.de + - jaime.marquinez.ferrandiz+travis@gmail.com # irc: # channels: # - "irc.freenode.org#youtube-dl" From 089e843b0f9ae9f99be6406896829dc591f80122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sat, 20 Apr 2013 11:40:05 +0200 Subject: [PATCH 091/221] Use _download_webpage in MetacafeIE --- youtube_dl/InfoExtractors.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index e0a26eb58..5052f3ec8 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -732,12 +732,7 @@ class MetacafeIE(InfoExtractor): # Retrieve video webpage to extract further information request = compat_urllib_request.Request('http://www.metacafe.com/watch/%s/' % video_id) - try: - self.report_download_webpage(video_id) - webpage = compat_urllib_request.urlopen(request).read() - except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: - self._downloader.report_error(u'unable retrieve video webpage: %s' % compat_str(err)) - return + webpage = self._download_webpage(request, video_id) # Extract URL, uploader and title from webpage self.report_extraction(video_id) From f7a9721e16139ba3e7bce76d04c3b43ff932f698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sat, 20 Apr 2013 12:06:58 +0200 Subject: [PATCH 092/221] Fix some metacafe videos, closes #562 --- youtube_dl/InfoExtractors.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 5052f3ec8..8bfb2809b 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -731,8 +731,7 @@ class MetacafeIE(InfoExtractor): return [self.url_result('http://www.youtube.com/watch?v=%s' % mobj2.group(1))] # Retrieve video webpage to extract further information - request = compat_urllib_request.Request('http://www.metacafe.com/watch/%s/' % video_id) - webpage = self._download_webpage(request, video_id) + webpage = self._download_webpage('http://www.metacafe.com/watch/%s/' % video_id, video_id) # Extract URL, uploader and title from webpage self.report_extraction(video_id) @@ -757,13 +756,13 @@ class MetacafeIE(InfoExtractor): if 'mediaData' not in vardict: self._downloader.report_error(u'unable to extract media URL') return - mobj = re.search(r'"mediaURL":"(http.*?)","key":"(.*?)"', vardict['mediaData'][0]) + mobj = re.search(r'"mediaURL":"(?Phttp.*?)",(.*?)"key":"(?P.*?)"', vardict['mediaData'][0]) if mobj is None: self._downloader.report_error(u'unable to extract media URL') return - mediaURL = mobj.group(1).replace('\\/', '/') + mediaURL = mobj.group('mediaURL').replace('\\/', '/') video_extension = mediaURL[-3:] - video_url = '%s?__gda__=%s' % (mediaURL, mobj.group(2)) + video_url = '%s?__gda__=%s' % (mediaURL, mobj.group('key')) mobj = re.search(r'(?im)(.*) - Video', webpage) if mobj is None: From 93412126422b1324e920dc5097ee57c3ad11371b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sat, 20 Apr 2013 12:42:57 +0200 Subject: [PATCH 093/221] Create a function in InfoExtractors that returns the InfoExtractor class with the given name --- test/test_download.py | 2 +- youtube_dl/InfoExtractors.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/test_download.py b/test/test_download.py index e3513efba..cf8028718 100644 --- a/test/test_download.py +++ b/test/test_download.py @@ -67,7 +67,7 @@ class TestDownload(unittest.TestCase): def generator(test_case): def test_template(self): - ie = getattr(youtube_dl.InfoExtractors, test_case['name'] + 'IE') + ie = youtube_dl.InfoExtractors.get_info_extractor(test_case['name'])#getattr(youtube_dl.InfoExtractors, test_case['name'] + 'IE') if not ie._WORKING: print('Skipping: IE marked as not _WORKING') return diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 8bfb2809b..eeedcf792 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -4469,3 +4469,7 @@ def gen_extractors(): ARDIE(), GenericIE() ] + +def get_info_extractor(ie_name): + """Returns the info extractor class with the given ie_name""" + return globals()[ie_name+'IE'] From 6de8f1afb72d35560396817cbc2ed96180daa019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sat, 20 Apr 2013 12:50:14 +0200 Subject: [PATCH 094/221] Allows to specify which IE should be used for extracting info for a result of type url --- youtube_dl/FileDownloader.py | 17 ++++++++++++++--- youtube_dl/InfoExtractors.py | 16 +++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 03346ab04..9c0c42f8d 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -17,6 +17,7 @@ if os.name == 'nt': import ctypes from .utils import * +from .InfoExtractors import get_info_extractor class FileDownloader(object): @@ -425,13 +426,23 @@ class FileDownloader(object): return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"' return None - def extract_info(self, url, download = True): + def extract_info(self, url, download = True, ie_name = None): ''' Returns a list with a dictionary for each video we find. If 'download', also downloads the videos. ''' suitable_found = False - for ie in self._ies: + + #We copy the original list + ies = list(self._ies) + + if ie_name is not None: + #We put in the first place the given info extractor + first_ie = get_info_extractor(ie_name)() + first_ie.set_downloader(self) + ies.insert(0, first_ie) + + for ie in ies: # Go to next InfoExtractor if not suitable if not ie.suitable(url): continue @@ -486,7 +497,7 @@ class FileDownloader(object): return ie_result elif result_type == 'url': #We get the video pointed by the url - result = self.extract_info(ie_result['url'], download)[0] + result = self.extract_info(ie_result['url'], download, ie_name = ie_result['ie_key'])[0] return result elif result_type == 'playlist': #We process each entry in the playlist diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index eeedcf792..e47d8b85d 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -154,7 +154,8 @@ class InfoExtractor(object): """Returns a url that points to a page that should be processed""" #TODO: ie should be the class used for getting the info video_info = {'_type': 'url', - 'url': url} + 'url': url, + 'ie_key': ie} return video_info def playlist_result(self, entries, playlist_id=None, playlist_title=None): """Returns a playlist""" @@ -728,7 +729,7 @@ class MetacafeIE(InfoExtractor): # Check if video comes from YouTube mobj2 = re.match(r'^yt-(.*)$', video_id) if mobj2 is not None: - return [self.url_result('http://www.youtube.com/watch?v=%s' % mobj2.group(1))] + return [self.url_result('http://www.youtube.com/watch?v=%s' % mobj2.group(1), 'Youtube')] # Retrieve video webpage to extract further information webpage = self._download_webpage('http://www.metacafe.com/watch/%s/' % video_id, video_id) @@ -1810,7 +1811,7 @@ class YoutubePlaylistIE(InfoExtractor): videos = [v[1] for v in sorted(videos)] - url_results = [self.url_result(url) for url in videos] + url_results = [self.url_result(url, 'Youtube') for url in videos] return [self.playlist_result(url_results, playlist_id)] @@ -1884,7 +1885,7 @@ class YoutubeChannelIE(InfoExtractor): self._downloader.to_screen(u'[youtube] Channel %s: Found %i videos' % (channel_id, len(video_ids))) urls = ['http://www.youtube.com/watch?v=%s' % id for id in video_ids] - url_entries = [self.url_result(url) for url in urls] + url_entries = [self.url_result(url, 'Youtube') for url in urls] return [self.playlist_result(url_entries, channel_id)] @@ -1956,7 +1957,7 @@ class YoutubeUserIE(InfoExtractor): pagenum += 1 urls = ['http://www.youtube.com/watch?v=%s' % video_id for video_id in video_ids] - url_results = [self.url_result(url) for url in urls] + url_results = [self.url_result(url, 'Youtube') for url in urls] return [self.playlist_result(url_results, playlist_title = username)] @@ -2035,11 +2036,8 @@ class BlipTVUserIE(InfoExtractor): pagenum += 1 - self._downloader.to_screen(u"[%s] user %s: Collected %d video ids (downloading %d of them)" % - (self.IE_NAME, username, all_ids_count, len(video_ids))) - urls = [u'http://blip.tv/%s' % video_id for video_id in video_ids] - url_entries = [self.url_result(url) for url in urls] + url_entries = [self.url_result(url, 'BlipTV') for url in urls] return [self.playlist_result(url_entries, playlist_title = username)] From e905b6f80ef276900160af87c89644d95f669d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sat, 20 Apr 2013 13:31:21 +0200 Subject: [PATCH 095/221] TEDIE can now return a playlist --- youtube_dl/InfoExtractors.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index e47d8b85d..b20e6b46d 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -4183,7 +4183,7 @@ class TEDIE(InfoExtractor): playlist_id=m.group('playlist_id') name=m.group('name') self._downloader.to_screen(u'[%s] Getting info of playlist %s: "%s"' % (self.IE_NAME,playlist_id,name)) - return self._playlist_videos_info(url,name,playlist_id) + return [self._playlist_videos_info(url,name,playlist_id)] def _talk_video_link(self,mediaSlug): '''Returns the video link for that mediaSlug''' @@ -4200,12 +4200,17 @@ class TEDIE(InfoExtractor): webpage=self._download_webpage(url, playlist_id, 'Downloading playlist webpage') m_videos=re.finditer(video_RE,webpage,re.VERBOSE) m_names=re.finditer(video_name_RE,webpage) - info=[] + + playlist_RE = r'div class="headline">(\s*?)

(\s*?)(?P.*?)' + m_playlist = re.search(playlist_RE, webpage) + playlist_title = m_playlist.group('playlist_title') + + playlist_entries = [] for m_video, m_name in zip(m_videos,m_names): video_id=m_video.group('video_id') talk_url='http://www.ted.com%s' % m_name.group('talk_url') - info.append(self._talk_info(talk_url,video_id)) - return info + playlist_entries.append(self.url_result(talk_url, 'TED')) + return self.playlist_result(playlist_entries, playlist_id = playlist_id, playlist_title = playlist_title) def _talk_info(self, url, video_id=0): """Return the video for the talk in the url""" From c72938240e8edd75296c1dbe262769e5002ce811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sat, 20 Apr 2013 18:57:05 +0200 Subject: [PATCH 096/221] Get the title of Youtube playlists --- test/test_youtube_lists.py | 2 ++ youtube_dl/InfoExtractors.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/test/test_youtube_lists.py b/test/test_youtube_lists.py index c5b22a5c4..c7f00af32 100644 --- a/test/test_youtube_lists.py +++ b/test/test_youtube_lists.py @@ -45,6 +45,7 @@ class TestYoutubeLists(unittest.TestCase): ie = YoutubePlaylistIE(dl) result = ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re')[0] self.assertIsPlaylist(result) + self.assertEqual(result['title'], 'ytdl test PL') ytie_results = [YoutubeIE()._extract_id(url['url']) for url in result['entries']] self.assertEqual(ytie_results, [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE']) @@ -52,6 +53,7 @@ class TestYoutubeLists(unittest.TestCase): dl = FakeDownloader() ie = YoutubePlaylistIE(dl) result = ie.extract('PLBB231211A4F62143')[0] + self.assertEqual(result['title'], 'Team Fortress 2') self.assertTrue(len(result['entries']) > 40) def test_youtube_playlist_long(self): diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index b20e6b46d..c50052a73 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1801,6 +1801,8 @@ class YoutubePlaylistIE(InfoExtractor): # Number of videos is a multiple of self._MAX_RESULTS break + playlist_title = response['feed']['title']['$t'] + videos += [ (entry['yt$position']['$t'], entry['content']['src']) for entry in response['feed']['entry'] if 'content' in entry ] @@ -1812,7 +1814,7 @@ class YoutubePlaylistIE(InfoExtractor): videos = [v[1] for v in sorted(videos)] url_results = [self.url_result(url, 'Youtube') for url in videos] - return [self.playlist_result(url_results, playlist_id)] + return [self.playlist_result(url_results, playlist_id, playlist_title)] class YoutubeChannelIE(InfoExtractor): From 8c416ad29a43bb1a73a0a29b82c0e1baef1ee90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sat, 20 Apr 2013 19:22:45 +0200 Subject: [PATCH 097/221] Remove calls to _downloader.download in Youtube searchs Instead, return the urls of the videos. --- youtube_dl/InfoExtractors.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index c50052a73..86a8ca511 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1510,11 +1510,9 @@ class YoutubeSearchIE(InfoExtractor): prefix = prefix[8:] query = query.encode('utf-8') if prefix == '': - self._download_n_results(query, 1) - return + return self._get_n_results(query, 1) elif prefix == 'all': - self._download_n_results(query, self._max_youtube_results) - return + self._get_n_results(query, self._max_youtube_results) else: try: n = int(prefix) @@ -1524,14 +1522,12 @@ class YoutubeSearchIE(InfoExtractor): elif n > self._max_youtube_results: self._downloader.report_warning(u'ytsearch returns max %i results (you requested %i)' % (self._max_youtube_results, n)) n = self._max_youtube_results - self._download_n_results(query, n) - return + return self._get_n_results(query, n) except ValueError: # parsing prefix as integer fails - self._download_n_results(query, 1) - return + return self._get_n_results(query, 1) - def _download_n_results(self, query, n): - """Downloads a specified number of results for a query""" + def _get_n_results(self, query, n): + """Get a specified number of results for a query""" video_ids = [] pagenum = 0 @@ -1560,9 +1556,8 @@ class YoutubeSearchIE(InfoExtractor): if len(video_ids) > n: video_ids = video_ids[:n] - for id in video_ids: - self._downloader.download(['http://www.youtube.com/watch?v=%s' % id]) - return + videos = [self.url_result('http://www.youtube.com/watch?v=%s' % id, 'Youtube') for id in video_ids] + return videos class GoogleSearchIE(InfoExtractor): From f17ce13a9260919c4d8cb652467023373c783bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sat, 20 Apr 2013 19:35:49 +0200 Subject: [PATCH 098/221] Write the method to_screen in InfoExtractor (related #608) Except the ones in youtube subtypes (user, channels ..) all calls to _downloader.to_screen has been changed. The calls not prefixed with the IE name hasn't been touched. --- youtube_dl/InfoExtractors.py | 156 ++++++++++++++++++----------------- 1 file changed, 80 insertions(+), 76 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 86a8ca511..d444e21d9 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -116,7 +116,7 @@ class InfoExtractor(object): if note is None: note = u'Downloading video webpage' if note is not False: - self._downloader.to_screen(u'[%s] %s: %s' % (self.IE_NAME, video_id, note)) + self.to_screen(u'%s: %s' % (video_id, note)) try: return compat_urllib_request.urlopen(url_or_request) except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: @@ -139,11 +139,15 @@ class InfoExtractor(object): url = url_or_request.get_full_url() except AttributeError: url = url_or_request - self._downloader.to_screen(u'Dumping request to ' + url) + self.to_screen(u'Dumping request to ' + url) dump = base64.b64encode(webpage_bytes).decode('ascii') self._downloader.to_screen(dump) return webpage_bytes.decode(encoding, 'replace') - + + def to_screen(self, msg): + """Print msg to screen, prefixing it with '[ie_name]'""" + self._downloader.to_screen(u'[%s] %s' % (self.IE_NAME, msg)) + #Methods for following #608 #They set the correct value of the '_type' key def video_result(self, video_info): @@ -236,48 +240,48 @@ class YoutubeIE(InfoExtractor): def report_lang(self): """Report attempt to set language.""" - self._downloader.to_screen(u'[youtube] Setting language') + self.to_screen(u'Setting language') def report_login(self): """Report attempt to log in.""" - self._downloader.to_screen(u'[youtube] Logging in') + self.to_screen(u'Logging in') def report_age_confirmation(self): """Report attempt to confirm age.""" - self._downloader.to_screen(u'[youtube] Confirming age') + self.to_screen(u'Confirming age') def report_video_webpage_download(self, video_id): """Report attempt to download video webpage.""" - self._downloader.to_screen(u'[youtube] %s: Downloading video webpage' % video_id) + self.to_screen(u'%s: Downloading video webpage' % video_id) def report_video_info_webpage_download(self, video_id): """Report attempt to download video info webpage.""" - self._downloader.to_screen(u'[youtube] %s: Downloading video info webpage' % video_id) + self.to_screen(u'%s: Downloading video info webpage' % video_id) def report_video_subtitles_download(self, video_id): """Report attempt to download video info webpage.""" - self._downloader.to_screen(u'[youtube] %s: Checking available subtitles' % video_id) + self.to_screen(u'%s: Checking available subtitles' % video_id) def report_video_subtitles_request(self, video_id, sub_lang, format): """Report attempt to download video info webpage.""" - self._downloader.to_screen(u'[youtube] %s: Downloading video subtitles for %s.%s' % (video_id, sub_lang, format)) + self.to_screen(u'%s: Downloading video subtitles for %s.%s' % (video_id, sub_lang, format)) def report_video_subtitles_available(self, video_id, sub_lang_list): """Report available subtitles.""" sub_lang = ",".join(list(sub_lang_list.keys())) - self._downloader.to_screen(u'[youtube] %s: Available subtitles for video: %s' % (video_id, sub_lang)) + self.to_screen(u'%s: Available subtitles for video: %s' % (video_id, sub_lang)) def report_information_extraction(self, video_id): """Report attempt to extract video information.""" - self._downloader.to_screen(u'[youtube] %s: Extracting video information' % video_id) + self.to_screen(u'%s: Extracting video information' % video_id) def report_unavailable_format(self, video_id, format): """Report extracted video URL.""" - self._downloader.to_screen(u'[youtube] %s: Format %s not available' % (video_id, format)) + self.to_screen(u'%s: Format %s not available' % (video_id, format)) def report_rtmp_download(self): """Indicate the download will use the RTMP protocol.""" - self._downloader.to_screen(u'[youtube] RTMP download detected') + self.to_screen(u'RTMP download detected') def _get_available_subtitles(self, video_id): self.report_video_subtitles_download(video_id) @@ -680,19 +684,19 @@ class MetacafeIE(InfoExtractor): def report_disclaimer(self): """Report disclaimer retrieval.""" - self._downloader.to_screen(u'[metacafe] Retrieving disclaimer') + self.to_screen(u'Retrieving disclaimer') def report_age_confirmation(self): """Report attempt to confirm age.""" - self._downloader.to_screen(u'[metacafe] Confirming age') + self.to_screen(u'Confirming age') def report_download_webpage(self, video_id): """Report webpage download.""" - self._downloader.to_screen(u'[metacafe] %s: Downloading webpage' % video_id) + self.to_screen(u'%s: Downloading webpage' % video_id) def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[metacafe] %s: Extracting information' % video_id) + self.to_screen(u'%s: Extracting information' % video_id) def _real_initialize(self): # Retrieve disclaimer @@ -799,7 +803,7 @@ class DailymotionIE(InfoExtractor): def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[dailymotion] %s: Extracting information' % video_id) + self.to_screen(u'%s: Extracting information' % video_id) def _real_extract(self, url): # Extract id and simplified title from URL @@ -828,7 +832,7 @@ class DailymotionIE(InfoExtractor): for key in ['hd1080URL', 'hd720URL', 'hqURL', 'sdURL', 'ldURL', 'video_url']: if key in flashvars: max_quality = key - self._downloader.to_screen(u'[dailymotion] Using %s' % key) + self.to_screen(u'Using %s' % key) break else: self._downloader.report_error(u'unable to extract video URL') @@ -887,11 +891,11 @@ class PhotobucketIE(InfoExtractor): def report_download_webpage(self, video_id): """Report webpage download.""" - self._downloader.to_screen(u'[photobucket] %s: Downloading webpage' % video_id) + self.to_screen(u'%s: Downloading webpage' % video_id) def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[photobucket] %s: Extracting information' % video_id) + self.to_screen(u'%s: Extracting information' % video_id) def _real_extract(self, url): # Extract id from URL @@ -956,11 +960,11 @@ class YahooIE(InfoExtractor): def report_download_webpage(self, video_id): """Report webpage download.""" - self._downloader.to_screen(u'[video.yahoo] %s: Downloading webpage' % video_id) + self.to_screen(u'%s: Downloading webpage' % video_id) def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[video.yahoo] %s: Extracting information' % video_id) + self.to_screen(u'%s: Extracting information' % video_id) def _real_extract(self, url, new_video=True): # Extract ID from URL @@ -1096,11 +1100,11 @@ class VimeoIE(InfoExtractor): def report_download_webpage(self, video_id): """Report webpage download.""" - self._downloader.to_screen(u'[vimeo] %s: Downloading webpage' % video_id) + self.to_screen(u'%s: Downloading webpage' % video_id) def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[vimeo] %s: Extracting information' % video_id) + self.to_screen(u'%s: Extracting information' % video_id) def _real_extract(self, url, new_video=True): # Extract ID from URL @@ -1182,7 +1186,7 @@ class VimeoIE(InfoExtractor): video_quality = files[quality][0][2] video_codec = files[quality][0][0] video_extension = files[quality][0][1] - self._downloader.to_screen(u'[vimeo] %s: Downloading %s file at %s quality' % (video_id, video_codec.upper(), video_quality)) + self.to_screen(u'%s: Downloading %s file at %s quality' % (video_id, video_codec.upper(), video_quality)) break else: self._downloader.report_error(u'no known codec found') @@ -1217,11 +1221,11 @@ class ArteTvIE(InfoExtractor): def report_download_webpage(self, video_id): """Report webpage download.""" - self._downloader.to_screen(u'[arte.tv] %s: Downloading webpage' % video_id) + self.to_screen(u'%s: Downloading webpage' % video_id) def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[arte.tv] %s: Extracting information' % video_id) + self.to_screen(u'%s: Extracting information' % video_id) def fetch_webpage(self, url): request = compat_urllib_request.Request(url) @@ -1352,12 +1356,12 @@ class GenericIE(InfoExtractor): def report_download_webpage(self, video_id): """Report webpage download.""" if not self._downloader.params.get('test', False): - self._downloader.to_screen(u'WARNING: Falling back on generic information extractor.') - self._downloader.to_screen(u'[generic] %s: Downloading webpage' % video_id) + self._downloader.report_warning(u'Falling back on generic information extractor.') + self.to_screen(u'%s: Downloading webpage' % video_id) def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[generic] %s: Extracting information' % video_id) + self.to_screen(u'%s: Extracting information' % video_id) def report_following_redirect(self, new_url): """Report information extraction.""" @@ -1575,7 +1579,7 @@ class GoogleSearchIE(InfoExtractor): def report_download_page(self, query, pagenum): """Report attempt to download playlist page with given number.""" query = query.decode(preferredencoding()) - self._downloader.to_screen(u'[video.google] query "%s": Downloading page %s' % (query, pagenum)) + self.to_screen(u'query "%s": Downloading page %s' % (query, pagenum)) def _real_extract(self, query): mobj = re.match(self._VALID_URL, query) @@ -1659,7 +1663,7 @@ class YahooSearchIE(InfoExtractor): def report_download_page(self, query, pagenum): """Report attempt to download playlist page with given number.""" query = query.decode(preferredencoding()) - self._downloader.to_screen(u'[video.yahoo] query "%s": Downloading page %s' % (query, pagenum)) + self.to_screen(u'query "%s": Downloading page %s' % (query, pagenum)) def _real_extract(self, query): mobj = re.match(self._VALID_URL, query) @@ -1970,8 +1974,8 @@ class BlipTVUserIE(InfoExtractor): def report_download_page(self, username, pagenum): """Report attempt to download user page.""" - self._downloader.to_screen(u'[%s] user %s: Downloading video ids from page %d' % - (self.IE_NAME, username, pagenum)) + self.to_screen(u'user %s: Downloading video ids from page %d' % + (username, pagenum)) def _real_extract(self, url): # Extract username @@ -2045,11 +2049,11 @@ class DepositFilesIE(InfoExtractor): def report_download_webpage(self, file_id): """Report webpage download.""" - self._downloader.to_screen(u'[DepositFiles] %s: Downloading webpage' % file_id) + self.to_screen(u'%s: Downloading webpage' % file_id) def report_extraction(self, file_id): """Report information extraction.""" - self._downloader.to_screen(u'[DepositFiles] %s: Extracting information' % file_id) + self.to_screen(u'%s: Extracting information' % file_id) def _real_extract(self, url): file_id = url.split('/')[-1] @@ -2108,7 +2112,7 @@ class FacebookIE(InfoExtractor): def report_login(self): """Report attempt to log in.""" - self._downloader.to_screen(u'[%s] Logging in' % self.IE_NAME) + self.to_screen(u'Logging in') def _real_initialize(self): if self._downloader is None: @@ -2206,11 +2210,11 @@ class BlipTVIE(InfoExtractor): def report_extraction(self, file_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, file_id)) + self.to_screen(u'%s: Extracting information' % file_id) def report_direct_download(self, title): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Direct download detected' % (self.IE_NAME, title)) + self.to_screen(u'%s: Direct download detected' % title) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) @@ -2310,7 +2314,7 @@ class MyVideoIE(InfoExtractor): def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[myvideo] %s: Extracting information' % video_id) + self.to_screen(u'%s: Extracting information' % video_id) def _real_extract(self,url): mobj = re.match(self._VALID_URL, url) @@ -2390,13 +2394,13 @@ class ComedyCentralIE(InfoExtractor): return re.match(cls._VALID_URL, url, re.VERBOSE) is not None def report_extraction(self, episode_id): - self._downloader.to_screen(u'[comedycentral] %s: Extracting information' % episode_id) + self.to_screen(u'%s: Extracting information' % 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)) + self.to_screen(u'%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) + self.to_screen(u'%s: Downloading show index' % episode_id) def _print_formats(self, formats): print('Available formats:') @@ -2551,10 +2555,10 @@ class EscapistIE(InfoExtractor): IE_NAME = u'escapist' def report_extraction(self, showName): - self._downloader.to_screen(u'[escapist] %s: Extracting information' % showName) + self.to_screen(u'%s: Extracting information' % showName) def report_config_download(self, showName): - self._downloader.to_screen(u'[escapist] %s: Downloading configuration' % showName) + self.to_screen(u'%s: Downloading configuration' % showName) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) @@ -2627,11 +2631,11 @@ class CollegeHumorIE(InfoExtractor): def report_manifest(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Downloading XML manifest' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Downloading XML manifest' % video_id) def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Extracting information' % video_id) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) @@ -2698,7 +2702,7 @@ class XVideosIE(InfoExtractor): def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Extracting information' % video_id) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) @@ -2766,11 +2770,11 @@ class SoundcloudIE(InfoExtractor): def report_resolve(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Resolving id' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Resolving id' % video_id) def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Retrieving stream' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Retrieving stream' % video_id) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) @@ -2839,11 +2843,11 @@ class SoundcloudSetIE(InfoExtractor): def report_resolve(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Resolving id' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Resolving id' % video_id) def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Retrieving stream' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Retrieving stream' % video_id) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) @@ -2910,7 +2914,7 @@ class InfoQIE(InfoExtractor): def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Extracting information' % video_id) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) @@ -2970,11 +2974,11 @@ class MixcloudIE(InfoExtractor): def report_download_json(self, file_id): """Report JSON download.""" - self._downloader.to_screen(u'[%s] Downloading json' % self.IE_NAME) + self.to_screen(u'Downloading json') def report_extraction(self, file_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, file_id)) + self.to_screen(u'%s: Extracting information' % file_id) def get_urls(self, jsonData, fmt, bitrate='best'): """Get urls from 'audio_formats' section in json""" @@ -3081,11 +3085,11 @@ class StanfordOpenClassroomIE(InfoExtractor): def report_download_webpage(self, objid): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Downloading webpage' % (self.IE_NAME, objid)) + self.to_screen(u'%s: Downloading webpage' % objid) def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Extracting information' % video_id) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) @@ -3193,7 +3197,7 @@ class MTVIE(InfoExtractor): def report_extraction(self, video_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Extracting information' % video_id) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) @@ -3271,11 +3275,11 @@ class YoukuIE(InfoExtractor): def report_download_webpage(self, file_id): """Report webpage download.""" - self._downloader.to_screen(u'[%s] %s: Downloading webpage' % (self.IE_NAME, file_id)) + self.to_screen(u'%s: Downloading webpage' % file_id) def report_extraction(self, file_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, file_id)) + self.to_screen(u'%s: Extracting information' % file_id) def _gen_sid(self): nowTime = int(time.time() * 1000) @@ -3388,11 +3392,11 @@ class XNXXIE(InfoExtractor): def report_webpage(self, video_id): """Report information extraction""" - self._downloader.to_screen(u'[%s] %s: Downloading webpage' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Downloading webpage' % video_id) def report_extraction(self, video_id): """Report information extraction""" - self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, video_id)) + self.to_screen(u'%s: Extracting information' % video_id) def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) @@ -3452,23 +3456,23 @@ class GooglePlusIE(InfoExtractor): def report_extract_entry(self, url): """Report downloading extry""" - self._downloader.to_screen(u'[plus.google] Downloading entry: %s' % url) + self.to_screen(u'Downloading entry: %s' % url) def report_date(self, upload_date): """Report downloading extry""" - self._downloader.to_screen(u'[plus.google] Entry date: %s' % upload_date) + self.to_screen(u'Entry date: %s' % upload_date) def report_uploader(self, uploader): """Report downloading extry""" - self._downloader.to_screen(u'[plus.google] Uploader: %s' % uploader) + self.to_screen(u'Uploader: %s' % uploader) def report_title(self, video_title): """Report downloading extry""" - self._downloader.to_screen(u'[plus.google] Title: %s' % video_title) + self.to_screen(u'Title: %s' % video_title) def report_extract_vid_page(self, video_page): """Report information extraction.""" - self._downloader.to_screen(u'[plus.google] Extracting video page: %s' % video_page) + self.to_screen(u'Extracting video page: %s' % video_page) def _real_extract(self, url): # Extract id from URL @@ -3614,12 +3618,12 @@ class JustinTVIE(InfoExtractor): def report_extraction(self, file_id): """Report information extraction.""" - self._downloader.to_screen(u'[%s] %s: Extracting information' % (self.IE_NAME, file_id)) + self.to_screen(u'%s: Extracting information' % file_id) def report_download_page(self, channel, offset): """Report attempt to download a single page of videos.""" - self._downloader.to_screen(u'[%s] %s: Downloading video information from %d to %d' % - (self.IE_NAME, channel, offset, offset + self._JUSTIN_PAGE_LIMIT)) + self.to_screen(u'%s: Downloading video information from %d to %d' % + (channel, offset, offset + self._JUSTIN_PAGE_LIMIT)) # Return count of items, list of *valid* items def _parse_page(self, url): @@ -3948,7 +3952,7 @@ class YouPornIE(InfoExtractor): if(len(links) == 0): raise ExtractorError(u'ERROR: no known formats available for video') - self._downloader.to_screen(u'[youporn] Links found: %d' % len(links)) + self.to_screen(u'Links found: %d' % len(links)) formats = [] for link in links: @@ -3984,7 +3988,7 @@ class YouPornIE(InfoExtractor): return req_format = self._downloader.params.get('format', None) - self._downloader.to_screen(u'[youporn] Format: %s' % req_format) + self.to_screen(u'Format: %s' % req_format) if req_format is None or req_format == 'best': return [formats[0]] @@ -4179,7 +4183,7 @@ class TEDIE(InfoExtractor): else : playlist_id=m.group('playlist_id') name=m.group('name') - self._downloader.to_screen(u'[%s] Getting info of playlist %s: "%s"' % (self.IE_NAME,playlist_id,name)) + self.to_screen(u'Getting info of playlist %s: "%s"' % (playlist_id,name)) return [self._playlist_videos_info(url,name,playlist_id)] def _talk_video_link(self,mediaSlug): @@ -4404,7 +4408,7 @@ class ARDIE(InfoExtractor): # there's two possibilities: RTMP stream or HTTP download info = {'id': video_id, 'title': title, 'ext': 'mp4'} if stream['rtmp_url']: - self._downloader.to_screen(u'[%s] RTMP download detected' % self.IE_NAME) + self.to_screen(u'RTMP download detected') assert stream['video_url'].startswith('mp4:') info["url"] = stream["rtmp_url"] info["play_path"] = stream['video_url'] From 41a6eb949a625983c55cdae0c06410392f3a3110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sat, 20 Apr 2013 21:12:29 +0200 Subject: [PATCH 099/221] Clean duplicate method report_extraction in InfoExtractors A lot of IEs had implemented the method in the same way. --- youtube_dl/InfoExtractors.py | 94 ++---------------------------------- 1 file changed, 4 insertions(+), 90 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index d444e21d9..7c9f09f77 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -148,6 +148,10 @@ class InfoExtractor(object): """Print msg to screen, prefixing it with '[ie_name]'""" self._downloader.to_screen(u'[%s] %s' % (self.IE_NAME, msg)) + def report_extraction(self, id_or_name): + """Report information extraction.""" + self.to_screen(u'%s: Extracting information' % id_or_name) + #Methods for following #608 #They set the correct value of the '_type' key def video_result(self, video_info): @@ -694,10 +698,6 @@ class MetacafeIE(InfoExtractor): """Report webpage download.""" self.to_screen(u'%s: Downloading webpage' % video_id) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_initialize(self): # Retrieve disclaimer request = compat_urllib_request.Request(self._DISCLAIMER) @@ -801,10 +801,6 @@ class DailymotionIE(InfoExtractor): def __init__(self, downloader=None): InfoExtractor.__init__(self, downloader) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_extract(self, url): # Extract id and simplified title from URL mobj = re.match(self._VALID_URL, url) @@ -893,10 +889,6 @@ class PhotobucketIE(InfoExtractor): """Report webpage download.""" self.to_screen(u'%s: Downloading webpage' % video_id) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_extract(self, url): # Extract id from URL mobj = re.match(self._VALID_URL, url) @@ -962,10 +954,6 @@ class YahooIE(InfoExtractor): """Report webpage download.""" self.to_screen(u'%s: Downloading webpage' % video_id) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_extract(self, url, new_video=True): # Extract ID from URL mobj = re.match(self._VALID_URL, url) @@ -1102,10 +1090,6 @@ class VimeoIE(InfoExtractor): """Report webpage download.""" self.to_screen(u'%s: Downloading webpage' % video_id) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_extract(self, url, new_video=True): # Extract ID from URL mobj = re.match(self._VALID_URL, url) @@ -1223,10 +1207,6 @@ class ArteTvIE(InfoExtractor): """Report webpage download.""" self.to_screen(u'%s: Downloading webpage' % video_id) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def fetch_webpage(self, url): request = compat_urllib_request.Request(url) try: @@ -1359,10 +1339,6 @@ class GenericIE(InfoExtractor): self._downloader.report_warning(u'Falling back on generic information extractor.') self.to_screen(u'%s: Downloading webpage' % video_id) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def report_following_redirect(self, new_url): """Report information extraction.""" self._downloader.to_screen(u'[redirect] Following redirect to %s' % new_url) @@ -2051,10 +2027,6 @@ class DepositFilesIE(InfoExtractor): """Report webpage download.""" self.to_screen(u'%s: Downloading webpage' % file_id) - def report_extraction(self, file_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % file_id) - def _real_extract(self, url): file_id = url.split('/')[-1] # Rebuild url in english locale @@ -2208,10 +2180,6 @@ class BlipTVIE(InfoExtractor): _URL_EXT = r'^.*\.([a-z0-9]+)$' IE_NAME = u'blip.tv' - def report_extraction(self, file_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % file_id) - def report_direct_download(self, title): """Report information extraction.""" self.to_screen(u'%s: Direct download detected' % title) @@ -2312,10 +2280,6 @@ class MyVideoIE(InfoExtractor): def __init__(self, downloader=None): InfoExtractor.__init__(self, downloader) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_extract(self,url): mobj = re.match(self._VALID_URL, url) if mobj is None: @@ -2393,9 +2357,6 @@ class ComedyCentralIE(InfoExtractor): """Receives a URL and returns True if suitable for this IE.""" return re.match(cls._VALID_URL, url, re.VERBOSE) is not None - def report_extraction(self, episode_id): - self.to_screen(u'%s: Extracting information' % episode_id) - def report_config_download(self, episode_id, media_id): self.to_screen(u'%s: Downloading configuration for %s' % (episode_id, media_id)) @@ -2554,9 +2515,6 @@ class EscapistIE(InfoExtractor): _VALID_URL = r'^(https?://)?(www\.)?escapistmagazine\.com/videos/view/(?P[^/]+)/(?P[^/?]+)[/?]?.*$' IE_NAME = u'escapist' - def report_extraction(self, showName): - self.to_screen(u'%s: Extracting information' % showName) - def report_config_download(self, showName): self.to_screen(u'%s: Downloading configuration' % showName) @@ -2633,10 +2591,6 @@ class CollegeHumorIE(InfoExtractor): """Report information extraction.""" self.to_screen(u'%s: Downloading XML manifest' % video_id) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: @@ -2700,10 +2654,6 @@ class XVideosIE(InfoExtractor): _VALID_URL = r'^(?:https?://)?(?:www\.)?xvideos\.com/video([0-9]+)(?:.*)' IE_NAME = u'xvideos' - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: @@ -2772,10 +2722,6 @@ class SoundcloudIE(InfoExtractor): """Report information extraction.""" self.to_screen(u'%s: Resolving id' % video_id) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Retrieving stream' % video_id) - def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: @@ -2845,10 +2791,6 @@ class SoundcloudSetIE(InfoExtractor): """Report information extraction.""" self.to_screen(u'%s: Resolving id' % video_id) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Retrieving stream' % video_id) - def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: @@ -2912,10 +2854,6 @@ class InfoQIE(InfoExtractor): """Information extractor for infoq.com""" _VALID_URL = r'^(?:https?://)?(?:www\.)?infoq\.com/[^/]+/[^/]+$' - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: @@ -2976,10 +2914,6 @@ class MixcloudIE(InfoExtractor): """Report JSON download.""" self.to_screen(u'Downloading json') - def report_extraction(self, file_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % file_id) - def get_urls(self, jsonData, fmt, bitrate='best'): """Get urls from 'audio_formats' section in json""" file_url = None @@ -3087,10 +3021,6 @@ class StanfordOpenClassroomIE(InfoExtractor): """Report information extraction.""" self.to_screen(u'%s: Downloading webpage' % objid) - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: @@ -3195,10 +3125,6 @@ class MTVIE(InfoExtractor): _VALID_URL = r'^(?Phttps?://)?(?:www\.)?mtv\.com/videos/[^/]+/(?P[0-9]+)/[^/]+$' IE_NAME = u'mtv' - def report_extraction(self, video_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: @@ -3277,10 +3203,6 @@ class YoukuIE(InfoExtractor): """Report webpage download.""" self.to_screen(u'%s: Downloading webpage' % file_id) - def report_extraction(self, file_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % file_id) - def _gen_sid(self): nowTime = int(time.time() * 1000) random1 = random.randint(1000,1998) @@ -3394,10 +3316,6 @@ class XNXXIE(InfoExtractor): """Report information extraction""" self.to_screen(u'%s: Downloading webpage' % video_id) - def report_extraction(self, video_id): - """Report information extraction""" - self.to_screen(u'%s: Extracting information' % video_id) - def _real_extract(self, url): mobj = re.match(self._VALID_URL, url) if mobj is None: @@ -3616,10 +3534,6 @@ class JustinTVIE(InfoExtractor): _JUSTIN_PAGE_LIMIT = 100 IE_NAME = u'justin.tv' - def report_extraction(self, file_id): - """Report information extraction.""" - self.to_screen(u'%s: Extracting information' % file_id) - def report_download_page(self, channel, offset): """Report attempt to download a single page of videos.""" self.to_screen(u'%s: Downloading video information from %d to %d' % From b0936ef42365ce7f2bd5b49e39d0ed42c4c3684e Mon Sep 17 00:00:00 2001 From: Alexander van Gessel Date: Sun, 21 Apr 2013 02:38:37 +0300 Subject: [PATCH 100/221] Use standard unit symbols in format_bytes --- youtube_dl/FileDownloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 9c0c42f8d..19766153c 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -121,7 +121,7 @@ class FileDownloader(object): exponent = 0 else: exponent = int(math.log(bytes, 1024.0)) - suffix = 'bkMGTPEZY'[exponent] + suffix = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'][exponent] converted = float(bytes) / float(1024 ** exponent) return '%.2f%s' % (converted, suffix) From c04bca6f604ba84995ef29a0361d85b1bc180fee Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Sun, 21 Apr 2013 12:52:45 +0200 Subject: [PATCH 101/221] release 2013.04.21 --- youtube_dl/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/version.py b/youtube_dl/version.py index 2fd5f40c8..33696b54b 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.04.18' +__version__ = '2013.04.21' From e11eb11906e7054e8dc14c86073546935acacae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sun, 21 Apr 2013 21:56:13 +0200 Subject: [PATCH 102/221] Allow to download videos with age check from Steam Also move method report_age_confirmation to the base IE class. --- youtube_dl/InfoExtractors.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 7c9f09f77..6ff0d49d7 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -152,6 +152,10 @@ class InfoExtractor(object): """Report information extraction.""" self.to_screen(u'%s: Extracting information' % id_or_name) + def report_age_confirmation(self): + """Report attempt to confirm age.""" + self.to_screen(u'Confirming age') + #Methods for following #608 #They set the correct value of the '_type' key def video_result(self, video_info): @@ -250,10 +254,6 @@ class YoutubeIE(InfoExtractor): """Report attempt to log in.""" self.to_screen(u'Logging in') - def report_age_confirmation(self): - """Report attempt to confirm age.""" - self.to_screen(u'Confirming age') - def report_video_webpage_download(self, video_id): """Report attempt to download video webpage.""" self.to_screen(u'%s: Downloading video webpage' % video_id) @@ -690,10 +690,6 @@ class MetacafeIE(InfoExtractor): """Report disclaimer retrieval.""" self.to_screen(u'Retrieving disclaimer') - def report_age_confirmation(self): - """Report attempt to confirm age.""" - self.to_screen(u'Confirming age') - def report_download_webpage(self, video_id): """Report webpage download.""" self.to_screen(u'%s: Downloading webpage' % video_id) @@ -3661,7 +3657,8 @@ class SteamIE(InfoExtractor): m = re.match(self._VALID_URL, url, re.VERBOSE) urlRE = r"'movie_(?P\d+)': \{\s*FILENAME: \"(?P[\w:/\.\?=]+)\"(,\s*MOVIE_NAME: \"(?P[\w:/\.\?=\+-]+)\")?\s*\}," gameID = m.group('gameID') - videourl = 'http://store.steampowered.com/video/%s/' % gameID + videourl = 'http://store.steampowered.com/agecheck/video/%s/?snr=1_agecheck_agecheck__age-gate&ageDay=1&ageMonth=January&ageYear=1970' % gameID + self.report_age_confirmation() webpage = self._download_webpage(videourl, gameID) mweb = re.finditer(urlRE, webpage) namesRE = r'(?P.+?)' From 9e1cf0c2004563015d69c5a01aa419c4b70ad696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sun, 21 Apr 2013 22:05:21 +0200 Subject: [PATCH 103/221] SteamIE returns a playlist With the game name as title. --- youtube_dl/InfoExtractors.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 6ff0d49d7..14a1d6523 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3655,11 +3655,13 @@ class SteamIE(InfoExtractor): def _real_extract(self, url): m = re.match(self._VALID_URL, url, re.VERBOSE) - urlRE = r"'movie_(?P\d+)': \{\s*FILENAME: \"(?P[\w:/\.\?=]+)\"(,\s*MOVIE_NAME: \"(?P[\w:/\.\?=\+-]+)\")?\s*\}," gameID = m.group('gameID') videourl = 'http://store.steampowered.com/agecheck/video/%s/?snr=1_agecheck_agecheck__age-gate&ageDay=1&ageMonth=January&ageYear=1970' % gameID self.report_age_confirmation() webpage = self._download_webpage(videourl, gameID) + game_title = re.search(r'', webpage).group('game_title') + + urlRE = r"'movie_(?P\d+)': \{\s*FILENAME: \"(?P[\w:/\.\?=]+)\"(,\s*MOVIE_NAME: \"(?P[\w:/\.\?=\+-]+)\")?\s*\}," mweb = re.finditer(urlRE, webpage) namesRE = r'(?P.+?)' titles = re.finditer(namesRE, webpage) @@ -3681,7 +3683,7 @@ class SteamIE(InfoExtractor): 'thumbnail': video_thumb } videos.append(info) - return videos + return [self.playlist_result(videos, gameID, game_title)] class UstreamIE(InfoExtractor): _VALID_URL = r'https?://www\.ustream\.tv/recorded/(?P\d+)' From 74e3452b9e15cb377984a1bf4764f2228c150717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Mon, 22 Apr 2013 10:06:07 +0200 Subject: [PATCH 104/221] Add playlist and playlist_index to the help string for the output option Also split the help string in different lines to make editing easier. --- youtube_dl/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index f46143e01..bac359ac7 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -238,7 +238,16 @@ def parseOpts(): action='store_true', dest='autonumber', help='number downloaded files starting from 00000', default=False) filesystem.add_option('-o', '--output', - dest='outtmpl', metavar='TEMPLATE', help='output filename template. Use %(title)s to get the title, %(uploader)s for the uploader name, %(uploader_id)s for the uploader nickname if different, %(autonumber)s to get an automatically incremented number, %(ext)s for the filename extension, %(upload_date)s for the upload date (YYYYMMDD), %(extractor)s for the provider (youtube, metacafe, etc), %(id)s for the video id and %% for a literal percent. Use - to output to stdout. Can also be used to download to a different directory, for example with -o \'/my/downloads/%(uploader)s/%(title)s-%(id)s.%(ext)s\' .') + dest='outtmpl', metavar='TEMPLATE', + help=('output filename template. Use %(title)s to get the title, ' + '%(uploader)s for the uploader name, %(uploader_id)s for the uploader nickname if different, ' + '%(autonumber)s to get an automatically incremented number, ' + '%(ext)s for the filename extension, %(upload_date)s for the upload date (YYYYMMDD), ' + '%(extractor)s for the provider (youtube, metacafe, etc), ' + '%(id)s for the video id , %(playlist)s for the playlist the video is in, ' + '%(playlist_index)s for the position in the playlist and %% for a literal percent. ' + 'Use - to output to stdout. Can also be used to download to a different directory, ' + 'for example with -o \'/my/downloads/%(uploader)s/%(title)s-%(id)s.%(ext)s\' .')) filesystem.add_option('--autonumber-size', dest='autonumber_size', metavar='NUMBER', help='Specifies the number of digits in %(autonumber)s when it is present in output filename template or --autonumber option is given') From 30f2999962ef411abc7c191e66b4c40ed5b86db5 Mon Sep 17 00:00:00 2001 From: Finn Petersen <4peterse@googlemail.com> Date: Mon, 22 Apr 2013 10:15:58 +0200 Subject: [PATCH 105/221] Added parenthesis for explicity --- youtube_dl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index b339427e8..87d3f222a 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -299,7 +299,7 @@ def parseOpts(arguments): systemConf = _readOptions('/etc/youtube-dl.conf') userConf = _readOptions(userConfFile) commandLineConf = sys.argv[1:] - argv = systemConf + userConf + commandLineConf if not arguments else arguments + argv = (systemConf + userConf + commandLineConf) if not arguments else arguments opts, args = parser.parse_args(argv) if opts.verbose: From c681a03918a8f67f25e3ae7cc4140b76d6a73578 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Mon, 22 Apr 2013 19:51:42 +0200 Subject: [PATCH 106/221] Fix --list-formats (Closes #799) --- youtube_dl/FileDownloader.py | 2 ++ youtube_dl/InfoExtractors.py | 9 +++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 9c0c42f8d..7139adf6b 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -458,6 +458,8 @@ class FileDownloader(object): # Extract information from URL and process it try: ie_results = ie.extract(url) + if ie_results is None: # Finished already (backwards compatibility; listformats and friends should be moved here) + break results = [] for ie_result in ie_results: if not 'extractor' in ie_result: diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 14a1d6523..ff1f07e9b 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -622,8 +622,7 @@ class YoutubeIE(InfoExtractor): format_list = available_formats existing_formats = [x for x in format_list if x in url_map] if len(existing_formats) == 0: - self._downloader.report_error(u'no known formats available for video') - return + raise ExtractorError(u'no known formats available for video') if self._downloader.params.get('listformats', None): self._print_formats(existing_formats) return @@ -643,11 +642,9 @@ class YoutubeIE(InfoExtractor): video_url_list = [(rf, url_map[rf])] break if video_url_list is None: - self._downloader.report_error(u'requested format not available') - return + raise ExtractorError(u'requested format not available') else: - self._downloader.report_error(u'no conn or url_encoded_fmt_stream_map information found in video info') - return + raise ExtractorError(u'no conn or url_encoded_fmt_stream_map information found in video info') results = [] for format_param, video_real_url in video_url_list: From 8cb94542f407a748d75e39a0f7730c68f02d23cf Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Mon, 22 Apr 2013 20:01:56 +0200 Subject: [PATCH 107/221] release 2013.04.22 --- README.md | 10 ++++++---- youtube_dl/version.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d42aab44a..5c7d8b66a 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,12 @@ which means you can modify it, redistribute it or use it however you like. extension, %(upload_date)s for the upload date (YYYYMMDD), %(extractor)s for the provider (youtube, metacafe, etc), %(id)s for the video id - and %% for a literal percent. Use - to output to - stdout. Can also be used to download to a - different directory, for example with -o '/my/dow - nloads/%(uploader)s/%(title)s-%(id)s.%(ext)s' . + , %(playlist)s for the playlist the video is in, + %(playlist_index)s for the position in the + playlist and %% for a literal percent. Use - to + output to stdout. Can also be used to download to + a different directory, for example with -o '/my/d + ownloads/%(uploader)s/%(title)s-%(id)s.%(ext)s' . --autonumber-size NUMBER Specifies the number of digits in %(autonumber)s when it is present in output filename template or --autonumber option is given diff --git a/youtube_dl/version.py b/youtube_dl/version.py index 33696b54b..ac8a05ab5 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.04.21' +__version__ = '2013.04.22' From c15e024141727e5eed8056d3b2732f4f7553b2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Mon, 22 Apr 2013 21:07:49 +0200 Subject: [PATCH 108/221] TumblrIE I haven't found many videos to test, so it may not work for all. --- test/tests.json | 9 +++++++++ youtube_dl/InfoExtractors.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/test/tests.json b/test/tests.json index 7808a07de..2b56738a0 100644 --- a/test/tests.json +++ b/test/tests.json @@ -337,6 +337,15 @@ "title": "11.04.2013 09:23 Uhr - Tagesschau in 100 Sekunden" }, "skip": "Requires rtmpdump" + }, + { + "name": "Tumblr", + "url": "http://birthdayproject2012.tumblr.com/post/17258355236/a-sample-video-from-leeann-if-you-need-an-idea", + "file": "17258355236.mp4", + "md5": "7c6a514d691b034ccf8567999e9e88a3", + "info_dict": { + "title": "A sample video from LeeAnn. (If you need an idea..." + } } ] diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index ff1f07e9b..6a6545c9b 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -4327,6 +4327,40 @@ class ARDIE(InfoExtractor): info["url"] = stream["video_url"] return [info] +class TumblrIE(InfoExtractor): + _VALID_URL = r'http://(?P.*?).tumblr.com/((post)|(video))/(?P\d*)/(.*?)' + + def _real_extract(self, url): + m_url = re.match(self._VALID_URL, url) + video_id = m_url.group('id') + blog = m_url.group('blog_name') + + url = 'http://%s.tumblr.com/post/%s/' % (blog, video_id) + webpage = self._download_webpage(url, video_id) + + re_video = r'src=\\x22(?Phttp://%s.tumblr.com/video_file/%s/(.*?))\\x22 type=\\x22video/(?P.*?)\\x22' % (blog, video_id) + video = re.search(re_video, webpage) + if video is None: + self.to_screen("No video founded") + return [] + video_url = video.group('video_url') + ext = video.group('ext') + + re_thumb = r'posters(.*?)\[\\x22(?P.*?)\\x22' # We pick the first poster + thumb = re.search(re_thumb, webpage).group('thumb').replace('\\', '') + + # The only place where you can get a title, it's not complete, + # but searching in other places doesn't work for all videos + re_title = r'(.*?) - (?P<title>.*?)' + title = unescapeHTML(re.search(re_title, webpage).group('title')) + + return [{'id': video_id, + 'url': video_url, + 'title': title, + 'thumbnail': thumb, + 'ext': ext + }] + def gen_extractors(): """ Return a list of an instance of every supported extractor. @@ -4381,6 +4415,7 @@ def gen_extractors(): SpiegelIE(), LiveLeakIE(), ARDIE(), + TumblrIE(), GenericIE() ] From 75b5c590a8470b40f5bba869be09393757182ddf Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Mon, 22 Apr 2013 23:05:05 +0200 Subject: [PATCH 109/221] Do not read configuration files if explicit arguments are given by a host program (#792) --- youtube_dl/__init__.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 8ec7435ca..f60fb841e 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -47,7 +47,7 @@ from .FileDownloader import * from .InfoExtractors import gen_extractors from .PostProcessor import * -def parseOpts(arguments): +def parseOpts(overrideArguments=None): def _readOptions(filename_bytes): try: optionf = open(filename_bytes) @@ -300,16 +300,21 @@ def parseOpts(arguments): parser.add_option_group(authentication) parser.add_option_group(postproc) - xdg_config_home = os.environ.get('XDG_CONFIG_HOME') - if xdg_config_home: - userConfFile = os.path.join(xdg_config_home, 'youtube-dl.conf') + if overrideArguments is not None: + opts, args = parser.parse_args(overrideArguments) + if opts.verbose: + print(u'[debug] Override config: ' + repr(overrideArguments)) else: - userConfFile = os.path.join(os.path.expanduser('~'), '.config', 'youtube-dl.conf') - systemConf = _readOptions('/etc/youtube-dl.conf') - userConf = _readOptions(userConfFile) - commandLineConf = sys.argv[1:] - argv = (systemConf + userConf + commandLineConf) if not arguments else arguments - opts, args = parser.parse_args(argv) + xdg_config_home = os.environ.get('XDG_CONFIG_HOME') + if xdg_config_home: + userConfFile = os.path.join(xdg_config_home, 'youtube-dl.conf') + else: + userConfFile = os.path.join(os.path.expanduser('~'), '.config', 'youtube-dl.conf') + systemConf = _readOptions('/etc/youtube-dl.conf') + userConf = _readOptions(userConfFile) + commandLineConf = sys.argv[1:] + argv = systemConf + userConf + commandLineConf + opts, args = parser.parse_args(argv) if opts.verbose: print(u'[debug] System config: ' + repr(systemConf)) From c76cb6d54843695a5221ea314f399eb9cdc61442 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Mon, 22 Apr 2013 23:15:05 +0200 Subject: [PATCH 110/221] Correct indentation --- youtube_dl/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index f60fb841e..7df297d02 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -315,11 +315,10 @@ def parseOpts(overrideArguments=None): commandLineConf = sys.argv[1:] argv = systemConf + userConf + commandLineConf opts, args = parser.parse_args(argv) - - if opts.verbose: - print(u'[debug] System config: ' + repr(systemConf)) - print(u'[debug] User config: ' + repr(userConf)) - print(u'[debug] Command-line args: ' + repr(commandLineConf)) + if opts.verbose: + print(u'[debug] System config: ' + repr(systemConf)) + print(u'[debug] User config: ' + repr(userConf)) + print(u'[debug] Command-line args: ' + repr(commandLineConf)) return parser, opts, args From f4b659f782b65fbc52348c034bfc13ba1f7994ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Tue, 23 Apr 2013 10:33:54 +0200 Subject: [PATCH 111/221] Document order of preference for format selection (closes #798) --- README.md | 3 ++- youtube_dl/__init__.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5c7d8b66a..0ab4b660c 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,8 @@ which means you can modify it, redistribute it or use it however you like. verbose) ## Video Format Options: - -f, --format FORMAT video format code + -f, --format FORMAT video format code, specifiy the order of + preference using slashes: "-f 22/17/18" --all-formats download all available video formats --prefer-free-formats prefer free video formats unless a specific one is requested diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 7df297d02..74375175d 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -165,7 +165,8 @@ def parseOpts(overrideArguments=None): video_format.add_option('-f', '--format', - action='store', dest='format', metavar='FORMAT', help='video format code') + action='store', dest='format', metavar='FORMAT', + help='video format code, specifiy the order of preference using slashes: "-f 22/17/18"') video_format.add_option('--all-formats', action='store_const', dest='format', help='download all available video formats', const='all') video_format.add_option('--prefer-free-formats', From 9edb0916f414d1686e2288c1d544ca7ce2eb280e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Tue, 23 Apr 2013 11:09:22 +0200 Subject: [PATCH 112/221] Disable colored messages in Windows (related #794) --- youtube_dl/FileDownloader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 100779756..30ab68278 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -254,7 +254,7 @@ class FileDownloader(object): Print the message to stderr, it will be prefixed with 'WARNING:' If stderr is a tty file the 'WARNING:' will be colored ''' - if sys.stderr.isatty(): + if sys.stderr.isatty() and os.name != 'nt': _msg_header=u'\033[0;33mWARNING:\033[0m' else: _msg_header=u'WARNING:' @@ -266,7 +266,7 @@ class FileDownloader(object): Do the same as trouble, but prefixes the message with 'ERROR:', colored in red if stderr is a tty file. ''' - if sys.stderr.isatty(): + if sys.stderr.isatty() and os.name != 'nt': _msg_header = u'\033[0;31mERROR:\033[0m' else: _msg_header = u'ERROR:' From 613bf66939c7bbba42f8b80b499661e79f33449c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Tue, 23 Apr 2013 11:31:37 +0200 Subject: [PATCH 113/221] More calls to trouble changed to report_error --- youtube_dl/FileDownloader.py | 14 ++++++------ youtube_dl/InfoExtractors.py | 42 ++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 30ab68278..d0378fb14 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -406,10 +406,10 @@ class FileDownloader(object): filename = self.params['outtmpl'] % template_dict return filename except KeyError as err: - self.trouble(u'ERROR: Erroneous output template') + self.report_error(u'Erroneous output template') return None except ValueError as err: - self.trouble(u'ERROR: Insufficient system charset ' + repr(preferredencoding())) + self.report_error(u'Insufficient system charset ' + repr(preferredencoding())) return None def _match_entry(self, info_dict): @@ -468,16 +468,16 @@ class FileDownloader(object): results.append(self.process_ie_result(ie_result, download)) return results except ExtractorError as de: # An error we somewhat expected - self.trouble(u'ERROR: ' + compat_str(de), de.format_traceback()) + self.report_error(compat_str(de), de.format_traceback()) break except Exception as e: if self.params.get('ignoreerrors', False): - self.trouble(u'ERROR: ' + compat_str(e), tb=compat_str(traceback.format_exc())) + self.report_error(compat_str(e), tb=compat_str(traceback.format_exc())) break else: raise if not suitable_found: - self.trouble(u'ERROR: no suitable InfoExtractor: %s' % url) + self.report_error(u'no suitable InfoExtractor: %s' % url) def process_ie_result(self, ie_result, download = True): """ @@ -636,7 +636,7 @@ class FileDownloader(object): with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile: subfile.write(sub) except (OSError, IOError): - self.trouble(u'ERROR: Cannot write subtitles file ' + descfn) + self.report_error(u'Cannot write subtitles file ' + descfn) return if self.params.get('onlysubtitles', False): return @@ -683,7 +683,7 @@ class FileDownloader(object): #It also downloads the videos videos = self.extract_info(url) except UnavailableVideoError: - self.trouble(u'\nERROR: unable to download video') + self.report_error(u'unable to download video') except MaxDownloadsReached: self.to_screen(u'[info] Maximum number of downloaded files reached.') raise diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 6a6545c9b..5cc0e9195 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1224,7 +1224,7 @@ class ArteTvIE(InfoExtractor): for (i, key, err) in matchTuples: if mobj.group(i) is None: - self._downloader.trouble(err) + self._downloader.report_error(err) return else: info[key] = mobj.group(i) @@ -1238,7 +1238,7 @@ class ArteTvIE(InfoExtractor): r'src="(.*?/videothek_js.*?\.js)', 0, [ - (1, 'url', u'ERROR: Invalid URL: %s' % url) + (1, 'url', u'Invalid URL: %s' % url) ] ) http_host = url.split('/')[2] @@ -1250,9 +1250,9 @@ class ArteTvIE(InfoExtractor): '(rtmp://.*?)\'', re.DOTALL, [ - (1, 'path', u'ERROR: could not extract video path: %s' % url), - (2, 'player', u'ERROR: could not extract video player: %s' % url), - (3, 'url', u'ERROR: could not extract video url: %s' % url) + (1, 'path', u'could not extract video path: %s' % url), + (2, 'player', u'could not extract video player: %s' % url), + (3, 'url', u'could not extract video url: %s' % url) ] ) video_url = u'%s/%s' % (info.get('url'), info.get('path')) @@ -1264,7 +1264,7 @@ class ArteTvIE(InfoExtractor): r'param name="movie".*?videorefFileUrl=(http[^\'"&]*)', 0, [ - (1, 'url', u'ERROR: Invalid URL: %s' % url) + (1, 'url', u'Invalid URL: %s' % url) ] ) next_url = compat_urllib_parse.unquote(info.get('url')) @@ -1273,7 +1273,7 @@ class ArteTvIE(InfoExtractor): r'