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 01/37] 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 02/37] 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 03/37] 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 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 04/37] 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 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 05/37] 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 06/37] 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 07/37] 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 08/37] 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 09/37] 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 10/37] 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 11/37] 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 12/37] 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 c3971870616fb24c298b8f6f1bf1ec7c16c75470 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Sat, 16 Mar 2013 23:52:17 +0100 Subject: [PATCH 13/37] 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 5011cded16d15bb03c2f172ddae81499d764e28a Mon Sep 17 00:00:00 2001 From: dodo Date: Sun, 24 Mar 2013 02:24:07 +0100 Subject: [PATCH 14/37] 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 15/37] 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 16/37] 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 17/37] 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 a91556fd74adf8ccfa4f923e21a0150e97d38bde Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister Date: Fri, 29 Mar 2013 00:19:58 +0100 Subject: [PATCH 18/37] 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 19/37] 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 43113d92cc89cb6c9ff98a1b45512a92c71abb23 Mon Sep 17 00:00:00 2001 From: kkalpakloglou Date: Tue, 26 Mar 2013 22:37:08 +0200 Subject: [PATCH 20/37] 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 21/37] 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 22/37] 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 23/37] 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 24/37] 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 25/37] 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 26/37] 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 27/37] 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 28/37] 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 29/37] 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 30/37] 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 31/37] 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 32/37] 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 33/37] 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 34/37] 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 35/37] 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 36/37] 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 37/37] 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'