]> git.gir.st - subscriptionfeed.git/blob - app/browse/protobuf.py
fix str-in-None exception, some comments
[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 offset: Optional[int64] = field(9, default=0)
40
41 # XXX: search pagination doesn't work -- probably moved to a continuation token
42 def make_sp(page=1, sort=None, date=None, type=None, len=None, features=[], extras=[]):
43 sortorder = dict(relevance=0, rating=1, date=2, views=3)
44 datefilter = dict(hour=1, day=2, week=3, month=4, year=5)
45 typefilter = dict(video=1, channel=2, playlist=3, movie=4, show=5)
46 lenfilter = dict(short=1, long=2)
47
48 return b64e(SearchRequest(
49 sorted=sortorder.get(sort),
50 filter=Filters(
51 date=datefilter.get(date),
52 type=typefilter.get(type),
53 length=lenfilter.get(len),
54 **{f:True for f in features},
55 ) if date or type or len or features else None,
56 extras=Extras(**{f:True for f in extras}),
57 offset=(int(page)-1)*20,
58 ).dumps())
59 # }}} SEARCH
60
61 # CHANNEL v1/v3 {{{
62 @message
63 @dataclass
64 class SearchOffset:
65 offset: int64 = field(3, default=0)
66 @message
67 @dataclass
68 class ChannelDataInner:
69 offset: int64 = field(1)
70 @message
71 @dataclass
72 class ChannelDataContainer:
73 data: str = field(1) # base64 of ChannelDataInner
74 @message
75 @dataclass
76 class Subparams:
77 type_s: str = field(2)
78 type_i: Optional[int64] = field(4)
79 page: Optional[str] = field(15)
80 sort: Optional[int64] = field(3, default=None)
81 unknown_const1: int64 = field(7, default=1)
82 unknown_const2: int64 = field(23, default=0)
83 # usually returns gridResponses. to switch to listResponses (cargo-culting
84 # invidious, playlist continuations (not yet supported) require list):
85 list_or_grid: Optional[int64] = field(6,default=2) # 2=list, None/1=grid
86 # invidious sets those, but no idea why:
87 #field12:int64 = field(12,default=1)
88 #field13:str = field(13,default="") # playlists in list mode don't work without this
89 field61: Optional[str] = field(61, default=None) # base64 channelData
90 @message
91 @dataclass
92 class Params:
93 subject: str = field(2) # ucid/plid
94 params: str = field(3) # b64e encoded
95 query: Optional[str] = field(11, default=None) # channel search
96 @message
97 @dataclass
98 class Continuation:
99 params: Params = field(80226972)
100 def make_channel_params(subject, typ="videos", page=1, sort=None, query=None, v3=False):
101 typestr = dict(videos="videos", playlists="playlists", search="search")
102 typeint = dict(videos=0, playlists=1, search=None) # not supporting autogen'd
103 sortorder = dict(popular=1, oldest=2, newest=3)
104 if typ == "playlists":
105 sortorder = dict(newest=3, modified=4)
106 elif typ == "search":
107 sortorder = dict()
108
109 if page and typ=="search":
110 _page = b64e(SearchOffset(offset=(page-1)*30).dumps(), padding=False)
111 else: _page = None
112
113 return b64e(Continuation(
114 params=Params(
115 subject=subject,
116 params=b64e(Subparams(
117 type_s=typestr.get(typ),
118 type_i=typeint.get(typ),
119 sort=sortorder.get(sort),
120 page=_page,
121 field61=b64e(
122 ChannelDataContainer(
123 data=b64e(
124 ChannelDataInner(
125 offset=(page-1)*30
126 ).dumps(), padding=False
127 )
128 ).dumps(), padding=False
129 ) if v3 else None
130 ).dumps()),
131 query=query,
132 ),
133 ).dumps())
134 # }}} CHANNEL v1/v3
135
136 # PLAYLIST {{{
137 @message
138 @dataclass
139 class PlaylistData:
140 offset: int64 = field(1)
141 @message
142 @dataclass
143 class PlaylistSubparams:
144 data: str = field(15)
145 def make_playlist_params(playlist_id, offset):
146 if playlist_id.startswith("UC"):
147 playlist_id = f"UU{playlist_id[2:]}" # "Uploads from <username>"
148
149 return b64e(Continuation(
150 params=Params(
151 subject="VL" + playlist_id,
152 params=b64e(PlaylistSubparams(
153 data="PT:" + b64e(PlaylistData(
154 offset=offset,
155 ).dumps(), padding=False)
156 ).dumps()),
157 ),
158 ).dumps())
159 # }}} PLAYLIST
Imprint / Impressum