# coding: utf-8 from __future__ import unicode_literals import re from .common import InfoExtractor from ..compat import compat_str from ..utils import ( ExtractorError, int_or_none, js_to_json, str_or_none, ) class LineTVIE(InfoExtractor): _VALID_URL = r'https?://tv\.line\.me/v/(?P\d+)_[^/]+-(?Pep\d+-\d+)' _TESTS = [{ 'url': 'https://tv.line.me/v/793123_goodbye-mrblack-ep1-1/list/69246', 'info_dict': { 'id': '793123_ep1-1', 'ext': 'mp4', 'title': 'Goodbye Mr.Black | EP.1-1', 'thumbnail': r're:^https?://.*\.jpg$', 'duration': 998.509, 'view_count': int, }, }, { 'url': 'https://tv.line.me/v/2587507_%E6%B4%BE%E9%81%A3%E5%A5%B3%E9%86%ABx-ep1-02/list/185245', 'only_matching': True, }] def _real_extract(self, url): series_id, segment = re.match(self._VALID_URL, url).groups() video_id = '%s_%s' % (series_id, segment) webpage = self._download_webpage(url, video_id) player_params = self._parse_json(self._search_regex( r'naver\.WebPlayer\(({[^}]+})\)', webpage, 'player parameters'), video_id, transform_source=js_to_json) video_info = self._download_json( 'https://global-nvapis.line.me/linetv/rmcnmv/vod_play_videoInfo.json', video_id, query={ 'videoId': player_params['videoId'], 'key': player_params['key'], }) stream = video_info['streams'][0] extra_query = '?__gda__=' + stream['key']['value'] formats = self._extract_m3u8_formats( stream['source'] + extra_query, video_id, ext='mp4', entry_protocol='m3u8_native', m3u8_id='hls') for a_format in formats: a_format['url'] += extra_query duration = None for video in video_info.get('videos', {}).get('list', []): encoding_option = video.get('encodingOption', {}) abr = video['bitrate']['audio'] vbr = video['bitrate']['video'] tbr = abr + vbr formats.append({ 'url': video['source'], 'format_id': 'http-%d' % int(tbr), 'height': encoding_option.get('height'), 'width': encoding_option.get('width'), 'abr': abr, 'vbr': vbr, 'filesize': video.get('size'), }) if video.get('duration') and duration is None: duration = video['duration'] self._sort_formats(formats) if not formats[0].get('width'): formats[0]['vcodec'] = 'none' title = self._og_search_title(webpage) # like_count requires an additional API request https://tv.line.me/api/likeit/getCount return { 'id': video_id, 'title': title, 'formats': formats, 'extra_param_to_segment_url': extra_query[1:], 'duration': duration, 'thumbnails': [{'url': thumbnail['source']} for thumbnail in video_info.get('thumbnails', {}).get('list', [])], 'view_count': video_info.get('meta', {}).get('count'), } class LineLiveBaseIE(InfoExtractor): _API_BASE_URL = 'https://live-api.line-apps.com/web/v4.0/channel/' def _parse_broadcast_item(self, item): broadcast_id = compat_str(item['id']) title = item['title'] is_live = item.get('isBroadcastingNow') thumbnails = [] for thumbnail_id, thumbnail_url in (item.get('thumbnailURLs') or {}).items(): if not thumbnail_url: continue thumbnails.append({ 'id': thumbnail_id, 'url': thumbnail_url, }) channel = item.get('channel') or {} channel_id = str_or_none(channel.get('id')) return { 'id': broadcast_id, 'title': self._live_title(title) if is_live else title, 'thumbnails': thumbnails, 'timestamp': int_or_none(item.get('createdAt')), 'channel': channel.get('name'), 'channel_id': channel_id, 'channel_url': 'https://live.line.me/channels/' + channel_id if channel_id else None, 'duration': int_or_none(item.get('archiveDuration')), 'view_count': int_or_none(item.get('viewerCount')), 'comment_count': int_or_none(item.get('chatCount')), 'is_live': is_live, } class LineLiveIE(LineLiveBaseIE): _VALID_URL = r'https?://live\.line\.me/channels/(?P\d+)/broadcast/(?P\d+)' _TESTS = [{ 'url': 'https://live.line.me/channels/4867368/broadcast/16331360', 'md5': 'bc931f26bf1d4f971e3b0982b3fab4a3', 'info_dict': { 'id': '16331360', 'title': '振りコピ講座😙😙😙', 'ext': 'mp4', 'timestamp': 1617095132, 'upload_date': '20210330', 'channel': '白川ゆめか', 'channel_id': '4867368', 'view_count': int, 'comment_count': int, 'is_live': False, } }, { # archiveStatus == 'DELETED' 'url': 'https://live.line.me/channels/4778159/broadcast/16378488', 'only_matching': True, }] def _real_extract(self, url): channel_id, broadcast_id = re.match(self._VALID_URL, url).groups() broadcast = self._download_json( self._API_BASE_URL + '%s/broadcast/%s' % (channel_id, broadcast_id), broadcast_id) item = broadcast['item'] info = self._parse_broadcast_item(item) protocol = 'm3u8' if info['is_live'] else 'm3u8_native' formats = [] for k, v in (broadcast.get(('live' if info['is_live'] else 'archived') + 'HLSURLs') or {}).items(): if not v: continue if k == 'abr': formats.extend(self._extract_m3u8_formats( v, broadcast_id, 'mp4', protocol, m3u8_id='hls', fatal=False)) continue f = { 'ext': 'mp4', 'format_id': 'hls-' + k, 'protocol': protocol, 'url': v, } if not k.isdigit(): f['vcodec'] = 'none' formats.append(f) if not formats: archive_status = item.get('archiveStatus') if archive_status != 'ARCHIVED': raise ExtractorError('this video has been ' + archive_status.lower(), expected=True) self._sort_formats(formats) info['formats'] = formats return info class LineLiveChannelIE(LineLiveBaseIE): _VALID_URL = r'https?://live\.line\.me/channels/(?P\d+)(?!/broadcast/\d+)(?:[/?&#]|$)' _TEST = { 'url': 'https://live.line.me/channels/5893542', 'info_dict': { 'id': '5893542', 'title': 'いくらちゃん', 'description': 'md5:c3a4af801f43b2fac0b02294976580be', }, 'playlist_mincount': 29 } def _archived_broadcasts_entries(self, archived_broadcasts, channel_id): while True: for row in (archived_broadcasts.get('rows') or []): share_url = str_or_none(row.get('shareURL')) if not share_url: continue info = self._parse_broadcast_item(row) info.update({ '_type': 'url', 'url': share_url, 'ie_key': LineLiveIE.ie_key(), }) yield info if not archived_broadcasts.get('hasNextPage'): return archived_broadcasts = self._download_json( self._API_BASE_URL + channel_id + '/archived_broadcasts', channel_id, query={ 'lastId': info['id'], }) def _real_extract(self, url): channel_id = self._match_id(url) channel = self._download_json(self._API_BASE_URL + channel_id, channel_id) return self.playlist_result( self._archived_broadcasts_entries(channel.get('archivedBroadcasts') or {}, channel_id), channel_id, channel.get('title'), channel.get('information'))