From 29c48f185ae8c9e0f7b101ef389dff6b728c6b74 Mon Sep 17 00:00:00 2001 From: runraid Date: Sat, 4 Jul 2020 19:13:35 -0700 Subject: [PATCH 1/5] [tiktok] Support watermarkless tiktok videos --- youtube_dl/extractor/extractors.py | 1 + youtube_dl/extractor/tiktok.py | 34 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/youtube_dl/extractor/extractors.py b/youtube_dl/extractor/extractors.py index 4b3092028..5b8128632 100644 --- a/youtube_dl/extractor/extractors.py +++ b/youtube_dl/extractor/extractors.py @@ -1146,6 +1146,7 @@ from .threeqsdn import ThreeQSDNIE from .tiktok import ( TikTokIE, TikTokUserIE, + TikTokWatermarklessIE, ) from .tinypic import TinyPicIE from .tmz import ( diff --git a/youtube_dl/extractor/tiktok.py b/youtube_dl/extractor/tiktok.py index 66088b9ab..2cbc844d9 100644 --- a/youtube_dl/extractor/tiktok.py +++ b/youtube_dl/extractor/tiktok.py @@ -136,3 +136,37 @@ class TikTokUserIE(TikTokBaseIE): entry['extractor_key'] = TikTokIE.ie_key() entries.append(entry) return self.playlist_result(entries, user_id) + + +class TikTokWatermarklessIE(InfoExtractor): + _VALID_URL = r'https?://(?:www\.)?tiktok\.com/@.+/video/(?P[0-9]+)+' + _TESTS = [{ + 'url': 'https://www.tiktok.com/@zoey.aune/video/6813765043914624262?lang=en', + 'info_dict': { + 'id': 'v09044400000bq7lmc8biaper9qalb50', + }, + }] + + def _real_extract(self, url): + video_id = self._match_id(url) + webpage = self._download_webpage('https://m.tiktok.com/v/%s.html' % video_id, video_id) + + next_data_json = self._html_search_regex([r']*>(.*?)'], + webpage, 'next_data') + watermarkedURL = self._parse_json(next_data_json, video_id).get('props', {}).get('pageProps', {}).get('videoData', {}).get('itemInfos', {}).get('video', {}).get('urls', {})[0] + # The vid is embedded in the first video file, so we need to download it (unfortunate!) + watermarkedVideoResponse = self._download_webpage(watermarkedURL, video_id) + idpos = watermarkedVideoResponse.index("vid:") + vidid = watermarkedVideoResponse[idpos + 4:idpos + 36] + watermarklessURL = "https://api2-16-h2.musical.ly/aweme/v1/play/?video_id={}&vr_type=0&is_play_url=1&source=PackSourceEnum_PUBLISH&media_type=4".format(vidid) + + return { + 'id': vidid, + 'title': self._og_search_title(webpage), + 'description': self._og_search_description(webpage), + 'url': watermarklessURL, + 'ext': 'mp4', + 'http_headers': { + 'User-Agent': 'okhttp', + } + } From 722261524932894c8c15cdea1218c8fb637907db Mon Sep 17 00:00:00 2001 From: runraid Date: Sun, 5 Jul 2020 10:28:35 -0700 Subject: [PATCH 2/5] Add additional metadata --- youtube_dl/extractor/extractors.py | 6 +- youtube_dl/extractor/tiktok.py | 225 +++++++++++------------------ 2 files changed, 85 insertions(+), 146 deletions(-) diff --git a/youtube_dl/extractor/extractors.py b/youtube_dl/extractor/extractors.py index 5b8128632..3d3dae7a4 100644 --- a/youtube_dl/extractor/extractors.py +++ b/youtube_dl/extractor/extractors.py @@ -1143,11 +1143,7 @@ from .thisamericanlife import ThisAmericanLifeIE from .thisav import ThisAVIE from .thisoldhouse import ThisOldHouseIE from .threeqsdn import ThreeQSDNIE -from .tiktok import ( - TikTokIE, - TikTokUserIE, - TikTokWatermarklessIE, -) +from .tiktok import TikTokIE from .tinypic import TinyPicIE from .tmz import ( TMZIE, diff --git a/youtube_dl/extractor/tiktok.py b/youtube_dl/extractor/tiktok.py index 2cbc844d9..41d12337c 100644 --- a/youtube_dl/extractor/tiktok.py +++ b/youtube_dl/extractor/tiktok.py @@ -1,171 +1,114 @@ # coding: utf-8 from __future__ import unicode_literals +from datetime import datetime from .common import InfoExtractor from ..utils import ( - compat_str, ExtractorError, int_or_none, str_or_none, - try_get, - url_or_none, + try_get ) -class TikTokBaseIE(InfoExtractor): - def _extract_aweme(self, data): - video = data['video'] - description = str_or_none(try_get(data, lambda x: x['desc'])) - width = int_or_none(try_get(data, lambda x: video['width'])) - height = int_or_none(try_get(data, lambda x: video['height'])) - - format_urls = set() - formats = [] - for format_id in ( - 'play_addr_lowbr', 'play_addr', 'play_addr_h264', - 'download_addr'): - for format in try_get( - video, lambda x: x[format_id]['url_list'], list) or []: - format_url = url_or_none(format) - if not format_url: - continue - if format_url in format_urls: - continue - format_urls.add(format_url) - formats.append({ - 'url': format_url, - 'ext': 'mp4', - 'height': height, - 'width': width, - }) - self._sort_formats(formats) - - thumbnail = url_or_none(try_get( - video, lambda x: x['cover']['url_list'][0], compat_str)) - uploader = try_get(data, lambda x: x['author']['nickname'], compat_str) - timestamp = int_or_none(data.get('create_time')) - comment_count = int_or_none(data.get('comment_count')) or int_or_none( - try_get(data, lambda x: x['statistics']['comment_count'])) - repost_count = int_or_none(try_get( - data, lambda x: x['statistics']['share_count'])) - - aweme_id = data['aweme_id'] - - return { - 'id': aweme_id, - 'title': uploader or aweme_id, - 'description': description, - 'thumbnail': thumbnail, - 'uploader': uploader, - 'timestamp': timestamp, - 'comment_count': comment_count, - 'repost_count': repost_count, - 'formats': formats, - } +class TikTokIE2(InfoExtractor): + _VALID_URL = r'https?://www\.tiktok\.com/@[\w\._]+/video/(?P\d+)' -class TikTokIE(TikTokBaseIE): - _VALID_URL = r'''(?x) - https?:// - (?: - (?:m\.)?tiktok\.com/v| - (?:www\.)?tiktok\.com/share/video - ) - /(?P\d+) - ''' - _TESTS = [{ - 'url': 'https://m.tiktok.com/v/6606727368545406213.html', - 'md5': 'd584b572e92fcd48888051f238022420', - 'info_dict': { - 'id': '6606727368545406213', - 'ext': 'mp4', - 'title': 'Zureeal', - 'description': '#bowsette#mario#cosplay#uk#lgbt#gaming#asian#bowsettecosplay', - 'thumbnail': r're:^https?://.*~noop.image', - 'uploader': 'Zureeal', - 'timestamp': 1538248586, - 'upload_date': '20180929', - 'comment_count': int, - 'repost_count': int, - } - }, { - 'url': 'https://www.tiktok.com/share/video/6606727368545406213', - 'only_matching': True, - }] - - def _real_extract(self, url): - video_id = self._match_id(url) - webpage = self._download_webpage( - 'https://m.tiktok.com/v/%s.html' % video_id, video_id) - data = self._parse_json(self._search_regex( - r'\bdata\s*=\s*({.+?})\s*;', webpage, 'data'), video_id) - return self._extract_aweme(data) - - -class TikTokUserIE(TikTokBaseIE): - _VALID_URL = r'''(?x) - https?:// - (?: - (?:m\.)?tiktok\.com/h5/share/usr| - (?:www\.)?tiktok\.com/share/user - ) - /(?P\d+) - ''' - _TESTS = [{ - 'url': 'https://m.tiktok.com/h5/share/usr/188294915489964032.html', - 'info_dict': { - 'id': '188294915489964032', - }, - 'playlist_mincount': 24, - }, { - 'url': 'https://www.tiktok.com/share/user/188294915489964032', - 'only_matching': True, - }] - - def _real_extract(self, url): - user_id = self._match_id(url) - data = self._download_json( - 'https://m.tiktok.com/h5/share/usr/list/%s/' % user_id, user_id, - query={'_signature': '_'}) - entries = [] - for aweme in data['aweme_list']: - try: - entry = self._extract_aweme(aweme) - except ExtractorError: - continue - entry['extractor_key'] = TikTokIE.ie_key() - entries.append(entry) - return self.playlist_result(entries, user_id) - - -class TikTokWatermarklessIE(InfoExtractor): +class TikTokIE(InfoExtractor): _VALID_URL = r'https?://(?:www\.)?tiktok\.com/@.+/video/(?P[0-9]+)+' _TESTS = [{ 'url': 'https://www.tiktok.com/@zoey.aune/video/6813765043914624262?lang=en', + 'md5': 'd679cc9a75bce136e5a2be41fd9f77e0', 'info_dict': { - 'id': 'v09044400000bq7lmc8biaper9qalb50', - }, + 'comment_count': int, + 'creator': 'Zoey', + 'description': 'md5:b22dea1b2dd4e18258ebe42cf5571a97', + 'duration': 10, + 'ext': 'mp4', + 'formats': list, + 'width': 576, + 'height': 1024, + 'id': '6813765043914624262', + 'like_count': int, + 'repost_count': int, + 'thumbnail': r're:^https?://[\w\/\.\-]+(~[\w\-]+\.image)?', + 'thumbnails': list, + 'timestamp': 1586453303, + 'title': 'Zoey on TikTok', + 'upload_date': '20200409', + 'uploader': 'zoey.aune', + 'uploader_id': '6651338645989621765', + 'uploader_url': r're:https://www.tiktok.com/@zoey.aune', + 'webpage_url': r're:https://www.tiktok.com/@zoey.aune/(video/)?6813765043914624262', + } }] def _real_extract(self, url): video_id = self._match_id(url) webpage = self._download_webpage('https://m.tiktok.com/v/%s.html' % video_id, video_id) - next_data_json = self._html_search_regex([r']*>(.*?)'], - webpage, 'next_data') - watermarkedURL = self._parse_json(next_data_json, video_id).get('props', {}).get('pageProps', {}).get('videoData', {}).get('itemInfos', {}).get('video', {}).get('urls', {})[0] - # The vid is embedded in the first video file, so we need to download it (unfortunate!) - watermarkedVideoResponse = self._download_webpage(watermarkedURL, video_id) - idpos = watermarkedVideoResponse.index("vid:") - vidid = watermarkedVideoResponse[idpos + 4:idpos + 36] - watermarklessURL = "https://api2-16-h2.musical.ly/aweme/v1/play/?video_id={}&vr_type=0&is_play_url=1&source=PackSourceEnum_PUBLISH&media_type=4".format(vidid) + # The webpage will have a json embedded in a '], + webpage, 'next_data') + json_data = self._parse_json(json_string, video_id) + video_data = try_get(json_data, lambda x: x['props']['pageProps'], expected_type=dict) + + # The watermarkless video ID is embedded in the first video file, so we need to download it and get the video ID. + watermarked_url = video_data['videoData']['itemInfos']['video']['urls'][0] + watermarked_response = self._download_webpage(watermarked_url, video_id) + idpos = watermarked_response.index("vid:") + watermarkless_video_id = watermarked_response[idpos + 4:idpos + 36] + watermarkless_url = "https://api2-16-h2.musical.ly/aweme/v1/play/?video_id={}&vr_type=0&is_play_url=1&source=PackSourceEnum_PUBLISH&media_type=4".format(watermarkless_video_id) + + # Get extra metadata + video_info = try_get(video_data, lambda x: x['videoData']['itemInfos'], dict) + author_info = try_get(video_data, lambda x: x['videoData']['authorInfos'], dict) + share_info = try_get(video_data, lambda x: x['shareMeta'], dict) + unique_id = str_or_none(author_info.get('uniqueId')) + timestamp = try_get(video_info, lambda x: int(x['createTime']), int) + date = datetime.fromtimestamp(timestamp).strftime('%Y%m%d') + height = try_get(video_info, lambda x: x['video']['videoMeta']['height'], int) + width = try_get(video_info, lambda x: x['video']['videoMeta']['width'], int) + thumbnails = [] + thumbnails.append({ + 'url': video_info.get('thumbnail') or self._og_search_thumbnail(webpage), + 'width': width, + 'height': height + }) + + formats = [] + formats.append({ + 'url': watermarkless_url, + 'ext': 'mp4', + 'height': height, + 'width': width + }) + + if video_data.get('statusCode') != 0: + raise ExtractorError('Video not available', video_id=video_id) return { - 'id': vidid, + 'id': video_id, 'title': self._og_search_title(webpage), - 'description': self._og_search_description(webpage), - 'url': watermarklessURL, + 'description': str_or_none(video_info.get('text')) or str_or_none(share_info.get('desc')), + 'comment_count': int_or_none(video_info.get('commentCount')), + 'duration': try_get(video_info, lambda x: x['video']['videoMeta']['duration'], int), + 'height': height, + 'like_count': int_or_none(video_info.get('diggCount')), + 'repost_count': int_or_none(video_info.get('shareCount')), + 'thumbnail': try_get(video_info, lambda x: x['covers'][0], str), + 'timestamp': timestamp, + 'width': width, + 'title': self._og_search_title(webpage), + 'creator': str_or_none(author_info.get('nickName')), + 'uploader': unique_id, + 'uploader_id': str_or_none(author_info.get('userId')), + 'uploader_url': 'https://www.tiktok.com/@' + unique_id, + 'thumbnails': thumbnails, + 'webpage_url': self._og_search_url(webpage), 'ext': 'mp4', + 'formats': formats, 'http_headers': { 'User-Agent': 'okhttp', } From caee5d2d0dc5822190e3dd563738df4264175681 Mon Sep 17 00:00:00 2001 From: runraid Date: Sun, 5 Jul 2020 10:31:44 -0700 Subject: [PATCH 3/5] remove class --- youtube_dl/extractor/tiktok.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/youtube_dl/extractor/tiktok.py b/youtube_dl/extractor/tiktok.py index 41d12337c..488f65709 100644 --- a/youtube_dl/extractor/tiktok.py +++ b/youtube_dl/extractor/tiktok.py @@ -11,10 +11,6 @@ from ..utils import ( ) -class TikTokIE2(InfoExtractor): - _VALID_URL = r'https?://www\.tiktok\.com/@[\w\._]+/video/(?P\d+)' - - class TikTokIE(InfoExtractor): _VALID_URL = r'https?://(?:www\.)?tiktok\.com/@.+/video/(?P[0-9]+)+' _TESTS = [{ From 41406fe8c1279fc947896e0d984ae6f1301fe777 Mon Sep 17 00:00:00 2001 From: runraid Date: Sun, 5 Jul 2020 11:45:51 -0700 Subject: [PATCH 4/5] flake --- youtube_dl/extractor/tiktok.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/youtube_dl/extractor/tiktok.py b/youtube_dl/extractor/tiktok.py index 488f65709..cd3ef3b23 100644 --- a/youtube_dl/extractor/tiktok.py +++ b/youtube_dl/extractor/tiktok.py @@ -63,7 +63,6 @@ class TikTokIE(InfoExtractor): share_info = try_get(video_data, lambda x: x['shareMeta'], dict) unique_id = str_or_none(author_info.get('uniqueId')) timestamp = try_get(video_info, lambda x: int(x['createTime']), int) - date = datetime.fromtimestamp(timestamp).strftime('%Y%m%d') height = try_get(video_info, lambda x: x['video']['videoMeta']['height'], int) width = try_get(video_info, lambda x: x['video']['videoMeta']['width'], int) thumbnails = [] @@ -96,7 +95,6 @@ class TikTokIE(InfoExtractor): 'thumbnail': try_get(video_info, lambda x: x['covers'][0], str), 'timestamp': timestamp, 'width': width, - 'title': self._og_search_title(webpage), 'creator': str_or_none(author_info.get('nickName')), 'uploader': unique_id, 'uploader_id': str_or_none(author_info.get('userId')), From 571e4f7f7877277ac61164cb4100b81edd44f857 Mon Sep 17 00:00:00 2001 From: runraid Date: Sun, 5 Jul 2020 16:43:58 -0700 Subject: [PATCH 5/5] Flake8 --- youtube_dl/extractor/tiktok.py | 1 - 1 file changed, 1 deletion(-) diff --git a/youtube_dl/extractor/tiktok.py b/youtube_dl/extractor/tiktok.py index cd3ef3b23..659bc536a 100644 --- a/youtube_dl/extractor/tiktok.py +++ b/youtube_dl/extractor/tiktok.py @@ -1,6 +1,5 @@ # coding: utf-8 from __future__ import unicode_literals -from datetime import datetime from .common import InfoExtractor from ..utils import (