aboutsummaryrefslogtreecommitdiffstats
path: root/hypervideo_dl/extractor/pixivsketch.py
blob: f0ad0b24a1429caf90fd1f611dc8006b41f1fcee (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# coding: utf-8
from __future__ import unicode_literals

from .common import InfoExtractor
from ..utils import (
    ExtractorError,
    traverse_obj,
    unified_timestamp,
)


class PixivSketchBaseIE(InfoExtractor):
    def _call_api(self, video_id, path, referer, note='Downloading JSON metadata'):
        response = self._download_json(f'https://sketch.pixiv.net/api/{path}', video_id, note=note, headers={
            'Referer': referer,
            'X-Requested-With': referer,
        })
        errors = traverse_obj(response, ('errors', ..., 'message'))
        if errors:
            raise ExtractorError(' '.join(f'{e}.' for e in errors))
        return response.get('data') or {}


class PixivSketchIE(PixivSketchBaseIE):
    IE_NAME = 'pixiv:sketch'
    _VALID_URL = r'https?://sketch\.pixiv\.net/@(?P<uploader_id>[a-zA-Z0-9_-]+)/lives/(?P<id>\d+)/?'
    _TESTS = [{
        'url': 'https://sketch.pixiv.net/@nuhutya/lives/3654620468641830507',
        'info_dict': {
            'id': '7370666691623196569',
            'title': 'まにあえクリスマス!',
            'uploader': 'ぬふちゃ',
            'uploader_id': 'nuhutya',
            'channel_id': '9844815',
            'age_limit': 0,
            'timestamp': 1640351536,
        },
        'skip': True,
    }, {
        # these two (age_limit > 0) requires you to login on website, but it's actually not required for download
        'url': 'https://sketch.pixiv.net/@namahyou/lives/4393103321546851377',
        'info_dict': {
            'id': '4907995960957946943',
            'title': 'クリスマスなんて知らん🖕',
            'uploader': 'すゃもり',
            'uploader_id': 'suya2mori2',
            'channel_id': '31169300',
            'age_limit': 15,
            'timestamp': 1640347640,
        },
        'skip': True,
    }, {
        'url': 'https://sketch.pixiv.net/@8aki/lives/3553803162487249670',
        'info_dict': {
            'id': '1593420639479156945',
            'title': 'おまけ本作業(リョナ有)',
            'uploader': 'おぶい / Obui',
            'uploader_id': 'oving',
            'channel_id': '17606',
            'age_limit': 18,
            'timestamp': 1640330263,
        },
        'skip': True,
    }]

    def _real_extract(self, url):
        video_id, uploader_id = self._match_valid_url(url).group('id', 'uploader_id')
        data = self._call_api(video_id, f'lives/{video_id}.json', url)

        if not traverse_obj(data, 'is_broadcasting'):
            raise ExtractorError(f'This live is offline. Use https://sketch.pixiv.net/@{uploader_id} for ongoing live.', expected=True)

        m3u8_url = traverse_obj(data, ('owner', 'hls_movie', 'url'))
        formats = self._extract_m3u8_formats(
            m3u8_url, video_id, ext='mp4',
            entry_protocol='m3u8_native', m3u8_id='hls')
        self._sort_formats(formats)

        return {
            'id': video_id,
            'title': data.get('name'),
            'formats': formats,
            'uploader': traverse_obj(data, ('user', 'name'), ('owner', 'user', 'name')),
            'uploader_id': traverse_obj(data, ('user', 'unique_name'), ('owner', 'user', 'unique_name')),
            'channel_id': str(traverse_obj(data, ('user', 'pixiv_user_id'), ('owner', 'user', 'pixiv_user_id'))),
            'age_limit': 18 if data.get('is_r18') else 15 if data.get('is_r15') else 0,
            'timestamp': unified_timestamp(data.get('created_at')),
            'is_live': True
        }


class PixivSketchUserIE(PixivSketchBaseIE):
    IE_NAME = 'pixiv:sketch:user'
    _VALID_URL = r'https?://sketch\.pixiv\.net/@(?P<id>[a-zA-Z0-9_-]+)/?'
    _TESTS = [{
        'url': 'https://sketch.pixiv.net/@nuhutya',
        'only_matching': True,
    }, {
        'url': 'https://sketch.pixiv.net/@namahyou',
        'only_matching': True,
    }, {
        'url': 'https://sketch.pixiv.net/@8aki',
        'only_matching': True,
    }]

    @classmethod
    def suitable(cls, url):
        return super(PixivSketchUserIE, cls).suitable(url) and not PixivSketchIE.suitable(url)

    def _real_extract(self, url):
        user_id = self._match_id(url)
        data = self._call_api(user_id, f'lives/users/@{user_id}.json', url)

        if not traverse_obj(data, 'is_broadcasting'):
            try:
                self._call_api(user_id, 'users/current.json', url, 'Investigating reason for request failure')
            except ExtractorError as ex:
                if ex.cause and ex.cause.code == 401:
                    self.raise_login_required(f'Please log in, or use direct link like https://sketch.pixiv.net/@{user_id}/1234567890', method='cookies')
            raise ExtractorError('This user is offline', expected=True)

        return self.url_result(f'https://sketch.pixiv.net/@{user_id}/lives/{data["id"]}')