11import 'dart:async' ;
2+ import 'dart:convert' ;
23import 'dart:io' ;
34
45import 'package:collection/collection.dart' ;
@@ -15,6 +16,9 @@ import 'package:spotube/extensions/context.dart';
1516import 'package:spotube/models/database/database.dart' ;
1617import 'package:spotube/provider/database/database.dart' ;
1718import 'package:spotube/utils/platform.dart' ;
19+ import 'package:otp_util/otp_util.dart' ;
20+ // ignore: implementation_imports
21+ import 'package:otp_util/src/utils/generic_util.dart' ;
1822
1923extension ExpirationAuthenticationTableData on AuthenticationTableData {
2024 bool get isExpired => DateTime .now ().isAfter (expiration);
@@ -100,6 +104,83 @@ class AuthenticationNotifier extends AsyncNotifier<AuthenticationTableData?> {
100104 .insert (refreshedCredentials, mode: InsertMode .replace);
101105 }
102106
107+ String base32FromBytes (Uint8List e, String secretSauce) {
108+ var t = 0 ;
109+ var n = 0 ;
110+ var r = "" ;
111+ for (int i = 0 ; i < e.length; i++ ) {
112+ n = n << 8 | e[i];
113+ t += 8 ;
114+ while (t >= 5 ) {
115+ r += secretSauce[n >>> t - 5 & 31 ];
116+ t -= 5 ;
117+ }
118+ }
119+ if (t > 0 ) {
120+ r += secretSauce[n << 5 - t & 31 ];
121+ }
122+ return r;
123+ }
124+
125+ Uint8List cleanBuffer (String e) {
126+ e = e.replaceAll (" " , "" );
127+ final t = List .filled (e.length ~ / 2 , 0 );
128+ final n = Uint8List .fromList (t);
129+ for (int r = 0 ; r < e.length; r += 2 ) {
130+ n[r ~ / 2 ] = int .parse (e.substring (r, r + 2 ), radix: 16 );
131+ }
132+ return n;
133+ }
134+
135+ Future <String > generateTotp () async {
136+ const secretSauce = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" ;
137+ final secretCipherBytes = const [
138+ 12 ,
139+ 56 ,
140+ 76 ,
141+ 33 ,
142+ 88 ,
143+ 44 ,
144+ 88 ,
145+ 33 ,
146+ 78 ,
147+ 78 ,
148+ 11 ,
149+ 66 ,
150+ 22 ,
151+ 22 ,
152+ 55 ,
153+ 69 ,
154+ 54
155+ ].mapIndexed ((t, e) => e ^ t % 33 + 9 ).toList ();
156+
157+ final secretBytes = cleanBuffer (
158+ utf8
159+ .encode (secretCipherBytes.join ("" ))
160+ .map ((e) => e.toRadixString (16 ))
161+ .join (),
162+ );
163+
164+ final secret = base32FromBytes (secretBytes, secretSauce);
165+
166+ final res = await dio.get ("https://open.spotify.com/server-time" );
167+ final serverTimeSeconds = res.data["serverTime" ] as int ;
168+
169+ final totp = TOTP (
170+ secret: secret,
171+ algorithm: OTPAlgorithm .SHA1 ,
172+ digits: 6 ,
173+ interval: 30 ,
174+ );
175+
176+ return totp.generateOTP (
177+ input: Util .timeFormat (
178+ time: DateTime .fromMillisecondsSinceEpoch (serverTimeSeconds * 1000 ),
179+ interval: 30 ,
180+ ),
181+ );
182+ }
183+
103184 Future <AuthenticationTableCompanion > credentialsFromCookie (
104185 String cookie,
105186 ) async {
@@ -108,10 +189,17 @@ class AuthenticationNotifier extends AsyncNotifier<AuthenticationTableData?> {
108189 .split ("; " )
109190 .firstWhereOrNull ((c) => c.trim ().startsWith ("sp_dc=" ))
110191 ? .trim ();
192+
193+ final totp = await generateTotp ();
194+ final timestamp = (DateTime .now ().millisecondsSinceEpoch / 1000 ).floor ();
195+
196+ final accessTokenUrl = Uri .parse (
197+ "https://open.spotify.com/get_access_token?reason=transport&productType=web_player"
198+ "&totp=$totp &totpVer=5&ts=$timestamp " ,
199+ );
200+
111201 final res = await dio.getUri (
112- Uri .parse (
113- "https://open.spotify.com/get_access_token?reason=transport&productType=web_player" ,
114- ),
202+ accessTokenUrl,
115203 options: Options (
116204 headers: {
117205 "Cookie" : spDc ?? "" ,
0 commit comments