2 from dataclasses
import dataclass
3 from typing
import Optional
5 from pure_protobuf
.dataclasses_
import field
, message
6 from pure_protobuf
.types
import int64
8 def b64e(b
, padding
=True):
9 return base64
.urlsafe_b64encode(b
).decode('ascii') \
10 .replace("=", "%3D" if padding
else "")
16 verbatim
: Optional
[bool] = field(1, default
=None) # don't fix spelling
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)
36 sorted: Optional
[int64
] = field(1, default
=None)
37 filter: Optional
[Filters
] = field(2, default
=None)
38 extras
: Optional
[Extras
] = field(8, default
=None)
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)
46 return b64e(SearchRequest(
47 sorted=sortorder
.get(sort
),
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
}),
62 offset
: int64
= field(3, default
=0)
65 class ChannelDataInner
:
66 offset
: int64
= field(1)
69 class ChannelDataContainer
:
70 data
: str = field(1) # base64 of ChannelDataInner
74 class NewChannelUserInfoInner
:
75 field1
: int = field(1, default
=0)
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)
88 class NewChannelDataUserSimple
:
89 uuid
: str = field(1, default
="00000000-0000-0000-0000-000000000000")
92 class NewChannelDataUser
:
93 data
: str = field(1) # b64
94 uuid
: str = field(2, default
="00000000-0000-0000-0000-000000000000")
97 class NewChannelDataSort
: # used for videos
98 user
: NewChannelDataUser
= field(1)
99 sort
: int64
= field(3, default
=1) # newest
102 class NewChannelDataSortSimple
: # used for streams
103 sort
: int64
= field(3, default
=1) # newest
104 user
: NewChannelDataUserSimple
= field(2, default
=NewChannelDataUserSimple())
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
118 class NewChannelDataContainer
:
119 data
: NewChannelDataInner
= field(3)
122 class NewChannelDataContainerOuter
:
123 data
: NewChannelDataContainer
= field(110)
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
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"
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":
161 elif typ
in ("videos", "streams", "shorts"): # XXX: not implemented
162 inner
= NewChannelDataSort(
163 user
=NewChannelDataUser(
164 data
=b64e(NewChannelUserInfo(
166 ).dumps(), padding
=False),
167 uuid
="00000000-0000-0000-0000-000000000000"
169 sort
=sortorder
.get(sort
, 1)
171 return b64e(Continuation(
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,
181 #videos|streams|shorts=NewChannelDataSortSimple(sort=sortorder.get(sort, 1))
188 if page
and typ
=="search":
189 _page
= b64e(SearchOffset(offset
=(page
-1)*30).dumps(), padding
=False)
192 return b64e(Continuation(
195 params
=b64e(Subparams(
196 type_s
=typestr
.get(typ
),
197 type_i
=typeint
.get(typ
),
198 sort
=sortorder
.get(sort
),
201 ChannelDataContainer(
205 ).dumps(), padding
=False
207 ).dumps(), padding
=False
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!?)
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):
243 # - LL: own liked videos
244 # - LM: own liked youtube-music videos
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
251 return b64e(Continuation(
253 subject
="VL" + playlist_id
,
254 params
=b64e(PlaylistSubparams(
255 data
="PT:" + b64e(PlaylistData(
258 ).dumps(), padding
=False)