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