From 7311fef854632130af5edcae7f6762f0fedbaccb Mon Sep 17 00:00:00 2001 From: Gino Lisignoli Date: Wed, 13 Feb 2013 14:02:31 +1300 Subject: [PATCH 01/40] Modified youtube-dl to write new lines with the --newline switch. This enables easier process monitoring when being called with external scripts. --- youtube_dl/FileDownloader.py | 5 ++++- youtube_dl/__init__.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 4f51ed8b0..ca047ba4c 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -305,7 +305,10 @@ class FileDownloader(object): """Report download progress.""" if self.params.get('noprogress', False): return - self.to_screen(u'\r[download] %s of %s at %s ETA %s' % + if self.params.get('newline', True): + self.to_screen(u'\r[download] %s of %s at %s ETA %s' % + (percent_str, data_len_str, speed_str, eta_str)) + else: self.to_screen(u'\r[download] %s of %s at %s ETA %s' % (percent_str, data_len_str, speed_str, eta_str), skip_eol=True) self.to_cons_title(u'youtube-dl - %s of %s at %s ETA %s' % (percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip())) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index abcb4f165..38b5fb163 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -202,6 +202,8 @@ def parseOpts(): verbosity.add_option('--get-format', action='store_true', dest='getformat', help='simulate, quiet but print output format', default=False) + verbosity.add_option('--newline', + action='store_true', dest='newline', help='Output progress bar as new lines', default=False) verbosity.add_option('--no-progress', action='store_true', dest='noprogress', help='do not print progress bar', default=False) verbosity.add_option('--console-title', @@ -210,7 +212,6 @@ def parseOpts(): verbosity.add_option('-v', '--verbose', action='store_true', dest='verbose', help='print various debugging information', default=False) - filesystem.add_option('-t', '--title', action='store_true', dest='usetitle', help='use title in file name', default=False) filesystem.add_option('--id', From 1528d6642d327571c1cd92a681deb819c41b5d67 Mon Sep 17 00:00:00 2001 From: Gino Lisignoli Date: Wed, 13 Feb 2013 16:43:08 +1300 Subject: [PATCH 02/40] Forgot to remove \r --- youtube-dl | Bin 3447 -> 59506 bytes youtube_dl/FileDownloader.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube-dl b/youtube-dl index e6f05c17327ed58f8db66e6dc7d2a38380355d61..ef9f332410cdea77f0bc2f47432a1a0a29c66786 100755 GIT binary patch literal 59506 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 GxBmd(Qkxk7 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{ diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index ca047ba4c..7ad9d9a76 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -306,7 +306,7 @@ class FileDownloader(object): if self.params.get('noprogress', False): return if self.params.get('newline', True): - self.to_screen(u'\r[download] %s of %s at %s ETA %s' % + self.to_screen(u'[download] %s of %s at %s ETA %s' % (percent_str, data_len_str, speed_str, eta_str)) else: self.to_screen(u'\r[download] %s of %s at %s ETA %s' % (percent_str, data_len_str, speed_str, eta_str), skip_eol=True) From 380a29dbf7c504dec4999f5bd18aaa7eeaf88ac9 Mon Sep 17 00:00:00 2001 From: glisignoli Date: Fri, 15 Feb 2013 15:55:11 +1300 Subject: [PATCH 03/40] Update youtube_dl/__init__.py --- youtube_dl/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 38b5fb163..035ab110c 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -203,7 +203,7 @@ def parseOpts(): action='store_true', dest='getformat', help='simulate, quiet but print output format', default=False) verbosity.add_option('--newline', - action='store_true', dest='newline', help='Output progress bar as new lines', default=False) + action='store_true', dest='newline', help='output progress bar as new lines', default=False) verbosity.add_option('--no-progress', action='store_true', dest='noprogress', help='do not print progress bar', default=False) verbosity.add_option('--console-title', From 355fc8e9443b991819b5ebf95464894bd131126c Mon Sep 17 00:00:00 2001 From: glisignoli Date: Fri, 15 Feb 2013 15:57:40 +1300 Subject: [PATCH 04/40] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a14dac9f4..7c09d0c0d 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ which means you can modify it, redistribute it or use it however you like. --get-description simulate, quiet but print video description --get-filename simulate, quiet but print output filename --get-format simulate, quiet but print output format + --newline output progress bar as new lines --no-progress do not print progress bar --console-title display progress in console titlebar -v, --verbose print various debugging information From 1ad5d872b9d3b79f997a7622f9d963bdae9afd69 Mon Sep 17 00:00:00 2001 From: bastik Date: Sat, 16 Feb 2013 13:46:13 +0100 Subject: [PATCH 05/40] added new InfoExtractor for myspass.de --- youtube_dl/InfoExtractors.py | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index ac69f82fe..57d5e9d36 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3968,6 +3968,63 @@ class KeekIE(InfoExtractor): } return [info] +class MyspassIE(InfoExtractor): + _VALID_URL = r'http://www.myspass.de/.*' + IE_NAME = u'myspass' + + def _real_extract(self, url): + META_DATA_URL_TEMPLATE = 'http://www.myspass.de/myspass/includes/apps/video/getvideometadataxml.php?id=%s' + + # video id is the last path element of the URL + # usually there is a trailing slash, so also try the second but last + url_path = compat_urllib_parse_urlparse(url).path + url_parent_path, video_id = os.path.split(url_path) + if not video_id: + _, video_id = os.path.split(url_parent_path) + + # get metadata + metadata_url = META_DATA_URL_TEMPLATE % video_id + metadata_text = self._download_webpage(metadata_url, video_id) + metadata = xml.etree.ElementTree.fromstring(metadata_text.encode('utf-8')) + + # 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') + 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') + return + title = title_el.text + format_id_el = metadata.find('format_id') + if format_id_el is None: + format = ext + else: + format = format_id_el.text + description_el = metadata.find('description') + if description_el is not None: + description = description_el.text + else: + description = None + imagePreview_el = metadata.find('imagePreview') + if imagePreview_el is not None: + thumbnail = imagePreview_el.text + else: + thumbnail = None + info = { + 'id': video_id, + 'url': video_url, + 'title': title, + 'ext': extension, + 'format': format, + 'thumbnail': thumbnail, + 'description': description + } + return [info] + def gen_extractors(): """ Return a list of an instance of every supported extractor. The order does matter; the first extractor matched is the one handling the URL. @@ -4015,6 +4072,7 @@ def gen_extractors(): RBMARadioIE(), EightTracksIE(), KeekIE(), + MyspassIE(), GenericIE() ] From 3a468f2d8b0261d4f45a7c5837f54edc33acdd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= Date: Sun, 17 Feb 2013 17:13:06 +0100 Subject: [PATCH 06/40] Basic support for TED --- test/tests.json | 9 +++++++++ youtube_dl/InfoExtractors.py | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/test/tests.json b/test/tests.json index 5c46af2c8..a6782ed4c 100644 --- a/test/tests.json +++ b/test/tests.json @@ -286,5 +286,14 @@ "title": "test chars: \"'/\\ä<>This is a test video for youtube-dl.For more information, contact phihag@phihag.de ." } + }, + { + "name": "TED", + "url": "http://www.ted.com/talks/dan_dennett_on_our_consciousness.html", + "file": "102.mp4", + "md5": "7bc087e71d16f18f9b8ab9fa62a8a031", + "info_dict": { + "title": "Dan Dennett: The illusion of consciousness" + } } ] diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index ac69f82fe..742b036d3 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3967,6 +3967,30 @@ class KeekIE(InfoExtractor): 'uploader': uploader } return [info] + +class TEDIE(InfoExtractor): + _VALID_URL=r'http://www.ted.com/talks/(?P\w+)' + def _real_extract(self, url): + m=re.match(self._VALID_URL, url) + videoName=m.group('videoName') + webpage=self._download_webpage(url, 0, 'Downloading \"%s\" page' % videoName) + #If the url includes the language we get the title translated + title_RE=r'

(?P[\s\w:/\.\?=\+-]*)</span></h1>' + title=re.search(title_RE, webpage).group('title') + info_RE=r'''<script\ type="text/javascript">var\ talkDetails\ =(.*?) + "id":(?P<videoID>[\d]+).*? + "mediaSlug":"(?P<mediaSlug>[\w\d]+?)"''' + info_match=re.search(info_RE,webpage,re.VERBOSE) + video_id=info_match.group('videoID') + mediaSlug=info_match.group('mediaSlug') + video_url='http://download.ted.com/talks/%s.mp4' % mediaSlug + info = { + 'id':video_id, + 'url':video_url, + 'ext': 'mp4', + 'title': title + } + return [info] def gen_extractors(): """ Return a list of an instance of every supported extractor. @@ -4015,6 +4039,7 @@ def gen_extractors(): RBMARadioIE(), EightTracksIE(), KeekIE(), + TEDIE(), GenericIE() ] From 59d4c2fe1b52a9cc51af789c43868da0a803f9f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Sun, 17 Feb 2013 17:25:02 +0100 Subject: [PATCH 07/40] fix some titles in TED --- 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 742b036d3..086aa5da3 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3975,7 +3975,7 @@ class TEDIE(InfoExtractor): videoName=m.group('videoName') webpage=self._download_webpage(url, 0, 'Downloading \"%s\" page' % videoName) #If the url includes the language we get the title translated - title_RE=r'<h1><span id="altHeadline" >(?P<title>[\s\w:/\.\?=\+-]*)</span></h1>' + title_RE=r'<h1><span id="altHeadline" >(?P<title>[\s\w:/\.\?=\+-\\\']*)</span></h1>' title=re.search(title_RE, webpage).group('title') info_RE=r'''<script\ type="text/javascript">var\ talkDetails\ =(.*?) "id":(?P<videoID>[\d]+).*? From 5717d91ab724a843e0cdbda6e9a0bc6e8c458856 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 18 Feb 2013 18:52:06 +0100 Subject: [PATCH 08/40] Correct --newline and give it a more meaningful title --- youtube_dl/FileDownloader.py | 5 +++-- youtube_dl/__init__.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 7ad9d9a76..0ac526389 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -305,10 +305,11 @@ class FileDownloader(object): """Report download progress.""" if self.params.get('noprogress', False): return - if self.params.get('newline', True): + if self.params.get('progress_with_newline', False): self.to_screen(u'[download] %s of %s at %s ETA %s' % (percent_str, data_len_str, speed_str, eta_str)) - else: self.to_screen(u'\r[download] %s of %s at %s ETA %s' % + else: + self.to_screen(u'\r[download] %s of %s at %s ETA %s' % (percent_str, data_len_str, speed_str, eta_str), skip_eol=True) self.to_cons_title(u'youtube-dl - %s of %s at %s ETA %s' % (percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip())) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 035ab110c..f05331644 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -203,7 +203,7 @@ def parseOpts(): action='store_true', dest='getformat', help='simulate, quiet but print output format', default=False) verbosity.add_option('--newline', - action='store_true', dest='newline', help='output progress bar as new lines', default=False) + action='store_true', dest='progress_with_newline', help='output progress bar as new lines', default=False) verbosity.add_option('--no-progress', action='store_true', dest='noprogress', help='do not print progress bar', default=False) verbosity.add_option('--console-title', @@ -439,6 +439,7 @@ def _real_main(): 'noresizebuffer': opts.noresizebuffer, 'continuedl': opts.continue_dl, 'noprogress': opts.noprogress, + 'progress_with_newline': opts.progress_with_newline, 'playliststart': opts.playliststart, 'playlistend': opts.playlistend, 'logtostderr': opts.outtmpl == '-', From b17c974a8868cc97d9c5ecf88789198fcfaa6ad2 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 18 Feb 2013 18:53:40 +0100 Subject: [PATCH 09/40] Mark DailyMotion as broken for now (#680) --- youtube_dl/InfoExtractors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index fe9bd97d0..9f63512d7 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -718,6 +718,7 @@ class DailymotionIE(InfoExtractor): _VALID_URL = r'(?i)(?:https?://)?(?:www\.)?dailymotion\.[a-z]{2,3}/video/([^/]+)' IE_NAME = u'dailymotion' + _WORKING = False def __init__(self, downloader=None): InfoExtractor.__init__(self, downloader) From 2a9983b78f170ce6695e2a0c7dbe381f1b484095 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 18 Feb 2013 19:11:32 +0100 Subject: [PATCH 10/40] Fix 8tracks --- 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 9f63512d7..627329ecd 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3913,7 +3913,7 @@ class EightTracksIE(InfoExtractor): webpage = self._download_webpage(url, playlist_id) - m = re.search(r"new TRAX.Mix\((.*?)\);\n*\s*TRAX.initSearchAutocomplete\('#search'\);", webpage, flags=re.DOTALL) + m = re.search(r"PAGE.mix = (.*?);\n", webpage, flags=re.DOTALL) if not m: raise ExtractorError(u'Cannot find trax information') json_like = m.group(1) From 414638cd508a48d81a16dc22a6ad684728cb9194 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Mon, 18 Feb 2013 21:42:06 +0100 Subject: [PATCH 11/40] TED: Add support for playlists --- youtube_dl/InfoExtractors.py | 68 ++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 627329ecd..53fab690a 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3970,12 +3970,60 @@ class KeekIE(InfoExtractor): return [info] class TEDIE(InfoExtractor): - _VALID_URL=r'http://www.ted.com/talks/(?P<videoName>\w+)' + _VALID_URL=r'''http://www.ted.com/ + ( + ((?P<type_playlist>playlists)/(?P<playlist_id>\d+)) # We have a playlist + | + ((?P<type_talk>talks)) # We have a simple talk + ) + /(?P<name>\w+) # Here goes the name and then ".html" + ''' + + def suitable(self, url): + """Receives a URL and returns True if suitable for this IE.""" + return re.match(self._VALID_URL, url, re.VERBOSE) is not None + def _real_extract(self, url): - m=re.match(self._VALID_URL, url) - videoName=m.group('videoName') - webpage=self._download_webpage(url, 0, 'Downloading \"%s\" page' % videoName) - #If the url includes the language we get the title translated + m=re.match(self._VALID_URL, url, re.VERBOSE) + if m.group('type_talk'): + return [self._talk_info(url)] + else : + playlist_id=m.group('playlist_id') + name=m.group('name') + self._downloader.to_screen(u'[%s] Getting info of playlist %s: "%s"' % (self.IE_NAME,playlist_id,name)) + return self._playlist_videos_info(url,name,playlist_id) + + def _talk_video_link(self,mediaSlug): + '''Returns the video link for that mediaSlug''' + return 'http://download.ted.com/talks/%s.mp4' % mediaSlug + + def _playlist_videos_info(self,url,name,playlist_id=0): + '''Returns the videos of the playlist''' + video_RE=r''' + <li\ id="talk_(\d+)"([.\s]*?)data-id="(?P<video_id>\d+)" + ([.\s]*?)data-playlist_item_id="(\d+)" + ([.\s]*?)data-mediaslug="(?P<mediaSlug>.+?)" + ''' + video_name_RE=r'<p\ class="talk-title"><a href="/talks/(.+).html">(?P<fullname>.+?)</a></p>' + webpage=self._download_webpage(url, playlist_id, 'Downloading playlist webpage') + m_videos=re.finditer(video_RE,webpage,re.VERBOSE) + m_names=re.finditer(video_name_RE,webpage) + info=[] + for m_video, m_name in zip(m_videos,m_names): + video_dic={ + 'id': m_video.group('video_id'), + 'url': self._talk_video_link(m_video.group('mediaSlug')), + 'ext': 'mp4', + 'title': m_name.group('fullname') + } + info.append(video_dic) + return info + def _talk_info(self, url, video_id=0): + """Return the video for the talk in the url""" + m=re.match(self._VALID_URL, url,re.VERBOSE) + 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>[\s\w:/\.\?=\+-\\\']*)</span></h1>' title=re.search(title_RE, webpage).group('title') info_RE=r'''<script\ type="text/javascript">var\ talkDetails\ =(.*?) @@ -3984,14 +4032,14 @@ class TEDIE(InfoExtractor): info_match=re.search(info_RE,webpage,re.VERBOSE) video_id=info_match.group('videoID') mediaSlug=info_match.group('mediaSlug') - video_url='http://download.ted.com/talks/%s.mp4' % mediaSlug + video_url=self._talk_video_link(mediaSlug) info = { - 'id':video_id, - 'url':video_url, + 'id': video_id, + 'url': video_url, 'ext': 'mp4', 'title': title - } - return [info] + } + return info class MySpassIE(InfoExtractor): _VALID_URL = r'http://www.myspass.de/.*' From 6d4363368affe197f1c3efbd34d18b365c3d929d Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 18 Feb 2013 22:32:56 +0100 Subject: [PATCH 12/40] Fix MyVideo IE --- 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 627329ecd..d7ddf4e37 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -2233,7 +2233,7 @@ class MyVideoIE(InfoExtractor): webpage = self._download_webpage(webpage_url, video_id) self.report_extraction(video_id) - mobj = re.search(r'<link rel=\'image_src\' href=\'(http://is[0-9].myvideo\.de/de/movie[0-9]+/[a-f0-9]+)/thumbs/[^.]+\.jpg\' />', + mobj = re.search(r'<link rel=\'image_src\' href=\'(http://is[0-9].myvideo\.de/de/movie[0-9]+/[a-f0-9]+)/thumbs/.*?\.jpg\' />', webpage) if mobj is None: self._downloader.trouble(u'ERROR: unable to extract media URL') From 7796e8c2cb2827ba7f7ef5e0c1dd0c299281026d Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 18 Feb 2013 23:12:48 +0100 Subject: [PATCH 13/40] facebook: also download lq videos --- youtube_dl/InfoExtractors.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index d7ddf4e37..36f025078 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -2098,6 +2098,10 @@ class FacebookIE(InfoExtractor): params_raw = compat_urllib_parse.unquote(data['params']) params = json.loads(params_raw) video_url = params['hd_src'] + if not video_url: + video_url = params['sd_src'] + if not video_url: + raise ExtractorError(u'Cannot find video URL') video_duration = int(params['video_duration']) m = re.search('<h2 class="uiHeaderTitle">([^<]+)</h2>', webpage) From 434eb6f26bde06b173c648968dd6c79ffd543eac Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 18 Feb 2013 23:19:57 +0100 Subject: [PATCH 14/40] Include man and bash completion in PyPi release --- Makefile | 11 +++++++++-- devscripts/release.sh | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 966a685e1..84ea70d2c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,10 @@ all: youtube-dl README.md README.txt youtube-dl.1 youtube-dl.bash-completion clean: - rm -rf youtube-dl youtube-dl.exe youtube-dl.1 youtube-dl.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ youtube-dl.tar.gz + rm -rf youtube-dl.1 youtube-dl.bash-completion README.txt MANIFEST build/ dist/ .coverage cover/ youtube-dl.tar.gz + +cleanall: clean + rm -f youtube-dl youtube-dl.exe PREFIX=/usr/local BINDIR=$(PREFIX)/bin @@ -23,7 +26,9 @@ test: tar: youtube-dl.tar.gz -.PHONY: all clean install test tar +.PHONY: all clean install test tar bash-completion pypi-files + +pypi-files: youtube-dl.bash-completion README.txt youtube-dl.1 youtube-dl: youtube_dl/*.py zip --quiet youtube-dl youtube_dl/*.py @@ -45,6 +50,8 @@ youtube-dl.1: README.md youtube-dl.bash-completion: youtube_dl/*.py devscripts/bash-completion.in python devscripts/bash-completion.py +bash-completion: youtube-dl.bash-completion + youtube-dl.tar.gz: youtube-dl README.md README.txt youtube-dl.1 youtube-dl.bash-completion @tar -czf youtube-dl.tar.gz --transform "s|^|youtube-dl/|" --owner 0 --group 0 \ --exclude '*.DS_Store' \ diff --git a/devscripts/release.sh b/devscripts/release.sh index a5f07fd61..f8a29f79c 100755 --- a/devscripts/release.sh +++ b/devscripts/release.sh @@ -21,7 +21,7 @@ if [ ! -z "`git status --porcelain | grep -v CHANGELOG`" ]; then echo 'ERROR: th if [ ! -f "updates_key.pem" ]; then echo 'ERROR: updates_key.pem missing'; exit 1; fi echo "\n### First of all, testing..." -make clean +make cleanall nosetests --with-coverage --cover-package=youtube_dl --cover-html test || exit 1 echo "\n### Changing version in version.py..." @@ -83,7 +83,9 @@ ROOT=$(pwd) ) rm -rf build +make pypi-files echo "Uploading to PyPi ..." python setup.py sdist upload +make clean echo "\n### DONE!" From a72b0f2b6fff380fbd9bd0590b6aea110e46087d Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 18 Feb 2013 23:22:01 +0100 Subject: [PATCH 15/40] Use proper echo commands --- devscripts/release.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/devscripts/release.sh b/devscripts/release.sh index f8a29f79c..ced5d4e2f 100755 --- a/devscripts/release.sh +++ b/devscripts/release.sh @@ -20,19 +20,19 @@ if [ ! -z "`git tag | grep "$version"`" ]; then echo 'ERROR: version already pre if [ ! -z "`git status --porcelain | grep -v CHANGELOG`" ]; then echo 'ERROR: the working directory is not clean; commit or stash changes'; exit 1; fi if [ ! -f "updates_key.pem" ]; then echo 'ERROR: updates_key.pem missing'; exit 1; fi -echo "\n### First of all, testing..." +/bin/echo -e "\n### First of all, testing..." make cleanall nosetests --with-coverage --cover-package=youtube_dl --cover-html test || exit 1 -echo "\n### Changing version in version.py..." +/bin/echo -e "\n### Changing version in version.py..." sed -i "s/__version__ = '.*'/__version__ = '$version'/" youtube_dl/version.py -echo "\n### Committing CHANGELOG README.md and youtube_dl/version.py..." +/bin/echo -e "\n### Committing CHANGELOG README.md and youtube_dl/version.py..." make README.md git add CHANGELOG README.md youtube_dl/version.py git commit -m "release $version" -echo "\n### Now tagging, signing and pushing..." +/bin/echo -e "\n### Now tagging, signing and pushing..." git tag -s -m "Release $version" "$version" git show "$version" read -p "Is it good, can I push? (y/n) " -n 1 @@ -42,7 +42,7 @@ MASTER=$(git rev-parse --abbrev-ref HEAD) git push origin $MASTER:master git push origin "$version" -echo "\n### OK, now it is time to build the binaries..." +/bin/echo -e "\n### OK, now it is time to build the binaries..." REV=$(git rev-parse HEAD) make youtube-dl youtube-dl.tar.gz wget "http://jeromelaheurte.net:8142/download/rg3/youtube-dl/youtube-dl.exe?rev=$REV" -O youtube-dl.exe || \ @@ -57,11 +57,11 @@ RELEASE_FILES="youtube-dl youtube-dl.exe youtube-dl-$version.tar.gz" (cd build/$version/ && sha512sum $RELEASE_FILES > SHA2-512SUMS) git checkout HEAD -- youtube-dl youtube-dl.exe -echo "\n### Signing and uploading the new binaries to youtube-dl.org..." +/bin/echo -e "\n### Signing and uploading the new binaries to youtube-dl.org..." for f in $RELEASE_FILES; do gpg --detach-sig "build/$version/$f"; done scp -r "build/$version" ytdl@youtube-dl.org:html/downloads/ -echo "\n### Now switching to gh-pages..." +/bin/echo -e "\n### Now switching to gh-pages..." git clone --branch gh-pages --single-branch . build/gh-pages ROOT=$(pwd) ( @@ -88,4 +88,4 @@ echo "Uploading to PyPi ..." python setup.py sdist upload make clean -echo "\n### DONE!" +/bin/echo -e "\n### DONE!" From e711babbd148187c14eb2b7895aced223527c956 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 18 Feb 2013 23:30:33 +0100 Subject: [PATCH 16/40] Fix YP IE --- 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 61ba2a1b7..415aacd02 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3729,13 +3729,13 @@ class YouPornIE(InfoExtractor): webpage = self._download_webpage(req, video_id) # Get the video title - result = re.search(r'videoTitleArea">(?P<title>.*)</h1>', webpage) + result = re.search(r'<h1.*?>(?P<title>.*)</h1>', webpage) if result is None: - raise ExtractorError(u'ERROR: unable to extract video title') + raise ExtractorError(u'Unable to extract video title') video_title = result.group('title').strip() # Get the video date - result = re.search(r'Date:</b>(?P<date>.*)</li>', webpage) + result = re.search(r'Date:</label>(?P<date>.*) </li>', webpage) if result is None: self._downloader.to_stderr(u'WARNING: unable to extract video date') upload_date = None @@ -3743,9 +3743,9 @@ class YouPornIE(InfoExtractor): upload_date = result.group('date').strip() # Get the video uploader - result = re.search(r'Submitted:</b>(?P<uploader>.*)</li>', webpage) + result = re.search(r'Submitted:</label>(?P<uploader>.*)</li>', webpage) if result is None: - self._downloader.to_stderr(u'ERROR: unable to extract uploader') + self._downloader.to_stderr(u'WARNING: unable to extract uploader') video_uploader = None else: video_uploader = result.group('uploader').strip() From d8f64574a418c90b5c7e185ea8ed79f95aa8bfc8 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 18 Feb 2013 23:37:20 +0100 Subject: [PATCH 17/40] release 2013.02.18 --- youtube_dl/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/version.py b/youtube_dl/version.py index 8b231ae80..34ba1087f 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.02.02' +__version__ = '2013.02.18' From 471cf47796a5cfbce060c25d3ca55743cae21dbe Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 18 Feb 2013 23:56:13 +0100 Subject: [PATCH 18/40] include bash completion and manpage in PyPi dist --- MANIFEST.in | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 81f8e05cd..8f8af7a7f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,5 @@ include README.md include test/*.py -include test/*.json \ No newline at end of file +include test/*.json +include youtube-dl.bash-completion +include youtube-dl.1 From c8cd8e5f55c202171a1b1b6798067fb8cb68f6ab Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Tue, 19 Feb 2013 00:06:04 +0100 Subject: [PATCH 19/40] release 2013.02.19 --- 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 34ba1087f..65d9194f5 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.02.18' +__version__ = '2013.02.19' From 7c038b3c32bac26d28782852d921a91e530317a6 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 21 Feb 2013 16:49:05 +0100 Subject: [PATCH 20/40] Import HTTPErrorProcessor from the correct module (Closes #696) --- 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 415aacd02..23bd21af5 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1330,7 +1330,7 @@ class GenericIE(InfoExtractor): opener = compat_urllib_request.OpenerDirector() for handler in [compat_urllib_request.HTTPHandler, compat_urllib_request.HTTPDefaultErrorHandler, HTTPMethodFallback, HEADRedirectHandler, - compat_urllib_error.HTTPErrorProcessor, compat_urllib_request.HTTPSHandler]: + compat_urllib_request.HTTPErrorProcessor, compat_urllib_request.HTTPSHandler]: opener.add_handler(handler()) response = opener.open(HeadRequest(url)) From 1013186a172a05d57e87857604719cae6b8d7049 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 21 Feb 2013 16:56:48 +0100 Subject: [PATCH 21/40] Also check for JSLoader of JWSPlayer (thanks to @maximeg, Closes #685) --- youtube_dl/InfoExtractors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 23bd21af5..d3c3ac264 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1366,6 +1366,9 @@ class GenericIE(InfoExtractor): if mobj is None: # Broaden the search a little bit mobj = re.search(r'[^A-Za-z0-9]?(?:file|source)=(http[^\'"&]*)', webpage) + if mobj is None: + # Broaden the search a little bit: JWPlayer JS loader + mobj = re.search(r'[^A-Za-z0-9]?file:\s*["\'](http[^\'"&]*)', webpage) if mobj is None: self._downloader.trouble(u'ERROR: Invalid URL: %s' % url) return From 8271226a55bd3daa7eddfe2efc243892de02ccf4 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Thu, 21 Feb 2013 17:09:39 +0100 Subject: [PATCH 22/40] Fix --match-title and --reject-title decoding (Closes #690) --- youtube_dl/FileDownloader.py | 2 -- youtube_dl/__init__.py | 5 +++-- youtube_dl/utils.py | 8 ++++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 0ac526389..53c2d1dce 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -370,12 +370,10 @@ class FileDownloader(object): title = info_dict['title'] matchtitle = self.params.get('matchtitle', False) if matchtitle: - matchtitle = matchtitle.decode('utf8') if not re.search(matchtitle, title, re.IGNORECASE): return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"' rejecttitle = self.params.get('rejecttitle', False) if rejecttitle: - rejecttitle = rejecttitle.decode('utf8') if re.search(rejecttitle, title, re.IGNORECASE): return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"' return None diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index f05331644..23e3c2ac2 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -412,6 +412,7 @@ def _real_main(): or (opts.useid and u'%(id)s.%(ext)s') or (opts.autonumber and u'%(autonumber)s-%(id)s.%(ext)s') or u'%(id)s.%(ext)s') + # File downloader fd = FileDownloader({ 'usenetrc': opts.usenetrc, @@ -450,8 +451,8 @@ def _real_main(): 'writeinfojson': opts.writeinfojson, 'writesubtitles': opts.writesubtitles, 'subtitleslang': opts.subtitleslang, - 'matchtitle': opts.matchtitle, - 'rejecttitle': opts.rejecttitle, + 'matchtitle': decodeOption(opts.matchtitle), + 'rejecttitle': decodeOption(opts.rejecttitle), 'max_downloads': opts.max_downloads, 'prefer_free_formats': opts.prefer_free_formats, 'verbose': opts.verbose, diff --git a/youtube_dl/utils.py b/youtube_dl/utils.py index e6ce028d6..95bd94843 100644 --- a/youtube_dl/utils.py +++ b/youtube_dl/utils.py @@ -420,6 +420,14 @@ def encodeFilename(s): encoding = 'utf-8' return s.encode(encoding, 'ignore') +def decodeOption(optval): + if optval is None: + return optval + if isinstance(optval, bytes): + optval = optval.decode(preferredencoding()) + + assert isinstance(optval, compat_str) + return optval class ExtractorError(Exception): """Error during info extraction.""" From 3bf79c752ede4503db2b6cd5e8ccb55d163f6598 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Fri, 22 Feb 2013 00:36:23 +0100 Subject: [PATCH 23/40] Print *all* release notes --- youtube_dl/update.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/youtube_dl/update.py b/youtube_dl/update.py index f6e3e5c69..8e5326196 100644 --- a/youtube_dl/update.py +++ b/youtube_dl/update.py @@ -77,10 +77,8 @@ def update_self(to_screen, verbose, filename): to_screen(u'Updating to version ' + versions_info['latest'] + '...') version = versions_info['versions'][versions_info['latest']] - if version.get('notes'): - to_screen(u'PLEASE NOTE:') - for note in version['notes']: - to_screen(note) + + print_notes(version_info['versions']) if not os.access(filename, os.W_OK): to_screen(u'ERROR: no write permissions on %s' % filename) @@ -158,3 +156,13 @@ del "%s" return to_screen(u'Updated youtube-dl. Restart youtube-dl to use the new version.') + +def print_notes(versions, fromVersion=__version__): + notes = [] + for v,vdata in sorted(versions.items()): + if v > fromVersion: + notes.extend(vdata.get('notes', [])) + if notes: + to_screen(u'PLEASE NOTE:') + for note in notes: + to_screen(note) From f636c344811df9ee712530dca31ce9797756b03e Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Fri, 22 Feb 2013 16:40:19 +0100 Subject: [PATCH 24/40] Stop early in nosetests (in release script) --- devscripts/release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devscripts/release.sh b/devscripts/release.sh index ced5d4e2f..ee650f221 100755 --- a/devscripts/release.sh +++ b/devscripts/release.sh @@ -22,7 +22,7 @@ if [ ! -f "updates_key.pem" ]; then echo 'ERROR: updates_key.pem missing'; exit /bin/echo -e "\n### First of all, testing..." make cleanall -nosetests --with-coverage --cover-package=youtube_dl --cover-html test || exit 1 +nosetests --with-coverage --cover-package=youtube_dl --cover-html test --stop || exit 1 /bin/echo -e "\n### Changing version in version.py..." sed -i "s/__version__ = '.*'/__version__ = '$version'/" youtube_dl/version.py From 4be0aa35393ccd0af72741eb0f6f97920399893d Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Fri, 22 Feb 2013 16:41:36 +0100 Subject: [PATCH 25/40] release 2012.02.22 --- 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 65d9194f5..bcc618c44 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.02.19' +__version__ = '2012.02.22' From 60bd48b175792d55cc91a3ad8c3109e2ed30fcb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Sat, 23 Feb 2013 16:48:15 +0100 Subject: [PATCH 26/40] Steam: get thumbnails --- 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 d3c3ac264..64a6cfbc8 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -3627,18 +3627,22 @@ class SteamIE(InfoExtractor): mweb = re.finditer(urlRE, webpage) namesRE = r'<span class="title">(?P<videoName>.+?)</span>' titles = re.finditer(namesRE, webpage) + thumbsRE = r'<img class="movie_thumb" src="(?P<thumbnail>.+?)">' + thumbs = re.finditer(thumbsRE, webpage) videos = [] - for vid,vtitle in zip(mweb,titles): + for vid,vtitle,thumb in zip(mweb,titles,thumbs): video_id = vid.group('videoID') title = vtitle.group('videoName') 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) info = { 'id':video_id, 'url':video_url, 'ext': 'flv', - 'title': unescapeHTML(title) + 'title': unescapeHTML(title), + 'thumbnail': video_thumb } videos.append(info) return videos From c85538dba10d04abc6260b2077184798df1668e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Marqui=CC=81nez=20Ferra=CC=81ndiz?= <jaimemf93@gmail.com> Date: Sat, 23 Feb 2013 17:27:49 +0100 Subject: [PATCH 27/40] TED: get thumbnails --- youtube_dl/InfoExtractors.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index d3c3ac264..7393eef41 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -4011,31 +4011,30 @@ class TEDIE(InfoExtractor): ([.\s]*?)data-playlist_item_id="(\d+)" ([.\s]*?)data-mediaslug="(?P<mediaSlug>.+?)" ''' - video_name_RE=r'<p\ class="talk-title"><a href="/talks/(.+).html">(?P<fullname>.+?)</a></p>' + video_name_RE=r'<p\ class="talk-title"><a href="(?P<talk_url>/talks/(.+).html)">(?P<fullname>.+?)</a></p>' webpage=self._download_webpage(url, playlist_id, 'Downloading playlist webpage') m_videos=re.finditer(video_RE,webpage,re.VERBOSE) m_names=re.finditer(video_name_RE,webpage) info=[] for m_video, m_name in zip(m_videos,m_names): - video_dic={ - 'id': m_video.group('video_id'), - 'url': self._talk_video_link(m_video.group('mediaSlug')), - 'ext': 'mp4', - 'title': m_name.group('fullname') - } - info.append(video_dic) + video_id=m_video.group('video_id') + talk_url='http://www.ted.com%s' % m_name.group('talk_url') + info.append(self._talk_info(talk_url,video_id)) return info + def _talk_info(self, url, video_id=0): """Return the video for the talk in the url""" m=re.match(self._VALID_URL, url,re.VERBOSE) 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>[\s\w:/\.\?=\+-\\\']*)</span></h1>' + title_RE=r'<h1><span id="altHeadline" >(?P<title>.*)</span></h1>' title=re.search(title_RE, webpage).group('title') info_RE=r'''<script\ type="text/javascript">var\ talkDetails\ =(.*?) "id":(?P<videoID>[\d]+).*? "mediaSlug":"(?P<mediaSlug>[\w\d]+?)"''' + thumb_RE=r'</span>[\s.]*</div>[\s.]*<img src="(?P<thumbnail>.*?)"' + thumb_match=re.search(thumb_RE,webpage) info_match=re.search(info_RE,webpage,re.VERBOSE) video_id=info_match.group('videoID') mediaSlug=info_match.group('mediaSlug') @@ -4044,7 +4043,8 @@ class TEDIE(InfoExtractor): 'id': video_id, 'url': video_url, 'ext': 'mp4', - 'title': title + 'title': title, + 'thumbnail': thumb_match.group('thumbnail') } return info From d1b7a243548643f364fd129f6993ab16b2fa24f8 Mon Sep 17 00:00:00 2001 From: Juan M <joksnet@gmail.com> Date: Sat, 23 Feb 2013 22:47:22 +0100 Subject: [PATCH 28/40] Decode the data requested to the api in utf-8. --- 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 d3c3ac264..89e7cd74c 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1472,7 +1472,7 @@ class YoutubeSearchIE(InfoExtractor): result_url = self._API_URL % (compat_urllib_parse.quote_plus(query), (50*pagenum)+1) request = compat_urllib_request.Request(result_url) try: - data = compat_urllib_request.urlopen(request).read() + 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)) return From 35d217133ffe1f14dfa395020ae3afade8fcea37 Mon Sep 17 00:00:00 2001 From: Juan M <joksnet@gmail.com> Date: Sat, 23 Feb 2013 22:52:52 +0100 Subject: [PATCH 29/40] Message for delete video it's not an error. When using youtube-dl from another python script with the quiet option on, and a post procesor for extract the audio. The message of deleting video shows in the first script logs (as it goes to stderr). There is no way to keep this quiet as it's treated as an error, even if, for me, it's not. --- youtube_dl/FileDownloader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 53c2d1dce..192ad37d2 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -552,7 +552,7 @@ class FileDownloader(object): self.to_stderr(u'ERROR: ' + e.msg) if keep_video is False and not self.params.get('keepvideo', False): try: - self.to_stderr(u'Deleting original file %s (pass -k to keep)' % filename) + self.to_screen(u'Deleting original file %s (pass -k to keep)' % filename) os.remove(encodeFilename(filename)) except (IOError, OSError): self.to_stderr(u'WARNING: Unable to remove downloaded video file') From ea05129ebd807dc0a724f70a5bf71736e2f8f9bc Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Sun, 24 Feb 2013 00:47:08 +0100 Subject: [PATCH 30/40] release 2013.02.22 --- 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 bcc618c44..eaef9a107 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2012.02.22' +__version__ = '2013.02.22' From cb9979779865eec87517b820eac6f3b2e3f21b47 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Sun, 24 Feb 2013 01:01:20 +0100 Subject: [PATCH 31/40] Test TED thumbnail --- test/tests.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/tests.json b/test/tests.json index a3c31ae51..fd7eb2d65 100644 --- a/test/tests.json +++ b/test/tests.json @@ -293,7 +293,8 @@ "file": "102.mp4", "md5": "7bc087e71d16f18f9b8ab9fa62a8a031", "info_dict": { - "title": "Dan Dennett: The illusion of consciousness" + "title": "Dan Dennett: The illusion of consciousness", + "thumbnail": "http://images.ted.com/images/ted/488_389x292.jpg" } }, { From 450e70997276b9628842653f37f2d68d4fb4ba4d Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Sun, 24 Feb 2013 23:23:50 +0100 Subject: [PATCH 32/40] Formalize URL creation (prepare for some cleanup in blip.tv:users) --- youtube_dl/InfoExtractors.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 3b1e14ba9..d661d517d 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1921,9 +1921,8 @@ class BlipTVUserIE(InfoExtractor): while True: self.report_download_page(username, pagenum) - - request = compat_urllib_request.Request( page_base + "&page=" + str(pagenum) ) - + url = page_base + "&page=" + str(pagenum) + request = compat_urllib_request.Request( url ) try: page = compat_urllib_request.urlopen(request).read().decode('utf-8') except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err: From 12887875a26c46822d26c626ef20b8693547835f Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 25 Feb 2013 00:22:55 +0100 Subject: [PATCH 33/40] Fix typo --- youtube_dl/update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/update.py b/youtube_dl/update.py index 8e5326196..b446dd94c 100644 --- a/youtube_dl/update.py +++ b/youtube_dl/update.py @@ -78,7 +78,7 @@ def update_self(to_screen, verbose, filename): to_screen(u'Updating to version ' + versions_info['latest'] + '...') version = versions_info['versions'][versions_info['latest']] - print_notes(version_info['versions']) + print_notes(versions_info['versions']) if not os.access(filename, os.W_OK): to_screen(u'ERROR: no write permissions on %s' % filename) From 97d0365f49e9662c88b14eb8c32dc4cb52933ae6 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Mon, 25 Feb 2013 00:28:19 +0100 Subject: [PATCH 34/40] release 2013.02.25 --- 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 eaef9a107..ce8f6ca23 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.02.22' +__version__ = '2013.02.25' From 9e07cf2955ce3fc7dc7c4676ec32da9bfd6e3990 Mon Sep 17 00:00:00 2001 From: Juan M <joksnet@gmail.com> Date: Tue, 26 Feb 2013 18:06:43 +0100 Subject: [PATCH 35/40] [YT Search] No results if items is not in response When a query results of 0 items, the key items is not present in the api_response dictionary, raising a KeyError. Intead, look for the key and call trouble if it's not present. --- youtube_dl/InfoExtractors.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index d661d517d..a9646433e 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1478,6 +1478,10 @@ class YoutubeSearchIE(InfoExtractor): return api_response = json.loads(data)['data'] + if not 'items' in api_response: + self._downloader.trouble(u'[youtube] No video results') + return + new_ids = list(video['id'] for video in api_response['items']) video_ids += new_ids From 6324fd1d7494449c168805db8e3f9fa45396367b Mon Sep 17 00:00:00 2001 From: Filippo Valsorda <filippo.valsorda@gmail.com> Date: Tue, 26 Feb 2013 10:39:26 +0100 Subject: [PATCH 36/40] Switch YTPlaylistIE to API (relevant: #586); fixes #651; fixes #673; fixes #661 --- test/test_youtube_lists.py | 20 ++++--- youtube_dl/InfoExtractors.py | 103 ++++++++++++++++++++--------------- 2 files changed, 71 insertions(+), 52 deletions(-) diff --git a/test/test_youtube_lists.py b/test/test_youtube_lists.py index 3044e0852..69b0f4447 100644 --- a/test/test_youtube_lists.py +++ b/test/test_youtube_lists.py @@ -8,7 +8,7 @@ import json import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from youtube_dl.InfoExtractors import YoutubeUserIE,YoutubePlaylistIE +from youtube_dl.InfoExtractors import YoutubeUserIE, YoutubePlaylistIE, YoutubeIE from youtube_dl.utils import * PARAMETERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "parameters.json") @@ -38,11 +38,8 @@ class TestYoutubeLists(unittest.TestCase): DL = FakeDownloader() IE = YoutubePlaylistIE(DL) IE.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re') - self.assertEqual(DL.result, [ - ['http://www.youtube.com/watch?v=bV9L5Ht9LgY'], - ['http://www.youtube.com/watch?v=FXxLjLQi3Fg'], - ['http://www.youtube.com/watch?v=tU3Bgo5qJZE'] - ]) + self.assertEqual(map(lambda x: YoutubeIE()._extract_id(x[0]), DL.result), + [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE' ]) def test_youtube_playlist_long(self): DL = FakeDownloader() @@ -50,14 +47,21 @@ class TestYoutubeLists(unittest.TestCase): IE.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q') self.assertTrue(len(DL.result) >= 799) + def test_youtube_playlist_with_deleted(self): + DL = FakeDownloader() + IE = YoutubePlaylistIE(DL) + IE.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC') + self.assertFalse('pElCt5oNDuI' in map(lambda x: YoutubeIE()._extract_id(x[0]), DL.result)) + self.assertFalse('KdPEApIVdWM' in map(lambda x: YoutubeIE()._extract_id(x[0]), DL.result)) + def test_youtube_course(self): DL = FakeDownloader() IE = YoutubePlaylistIE(DL) # TODO find a > 100 (paginating?) videos course IE.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8') - self.assertEqual(DL.result[0], ['http://www.youtube.com/watch?v=j9WZyLZCBzs']) + self.assertEqual(YoutubeIE()._extract_id(DL.result[0][0]), 'j9WZyLZCBzs') self.assertEqual(len(DL.result), 25) - self.assertEqual(DL.result[-1], ['http://www.youtube.com/watch?v=rYefUsYuEp0']) + self.assertEqual(YoutubeIE()._extract_id(DL.result[-1][0]), 'rYefUsYuEp0') def test_youtube_channel(self): # I give up, please find a channel that does paginate and test this like test_youtube_playlist_long diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index d661d517d..021579ce0 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -15,6 +15,7 @@ import email.utils import xml.etree.ElementTree import random import math +import operator from .utils import * @@ -1662,22 +1663,40 @@ class YahooSearchIE(InfoExtractor): class YoutubePlaylistIE(InfoExtractor): """Information Extractor for YouTube playlists.""" - _VALID_URL = r'(?:(?:https?://)?(?:\w+\.)?youtube\.com/(?:(?:course|view_play_list|my_playlists|artist|playlist)\?.*?(p|a|list)=|user/.*?/user/|p/|user/.*?#[pg]/c/)(?:PL|EC)?|PL|EC)([0-9A-Za-z-_]{10,})(?:/.*?/([0-9A-Za-z_-]+))?.*' - _TEMPLATE_URL = 'http://www.youtube.com/%s?%s=%s&page=%s&gl=US&hl=en' - _VIDEO_INDICATOR_TEMPLATE = r'/watch\?v=(.+?)&([^&"]+&)*list=.*?%s' - _MORE_PAGES_INDICATOR = u"Next \N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}" + _VALID_URL = r"""(?: + (?:https?://)? + (?:\w+\.)? + youtube\.com/ + (?: + (?:course|view_play_list|my_playlists|artist|playlist) + \? .*? (p|a|list)= + | user/.*?/user/ + | p/ + | user/.*?#[pg]/c/ + ) + (?:PL|EC)? + |PL|EC) + ([0-9A-Za-z-_]{10,}) + (?:/.*?/([0-9A-Za-z_-]+))? + .*""" + _TEMPLATE_URL = 'https://gdata.youtube.com/feeds/api/playlists/%s?max-results=%i&start-index=%i&v=2&alt=json' + _MAX_RESULTS = 50 IE_NAME = u'youtube:playlist' def __init__(self, downloader=None): InfoExtractor.__init__(self, downloader) + def suitable(self, url): + """Receives a URL and returns True if suitable for this IE.""" + return re.match(self._VALID_URL, url, re.VERBOSE) is not None + def report_download_page(self, playlist_id, pagenum): """Report attempt to download playlist page with given number.""" self._downloader.to_screen(u'[youtube] PL %s: Downloading page #%s' % (playlist_id, pagenum)) def _real_extract(self, url): # Extract playlist id - mobj = re.match(self._VALID_URL, url) + mobj = re.match(self._VALID_URL, url, re.VERBOSE) if mobj is None: self._downloader.trouble(u'ERROR: invalid url: %s' % url) return @@ -1687,55 +1706,51 @@ class YoutubePlaylistIE(InfoExtractor): self._downloader.download([mobj.group(3)]) return - # Download playlist pages - # prefix is 'p' as default for playlists but there are other types that need extra care - playlist_prefix = mobj.group(1) - if playlist_prefix == 'a': - playlist_access = 'artist' - else: - playlist_prefix = 'p' - playlist_access = 'view_play_list' + # Download playlist videos from API playlist_id = mobj.group(2) - video_ids = [] - pagenum = 1 + page_num = 1 + videos = [] while True: - self.report_download_page(playlist_id, pagenum) - url = self._TEMPLATE_URL % (playlist_access, playlist_prefix, playlist_id, pagenum) - request = compat_urllib_request.Request(url) + self.report_download_page(playlist_id, page_num) + + url = self._TEMPLATE_URL % (playlist_id, self._MAX_RESULTS, self._MAX_RESULTS * (page_num - 1) + 1) try: - page = compat_urllib_request.urlopen(request).read().decode('utf-8') + 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)) return - # Extract video identifiers - ids_in_page = [] - for mobj in re.finditer(self._VIDEO_INDICATOR_TEMPLATE % playlist_id, page): - if mobj.group(1) not in ids_in_page: - ids_in_page.append(mobj.group(1)) - video_ids.extend(ids_in_page) + try: + response = json.loads(page) + except ValueError as err: + self._downloader.trouble(u'ERROR: Invalid JSON in API response: ' + compat_str(err)) + return - if self._MORE_PAGES_INDICATOR not in page: + videos += [(entry['yt$position']['$t'], entry['content']['src']) for entry in response['feed']['entry']] + + if len(response['feed']['entry']) < self._MAX_RESULTS: break - pagenum = pagenum + 1 + page_num += 1 - total = len(video_ids) + videos = map(operator.itemgetter(1), sorted(videos)) + + total = len(videos) playliststart = self._downloader.params.get('playliststart', 1) - 1 playlistend = self._downloader.params.get('playlistend', -1) if playlistend == -1: - video_ids = video_ids[playliststart:] + videos = videos[playliststart:] else: - video_ids = video_ids[playliststart:playlistend] + videos = videos[playliststart:playlistend] - if len(video_ids) == total: + if len(videos) == total: self._downloader.to_screen(u'[youtube] PL %s: Found %i videos' % (playlist_id, total)) else: - self._downloader.to_screen(u'[youtube] PL %s: Found %i videos, downloading %i' % (playlist_id, total, len(video_ids))) + self._downloader.to_screen(u'[youtube] PL %s: Found %i videos, downloading %i' % (playlist_id, total, len(videos))) - for id in video_ids: - self._downloader.download(['http://www.youtube.com/watch?v=%s' % id]) + for video in videos: + self._downloader.download([video]) return @@ -3605,9 +3620,9 @@ class TweetReelIE(InfoExtractor): 'upload_date': upload_date } return [info] - + class SteamIE(InfoExtractor): - _VALID_URL = r"""http://store.steampowered.com/ + _VALID_URL = r"""http://store.steampowered.com/ (?P<urltype>video|app)/ #If the page is only for videos or for a game (?P<gameID>\d+)/? (?P<videoID>\d*)(?P<extra>\??) #For urltype == video we sometimes get the videoID @@ -3707,7 +3722,7 @@ class RBMARadioIE(InfoExtractor): class YouPornIE(InfoExtractor): """Information extractor for youporn.com.""" _VALID_URL = r'^(?:https?://)?(?:\w+\.)?youporn\.com/watch/(?P<videoid>[0-9]+)/(?P<title>[^/]+)' - + def _print_formats(self, formats): """Print all available formats""" print(u'Available formats:') @@ -3769,8 +3784,8 @@ class YouPornIE(InfoExtractor): links = re.findall(LINK_RE, download_list_html) if(len(links) == 0): raise ExtractorError(u'ERROR: no known formats available for video') - - self._downloader.to_screen(u'[youporn] Links found: %d' % len(links)) + + self._downloader.to_screen(u'[youporn] Links found: %d' % len(links)) formats = [] for link in links: @@ -3821,7 +3836,7 @@ class YouPornIE(InfoExtractor): return return [format] - + class PornotubeIE(InfoExtractor): """Information extractor for pornotube.com.""" @@ -3893,7 +3908,7 @@ class YouJizzIE(InfoExtractor): embed_page_url = result.group(0).strip() video_id = result.group('videoid') - + webpage = self._download_webpage(embed_page_url, video_id) # Get the video URL @@ -4053,7 +4068,7 @@ class TEDIE(InfoExtractor): class MySpassIE(InfoExtractor): _VALID_URL = r'http://www.myspass.de/.*' - + def _real_extract(self, url): META_DATA_URL_TEMPLATE = 'http://www.myspass.de/myspass/includes/apps/video/getvideometadataxml.php?id=%s' @@ -4063,12 +4078,12 @@ class MySpassIE(InfoExtractor): url_parent_path, video_id = os.path.split(url_path) if not video_id: _, video_id = os.path.split(url_parent_path) - + # get metadata metadata_url = META_DATA_URL_TEMPLATE % video_id metadata_text = self._download_webpage(metadata_url, video_id) metadata = xml.etree.ElementTree.fromstring(metadata_text.encode('utf-8')) - + # extract values from metadata url_flv_el = metadata.find('url_flv') if url_flv_el is None: From 89de9eb125abd273d50c9c5cc2d2ce80fa8922f3 Mon Sep 17 00:00:00 2001 From: Filippo Valsorda <filippo.valsorda@gmail.com> Date: Tue, 26 Feb 2013 19:02:31 +0100 Subject: [PATCH 37/40] Modified Youtube video/playlist matching; fixes #668; fixes #585 --- test/test_all_urls.py | 14 +++++--- test/test_youtube_lists.py | 13 ++++++++ youtube_dl/InfoExtractors.py | 65 ++++++++++++++++++++---------------- 3 files changed, 59 insertions(+), 33 deletions(-) diff --git a/test/test_all_urls.py b/test/test_all_urls.py index 06de8e7b8..69717b3fc 100644 --- a/test/test_all_urls.py +++ b/test/test_all_urls.py @@ -11,12 +11,18 @@ from youtube_dl.InfoExtractors import YoutubeIE, YoutubePlaylistIE class TestAllURLsMatching(unittest.TestCase): def test_youtube_playlist_matching(self): - self.assertTrue(YoutubePlaylistIE().suitable(u'ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')) - self.assertTrue(YoutubePlaylistIE().suitable(u'PL63F0C78739B09958')) - self.assertFalse(YoutubePlaylistIE().suitable(u'PLtS2H6bU1M')) + self.assertTrue(YoutubePlaylistIE.suitable(u'ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')) + self.assertTrue(YoutubePlaylistIE.suitable(u'UUBABnxM4Ar9ten8Mdjj1j0Q')) #585 + self.assertTrue(YoutubePlaylistIE.suitable(u'PL63F0C78739B09958')) + self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q')) + self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8')) + self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC')) + self.assertTrue(YoutubePlaylistIE.suitable(u'https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012')) #668 + self.assertFalse(YoutubePlaylistIE.suitable(u'PLtS2H6bU1M')) def test_youtube_matching(self): - self.assertTrue(YoutubeIE().suitable(u'PLtS2H6bU1M')) + self.assertTrue(YoutubeIE.suitable(u'PLtS2H6bU1M')) + self.assertFalse(YoutubeIE.suitable(u'https://www.youtube.com/watch?v=AV6J6_AeFEQ&playnext=1&list=PL4023E734DA416012')) #668 def test_youtube_extract(self): self.assertEqual(YoutubeIE()._extract_id('http://www.youtube.com/watch?&v=BaW_jenozKc'), 'BaW_jenozKc') diff --git a/test/test_youtube_lists.py b/test/test_youtube_lists.py index 69b0f4447..7055d5a7a 100644 --- a/test/test_youtube_lists.py +++ b/test/test_youtube_lists.py @@ -41,6 +41,18 @@ class TestYoutubeLists(unittest.TestCase): self.assertEqual(map(lambda x: YoutubeIE()._extract_id(x[0]), DL.result), [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE' ]) + #661 + DL = FakeDownloader() + IE = YoutubePlaylistIE(DL) + IE.extract('PLMCmkNmxw6Z9eduM7BZjSEh7HiU543Ig0') + self.assertTrue(len(DL.result) > 20) + + #673 + DL = FakeDownloader() + IE = YoutubePlaylistIE(DL) + IE.extract('PLBB231211A4F62143') + self.assertTrue(len(DL.result) > 40) + def test_youtube_playlist_long(self): DL = FakeDownloader() IE = YoutubePlaylistIE(DL) @@ -48,6 +60,7 @@ class TestYoutubeLists(unittest.TestCase): self.assertTrue(len(DL.result) >= 799) def test_youtube_playlist_with_deleted(self): + #651 DL = FakeDownloader() IE = YoutubePlaylistIE(DL) IE.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC') diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index 021579ce0..490f30491 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -74,13 +74,15 @@ class InfoExtractor(object): self._ready = False self.set_downloader(downloader) - def suitable(self, url): + @classmethod + def suitable(cls, url): """Receives a URL and returns True if suitable for this IE.""" - return re.match(self._VALID_URL, url) is not None + return re.match(cls._VALID_URL, url) is not None - def working(self): + @classmethod + def working(cls): """Getter method for _WORKING.""" - return self._WORKING + return cls._WORKING def initialize(self): """Initializes an instance (authentication, etc).""" @@ -137,7 +139,6 @@ class YoutubeIE(InfoExtractor): (?:youtu\.be/|(?:\w+\.)?youtube(?:-nocookie)?\.com/| tube\.majestyc\.net/) # the various hostnames, with wildcard subdomains (?:.*?\#/)? # handle anchor (#/) redirect urls - (?!view_play_list|my_playlists|artist|playlist) # ignore playlist URLs (?: # the various things that can precede the ID: (?:(?:v|embed|e)/) # v/ or embed/ or e/ |(?: # or the v= param in all its forms @@ -189,9 +190,11 @@ class YoutubeIE(InfoExtractor): } IE_NAME = u'youtube' - def suitable(self, url): + @classmethod + def suitable(cls, url): """Receives a URL and returns True if suitable for this IE.""" - return re.match(self._VALID_URL, url, re.VERBOSE) is not None + if YoutubePlaylistIE.suitable(url): return False + return re.match(cls._VALID_URL, url, re.VERBOSE) is not None def report_lang(self): """Report attempt to set language.""" @@ -1668,17 +1671,17 @@ class YoutubePlaylistIE(InfoExtractor): (?:\w+\.)? youtube\.com/ (?: - (?:course|view_play_list|my_playlists|artist|playlist) - \? .*? (p|a|list)= + (?:course|view_play_list|my_playlists|artist|playlist|watch) + \? (?:.*?&)*? (?:p|a|list)= | user/.*?/user/ | p/ | user/.*?#[pg]/c/ ) - (?:PL|EC)? - |PL|EC) - ([0-9A-Za-z-_]{10,}) - (?:/.*?/([0-9A-Za-z_-]+))? - .*""" + ((?:PL|EC|UU)?[0-9A-Za-z-_]{10,}) + .* + | + ((?:PL|EC|UU)[0-9A-Za-z-_]{10,}) + )""" _TEMPLATE_URL = 'https://gdata.youtube.com/feeds/api/playlists/%s?max-results=%i&start-index=%i&v=2&alt=json' _MAX_RESULTS = 50 IE_NAME = u'youtube:playlist' @@ -1686,9 +1689,10 @@ class YoutubePlaylistIE(InfoExtractor): def __init__(self, downloader=None): InfoExtractor.__init__(self, downloader) - def suitable(self, url): + @classmethod + def suitable(cls, url): """Receives a URL and returns True if suitable for this IE.""" - return re.match(self._VALID_URL, url, re.VERBOSE) is not None + return re.match(cls._VALID_URL, url, re.VERBOSE) is not None def report_download_page(self, playlist_id, pagenum): """Report attempt to download playlist page with given number.""" @@ -1701,13 +1705,8 @@ class YoutubePlaylistIE(InfoExtractor): self._downloader.trouble(u'ERROR: invalid url: %s' % url) return - # Single video case - if mobj.group(3) is not None: - self._downloader.download([mobj.group(3)]) - return - # Download playlist videos from API - playlist_id = mobj.group(2) + playlist_id = mobj.group(1) or mobj.group(2) page_num = 1 videos = [] @@ -1727,7 +1726,12 @@ class YoutubePlaylistIE(InfoExtractor): self._downloader.trouble(u'ERROR: Invalid JSON in API response: ' + compat_str(err)) return - videos += [(entry['yt$position']['$t'], entry['content']['src']) for entry in response['feed']['entry']] + if not 'feed' in response or not 'entry' in response['feed']: + self._downloader.trouble(u'ERROR: Got a malformed response from YouTube API') + return + videos += [ (entry['yt$position']['$t'], entry['content']['src']) + for entry in response['feed']['entry'] + if 'content' in entry ] if len(response['feed']['entry']) < self._MAX_RESULTS: break @@ -2313,9 +2317,10 @@ class ComedyCentralIE(InfoExtractor): '400': '384x216', } - def suitable(self, url): + @classmethod + def suitable(cls, url): """Receives a URL and returns True if suitable for this IE.""" - return re.match(self._VALID_URL, url, re.VERBOSE) is not None + return re.match(cls._VALID_URL, url, re.VERBOSE) is not None def report_extraction(self, episode_id): self._downloader.to_screen(u'[comedycentral] %s: Extracting information' % episode_id) @@ -3628,9 +3633,10 @@ class SteamIE(InfoExtractor): (?P<videoID>\d*)(?P<extra>\??) #For urltype == video we sometimes get the videoID """ - def suitable(self, url): + @classmethod + def suitable(cls, url): """Receives a URL and returns True if suitable for this IE.""" - return re.match(self._VALID_URL, url, re.VERBOSE) is not None + return re.match(cls._VALID_URL, url, re.VERBOSE) is not None def _real_extract(self, url): m = re.match(self._VALID_URL, url, re.VERBOSE) @@ -4004,9 +4010,10 @@ class TEDIE(InfoExtractor): /(?P<name>\w+) # Here goes the name and then ".html" ''' - def suitable(self, url): + @classmethod + def suitable(cls, url): """Receives a URL and returns True if suitable for this IE.""" - return re.match(self._VALID_URL, url, re.VERBOSE) is not None + return re.match(cls._VALID_URL, url, re.VERBOSE) is not None def _real_extract(self, url): m=re.match(self._VALID_URL, url, re.VERBOSE) From 679790eee1e2c90e36e6a722e4c59c8a25c72934 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Tue, 26 Feb 2013 20:03:19 +0100 Subject: [PATCH 38/40] Do not user upper-case for non-constants --- test/test_youtube_lists.py | 62 +++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/test/test_youtube_lists.py b/test/test_youtube_lists.py index 7055d5a7a..bf0a9b34e 100644 --- a/test/test_youtube_lists.py +++ b/test/test_youtube_lists.py @@ -35,56 +35,56 @@ class FakeDownloader(object): class TestYoutubeLists(unittest.TestCase): def test_youtube_playlist(self): - DL = FakeDownloader() - IE = YoutubePlaylistIE(DL) - IE.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re') - self.assertEqual(map(lambda x: YoutubeIE()._extract_id(x[0]), DL.result), + dl = FakeDownloader() + ie = YoutubePlaylistIE(dl) + ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re') + self.assertEqual(map(lambda x: YoutubeIE()._extract_id(x[0]), dl.result), [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE' ]) #661 - DL = FakeDownloader() - IE = YoutubePlaylistIE(DL) - IE.extract('PLMCmkNmxw6Z9eduM7BZjSEh7HiU543Ig0') - self.assertTrue(len(DL.result) > 20) + dl = FakeDownloader() + ie = YoutubePlaylistIE(dl) + ie.extract('PLMCmkNmxw6Z9eduM7BZjSEh7HiU543Ig0') + self.assertTrue(len(dl.result) > 20) #673 - DL = FakeDownloader() - IE = YoutubePlaylistIE(DL) - IE.extract('PLBB231211A4F62143') - self.assertTrue(len(DL.result) > 40) + dl = FakeDownloader() + ie = YoutubePlaylistIE(dl) + ie.extract('PLBB231211A4F62143') + self.assertTrue(len(dl.result) > 40) def test_youtube_playlist_long(self): - DL = FakeDownloader() - IE = YoutubePlaylistIE(DL) - IE.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q') - self.assertTrue(len(DL.result) >= 799) + dl = FakeDownloader() + ie = YoutubePlaylistIE(dl) + ie.extract('https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q') + self.assertTrue(len(dl.result) >= 799) def test_youtube_playlist_with_deleted(self): #651 - DL = FakeDownloader() - IE = YoutubePlaylistIE(DL) - IE.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC') - self.assertFalse('pElCt5oNDuI' in map(lambda x: YoutubeIE()._extract_id(x[0]), DL.result)) - self.assertFalse('KdPEApIVdWM' in map(lambda x: YoutubeIE()._extract_id(x[0]), DL.result)) + dl = FakeDownloader() + ie = YoutubePlaylistIE(dl) + ie.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC') + self.assertFalse('pElCt5oNDuI' in map(lambda x: YoutubeIE()._extract_id(x[0]), dl.result)) + self.assertFalse('KdPEApIVdWM' in map(lambda x: YoutubeIE()._extract_id(x[0]), dl.result)) def test_youtube_course(self): - DL = FakeDownloader() - IE = YoutubePlaylistIE(DL) + dl = FakeDownloader() + ie = YoutubePlaylistIE(dl) # TODO find a > 100 (paginating?) videos course - IE.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8') - self.assertEqual(YoutubeIE()._extract_id(DL.result[0][0]), 'j9WZyLZCBzs') - self.assertEqual(len(DL.result), 25) - self.assertEqual(YoutubeIE()._extract_id(DL.result[-1][0]), 'rYefUsYuEp0') + ie.extract('https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8') + self.assertEqual(YoutubeIE()._extract_id(dl.result[0][0]), 'j9WZyLZCBzs') + self.assertEqual(len(dl.result), 25) + self.assertEqual(YoutubeIE()._extract_id(dl.result[-1][0]), 'rYefUsYuEp0') def test_youtube_channel(self): # I give up, please find a channel that does paginate and test this like test_youtube_playlist_long pass # TODO def test_youtube_user(self): - DL = FakeDownloader() - IE = YoutubeUserIE(DL) - IE.extract('https://www.youtube.com/user/TheLinuxFoundation') - self.assertTrue(len(DL.result) >= 320) + dl = FakeDownloader() + ie = YoutubeUserIE(dl) + ie.extract('https://www.youtube.com/user/TheLinuxFoundation') + self.assertTrue(len(dl.result) >= 320) if __name__ == '__main__': unittest.main() From acb8752f8062649c8aab820c6604b93866de63af Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Tue, 26 Feb 2013 20:07:38 +0100 Subject: [PATCH 39/40] fix tests in Python3, and make them parallelizable --- test/test_youtube_lists.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/test_youtube_lists.py b/test/test_youtube_lists.py index bf0a9b34e..9c2e82ea3 100644 --- a/test/test_youtube_lists.py +++ b/test/test_youtube_lists.py @@ -38,16 +38,16 @@ class TestYoutubeLists(unittest.TestCase): dl = FakeDownloader() ie = YoutubePlaylistIE(dl) ie.extract('https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re') - self.assertEqual(map(lambda x: YoutubeIE()._extract_id(x[0]), dl.result), - [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE' ]) + ytie_results = [YoutubeIE()._extract_id(r[0]) for r in dl.result] + self.assertEqual(ytie_results, [ 'bV9L5Ht9LgY', 'FXxLjLQi3Fg', 'tU3Bgo5qJZE']) - #661 + def test_issue_661(self): dl = FakeDownloader() ie = YoutubePlaylistIE(dl) ie.extract('PLMCmkNmxw6Z9eduM7BZjSEh7HiU543Ig0') self.assertTrue(len(dl.result) > 20) - #673 + def test_issue_673(self): dl = FakeDownloader() ie = YoutubePlaylistIE(dl) ie.extract('PLBB231211A4F62143') @@ -64,8 +64,9 @@ class TestYoutubeLists(unittest.TestCase): dl = FakeDownloader() ie = YoutubePlaylistIE(dl) ie.extract('https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC') - self.assertFalse('pElCt5oNDuI' in map(lambda x: YoutubeIE()._extract_id(x[0]), dl.result)) - self.assertFalse('KdPEApIVdWM' in map(lambda x: YoutubeIE()._extract_id(x[0]), dl.result)) + ytie_results = [YoutubeIE()._extract_id(r[0]) for r in dl.result] + self.assertFalse('pElCt5oNDuI' in ytie_results) + self.assertFalse('KdPEApIVdWM' in ytie_results) def test_youtube_course(self): dl = FakeDownloader() From 691db5ba02d3fccee605cea83ad25ba217f851a7 Mon Sep 17 00:00:00 2001 From: Philipp Hagemeister <phihag@phihag.de> Date: Tue, 26 Feb 2013 21:21:50 +0100 Subject: [PATCH 40/40] Don't be too clever (Fixes Python 3) --- youtube_dl/InfoExtractors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/youtube_dl/InfoExtractors.py b/youtube_dl/InfoExtractors.py index d52506389..a94648dcf 100755 --- a/youtube_dl/InfoExtractors.py +++ b/youtube_dl/InfoExtractors.py @@ -1741,8 +1741,7 @@ class YoutubePlaylistIE(InfoExtractor): break page_num += 1 - videos = map(operator.itemgetter(1), sorted(videos)) - + videos = [v[1] for v in sorted(videos)] total = len(videos) playliststart = self._downloader.params.get('playliststart', 1) - 1