]> git.gir.st - subscriptionfeed.git/blob - app/dangerous/protobuf.py
hacky way to get v1 and v2 channel responses (needs cleanup!)
[subscriptionfeed.git] / app / dangerous / protobuf.py
1 import base64
2 from dataclasses import dataclass
3 from typing import Optional
4
5 from pure_protobuf.dataclasses_ import field, message
6 from pure_protobuf.types import int64
7
8 def b64e(b, padding=True):
9 return base64.urlsafe_b64encode(b).decode('ascii') \
10 .replace("=", "%3D" if padding else "")
11
12 # SEARCH {{{
13 @message
14 @dataclass
15 class Extras:
16 dont_fix_spelling: Optional[bool] = field(1, default=None)
17 @message
18 @dataclass
19 class Filters: # adapted from invidious
20 date: Optional[int64] = field(1, default=None)
21 type: Optional[int64] = field(2, default=None)
22 length: Optional[int64] = field(3, default=None)
23 is_hd: Optional[bool] = field(4, default=None)
24 subtitles: Optional[bool] = field(5, default=None)
25 ccommons: Optional[bool] = field(6, default=None)
26 is_3d: Optional[bool] = field(7, default=None)
27 live: Optional[bool] = field(8, default=None)
28 purchased: Optional[bool] = field(9, default=None)
29 is_4k: Optional[bool] = field(14, default=None)
30 is_360: Optional[bool] = field(15, default=None)
31 location: Optional[bool] = field(23, default=None)
32 is_hdr: Optional[bool] = field(25, default=None)
33 @message
34 @dataclass
35 class SearchRequest:
36 sorted: Optional[int64] = field(1, default=None)
37 filter: Optional[Filters] = field(2, default=None)
38 extras: Optional[Extras] = field(8, default=None)
39
40 def make_sp(sort=None, date=None, type=None, len=None, features=[], extras=[]):
41 sortorder = dict(relevance=0, rating=1, date=2, views=3)
42 datefilter = dict(hour=1, day=2, week=3, month=4, year=5)
43 typefilter = dict(video=1, channel=2, playlist=3, movie=4, show=5)
44 lenfilter = dict(short=1, long=2)
45
46 return b64e(SearchRequest(
47 sorted=sortorder.get(sort),
48 filter=Filters(
49 date=datefilter.get(date),
50 type=typefilter.get(type),
51 length=lenfilter.get(len),
52 **{f:True for f in features},
53 ) if date or type or len or features else None,
54 extras=Extras(**{f:True for f in extras}),
55 ).dumps())
56 # }}} SEARCH
57
58 # CHANNEL v1 {{{
59 @message
60 @dataclass
61 class Subparams:
62 type_s: str = field(2)
63 type_i: Optional[int64] = field(4)
64 page: Optional[str] = field(15)
65 sort: Optional[int64] = field(3, default=None)
66 unknown_const1: int64 = field(7, default=1)
67 unknown_const2: int64 = field(23, default=0)
68 # usually returns gridResponses. to switch to listResponses (cargo-culting
69 # invidious, playlist continuations (not yet supported) require list):
70 list_or_grid: Optional[int64] = field(6,default=2) # 2=list, None/1=grid
71 # invidious sets those, but no idea why:
72 #field12:int64 = field(12,default=1)
73 #field13:str = field(13,default="") # playlists in list mode don't work without this
74 field61: Optional[str] = field(61, default=None) # base64 channelData
75 @message
76 @dataclass
77 class Params:
78 subject: str = field(2) # ucid/plid
79 params: str = field(3) # b64e encoded
80 query: Optional[str] = field(11, default=None) # channel search
81 @message
82 @dataclass
83 class Continuation:
84 params: Params = field(80226972)
85 def make_channel_params(subject, typ="videos", page=1, sort=None, query=None, v2=False):
86 typestr = dict(videos="videos", playlists="playlists", search="search")
87 typeint = dict(videos=0, playlists=1, search=None) # not supporting autogen'd
88 sortorder = dict(newest=None, popular=1, oldest=2) # v1: newest=None, v2: newest=3
89 if typ == "playlists":
90 sortorder = dict(oldest=2, newest=3, modified=4)
91 elif typ == "search":
92 sortorder = dict()
93
94 return b64e(Continuation(
95 params=Params(
96 subject=subject,
97 params=b64e(Subparams(
98 type_s=typestr.get(typ),
99 type_i=typeint.get(typ),
100 sort=sortorder.get(sort) or 3,
101 #page=str(page) if page else None # Note: ucid/playlists doesn't support pagination
102 page=None if v2 else str(page),
103 field61=b64e(
104 ChannelData(
105 data=ChannelData2(
106 data2=ChannelData3(
107 data3=b64e(
108 ChannelData4(
109 offset=(page-1)*30
110 ).dumps(), padding=False
111 )
112 )
113 )
114 ).dumps(), padding=False
115 ) if v2 else None
116 ).dumps()),
117 query=query,
118 ),
119 ).dumps())
120 # }}} CHANNEL v1
121
122 # CHANNEL v2 {{{
123 # NOTE: on hosts that are supposed to use v2, accessing v1 results in an "Unknown error.". on v1-hosts, accessing v2 always returns the first page.
124 @message
125 @dataclass
126 class ChannelData4:
127 offset: int64 = field(1)
128 @message
129 @dataclass
130 class ChannelData3:
131 data3: str = field(1) # base64 of ChannelData4
132 @message
133 @dataclass
134 class ChannelData2:
135 const1: int64 = field(1, default=6307666885028338688)
136 data2: ChannelData3 = field(2, default=None)
137 @message
138 @dataclass
139 class ChannelData:
140 data: ChannelData2 = field(1)
141 # }}} CHANNEL v2
142
143 # PLAYLIST {{{
144 @message
145 @dataclass
146 class PlaylistData:
147 offset: int64 = field(1)
148 @message
149 @dataclass
150 class PlaylistSubparams:
151 data: str = field(15)
152 def make_playlist_params(playlist_id, offset):
153 if playlist_id.startswith("UC"):
154 playlist_id = f"UU{playlist_id[2:]}"
155
156 return b64e(Continuation(
157 params=Params(
158 subject="VL" + playlist_id,
159 params=b64e(PlaylistSubparams(
160 data="PT:" + b64e(PlaylistData(
161 offset=offset,
162 ).dumps(), padding=False)
163 ).dumps()),
164 ),
165 ).dumps())
166 # }}} PLAYLIST
Imprint / Impressum