From 223544552fcfec0c5c6a83326520c614e4489cbb Mon Sep 17 00:00:00 2001 From: Hannu Lintala Date: Sat, 9 May 2015 03:53:43 +0300 Subject: [PATCH 1/3] [Ruutu] Add new extractor --- youtube_dl/extractor/__init__.py | 1 + youtube_dl/extractor/ruutu.py | 88 ++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 youtube_dl/extractor/ruutu.py diff --git a/youtube_dl/extractor/__init__.py b/youtube_dl/extractor/__init__.py index 8ec0c1032..860023d14 100644 --- a/youtube_dl/extractor/__init__.py +++ b/youtube_dl/extractor/__init__.py @@ -448,6 +448,7 @@ RutubePersonIE, ) from .rutv import RUTVIE +from .ruutu import RuutuIE from .sandia import SandiaIE from .safari import ( SafariIE, diff --git a/youtube_dl/extractor/ruutu.py b/youtube_dl/extractor/ruutu.py new file mode 100644 index 000000000..e346434f9 --- /dev/null +++ b/youtube_dl/extractor/ruutu.py @@ -0,0 +1,88 @@ +# coding: utf-8 +from __future__ import unicode_literals + +from .common import InfoExtractor +from ..compat import compat_urllib_parse_urlparse +import re + + +class RuutuIE(InfoExtractor): + _VALID_URL = r'http://(www\.)?ruutu\.fi/ohjelmat/(?:[^/]+/)?(?P.*)$' + _TESTS = [ + { + 'url': 'http://www.ruutu.fi/ohjelmat/oletko-aina-halunnut-tietaa-mita-tapahtuu-vain-hetki-ennen-lahetysta-nyt-se-selvisi', + 'md5': 'ab2093f39be1ca8581963451b3c0234f', + 'info_dict': { + 'id': 'oletko-aina-halunnut-tietaa-mita-tapahtuu-vain-hetki-ennen-lahetysta-nyt-se-selvisi', + 'ext': 'mp4', + 'title': 'Oletko aina halunnut tietää mitä tapahtuu vain hetki ennen lähetystä? - Nyt se selvisi!', + 'description': 'Toinen toistaan huikeampia ohjelmaideoita ja täysin päätöntä sekoilua? No sitä juuri nimenomaan. Metro Helsingin Iltapäivän vieraaksi saapui Tuomas Kauhanen ja he Petra Kalliomaan kanssa keskustelivat hieman ennen lähetyksen alkua, mutta kamerat olivatkin jo päällä.', + }, + 'params': { + 'format': 'http-1000', + } + }, + { + 'url': 'http://www.ruutu.fi/ohjelmat/superpesis/superpesis-katso-koko-kausi-ruudussa', + 'md5': '065a10ae4d5b8cfd9d0c3d332465e3d9', + 'info_dict': { + 'id': 'superpesis-katso-koko-kausi-ruudussa', + 'ext': 'mp4', + 'title': 'Superpesis: katso koko kausi Ruudussa', + 'description': 'Huippujännittävän Superpesiksen suoria ottelulähetyksiä seurataan Ruudussa kauden alusta viimeiseen finaaliin asti. Katso lisätiedot osoitteesta ruutu.fi/superpesis.', + }, + 'params': { + 'format': 'http-1000', + } + }, + ] + + def _real_extract(self, url): + mobj = re.match(self._VALID_URL, url) + + video_id = mobj.group('id') + webpage = self._download_webpage(url, video_id) + media_id = self._html_search_regex(r'data-media-id="(\d+)"', webpage, 'media_id') + media_json = self._parse_json(self._search_regex(r'jQuery.extend\([^,]+, (.*)\);', webpage, 'media_data'), video_id) + xml_url = media_json['ruutuplayer']['xmlUrl'].replace('{ID}', media_id) + media_xml = self._download_xml(xml_url, media_id) + + formats = [] + parsed_urls = [] + for fmt in media_xml.findall('.//Clip//'): + url = fmt.text + if not fmt.tag.endswith('File') or url in parsed_urls or \ + 'NOT_USED' in url: + continue + + if url.endswith('m3u8'): + formats.extend(self._extract_m3u8_formats(url, media_id, m3u8_id='hls')) + parsed_urls.append(url) + elif url.endswith('f4m'): + formats.extend(self._extract_f4m_formats(url, media_id, f4m_id='hds')) + parsed_urls.append(url) + else: + proto = compat_urllib_parse_urlparse(url).scheme + width_str, height_str = fmt.get('resolution').split('x') + tbr = int(fmt.get('bitrate', 0)) + formats.append({ + 'format_id': '%s-%d' % (proto, tbr), + 'url': url, + 'width': int(width_str), + 'height': int(height_str), + 'tbr': tbr, + 'ext': url.rsplit('.', 1)[-1], + 'live': True, + 'protocol': proto, + }) + self._sort_formats(formats) + + return { + 'id': video_id, + 'title': self._og_search_title(webpage), + 'formats': formats, + 'description': self._og_search_description(webpage), + 'thumbnail': self._og_search_thumbnail(webpage), + 'duration': int(media_xml.find('.//Runtime').text), + 'age_limit': int(media_xml.find('.//AgeLimit').text), + } From d00735a0c5aabd38b37bfea76a93ae8c47a8d419 Mon Sep 17 00:00:00 2001 From: Hannu Lintala Date: Sat, 6 Jun 2015 23:01:23 +0300 Subject: [PATCH 2/3] [ruutu] Don't use fallback for DASH and other non-HTTP urls --- youtube_dl/extractor/ruutu.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/youtube_dl/extractor/ruutu.py b/youtube_dl/extractor/ruutu.py index e346434f9..59e0b12fd 100644 --- a/youtube_dl/extractor/ruutu.py +++ b/youtube_dl/extractor/ruutu.py @@ -62,6 +62,8 @@ def _real_extract(self, url): formats.extend(self._extract_f4m_formats(url, media_id, f4m_id='hds')) parsed_urls.append(url) else: + if not fmt.tag.startswith('HTTP'): + continue proto = compat_urllib_parse_urlparse(url).scheme width_str, height_str = fmt.get('resolution').split('x') tbr = int(fmt.get('bitrate', 0)) From 9414338a48ca815fd666aad496ebabd6d0c76e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergey=20M=E2=80=A4?= Date: Sun, 7 Jun 2015 05:37:29 +0600 Subject: [PATCH 3/3] [ruutu] Improve, make more robust and fix python 2.6 support --- youtube_dl/extractor/ruutu.py | 129 +++++++++++++++++++++------------- 1 file changed, 79 insertions(+), 50 deletions(-) diff --git a/youtube_dl/extractor/ruutu.py b/youtube_dl/extractor/ruutu.py index 59e0b12fd..4e22628d0 100644 --- a/youtube_dl/extractor/ruutu.py +++ b/youtube_dl/extractor/ruutu.py @@ -3,88 +3,117 @@ from .common import InfoExtractor from ..compat import compat_urllib_parse_urlparse -import re +from ..utils import ( + determine_ext, + int_or_none, + xpath_text, +) class RuutuIE(InfoExtractor): - _VALID_URL = r'http://(www\.)?ruutu\.fi/ohjelmat/(?:[^/]+/)?(?P.*)$' + _VALID_URL = r'http://(?:www\.)?ruutu\.fi/ohjelmat/(?:[^/?#]+/)*(?P[^/?#]+)' _TESTS = [ { 'url': 'http://www.ruutu.fi/ohjelmat/oletko-aina-halunnut-tietaa-mita-tapahtuu-vain-hetki-ennen-lahetysta-nyt-se-selvisi', 'md5': 'ab2093f39be1ca8581963451b3c0234f', 'info_dict': { - 'id': 'oletko-aina-halunnut-tietaa-mita-tapahtuu-vain-hetki-ennen-lahetysta-nyt-se-selvisi', + 'id': '2058907', + 'display_id': 'oletko-aina-halunnut-tietaa-mita-tapahtuu-vain-hetki-ennen-lahetysta-nyt-se-selvisi', 'ext': 'mp4', 'title': 'Oletko aina halunnut tietää mitä tapahtuu vain hetki ennen lähetystä? - Nyt se selvisi!', - 'description': 'Toinen toistaan huikeampia ohjelmaideoita ja täysin päätöntä sekoilua? No sitä juuri nimenomaan. Metro Helsingin Iltapäivän vieraaksi saapui Tuomas Kauhanen ja he Petra Kalliomaan kanssa keskustelivat hieman ennen lähetyksen alkua, mutta kamerat olivatkin jo päällä.', + 'description': 'md5:cfc6ccf0e57a814360df464a91ff67d6', + 'thumbnail': 're:^https?://.*\.jpg$', + 'duration': 114, + 'age_limit': 0, }, - 'params': { - 'format': 'http-1000', - } }, { 'url': 'http://www.ruutu.fi/ohjelmat/superpesis/superpesis-katso-koko-kausi-ruudussa', 'md5': '065a10ae4d5b8cfd9d0c3d332465e3d9', 'info_dict': { - 'id': 'superpesis-katso-koko-kausi-ruudussa', + 'id': '2057306', + 'display_id': 'superpesis-katso-koko-kausi-ruudussa', 'ext': 'mp4', 'title': 'Superpesis: katso koko kausi Ruudussa', - 'description': 'Huippujännittävän Superpesiksen suoria ottelulähetyksiä seurataan Ruudussa kauden alusta viimeiseen finaaliin asti. Katso lisätiedot osoitteesta ruutu.fi/superpesis.', + 'description': 'md5:44c44a99fdbe5b380ab74ebd75f0af77', + 'thumbnail': 're:^https?://.*\.jpg$', + 'duration': 40, + 'age_limit': 0, }, - 'params': { - 'format': 'http-1000', - } }, ] def _real_extract(self, url): - mobj = re.match(self._VALID_URL, url) + display_id = self._match_id(url) - video_id = mobj.group('id') - webpage = self._download_webpage(url, video_id) - media_id = self._html_search_regex(r'data-media-id="(\d+)"', webpage, 'media_id') - media_json = self._parse_json(self._search_regex(r'jQuery.extend\([^,]+, (.*)\);', webpage, 'media_data'), video_id) - xml_url = media_json['ruutuplayer']['xmlUrl'].replace('{ID}', media_id) - media_xml = self._download_xml(xml_url, media_id) + webpage = self._download_webpage(url, display_id) + + video_id = self._search_regex( + r'data-media-id="(\d+)"', webpage, 'media id') + + video_xml_url = None + + media_data = self._search_regex( + r'jQuery\.extend\([^,]+,\s*(.+?)\);', webpage, + 'media data', default=None) + if media_data: + media_json = self._parse_json(media_data, display_id, fatal=False) + if media_json: + xml_url = media_json.get('ruutuplayer', {}).get('xmlUrl') + if xml_url: + video_xml_url = xml_url.replace('{ID}', video_id) + + if not video_xml_url: + video_xml_url = 'http://gatling.ruutu.fi/media-xml-cache?id=%s' % video_id + + video_xml = self._download_xml(video_xml_url, video_id) formats = [] - parsed_urls = [] - for fmt in media_xml.findall('.//Clip//'): - url = fmt.text - if not fmt.tag.endswith('File') or url in parsed_urls or \ - 'NOT_USED' in url: - continue + processed_urls = [] - if url.endswith('m3u8'): - formats.extend(self._extract_m3u8_formats(url, media_id, m3u8_id='hls')) - parsed_urls.append(url) - elif url.endswith('f4m'): - formats.extend(self._extract_f4m_formats(url, media_id, f4m_id='hds')) - parsed_urls.append(url) - else: - if not fmt.tag.startswith('HTTP'): - continue - proto = compat_urllib_parse_urlparse(url).scheme - width_str, height_str = fmt.get('resolution').split('x') - tbr = int(fmt.get('bitrate', 0)) - formats.append({ - 'format_id': '%s-%d' % (proto, tbr), - 'url': url, - 'width': int(width_str), - 'height': int(height_str), - 'tbr': tbr, - 'ext': url.rsplit('.', 1)[-1], - 'live': True, - 'protocol': proto, - }) + def extract_formats(node): + for child in node: + if child.tag.endswith('Files'): + extract_formats(child) + elif child.tag.endswith('File'): + video_url = child.text + if not video_url or video_url in processed_urls or 'NOT_USED' in video_url: + return + processed_urls.append(video_url) + ext = determine_ext(video_url) + if ext == 'm3u8': + formats.extend(self._extract_m3u8_formats( + video_url, video_id, 'mp4', m3u8_id='hls')) + elif ext == 'f4m': + formats.extend(self._extract_f4m_formats( + video_url, video_id, f4m_id='hds')) + else: + proto = compat_urllib_parse_urlparse(video_url).scheme + if not child.tag.startswith('HTTP') and proto != 'rtmp': + continue + preference = -1 if proto == 'rtmp' else 1 + label = child.get('label') + tbr = int_or_none(child.get('bitrate')) + width, height = [int_or_none(x) for x in child.get('resolution', '').split('x')] + formats.append({ + 'format_id': '%s-%s' % (proto, label if label else tbr), + 'url': video_url, + 'width': width, + 'height': height, + 'tbr': tbr, + 'preference': preference, + }) + + extract_formats(video_xml.find('./Clip')) self._sort_formats(formats) return { 'id': video_id, + 'display_id': display_id, 'title': self._og_search_title(webpage), - 'formats': formats, 'description': self._og_search_description(webpage), 'thumbnail': self._og_search_thumbnail(webpage), - 'duration': int(media_xml.find('.//Runtime').text), - 'age_limit': int(media_xml.find('.//AgeLimit').text), + 'duration': int_or_none(xpath_text(video_xml, './/Runtime', 'duration')), + 'age_limit': int_or_none(xpath_text(video_xml, './/AgeLimit', 'age limit')), + 'formats': formats, }