From d3725a6ab25bc04ded72a7eed7d8edcb8eef6091 Mon Sep 17 00:00:00 2001 From: Oliver Pfeiffer Date: Wed, 1 Dec 2021 21:50:40 +0100 Subject: [PATCH 1/3] [douyin] Add new extractor --- youtube_dl/extractor/douyin.py | 94 ++++++++++++++++++++++++++++++ youtube_dl/extractor/extractors.py | 4 ++ 2 files changed, 98 insertions(+) create mode 100644 youtube_dl/extractor/douyin.py diff --git a/youtube_dl/extractor/douyin.py b/youtube_dl/extractor/douyin.py new file mode 100644 index 000000000..6a3324063 --- /dev/null +++ b/youtube_dl/extractor/douyin.py @@ -0,0 +1,94 @@ +# coding: utf-8 +from __future__ import unicode_literals + +from .common import InfoExtractor +from ..utils import ( + ExtractorError, + float_or_none, + try_get, + orderedSet +) + + +class DouyinVideoIE(InfoExtractor): + _VALID_URL = r'https?://(?:www\.)?douyin\.com/video/(?P[0-9]+)' + _TEST = { + 'url': 'https://www.douyin.com/video/6989098563519270181', + 'md5': 'b4c99f3be1be343948a9eaaea8303d27', + 'info_dict': { + 'id': '6989098563519270181', + 'url': r're:^https://v3-dy-o.zjcdn.com/[0-9a-f]{32}/[0-9a-f]{8}/video/tos/cn/tos-cn-ve-15/[0-9a-f]{32}/?.*', + 'title': '杨集#我的家乡 ', + 'uploader': '永恒的爱', + 'uploader_id': '104081949894', + 'timestamp': 1627276320000.0, + 'ext': 'mp4' + } + } + + def _real_extract(self, url): + video_id = self._match_id(url) + + iteminfo = self._download_json('https://www.douyin.com/web/api/v2/aweme/iteminfo', + video_id, query={'item_ids': video_id}) + if iteminfo['status_code']: + raise ExtractorError(iteminfo['status_msg'], video_id=video_id) + + item_list = iteminfo.get('item_list') + if not item_list: + raise ExtractorError('The video you want to download does not exist any more', + video_id=video_id, expected=True) + item = item_list[0] + + info_dict = {} + info_dict['id'] = video_id + info_dict['title'] = item['desc'] + info_dict['url'] = item['video']['play_addr']['url_list'][0] + info_dict['uploader'] = try_get(item, lambda x: x['author']['nickname']) + info_dict['uploader_id'] = try_get(item, lambda x: x['author']['uid']) + info_dict['duration'] = float_or_none(item.get('duration') or try_get(item, lambda x: x['video']['duration'], int), scale=1000) + info_dict['timestamp'] = float_or_none(item.get('create_time'), invscale=1000) + info_dict['width'] = try_get(item, lambda x: x['video']['width']) + info_dict['height'] = try_get(item, lambda x: x['video']['height']) + info_dict['vbr'] = try_get(item, lambda x: x['video']['bit_rate']) + info_dict['ext'] = 'mp4' + + return info_dict + + +class DouyinUserIE(InfoExtractor): + _VALID_URL = r'https?://(?:www\.)?douyin\.com/user/(?P[a-zA-Z0-9_\-]+)' + _TEST = { + 'url': 'https://www.douyin.com/user/MS4wLjABAAAAP5Q7Z-SwleIzAACYIu-LrwGbEZzN2dc5PT3hGNSTkSM', + 'info_dict': { + 'id': 'MS4wLjABAAAAP5Q7Z-SwleIzAACYIu-LrwGbEZzN2dc5PT3hGNSTkSM' + }, + 'playlist_mincount': 31 + } + + def _real_extract(self, url): + sec_uid = self._match_id(url) + + has_more = True + max_cursor = '' + + entries = [] + while has_more: + post = self._download_json('https://www.douyin.com/web/api/v2/aweme/post', + sec_uid, query={'sec_uid': sec_uid, 'max_cursor': max_cursor, 'count': 50}) + if post['status_code']: + raise ExtractorError(post['status_msg'], video_id=sec_uid) + + aweme_list = post.get('aweme_list') + if aweme_list is None: + raise ExtractorError('JSON response does not contain aweme_list', video_id=sec_uid) + + entries.extend([self.url_result('https://www.douyin.com/video/%s' % aweme['aweme_id'], + ie=DouyinVideoIE.ie_key(), video_id=aweme['aweme_id']) + for aweme in aweme_list]) + + has_more = post['has_more'] + max_cursor = post['max_cursor'] + + return self.playlist_result(orderedSet(entries), sec_uid) + diff --git a/youtube_dl/extractor/extractors.py b/youtube_dl/extractor/extractors.py index 6e8fc3961..a77a1ccb6 100644 --- a/youtube_dl/extractor/extractors.py +++ b/youtube_dl/extractor/extractors.py @@ -290,6 +290,10 @@ from .dfb import DFBIE from .dhm import DHMIE from .digg import DiggIE from .dotsub import DotsubIE +from .douyin import ( + DouyinVideoIE, + DouyinUserIE +) from .douyutv import ( DouyuShowIE, DouyuTVIE, From 0ec08cef90addb812b946c12f715370d2d66ffe9 Mon Sep 17 00:00:00 2001 From: Oliver Pfeiffer Date: Fri, 17 Dec 2021 21:54:41 +0100 Subject: [PATCH 2/3] [douyin] Incorporated dirkf's remarks --- youtube_dl/extractor/douyin.py | 43 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/youtube_dl/extractor/douyin.py b/youtube_dl/extractor/douyin.py index 6a3324063..082f7e850 100644 --- a/youtube_dl/extractor/douyin.py +++ b/youtube_dl/extractor/douyin.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from .common import InfoExtractor from ..utils import ( ExtractorError, - float_or_none, + int_or_none, try_get, orderedSet ) @@ -30,9 +30,10 @@ class DouyinVideoIE(InfoExtractor): video_id = self._match_id(url) iteminfo = self._download_json('https://www.douyin.com/web/api/v2/aweme/iteminfo', - video_id, query={'item_ids': video_id}) - if iteminfo['status_code']: - raise ExtractorError(iteminfo['status_msg'], video_id=video_id) + video_id, query={'item_ids': video_id}) or {} + status_code = iteminfo.get('status_code', 'status_code missing') + if status_code: + raise ExtractorError('%s (%s)' % (iteminfo.get('status_msg', 'status_msg missing'), status_code), video_id=video_id) item_list = iteminfo.get('item_list') if not item_list: @@ -40,20 +41,19 @@ class DouyinVideoIE(InfoExtractor): video_id=video_id, expected=True) item = item_list[0] - info_dict = {} - info_dict['id'] = video_id - info_dict['title'] = item['desc'] - info_dict['url'] = item['video']['play_addr']['url_list'][0] - info_dict['uploader'] = try_get(item, lambda x: x['author']['nickname']) - info_dict['uploader_id'] = try_get(item, lambda x: x['author']['uid']) - info_dict['duration'] = float_or_none(item.get('duration') or try_get(item, lambda x: x['video']['duration'], int), scale=1000) - info_dict['timestamp'] = float_or_none(item.get('create_time'), invscale=1000) - info_dict['width'] = try_get(item, lambda x: x['video']['width']) - info_dict['height'] = try_get(item, lambda x: x['video']['height']) - info_dict['vbr'] = try_get(item, lambda x: x['video']['bit_rate']) - info_dict['ext'] = 'mp4' - - return info_dict + return { + 'id': video_id, + 'title': item['desc'], + 'url': item['video']['play_addr']['url_list'][0], + 'uploader': try_get(item, lambda x: x['author']['nickname']), + 'uploader_id': try_get(item, lambda x: x['author']['uid']), + 'duration': int_or_none(item.get('duration') or try_get(item, lambda x: x['video']['duration'], int), scale=1000), + 'timestamp': int_or_none(item.get('create_time'), invscale=1000), + 'width': try_get(item, lambda x: x['video']['width']), + 'height': try_get(item, lambda x: x['video']['height']), + 'vbr': try_get(item, lambda x: x['video']['bit_rate']), + 'ext': 'mp4' + } class DouyinUserIE(InfoExtractor): @@ -75,9 +75,10 @@ class DouyinUserIE(InfoExtractor): entries = [] while has_more: post = self._download_json('https://www.douyin.com/web/api/v2/aweme/post', - sec_uid, query={'sec_uid': sec_uid, 'max_cursor': max_cursor, 'count': 50}) - if post['status_code']: - raise ExtractorError(post['status_msg'], video_id=sec_uid) + sec_uid, query={'sec_uid': sec_uid, 'max_cursor': max_cursor, 'count': 50}) or {} + status_code = post.get('status_code', 'status_code missing') + if status_code: + raise ExtractorError('%s (%s)' % (post.get('status_msg', 'status_msg missing'), status_code), video_id=sec_uid) aweme_list = post.get('aweme_list') if aweme_list is None: From 8934a232fb1e2491932fb627df3b4329f8318317 Mon Sep 17 00:00:00 2001 From: Oliver Pfeiffer Date: Wed, 20 Apr 2022 23:05:08 +0200 Subject: [PATCH 3/3] [douyin] Incorporated dirkf's remarks part 2. Adjusted test data. --- youtube_dl/extractor/douyin.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/youtube_dl/extractor/douyin.py b/youtube_dl/extractor/douyin.py index 082f7e850..1ec49ce8e 100644 --- a/youtube_dl/extractor/douyin.py +++ b/youtube_dl/extractor/douyin.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from .common import InfoExtractor +from ..compat import compat_str from ..utils import ( ExtractorError, int_or_none, @@ -14,12 +15,12 @@ class DouyinVideoIE(InfoExtractor): _VALID_URL = r'https?://(?:www\.)?douyin\.com/video/(?P[0-9]+)' _TEST = { 'url': 'https://www.douyin.com/video/6989098563519270181', - 'md5': 'b4c99f3be1be343948a9eaaea8303d27', + 'md5': '99c5667992b8a8d46c145907f677c92b', 'info_dict': { 'id': '6989098563519270181', - 'url': r're:^https://v3-dy-o.zjcdn.com/[0-9a-f]{32}/[0-9a-f]{8}/video/tos/cn/tos-cn-ve-15/[0-9a-f]{32}/?.*', + 'url': 'https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300fg10000c3v47dbc77u9fvb20tbg&ratio=720p&line=0', 'title': '杨集#我的家乡 ', - 'uploader': '永恒的爱', + 'uploader': '🌹永恒的爱🌹', 'uploader_id': '104081949894', 'timestamp': 1627276320000.0, 'ext': 'mp4' @@ -45,13 +46,13 @@ class DouyinVideoIE(InfoExtractor): 'id': video_id, 'title': item['desc'], 'url': item['video']['play_addr']['url_list'][0], - 'uploader': try_get(item, lambda x: x['author']['nickname']), - 'uploader_id': try_get(item, lambda x: x['author']['uid']), + 'uploader': try_get(item, lambda x: x['author']['nickname'], compat_str), + 'uploader_id': try_get(item, lambda x: x['author']['uid'], compat_str), 'duration': int_or_none(item.get('duration') or try_get(item, lambda x: x['video']['duration'], int), scale=1000), 'timestamp': int_or_none(item.get('create_time'), invscale=1000), - 'width': try_get(item, lambda x: x['video']['width']), - 'height': try_get(item, lambda x: x['video']['height']), - 'vbr': try_get(item, lambda x: x['video']['bit_rate']), + 'width': try_get(item, lambda x: x['video']['width'], int), + 'height': try_get(item, lambda x: x['video']['height'], int), + 'vbr': try_get(item, lambda x: x['video']['bit_rate'], int), 'ext': 'mp4' } @@ -84,12 +85,13 @@ class DouyinUserIE(InfoExtractor): if aweme_list is None: raise ExtractorError('JSON response does not contain aweme_list', video_id=sec_uid) - entries.extend([self.url_result('https://www.douyin.com/video/%s' % aweme['aweme_id'], - ie=DouyinVideoIE.ie_key(), video_id=aweme['aweme_id']) - for aweme in aweme_list]) + entries.extend([self.url_result('https://www.douyin.com/video/%s' % aweme_id, + ie=DouyinVideoIE.ie_key(), video_id=aweme_id) + for aweme_id in filter(None, + (aweme.get('aweme_id') for aweme in aweme_list + if isinstance(aweme, dict)))]) - has_more = post['has_more'] - max_cursor = post['max_cursor'] + has_more = post.get('has_more') + max_cursor = post.get('max_cursor') return self.playlist_result(orderedSet(entries), sec_uid) -