aboutsummaryrefslogtreecommitdiffstats
path: root/youtube/playlist.py
blob: c7e0410793cadce49c1d73620f3960dc203eb143 (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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
from youtube import util, yt_data_extract, proto, local_playlist
from youtube import yt_app
import settings

import base64
import urllib
import json
import string
import gevent
import math
from flask import request, abort
import flask


def playlist_ctoken(playlist_id, offset, include_shorts=True):

    offset = proto.uint(1, offset)
    offset = b'PT:' + proto.unpadded_b64encode(offset)
    offset = proto.string(15, offset)
    if not include_shorts:
        offset += proto.string(104, proto.uint(2, 1))

    continuation_info = proto.string(3, proto.percent_b64encode(offset))

    playlist_id = proto.string(2, 'VL' + playlist_id)
    pointless_nest = proto.string(80226972, playlist_id + continuation_info)

    return base64.urlsafe_b64encode(pointless_nest).decode('ascii')


def playlist_first_page(playlist_id, report_text="Retrieved playlist",
                        use_mobile=False):
    if use_mobile:
        url = 'https://m.youtube.com/playlist?list=' + playlist_id + '&pbj=1'
        content = util.fetch_url(
            url, util.mobile_xhr_headers,
            report_text=report_text, debug_name='playlist_first_page'
        )
        content = json.loads(content.decode('utf-8'))
    else:
        url = 'https://www.youtube.com/playlist?list=' + playlist_id + '&pbj=1'
        content = util.fetch_url(
            url, util.desktop_xhr_headers,
            report_text=report_text, debug_name='playlist_first_page'
        )
        content = json.loads(content.decode('utf-8'))

    return content


def get_videos(playlist_id, page, include_shorts=True, use_mobile=False,
               report_text='Retrieved playlist'):
    # mobile requests return 20 videos per page
    if use_mobile:
        page_size = 20
        headers = util.mobile_xhr_headers
    # desktop requests return 100 videos per page
    else:
        page_size = 100
        headers = util.desktop_xhr_headers

    url = "https://m.youtube.com/playlist?ctoken="
    url += playlist_ctoken(playlist_id, (int(page)-1)*page_size,
                           include_shorts=include_shorts)
    url += "&pbj=1"
    content = util.fetch_url(
        url, headers, report_text=report_text,
        debug_name='playlist_videos'
    )

    info = json.loads(content.decode('utf-8'))
    return info


@yt_app.route('/playlist')
def get_playlist_page():
    if 'list' not in request.args:
        abort(400)

    playlist_id = request.args.get('list')

    # Radio/Mix playlists (RD...) only work as watch page, not playlist page
    if playlist_id.startswith('RD'):
        first_video_id = playlist_id[2:]  # video ID after 'RD' prefix
        return flask.redirect(
            util.URL_ORIGIN + '/watch?v=' + first_video_id + '&list=' + playlist_id,
            302
        )

    page = request.args.get('page', '1')

    if page == '1':
        first_page_json = playlist_first_page(playlist_id)
        this_page_json = first_page_json
    else:
        tasks = (
            gevent.spawn(
                playlist_first_page, playlist_id,
                report_text="Retrieved playlist info", use_mobile=True
            ),
            gevent.spawn(get_videos, playlist_id, page)
        )
        gevent.joinall(tasks)
        util.check_gevent_exceptions(*tasks)
        first_page_json, this_page_json = tasks[0].value, tasks[1].value

    info = yt_data_extract.extract_playlist_info(this_page_json)
    if info['error']:
        return flask.render_template('error.html', error_message=info['error'])

    if page != '1':
        info['metadata'] = yt_data_extract.extract_playlist_metadata(first_page_json)

    util.prefix_urls(info['metadata'])
    for item in info.get('items', ()):
        util.prefix_urls(item)
        util.add_extra_html_info(item)
        if 'id' in item and not item.get('thumbnail'):
            item['thumbnail'] = f"{settings.img_prefix}https://i.ytimg.com/vi/{item['id']}/hqdefault.jpg"

        item['url'] += '&list=' + playlist_id
        if item['index']:
            item['url'] += '&index=' + str(item['index'])

    video_count = yt_data_extract.deep_get(info, 'metadata', 'video_count')
    if video_count is None:
        video_count = 1000

    return flask.render_template(
        'playlist.html',
        header_playlist_names=local_playlist.get_playlist_names(),
        video_list=info.get('items', []),
        num_pages=math.ceil(video_count/100),
        parameters_dictionary=request.args,

        **info['metadata']
    ).encode('utf-8')