]> git.gir.st - subscriptionfeed.git/blob - app/browse/protobuf.py
WIP: port channel videos+livestreams
[subscriptionfeed.git] / app / browse / 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 verbatim: Optional[bool] = field(1, default=None) # don't fix spelling
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/v3 {{{
59 @message
60 @dataclass
61 class SearchOffset:
62 offset: int64 = field(3, default=0)
63 @message
64 @dataclass
65 class ChannelDataInner:
66 offset: int64 = field(1)
67 @message
68 @dataclass
69 class ChannelDataContainer:
70 data: str = field(1) # base64 of ChannelDataInner
71
72 @message
73 @dataclass
74 class NewChannelUserInfoInner:
75 field1: int = field(1, default=0)
76 @message
77 @dataclass
78 class NewChannelUserInfo:
79 offset: int = field(7)
80 field2: NewChannelUserInfoInner = field(2, default=NewChannelUserInfoInner()) # otherwise page>=2 fails
81 field10: int = field(10, default=0) # otherwise 'newest' fails
82 # youtube sets those, but works without (values from invidious, yt's are dynamic):
83 #field5: int = field(5, default=50)
84 #field6: int = field(6, default=1)
85 #field9: int = field(9, default=1)
86 @message
87 @dataclass
88 class NewChannelDataUserSimple:
89 uuid: str = field(1, default="00000000-0000-0000-0000-000000000000")
90 @message
91 @dataclass
92 class NewChannelDataUser:
93 data: str = field(1) # b64
94 uuid: str = field(2, default="00000000-0000-0000-0000-000000000000")
95 @message
96 @dataclass
97 class NewChannelDataSort: # used for videos
98 user: NewChannelDataUser = field(1)
99 sort: int64 = field(3, default=1) # newest
100 @message
101 @dataclass
102 class NewChannelDataSortSimple: # used for streams
103 sort: int64 = field(3, default=1) # newest
104 user: NewChannelDataUserSimple = field(2, default=NewChannelDataUserSimple())
105 @message
106 @dataclass
107 class NewChannelDataInner:
108 videos: Optional[NewChannelDataSort] = field(15, default=None) # videos
109 streams: Optional[NewChannelDataSort] = field(14, default=None) # livestreams
110 shorts: Optional[NewChannelDataSort] = field(10, default=None) # shorts
111 # TODO: above format is constructed by invidious, but youtube uses the one below.
112 # *Simple returns a different result json, that must be handled in innertube.py
113 #videos: Optional[NewChannelDataSortSimple] = field(15, default=None) # videos
114 #streams: Optional[NewChannelDataSortSimple] = field(14, default=None) # livestreams
115 #shorts: Optional[NewChannelDataSortSimple] = field(10, default=None) # shorts
116 @message
117 @dataclass
118 class NewChannelDataContainer:
119 data: NewChannelDataInner = field(3)
120 @message
121 @dataclass
122 class NewChannelDataContainerOuter:
123 data: NewChannelDataContainer = field(110)
124
125 @message
126 @dataclass
127 class Subparams:
128 type_s: str = field(2)
129 type_i: Optional[int64] = field(4)
130 page: Optional[str] = field(15)
131 sort: Optional[int64] = field(3, default=None)
132 unknown_const1: int64 = field(7, default=1)
133 unknown_const2: int64 = field(23, default=0)
134 # usually returns gridResponses. to switch to listResponses (cargo-culting
135 # invidious, playlist continuations (not yet supported) require list):
136 list_or_grid: Optional[int64] = field(6,default=2) # 2=list, None/1=grid
137 # invidious sets those, but no idea why:
138 #field12:int64 = field(12,default=1)
139 #field13:str = field(13,default="") # playlists in list mode don't work without this
140 field61: Optional[str] = field(61, default=None) # base64 channelData
141 @message
142 @dataclass
143 class Params:
144 subject: str = field(2) # ucid/plid
145 params: str = field(3) # b64e encoded
146 query: Optional[str] = field(11, default=None) # channel search
147 # defined by invidious, but not sent by youtube with NewChannel* structs:
148 #param35: Optional[str] = field(35, default=None) # "browse-feed#{ucid}videos102"
149 @message
150 @dataclass
151 class Continuation:
152 params: Params = field(80226972)
153 def make_channel_params(subject, typ="videos", page=1, sort=None, query=None, v3=False):
154 typestr = dict(videos="videos", playlists="playlists", search="search")
155 typeint = dict(videos=0, playlists=1, search=None) # not supporting autogen'd
156 sortorder = dict(newest=1, popular=2)
157 if typ == "playlists":
158 sortorder = dict(newest=3, modified=4)
159 elif typ == "search":
160 sortorder = dict()
161 elif typ in ("videos", "streams", "shorts"): # XXX: not implemented
162 inner = NewChannelDataSort(
163 user=NewChannelDataUser(
164 data=b64e(NewChannelUserInfo(
165 offset= page*30,
166 ).dumps(), padding=False),
167 uuid="00000000-0000-0000-0000-000000000000"
168 ),
169 sort=sortorder.get(sort, 1)
170 )
171 return b64e(Continuation(
172 params=Params(
173 subject=subject,
174 params=b64e(NewChannelDataContainerOuter(
175 data=NewChannelDataContainer(
176 data=NewChannelDataInner(
177 videos=inner if typ=="videos" else None,
178 streams=inner if typ=="streams" else None,
179 shorts=inner if typ=="shorts" else None,
180
181 #videos|streams|shorts=NewChannelDataSortSimple(sort=sortorder.get(sort, 1))
182 )
183 )
184 ).dumps())
185 )
186 ).dumps())
187
188 if page and typ=="search":
189 _page = b64e(SearchOffset(offset=(page-1)*30).dumps(), padding=False)
190 else: _page = None
191
192 return b64e(Continuation(
193 params=Params(
194 subject=subject,
195 params=b64e(Subparams(
196 type_s=typestr.get(typ),
197 type_i=typeint.get(typ),
198 sort=sortorder.get(sort),
199 page=_page,
200 field61=b64e(
201 ChannelDataContainer(
202 data=b64e(
203 ChannelDataInner(
204 offset=(page-1)*30
205 ).dumps(), padding=False
206 )
207 ).dumps(), padding=False
208 ) if v3 else None
209 ).dumps()),
210 query=query,
211 ),
212 ).dumps())
213 # }}} CHANNEL v1/v3
214
215 # PLAYLIST {{{
216 @message
217 @dataclass
218 class PlaylistData:
219 offset: Optional[int64] = field(1) # normal playlists
220 after: Optional[str] = field(2) # mix playlist (RDCL*, RDCM*): video id of last loaded item (how to get first page!?)
221 @message
222 @dataclass
223 class PlaylistSubparams:
224 data: str = field(15)
225 def make_playlist_params(playlist_id, offset):
226 # TODO: special type of playlists: (see https://github.com/nlitsme/youtube_tool)
227 # - PL*: normal playlists
228 # - special channel playlists (suffix=channel_id without UC):
229 # - UU*: newest uploads (works)
230 # - PU*: popular uploads (works)
231 # Note: youtubekids (e.g. PLMYEtVRpaqY00V9W81Cwmzp6N6vZqfUKD4) fallback-only (youtube webui 404's)
232 # - FL*: favourited uploads (works; 404's if private)
233 # - LL*: liked uploads (untested)
234 # - RDCMUC*: mix for channel (fallback only; e.g. RDCMUCYO_jab_esuFRV4b17AJtAw, undisplayable on youtube (only works on watch))
235 # - CL, EL, MQ, TT: unknown
236 # - OLAK5uy_ music album playlist (works, e.g. OLAK5uy_m4xAFdmMC5rX3Ji3g93pQe3hqLZw_9LhM)
237 # - RD*: mix playlist (works with after=; e.g. RDCLAK5uy_kLWIr9gv1XLlPbaDS965-Db4TrBoUTxQ8, RDCLAK5uy_mx8mvGaCkyI12AedAu8yIbBpEYmg6Ayow, RDCLAK5uy_n340faD9pPGRVEVwozYTzIpfXuqmQwcgA, RDCLAK5uy_kQmVjdr90p2onIIAnijgU8u9iUUAOXM2I)
238 # https://www.youtube.com/watch?v=FTQbiNvZqaY&list=RDCLAK5uy_khNGopKCT_t38MZ1W7z4kERrqprkXovxo&start_radio=1
239 # - EC, UL, TL: unknown
240 # - own user playlists (obviously not working):
241 # - RDMM: my mix
242 # - WL: watch later
243 # - LL: own liked videos
244 # - LM: own liked youtube-music videos
245 after = None
246 if playlist_id.startswith("RD"):
247 # NOTE: this only fixes RDCLAK5uy_, not RDCMUC
248 after = '___________' # any video id that doesn't exist within the playlist requests the beginning
249 offset = None
250
251 return b64e(Continuation(
252 params=Params(
253 subject="VL" + playlist_id,
254 params=b64e(PlaylistSubparams(
255 data="PT:" + b64e(PlaylistData(
256 offset=offset,
257 after=after,
258 ).dumps(), padding=False)
259 ).dumps()),
260 ),
261 ).dumps())
262 # }}} PLAYLIST
Imprint / Impressum