diff options
34 files changed, 355 insertions, 141 deletions
diff --git a/test/test_write_info_json.py b/test/test_write_info_json.py index a5b6f6972..30c4859fd 100644 --- a/test/test_write_info_json.py +++ b/test/test_write_info_json.py @@ -31,7 +31,7 @@ params = get_params({  TEST_ID = 'BaW_jenozKc' -INFO_JSON_FILE = TEST_ID + '.mp4.info.json' +INFO_JSON_FILE = TEST_ID + '.info.json'  DESCRIPTION_FILE = TEST_ID + '.mp4.description'  EXPECTED_DESCRIPTION = u'''test chars:  "'/\ä↭𝕐 diff --git a/youtube_dl/FileDownloader.py b/youtube_dl/FileDownloader.py index 088f59586..e5a542ed5 100644 --- a/youtube_dl/FileDownloader.py +++ b/youtube_dl/FileDownloader.py @@ -5,9 +5,6 @@ import subprocess  import sys  import time -if os.name == 'nt': -    import ctypes -  from .utils import (      compat_urllib_error,      compat_urllib_request, @@ -151,16 +148,8 @@ class FileDownloader(object):      def to_stderr(self, message):          self.ydl.to_screen(message) -    def to_cons_title(self, message): -        """Set console/terminal window title to message.""" -        if not self.params.get('consoletitle', False): -            return -        if os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow(): -            # c_wchar_p() might not be necessary if `message` is -            # already of type unicode() -            ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message)) -        elif 'TERM' in os.environ: -            self.to_screen('\033]0;%s\007' % message, skip_eol=True) +    def to_console_title(self, message): +        self.ydl.to_console_title(message)      def trouble(self, *args, **kargs):          self.ydl.trouble(*args, **kargs) @@ -249,7 +238,7 @@ class FileDownloader(object):          else:              self.to_screen(u'\r%s[download] %s of %s at %s ETA %s' %                  (clear_line, 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' % +        self.to_console_title(u'youtube-dl - %s of %s at %s ETA %s' %                  (percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip()))      def report_resuming_byte(self, resume_len): diff --git a/youtube_dl/YoutubeDL.py b/youtube_dl/YoutubeDL.py index d29e8bec5..a2e3df1f9 100644 --- a/youtube_dl/YoutubeDL.py +++ b/youtube_dl/YoutubeDL.py @@ -5,6 +5,7 @@ from __future__ import absolute_import  import errno  import io +import json  import os  import re  import shutil @@ -13,7 +14,34 @@ import sys  import time  import traceback -from .utils import * +if os.name == 'nt': +    import ctypes + +from .utils import ( +    compat_http_client, +    compat_print, +    compat_str, +    compat_urllib_error, +    compat_urllib_request, +    ContentTooShortError, +    date_from_str, +    DateRange, +    determine_ext, +    DownloadError, +    encodeFilename, +    ExtractorError, +    locked_file, +    MaxDownloadsReached, +    PostProcessingError, +    preferredencoding, +    SameFileError, +    sanitize_filename, +    subtitles_filename, +    takewhile_inclusive, +    UnavailableVideoError, +    write_json_file, +    write_string, +)  from .extractor import get_info_extractor, gen_extractors  from .FileDownloader import FileDownloader @@ -57,6 +85,7 @@ class YoutubeDL(object):      forcethumbnail:    Force printing thumbnail URL.      forcedescription:  Force printing description.      forcefilename:     Force printing final filename. +    forcejson:         Force printing info_dict as JSON.      simulate:          Do not download the video files.      format:            Video format code.      format_limit:      Highest quality format to try. @@ -176,6 +205,37 @@ class YoutubeDL(object):              output = output.encode(preferredencoding())          sys.stderr.write(output) +    def to_console_title(self, message): +        if not self.params.get('consoletitle', False): +            return +        if os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow(): +            # c_wchar_p() might not be necessary if `message` is +            # already of type unicode() +            ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message)) +        elif 'TERM' in os.environ: +            write_string(u'\033]0;%s\007' % message, self._screen_file) + +    def save_console_title(self): +        if not self.params.get('consoletitle', False): +            return +        if 'TERM' in os.environ: +            # Save the title on stack +            write_string(u'\033[22;0t', self._screen_file) + +    def restore_console_title(self): +        if not self.params.get('consoletitle', False): +            return +        if 'TERM' in os.environ: +            # Restore the title from stack +            write_string(u'\033[23;0t', self._screen_file) + +    def __enter__(self): +        self.save_console_title() +        return self + +    def __exit__(self, *args): +        self.restore_console_title() +      def fixed_template(self):          """Checks if the output template is fixed."""          return (re.search(u'(?u)%\\(.+?\\)s', self.params['outtmpl']) is None) @@ -254,7 +314,7 @@ class YoutubeDL(object):          """Report file has already been fully downloaded."""          try:              self.to_screen(u'[download] %s has already been downloaded' % file_name) -        except (UnicodeEncodeError) as err: +        except UnicodeEncodeError:              self.to_screen(u'[download] The file has already been downloaded')      def increment_downloads(self): @@ -593,6 +653,8 @@ class YoutubeDL(object):              compat_print(filename)          if self.params.get('forceformat', False):              compat_print(info_dict['format']) +        if self.params.get('forcejson', False): +            compat_print(json.dumps(info_dict))          # Do nothing else if in simulate mode          if self.params.get('simulate', False): @@ -655,7 +717,7 @@ class YoutubeDL(object):                      return          if self.params.get('writeinfojson', False): -            infofn = filename + u'.info.json' +            infofn = os.path.splitext(filename)[0] + u'.info.json'              self.report_writeinfojson(infofn)              try:                  json_info_dict = dict((k, v) for k, v in info_dict.items() if not k in ['urlhandle']) diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 4dee487ab..e27bd4544 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -306,6 +306,9 @@ def parseOpts(overrideArguments=None):      verbosity.add_option('--get-format',              action='store_true', dest='getformat',              help='simulate, quiet but print output format', default=False) +    verbosity.add_option('-j', '--dump-json', +            action='store_true', dest='dumpjson', +            help='simulate, quiet but print JSON information', default=False)      verbosity.add_option('--newline',              action='store_true', dest='progress_with_newline', help='output progress bar as new lines', default=False)      verbosity.add_option('--no-progress', @@ -603,13 +606,12 @@ def _real_main(argv=None):                       u' file! Use "%%(ext)s" instead of %r' %                       determine_ext(outtmpl, u'')) -    # YoutubeDL -    ydl = YoutubeDL({ +    ydl_opts = {          'usenetrc': opts.usenetrc,          'username': opts.username,          'password': opts.password,          'videopassword': opts.videopassword, -        'quiet': (opts.quiet or opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat), +        'quiet': (opts.quiet or opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat or opts.dumpjson),          'forceurl': opts.geturl,          'forcetitle': opts.gettitle,          'forceid': opts.getid, @@ -617,8 +619,9 @@ def _real_main(argv=None):          'forcedescription': opts.getdescription,          'forcefilename': opts.getfilename,          'forceformat': opts.getformat, +        'forcejson': opts.dumpjson,          'simulate': opts.simulate, -        'skip_download': (opts.skip_download or opts.simulate or opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat), +        'skip_download': (opts.skip_download or opts.simulate or opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat or opts.dumpjson),          'format': opts.format,          'format_limit': opts.format_limit,          'listformats': opts.listformats, @@ -667,61 +670,63 @@ def _real_main(argv=None):          'youtube_print_sig_code': opts.youtube_print_sig_code,          'age_limit': opts.age_limit,          'download_archive': opts.download_archive, -        }) +    } -    if opts.verbose: -        write_string(u'[debug] youtube-dl version ' + __version__ + u'\n') -        try: -            sp = subprocess.Popen( -                ['git', 'rev-parse', '--short', 'HEAD'], -                stdout=subprocess.PIPE, stderr=subprocess.PIPE, -                cwd=os.path.dirname(os.path.abspath(__file__))) -            out, err = sp.communicate() -            out = out.decode().strip() -            if re.match('[0-9a-f]+', out): -                write_string(u'[debug] Git HEAD: ' + out + u'\n') -        except: +    with YoutubeDL(ydl_opts) as ydl: +        if opts.verbose: +            write_string(u'[debug] youtube-dl version ' + __version__ + u'\n')              try: -                sys.exc_clear() +                sp = subprocess.Popen( +                    ['git', 'rev-parse', '--short', 'HEAD'], +                    stdout=subprocess.PIPE, stderr=subprocess.PIPE, +                    cwd=os.path.dirname(os.path.abspath(__file__))) +                out, err = sp.communicate() +                out = out.decode().strip() +                if re.match('[0-9a-f]+', out): +                    write_string(u'[debug] Git HEAD: ' + out + u'\n')              except: -                pass -        write_string(u'[debug] Python version %s - %s' %(platform.python_version(), platform_name()) + u'\n') - -        proxy_map = {} -        for handler in opener.handlers: -            if hasattr(handler, 'proxies'): -                proxy_map.update(handler.proxies) -        write_string(u'[debug] Proxy map: ' + compat_str(proxy_map) + u'\n') - -    ydl.add_default_info_extractors() - -    # PostProcessors -    # Add the metadata pp first, the other pps will copy it -    if opts.addmetadata: -        ydl.add_post_processor(FFmpegMetadataPP()) -    if opts.extractaudio: -        ydl.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, nopostoverwrites=opts.nopostoverwrites)) -    if opts.recodevideo: -        ydl.add_post_processor(FFmpegVideoConvertor(preferedformat=opts.recodevideo)) -    if opts.embedsubtitles: -        ydl.add_post_processor(FFmpegEmbedSubtitlePP(subtitlesformat=opts.subtitlesformat)) - -    # Update version -    if opts.update_self: -        update_self(ydl.to_screen, opts.verbose) - -    # Maybe do nothing -    if len(all_urls) < 1: -        if not opts.update_self: -            parser.error(u'you must provide at least one URL') -        else: -            sys.exit() +                try: +                    sys.exc_clear() +                except: +                    pass +            write_string(u'[debug] Python version %s - %s' % +                         (platform.python_version(), platform_name()) + u'\n') + +            proxy_map = {} +            for handler in opener.handlers: +                if hasattr(handler, 'proxies'): +                    proxy_map.update(handler.proxies) +            write_string(u'[debug] Proxy map: ' + compat_str(proxy_map) + u'\n') + +        ydl.add_default_info_extractors() + +        # PostProcessors +        # Add the metadata pp first, the other pps will copy it +        if opts.addmetadata: +            ydl.add_post_processor(FFmpegMetadataPP()) +        if opts.extractaudio: +            ydl.add_post_processor(FFmpegExtractAudioPP(preferredcodec=opts.audioformat, preferredquality=opts.audioquality, nopostoverwrites=opts.nopostoverwrites)) +        if opts.recodevideo: +            ydl.add_post_processor(FFmpegVideoConvertor(preferedformat=opts.recodevideo)) +        if opts.embedsubtitles: +            ydl.add_post_processor(FFmpegEmbedSubtitlePP(subtitlesformat=opts.subtitlesformat)) + +        # Update version +        if opts.update_self: +            update_self(ydl.to_screen, opts.verbose) + +        # Maybe do nothing +        if len(all_urls) < 1: +            if not opts.update_self: +                parser.error(u'you must provide at least one URL') +            else: +                sys.exit() -    try: -        retcode = ydl.download(all_urls) -    except MaxDownloadsReached: -        ydl.to_screen(u'--max-download limit reached, aborting.') -        retcode = 101 +        try: +            retcode = ydl.download(all_urls) +        except MaxDownloadsReached: +            ydl.to_screen(u'--max-download limit reached, aborting.') +            retcode = 101      # Dump cookie jar if requested      if opts.cookiefile is not None: diff --git a/youtube_dl/extractor/__init__.py b/youtube_dl/extractor/__init__.py index a30de3033..ffb74df9f 100644 --- a/youtube_dl/extractor/__init__.py +++ b/youtube_dl/extractor/__init__.py @@ -26,6 +26,7 @@ from .comedycentral import ComedyCentralIE  from .condenast import CondeNastIE  from .criterion import CriterionIE  from .cspan import CSpanIE +from .d8 import D8IE  from .dailymotion import (      DailymotionIE,      DailymotionPlaylistIE, @@ -117,7 +118,10 @@ from .slashdot import SlashdotIE  from .slideshare import SlideshareIE  from .sohu import SohuIE  from .soundcloud import SoundcloudIE, SoundcloudSetIE, SoundcloudUserIE -from .southparkstudios import SouthParkStudiosIE +from .southparkstudios import ( +    SouthParkStudiosIE, +    SouthparkDeIE, +)  from .space import SpaceIE  from .spankwire import SpankwireIE  from .spiegel import SpiegelIE @@ -130,6 +134,7 @@ from .techtalks import TechTalksIE  from .ted import TEDIE  from .tf1 import TF1IE  from .thisav import ThisAVIE +from .toutv import TouTvIE  from .traileraddict import TrailerAddictIE  from .trilulilu import TriluliluIE  from .tube8 import Tube8IE diff --git a/youtube_dl/extractor/arte.py b/youtube_dl/extractor/arte.py index b35a679e3..44d0b5d70 100644 --- a/youtube_dl/extractor/arte.py +++ b/youtube_dl/extractor/arte.py @@ -69,7 +69,7 @@ class ArteTvIE(InfoExtractor):              lang = mobj.group('lang')              return self._extract_liveweb(url, name, lang) -        if re.search(self._LIVE_URL, video_id) is not None: +        if re.search(self._LIVE_URL, url) is not None:              raise ExtractorError(u'Arte live streams are not yet supported, sorry')              # self.extractLiveStream(url)              # return @@ -115,7 +115,7 @@ class ArteTvIE(InfoExtractor):          event_doc = config_doc.find('event')          url_node = event_doc.find('video').find('urlHd')          if url_node is None: -            url_node = video_doc.find('urlSd') +            url_node = event_doc.find('urlSd')          return {'id': video_id,                  'title': event_doc.find('name%s' % lang.capitalize()).text, diff --git a/youtube_dl/extractor/auengine.py b/youtube_dl/extractor/auengine.py index 0febbff4f..95c038003 100644 --- a/youtube_dl/extractor/auengine.py +++ b/youtube_dl/extractor/auengine.py @@ -1,10 +1,10 @@ -import os.path  import re  from .common import InfoExtractor  from ..utils import (      compat_urllib_parse, -    compat_urllib_parse_urlparse, +    determine_ext, +    ExtractorError,  )  class AUEngineIE(InfoExtractor): @@ -25,22 +25,25 @@ class AUEngineIE(InfoExtractor):          title = self._html_search_regex(r'<title>(?P<title>.+?)</title>',                  webpage, u'title')          title = title.strip() -        links = re.findall(r'[^A-Za-z0-9]?(?:file|url):\s*["\'](http[^\'"&]*)', webpage) -        links = [compat_urllib_parse.unquote(l) for l in links] +        links = re.findall(r'\s(?:file|url):\s*["\']([^\'"]+)["\']', webpage) +        links = map(compat_urllib_parse.unquote, links) + +        thumbnail = None +        video_url = None          for link in links: -            root, pathext = os.path.splitext(compat_urllib_parse_urlparse(link).path) -            if pathext == '.png': +            if link.endswith('.png'):                  thumbnail = link -            elif pathext == '.mp4': -                url = link -                ext = pathext +            elif '/videos/' in link: +                video_url = link +        if not video_url: +            raise ExtractorError(u'Could not find video URL') +        ext = u'.' + determine_ext(video_url)          if ext == title[-len(ext):]:              title = title[:-len(ext)] -        ext = ext[1:] -        return [{ + +        return {              'id':        video_id, -            'url':       url, -            'ext':       ext, +            'url':       video_url,              'title':     title,              'thumbnail': thumbnail, -        }] +        } diff --git a/youtube_dl/extractor/canalplus.py b/youtube_dl/extractor/canalplus.py index 1db9b24cf..bfa2a8b40 100644 --- a/youtube_dl/extractor/canalplus.py +++ b/youtube_dl/extractor/canalplus.py @@ -5,6 +5,7 @@ import xml.etree.ElementTree  from .common import InfoExtractor  from ..utils import unified_strdate +  class CanalplusIE(InfoExtractor):      _VALID_URL = r'https?://(www\.canalplus\.fr/.*?/(?P<path>.*)|player\.canalplus\.fr/#/(?P<id>\d+))'      _VIDEO_INFO_TEMPLATE = 'http://service.canal-plus.com/video/rest/getVideosLiees/cplus/%s' @@ -25,7 +26,7 @@ class CanalplusIE(InfoExtractor):      def _real_extract(self, url):          mobj = re.match(self._VALID_URL, url) -        video_id = mobj.group('id') +        video_id = mobj.groupdict().get('id')          if video_id is None:              webpage = self._download_webpage(url, mobj.group('path'))              video_id = self._search_regex(r'videoId = "(\d+)";', webpage, u'video id') diff --git a/youtube_dl/extractor/collegehumor.py b/youtube_dl/extractor/collegehumor.py index 8d4c93d6d..0c29acfb1 100644 --- a/youtube_dl/extractor/collegehumor.py +++ b/youtube_dl/extractor/collegehumor.py @@ -71,10 +71,8 @@ class CollegeHumorIE(InfoExtractor):              adoc = xml.etree.ElementTree.fromstring(manifestXml)              try: -                media_node = adoc.findall('./{http://ns.adobe.com/f4m/1.0}media')[0] -                node_id = media_node.attrib['url']                  video_id = adoc.findall('./{http://ns.adobe.com/f4m/1.0}id')[0].text -            except IndexError as err: +            except IndexError:                  raise ExtractorError(u'Invalid manifest file')              url_pr = compat_urllib_parse_urlparse(info['thumbnail'])              info['url'] = url_pr.scheme + '://' + url_pr.netloc + video_id[:-2].replace('.csmil','').replace(',','') diff --git a/youtube_dl/extractor/common.py b/youtube_dl/extractor/common.py index f787d0a3c..eb3435c77 100644 --- a/youtube_dl/extractor/common.py +++ b/youtube_dl/extractor/common.py @@ -350,6 +350,17 @@ class InfoExtractor(object):          if secure: regexes = self._og_regexes('video:secure_url') + regexes          return self._html_search_regex(regexes, html, name, **kargs) +    def _html_search_meta(self, name, html, display_name=None): +        if display_name is None: +            display_name = name +        return self._html_search_regex( +            r'''(?ix)<meta(?=[^>]+(?:name|property)=["\']%s["\']) +                    [^>]+content=["\']([^"\']+)["\']''' % re.escape(name), +            html, display_name, fatal=False) + +    def _dc_search_uploader(self, html): +        return self._html_search_meta('dc.creator', html, 'uploader') +      def _rta_search(self, html):          # See http://www.rtalabel.org/index.php?content=howtofaq#single          if re.search(r'(?ix)<meta\s+name="rating"\s+' @@ -358,6 +369,23 @@ class InfoExtractor(object):              return 18          return 0 +    def _media_rating_search(self, html): +        # See http://www.tjg-designs.com/WP/metadata-code-examples-adding-metadata-to-your-web-pages/ +        rating = self._html_search_meta('rating', html) + +        if not rating: +            return None + +        RATING_TABLE = { +            'safe for kids': 0, +            'general': 8, +            '14 years': 14, +            'mature': 17, +            'restricted': 19, +        } +        return RATING_TABLE.get(rating.lower(), None) + +  class SearchInfoExtractor(InfoExtractor):      """ diff --git a/youtube_dl/extractor/d8.py b/youtube_dl/extractor/d8.py new file mode 100644 index 000000000..a56842b16 --- /dev/null +++ b/youtube_dl/extractor/d8.py @@ -0,0 +1,22 @@ +# encoding: utf-8 +from .canalplus import CanalplusIE + + +class D8IE(CanalplusIE): +    _VALID_URL = r'https?://www\.d8\.tv/.*?/(?P<path>.*)' +    _VIDEO_INFO_TEMPLATE = 'http://service.canal-plus.com/video/rest/getVideosLiees/d8/%s' +    IE_NAME = u'd8.tv' + +    _TEST = { +        u'url': u'http://www.d8.tv/d8-docs-mags/pid6589-d8-campagne-intime.html', +        u'file': u'966289.flv', +        u'info_dict': { +            u'title': u'Campagne intime - Documentaire exceptionnel', +            u'description': u'md5:d2643b799fb190846ae09c61e59a859f', +            u'upload_date': u'20131108', +        }, +        u'params': { +            # rtmp +            u'skip_download': True, +        }, +    } diff --git a/youtube_dl/extractor/eighttracks.py b/youtube_dl/extractor/eighttracks.py index 2cfbcd363..f21ef8853 100644 --- a/youtube_dl/extractor/eighttracks.py +++ b/youtube_dl/extractor/eighttracks.py @@ -1,4 +1,3 @@ -import itertools  import json  import random  import re diff --git a/youtube_dl/extractor/facebook.py b/youtube_dl/extractor/facebook.py index f8bdfc2d3..3b210710e 100644 --- a/youtube_dl/extractor/facebook.py +++ b/youtube_dl/extractor/facebook.py @@ -1,5 +1,4 @@  import json -import netrc  import re  import socket diff --git a/youtube_dl/extractor/fktv.py b/youtube_dl/extractor/fktv.py index 9c89362ef..dba1a8dc2 100644 --- a/youtube_dl/extractor/fktv.py +++ b/youtube_dl/extractor/fktv.py @@ -39,7 +39,6 @@ class FKTVIE(InfoExtractor):          for i, _ in enumerate(files, 1):              video_id = '%04d%d' % (episode, i)              video_url = 'http://dl%d.fernsehkritik.tv/fernsehkritik%d%s.flv' % (server, episode, '' if i == 1 else '-%d' % i) -            video_title = 'Fernsehkritik %d.%d' % (episode, i)              videos.append({                  'id': video_id,                  'url': video_url, diff --git a/youtube_dl/extractor/gamespot.py b/youtube_dl/extractor/gamespot.py index 098768361..9645b00c3 100644 --- a/youtube_dl/extractor/gamespot.py +++ b/youtube_dl/extractor/gamespot.py @@ -24,7 +24,7 @@ class GameSpotIE(InfoExtractor):      def _real_extract(self, url):          mobj = re.match(self._VALID_URL, url) -        page_id = video_id = mobj.group('page_id') +        page_id = mobj.group('page_id')          webpage = self._download_webpage(url, page_id)          data_video_json = self._search_regex(r'data-video=\'(.*?)\'', webpage, u'data video')          data_video = json.loads(unescapeHTML(data_video_json)) diff --git a/youtube_dl/extractor/generic.py b/youtube_dl/extractor/generic.py index c7552fddb..e1d6a2a01 100644 --- a/youtube_dl/extractor/generic.py +++ b/youtube_dl/extractor/generic.py @@ -162,6 +162,16 @@ class GenericIE(InfoExtractor):              raise ExtractorError(u'Failed to download URL: %s' % url)          self.report_extraction(video_id) + +        # it's tempting to parse this further, but you would +        # have to take into account all the variations like +        #   Video Title - Site Name +        #   Site Name | Video Title +        #   Video Title - Tagline | Site Name +        # and so on and so forth; it's just not practical +        video_title = self._html_search_regex(r'<title>(.*)</title>', +            webpage, u'video title', default=u'video', flags=re.DOTALL) +          # Look for BrightCove:          bc_url = BrightcoveIE._extract_brightcove_url(webpage)          if bc_url is not None: @@ -177,11 +187,13 @@ class GenericIE(InfoExtractor):              return self.url_result(surl, 'Vimeo')          # Look for embedded YouTube player -        mobj = re.search( -            r'<iframe[^>]+?src=(["\'])(?P<url>https?://(?:www\.)?youtube.com/embed/.+?)\1', webpage) -        if mobj: -            surl = unescapeHTML(mobj.group(u'url')) -            return self.url_result(surl, 'Youtube') +        matches = re.findall( +            r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?:)?//(?:www\.)?youtube.com/embed/.+?)\1', webpage) +        if matches: +            urlrs = [self.url_result(unescapeHTML(tuppl[1]), 'Youtube') +                     for tuppl in matches] +            return self.playlist_result( +                urlrs, playlist_id=video_id, playlist_title=video_title)          # Look for Bandcamp pages with custom domain          mobj = re.search(r'<meta property="og:url"[^>]*?content="(.*?bandcamp\.com.*?)"', webpage) @@ -226,15 +238,6 @@ class GenericIE(InfoExtractor):          video_extension = os.path.splitext(video_id)[1][1:]          video_id = os.path.splitext(video_id)[0] -        # it's tempting to parse this further, but you would -        # have to take into account all the variations like -        #   Video Title - Site Name -        #   Site Name | Video Title -        #   Video Title - Tagline | Site Name -        # and so on and so forth; it's just not practical -        video_title = self._html_search_regex(r'<title>(.*)</title>', -            webpage, u'video title', default=u'video', flags=re.DOTALL) -          # video uploader is domain name          video_uploader = self._search_regex(r'(?:https?://)?([^/]*)/.*',              url, u'video uploader') diff --git a/youtube_dl/extractor/jeuxvideo.py b/youtube_dl/extractor/jeuxvideo.py index 6bb54b932..0020c47cf 100644 --- a/youtube_dl/extractor/jeuxvideo.py +++ b/youtube_dl/extractor/jeuxvideo.py @@ -22,7 +22,7 @@ class JeuxVideoIE(InfoExtractor):      def _real_extract(self, url):          mobj = re.match(self._VALID_URL, url) -        title = re.match(self._VALID_URL, url).group(1) +        title = mobj.group(1)          webpage = self._download_webpage(url, title)          xml_link = self._html_search_regex(              r'<param name="flashvars" value="config=(.*?)" />', diff --git a/youtube_dl/extractor/livestream.py b/youtube_dl/extractor/livestream.py index 1a3e0ae6b..5f548437c 100644 --- a/youtube_dl/extractor/livestream.py +++ b/youtube_dl/extractor/livestream.py @@ -6,9 +6,7 @@ from .common import InfoExtractor  from ..utils import (      compat_urllib_parse_urlparse,      compat_urlparse, -    get_meta_content,      xpath_with_ns, -    ExtractorError,  ) diff --git a/youtube_dl/extractor/mtv.py b/youtube_dl/extractor/mtv.py index 24a79ae13..04afd6c4c 100644 --- a/youtube_dl/extractor/mtv.py +++ b/youtube_dl/extractor/mtv.py @@ -48,7 +48,7 @@ class MTVIE(InfoExtractor):      def _transform_rtmp_url(rtmp_video_url):          m = re.match(r'^rtmpe?://.*?/(?P<finalid>gsp\..+?/.*)$', rtmp_video_url)          if not m: -            raise ExtractorError(u'Cannot transform RTMP url') +            return rtmp_video_url          base = 'http://mtvnmobile.vo.llnwd.net/kip0/_pxn=1+_pxI0=Ripod-h264+_pxL0=undefined+_pxM0=+_pxK=18639+_pxE=mp4/44620/mtvnorigin/'          return base + m.group('finalid') @@ -59,7 +59,6 @@ class MTVIE(InfoExtractor):          if '/error_country_block.swf' in metadataXml:              raise ExtractorError(u'This video is not available from your country.', expected=True)          mdoc = xml.etree.ElementTree.fromstring(metadataXml.encode('utf-8')) -        renditions = mdoc.findall('.//rendition')          formats = []          for rendition in mdoc.findall('.//rendition'): diff --git a/youtube_dl/extractor/pornhub.py b/youtube_dl/extractor/pornhub.py index 75cf4bb9f..8b3471919 100644 --- a/youtube_dl/extractor/pornhub.py +++ b/youtube_dl/extractor/pornhub.py @@ -6,7 +6,6 @@ from ..utils import (      compat_urllib_parse_urlparse,      compat_urllib_request,      compat_urllib_parse, -    unescapeHTML,  )  from ..aes import (      aes_decrypt_text diff --git a/youtube_dl/extractor/soundcloud.py b/youtube_dl/extractor/soundcloud.py index 83e1f055f..687457e10 100644 --- a/youtube_dl/extractor/soundcloud.py +++ b/youtube_dl/extractor/soundcloud.py @@ -158,7 +158,6 @@ class SoundcloudSetIE(SoundcloudIE):          resolv_url = self._resolv_url(url)          info_json = self._download_webpage(resolv_url, full_title) -        videos = []          info = json.loads(info_json)          if 'errors' in info:              for err in info['errors']: diff --git a/youtube_dl/extractor/southparkstudios.py b/youtube_dl/extractor/southparkstudios.py index b1e96b679..a711531e6 100644 --- a/youtube_dl/extractor/southparkstudios.py +++ b/youtube_dl/extractor/southparkstudios.py @@ -5,21 +5,19 @@ from .mtv import MTVIE, _media_xml_tag  class SouthParkStudiosIE(MTVIE):      IE_NAME = u'southparkstudios.com' -    _VALID_URL = r'https?://www\.southparkstudios\.com/(clips|full-episodes)/(?P<id>.+?)(\?|#|$)' +    _VALID_URL = r'(https?://)?(www\.)?(?P<url>southparkstudios\.com/(clips|full-episodes)/(?P<id>.+?)(\?|#|$))'      _FEED_URL = 'http://www.southparkstudios.com/feeds/video-player/mrss' -    _TEST = { +    # Overwrite MTVIE properties we don't want +    _TESTS = [{          u'url': u'http://www.southparkstudios.com/clips/104437/bat-daded#tab=featured',          u'file': u'a7bff6c2-ed00-11e0-aca6-0026b9414f30.mp4',          u'info_dict': {              u'title': u'Bat Daded',              u'description': u'Randy disqualifies South Park by getting into a fight with Bat Dad.',          }, -    } - -    # Overwrite MTVIE properties we don't want -    _TESTS = [] +    }]      def _get_thumbnail_url(self, uri, itemdoc):          search_path = '%s/%s' % (_media_xml_tag('group'), _media_xml_tag('thumbnail')) @@ -31,8 +29,23 @@ class SouthParkStudiosIE(MTVIE):      def _real_extract(self, url):          mobj = re.match(self._VALID_URL, url) +        url = u'http://www.' + mobj.group(u'url')          video_id = mobj.group('id')          webpage = self._download_webpage(url, video_id)          mgid = self._search_regex(r'swfobject.embedSWF\(".*?(mgid:.*?)"',                                    webpage, u'mgid')          return self._get_videos_info(mgid) + +class SouthparkDeIE(SouthParkStudiosIE): +    IE_NAME = u'southpark.de' +    _VALID_URL = r'(https?://)?(www\.)?(?P<url>southpark\.de/(clips|alle-episoden)/(?P<id>.+?)(\?|#|$))' +    _FEED_URL = 'http://www.southpark.de/feeds/video-player/mrss/' + +    _TESTS = [{ +        u'url': u'http://www.southpark.de/clips/uygssh/the-government-wont-respect-my-privacy#tab=featured', +        u'file': u'85487c96-b3b9-4e39-9127-ad88583d9bf2.mp4', +        u'info_dict': { +            u'title': u'The Government Won\'t Respect My Privacy', +            u'description': u'Cartman explains the benefits of "Shitter" to Stan, Kyle and Craig.', +        }, +    }] diff --git a/youtube_dl/extractor/spankwire.py b/youtube_dl/extractor/spankwire.py index 97f9c268a..794550c81 100644 --- a/youtube_dl/extractor/spankwire.py +++ b/youtube_dl/extractor/spankwire.py @@ -6,7 +6,6 @@ from ..utils import (      compat_urllib_parse_urlparse,      compat_urllib_request,      compat_urllib_parse, -    unescapeHTML,  )  from ..aes import (      aes_decrypt_text diff --git a/youtube_dl/extractor/spiegel.py b/youtube_dl/extractor/spiegel.py index 6dc2eda6d..19ce585cf 100644 --- a/youtube_dl/extractor/spiegel.py +++ b/youtube_dl/extractor/spiegel.py @@ -2,7 +2,6 @@ import re  import xml.etree.ElementTree  from .common import InfoExtractor -from ..utils import determine_ext  class SpiegelIE(InfoExtractor): diff --git a/youtube_dl/extractor/teamcoco.py b/youtube_dl/extractor/teamcoco.py index bc48620f0..165d9f88b 100644 --- a/youtube_dl/extractor/teamcoco.py +++ b/youtube_dl/extractor/teamcoco.py @@ -60,7 +60,7 @@ class TeamcocoIE(InfoExtractor):                  return -1          formats.sort(key=sort_key)          if not formats: -            raise RegexNotFoundError(u'Unable to extract video URL') +            raise ExtractorError(u'Unable to extract video URL')          return {              'id':          video_id, diff --git a/youtube_dl/extractor/ted.py b/youtube_dl/extractor/ted.py index 2e497c86e..4bca62ba0 100644 --- a/youtube_dl/extractor/ted.py +++ b/youtube_dl/extractor/ted.py @@ -4,7 +4,6 @@ import re  from .subtitles import SubtitlesInfoExtractor  from ..utils import ( -    compat_str,      RegexNotFoundError,  ) @@ -113,6 +112,6 @@ class TEDIE(SubtitlesInfoExtractor):                      url = 'http://www.ted.com/talks/subtitles/id/%s/lang/%s/format/srt' % (video_id, l)                      sub_lang_list[l] = url                  return sub_lang_list -        except RegexNotFoundError as err: +        except RegexNotFoundError:              self._downloader.report_warning(u'video doesn\'t have subtitles')          return {} diff --git a/youtube_dl/extractor/toutv.py b/youtube_dl/extractor/toutv.py new file mode 100644 index 000000000..73ea67da9 --- /dev/null +++ b/youtube_dl/extractor/toutv.py @@ -0,0 +1,75 @@ +# coding: utf-8 +import re +import xml.etree.ElementTree + +from .common import InfoExtractor +from ..utils import ( +    ExtractorError, +    unified_strdate, +) + + +class TouTvIE(InfoExtractor): +    IE_NAME = u'tou.tv' +    _VALID_URL = r'https?://www\.tou\.tv/(?P<id>[a-zA-Z0-9_-]+(?:/(?P<episode>S[0-9]+E[0-9]+)))' + +    _TEST = { +        u'url': u'http://www.tou.tv/30-vies/S04E41', +        u'file': u'30-vies_S04E41.mp4', +        u'info_dict': { +            u'title': u'30 vies Saison 4 / Épisode 41', +            u'description': u'md5:da363002db82ccbe4dafeb9cab039b09', +            u'age_limit': 8, +            u'uploader': u'Groupe des Nouveaux Médias', +            u'duration': 1296, +            u'upload_date': u'20131118', +            u'thumbnail': u'http://static.tou.tv/medias/images/2013-11-18_19_00_00_30VIES_0341_01_L.jpeg', +        }, +        u'params': { +            u'skip_download': True,  # Requires rtmpdump +        }, +        u'xskip': 'Only available in Canada' +    } + +    def _real_extract(self, url): +        mobj = re.match(self._VALID_URL, url) +        video_id = mobj.group('id') +        webpage = self._download_webpage(url, video_id) + +        mediaId = self._search_regex( +            r'"idMedia":\s*"([^"]+)"', webpage, u'media ID') + +        # TODO test from de +        streams_url = u'http://release.theplatform.com/content.select?pid=' + mediaId +        streams_webpage = self._download_webpage( +            streams_url, video_id, note=u'Downloading stream list') + +        streams_doc = xml.etree.ElementTree.fromstring( +            streams_webpage.encode('utf-8')) +        video_url = next(n.text +                         for n in streams_doc.findall('.//choice/url') +                         if u'//ad.doubleclick' not in n.text) +        if video_url.endswith('/Unavailable.flv'): +            raise ExtractorError( +                u'Access to this video is blocked from outside of Canada', +                expected=True) + +        duration_str = self._html_search_meta( +            'video:duration', webpage, u'duration') +        duration = int(duration_str) if duration_str else None +        upload_date_str = self._html_search_meta( +            'video:release_date', webpage, u'upload date') +        upload_date = unified_strdate(upload_date_str) if upload_date_str else None + +        return { +            'id': video_id, +            'title': self._og_search_title(webpage), +            'url': video_url, +            'description': self._og_search_description(webpage), +            'uploader': self._dc_search_uploader(webpage), +            'thumbnail': self._og_search_thumbnail(webpage), +            'age_limit': self._media_rating_search(webpage), +            'duration': duration, +            'upload_date': upload_date, +            'ext': 'mp4', +        } diff --git a/youtube_dl/extractor/tube8.py b/youtube_dl/extractor/tube8.py index d4b7603c7..4d9d41db3 100644 --- a/youtube_dl/extractor/tube8.py +++ b/youtube_dl/extractor/tube8.py @@ -5,8 +5,6 @@ from .common import InfoExtractor  from ..utils import (      compat_urllib_parse_urlparse,      compat_urllib_request, -    compat_urllib_parse, -    unescapeHTML,  )  from ..aes import (      aes_decrypt_text diff --git a/youtube_dl/extractor/xtube.py b/youtube_dl/extractor/xtube.py index 03ad88bed..e3458d2bd 100644 --- a/youtube_dl/extractor/xtube.py +++ b/youtube_dl/extractor/xtube.py @@ -5,7 +5,6 @@ from .common import InfoExtractor  from ..utils import (      compat_urllib_parse_urlparse,      compat_urllib_request, -    compat_urllib_parse,  )  class XTubeIE(InfoExtractor): diff --git a/youtube_dl/extractor/youtube.py b/youtube_dl/extractor/youtube.py index 8c0e6f252..41838237c 100644 --- a/youtube_dl/extractor/youtube.py +++ b/youtube_dl/extractor/youtube.py @@ -139,10 +139,10 @@ class YoutubeBaseInfoExtractor(InfoExtractor):  class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):      IE_DESC = u'YouTube.com' -    _VALID_URL = r"""^ +    _VALID_URL = r"""(?x)^                       ( -                         (?:https?://)?                                       # http(s):// (optional) -                         (?:(?:(?:(?:\w+\.)?youtube(?:-nocookie)?\.com/| +                         (?:https?://|//)?                                    # http(s):// or protocol-independent URL (optional) +                         (?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/|                              tube\.majestyc\.net/|                              youtube\.googleapis\.com/)                        # the various hostnames, with wildcard subdomains                           (?:.*?\#/)?                                          # handle anchor (#/) redirect urls @@ -363,6 +363,18 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):                  u"uploader_id": u"justintimberlakeVEVO"              }          }, +        { +            u"url":  u"//www.YouTube.com/watch?v=yZIXLfi8CZQ", +            u"file":  u"yZIXLfi8CZQ.mp4", +            u"note": u"Embed-only video (#1746)", +            u"info_dict": { +                u"upload_date": u"20120608", +                u"title": u"Principal Sexually Assaults A Teacher - Episode 117 - 8th June 2012", +                u"description": u"md5:09b78bd971f1e3e289601dfba15ca4f7", +                u"uploader": u"SET India", +                u"uploader_id": u"setindia" +            } +        },      ] @@ -370,7 +382,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):      def suitable(cls, url):          """Receives a URL and returns True if suitable for this IE."""          if YoutubePlaylistIE.suitable(url): return False -        return re.match(cls._VALID_URL, url, re.VERBOSE) is not None +        return re.match(cls._VALID_URL, url) is not None      def __init__(self, *args, **kwargs):          super(YoutubeIE, self).__init__(*args, **kwargs) @@ -1272,7 +1284,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):              # We simulate the access to the video from www.youtube.com/v/{video_id}              # this can be viewed without login into Youtube              data = compat_urllib_parse.urlencode({'video_id': video_id, -                                                  'el': 'embedded', +                                                  'el': 'player_embedded',                                                    'gl': 'US',                                                    'hl': 'en',                                                    'eurl': 'https://youtube.googleapis.com/v/' + video_id, @@ -1301,6 +1313,11 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):              else:                  raise ExtractorError(u'"token" parameter not in video info for unknown reason') +        if 'view_count' in video_info: +            view_count = int(video_info['view_count'][0]) +        else: +            view_count = None +          # Check for "rental" videos          if 'ypc_video_rental_bar_text' in video_info and 'author' not in video_info:              raise ExtractorError(u'"rental" videos not supported') @@ -1489,6 +1506,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):                  'age_limit':    18 if age_gate else 0,                  'annotations':  video_annotations,                  'webpage_url': 'https://www.youtube.com/watch?v=%s' % video_id, +                'view_count': view_count,              })          return results diff --git a/youtube_dl/extractor/zdf.py b/youtube_dl/extractor/zdf.py index faed7ff7f..c6a9d06f2 100644 --- a/youtube_dl/extractor/zdf.py +++ b/youtube_dl/extractor/zdf.py @@ -53,7 +53,7 @@ class ZDFIE(InfoExtractor):              video_id,              u'Get stream URL') -        MMS_STREAM = r'href="(?P<video_url>mms://[^"]*)"' +        #MMS_STREAM = r'href="(?P<video_url>mms://[^"]*)"'          RTSP_STREAM = r'(?P<video_url>rtsp://[^"]*.mp4)'          mobj = re.search(self._MEDIA_STREAM, media_link) diff --git a/youtube_dl/update.py b/youtube_dl/update.py index 0689a4891..f41b4785a 100644 --- a/youtube_dl/update.py +++ b/youtube_dl/update.py @@ -2,11 +2,15 @@ import io  import json  import traceback  import hashlib +import os  import subprocess  import sys  from zipimport import zipimporter -from .utils import * +from .utils import ( +    compat_str, +    compat_urllib_request, +)  from .version import __version__  def rsa_verify(message, signature, key): diff --git a/youtube_dl/utils.py b/youtube_dl/utils.py index 1d9785341..b50c8166f 100644 --- a/youtube_dl/utils.py +++ b/youtube_dl/utils.py @@ -734,6 +734,8 @@ def unified_strdate(date_str):          '%Y/%m/%d %H:%M:%S',          '%d.%m.%Y %H:%M',          '%Y-%m-%dT%H:%M:%SZ', +        '%Y-%m-%dT%H:%M:%S.%fZ', +        '%Y-%m-%dT%H:%M:%S.%f0Z',          '%Y-%m-%dT%H:%M:%S',      ]      for expression in format_expressions: diff --git a/youtube_dl/version.py b/youtube_dl/version.py index b04238eb5..e9ff3f640 100644 --- a/youtube_dl/version.py +++ b/youtube_dl/version.py @@ -1,2 +1,2 @@ -__version__ = '2013.11.15.1' +__version__ = '2013.11.19'  | 
