11 Commits

Author SHA1 Message Date
3df93e47d2 🚀 Launch 1.2.1+12 2024-09-06 22:44:39 +08:00
6d2a027d9b Kugou music source 2024-09-06 22:19:26 +08:00
222d50d80d 🐛 Bug fixes of querying backend 2024-09-06 18:10:12 +08:00
499bca5b1c Better netease music check 2024-09-06 16:37:49 +08:00
252e4619f7 🐛 Fix player view layout issue 2024-09-06 16:22:39 +08:00
463cb9870f 🐛 Fixes 2024-09-06 13:26:20 +08:00
2cee7ee958 🐛 Search siblings in netease won't show duration 2024-09-06 12:55:10 +08:00
e30e7a5c24 🐛 Fix source track details 2024-09-06 12:52:57 +08:00
6509cd2511 Netease cloud music login 2024-09-06 12:41:35 +08:00
6cdc025c40 🐛 Will throw track not found when has no privilege to play on netease cloud music 2024-09-05 21:53:42 +08:00
de3ad4b21e 🐛 Bug fixes 2024-09-05 13:14:02 +08:00
24 changed files with 1137 additions and 391 deletions

View File

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
@ -55,5 +57,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@ -9,15 +9,23 @@ import 'package:rhythm_box/providers/database.dart';
import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/database/database.dart';
extension ExpirationAuthenticationTableData on AuthenticationTableData { extension ExpirationAuthenticationTableData on AuthenticationTableData {
bool get isExpired => DateTime.now().isAfter(expiration); bool get isExpired => DateTime.now().isAfter(spotifyExpiration);
String? getCookie(String key) => cookie.value String? getCookie(String key) => spotifyCookie.value
.split('; ') .split('; ')
.firstWhereOrNull((c) => c.trim().startsWith('$key=')) .firstWhereOrNull((c) => c.trim().startsWith('$key='))
?.trim() ?.trim()
.split('=') .split('=')
.last .last
.replaceAll(';', ''); .replaceAll(';', '');
String? getNeteaseCookie(String key) => neteaseCookie?.value
.split(';')
.firstWhereOrNull((c) => c.trim().startsWith('$key='))
?.trim()
.split('=')
.last
.replaceAll(';', '');
} }
class AuthenticationProvider extends GetxController { class AuthenticationProvider extends GetxController {
@ -55,18 +63,18 @@ class AuthenticationProvider extends GetxController {
void _setRefreshTimer() { void _setRefreshTimer() {
refreshTimer?.cancel(); refreshTimer?.cancel();
if (auth.value != null && auth.value!.isExpired) { if (auth.value != null && auth.value!.isExpired) {
refreshCredentials(); refreshSpotifyCredentials();
} }
refreshTimer = Timer( refreshTimer = Timer(
auth.value!.expiration.difference(DateTime.now()), auth.value!.spotifyExpiration.difference(DateTime.now()),
() => refreshCredentials(), () => refreshSpotifyCredentials(),
); );
} }
Future<void> refreshCredentials() async { Future<void> refreshSpotifyCredentials() async {
final database = Get.find<DatabaseProvider>().database; final database = Get.find<DatabaseProvider>().database;
final refreshedCredentials = final refreshedCredentials =
await credentialsFromCookie(auth.value!.cookie.value); await credentialsFromCookie(auth.value!.spotifyCookie.value);
await database await database
.update(database.authenticationTable) .update(database.authenticationTable)
@ -112,9 +120,10 @@ class AuthenticationProvider extends GetxController {
return AuthenticationTableCompanion.insert( return AuthenticationTableCompanion.insert(
id: const Value(0), id: const Value(0),
cookie: DecryptedText("${res.headers["set-cookie"]?.join(";")}; $spDc"), spotifyCookie:
accessToken: DecryptedText(body['accessToken']), DecryptedText("${res.headers["set-cookie"]?.join(";")}; $spDc"),
expiration: DateTime.fromMillisecondsSinceEpoch( spotifyAccessToken: DecryptedText(body['accessToken']),
spotifyExpiration: DateTime.fromMillisecondsSinceEpoch(
body['accessTokenExpirationTimestampMs']), body['accessTokenExpirationTimestampMs']),
); );
} catch (e) { } catch (e) {
@ -123,6 +132,21 @@ class AuthenticationProvider extends GetxController {
} }
} }
Future<void> setNeteaseCredentials(String cookie) async {
final database = Get.find<DatabaseProvider>().database;
await database.update(database.authenticationTable).replace(
AuthenticationTableCompanion.insert(
id: const Value(0),
spotifyCookie: auth.value!.spotifyCookie,
spotifyAccessToken: auth.value!.spotifyAccessToken,
spotifyExpiration: auth.value!.spotifyExpiration,
neteaseCookie: Value(DecryptedText(cookie)),
neteaseExpiration: const Value(null),
),
);
await loadAuthenticationData();
}
Future<void> logout() async { Future<void> logout() async {
auth.value = null; auth.value = null;
final database = Get.find<DatabaseProvider>().database; final database = Get.find<DatabaseProvider>().database;
@ -132,6 +156,21 @@ class AuthenticationProvider extends GetxController {
// Additional cleanup if necessary // Additional cleanup if necessary
} }
Future<void> logoutNetease() async {
final database = Get.find<DatabaseProvider>().database;
await database.update(database.authenticationTable).replace(
AuthenticationTableCompanion.insert(
id: const Value(0),
spotifyCookie: auth.value!.spotifyCookie,
spotifyAccessToken: auth.value!.spotifyAccessToken,
spotifyExpiration: auth.value!.spotifyExpiration,
neteaseCookie: const Value(null),
neteaseExpiration: const Value(null),
),
);
await loadAuthenticationData();
}
@override @override
void onClose() { void onClose() {
refreshTimer?.cancel(); refreshTimer?.cancel();

View File

@ -44,7 +44,7 @@ class SpotifyProvider extends GetxController {
log('[SpotifyApi] Using user credentials...'); log('[SpotifyApi] Using user credentials...');
final AuthenticationProvider authenticate = Get.find(); final AuthenticationProvider authenticate = Get.find();
return SpotifyApi.withAccessToken( return SpotifyApi.withAccessToken(
authenticate.auth.value!.accessToken.value); authenticate.auth.value!.spotifyAccessToken.value);
} }
@override @override

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:rhythm_box/screens/about.dart'; import 'package:rhythm_box/screens/about.dart';
import 'package:rhythm_box/screens/album/view.dart'; import 'package:rhythm_box/screens/album/view.dart';
import 'package:rhythm_box/screens/auth/login_netease.dart';
import 'package:rhythm_box/screens/auth/mobile_login.dart'; import 'package:rhythm_box/screens/auth/mobile_login.dart';
import 'package:rhythm_box/screens/explore.dart'; import 'package:rhythm_box/screens/explore.dart';
import 'package:rhythm_box/screens/library/view.dart'; import 'package:rhythm_box/screens/library/view.dart';
@ -104,6 +105,11 @@ final router = GoRouter(routes: [
name: 'authMobileLogin', name: 'authMobileLogin',
builder: (context, state) => const MobileLogin(), builder: (context, state) => const MobileLogin(),
), ),
GoRoute(
path: '/auth/netease/login',
name: 'authMobileLoginNetease',
builder: (context, state) => const LoginNeteaseScreen(),
),
], ],
), ),
]); ]);

View File

@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:get/get.dart';
import 'package:go_router/go_router.dart';
import 'package:rhythm_box/providers/auth.dart';
import 'package:rhythm_box/providers/error_notifier.dart';
import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
import 'package:rhythm_box/widgets/sized_container.dart';
class LoginNeteaseScreen extends StatefulWidget {
const LoginNeteaseScreen({super.key});
@override
State<LoginNeteaseScreen> createState() => _LoginNeteaseScreenState();
}
class _LoginNeteaseScreenState extends State<LoginNeteaseScreen> {
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _phoneRegionController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
late final AuthenticationProvider _auth = Get.find();
bool _isLogging = false;
Future<void> _sentCaptcha() async {
setState(() => _isLogging = true);
final phone = _phoneController.text;
var region = _phoneRegionController.text;
if (region.isEmpty) region = '86';
final client = NeteaseSourcedTrack.getClient();
final resp = await client.get(
'/captcha/sent?phone=$phone&ctcode=$region&timestamp=${DateTime.now().millisecondsSinceEpoch}',
);
if (resp.statusCode != 200 || resp.body?['code'] != 200) {
Get.find<ErrorNotifier>().showError(
resp.bodyString ?? resp.status.toString(),
);
}
setState(() => _isLogging = false);
}
Future<void> _performLogin() async {
setState(() => _isLogging = true);
final phone = _phoneController.text;
final password = _passwordController.text;
var region = _phoneRegionController.text;
if (region.isEmpty) region = '86';
final client = NeteaseSourcedTrack.getClient();
final resp = await client.get(
'/login/cellphone?phone=$phone&captcha=$password&countrycode=$region&timestamp=${DateTime.now().millisecondsSinceEpoch}',
);
if (resp.statusCode != 200 || resp.body?['code'] != 200) {
Get.find<ErrorNotifier>().showError(
resp.bodyString ?? resp.status.toString(),
);
setState(() => _isLogging = false);
return;
}
await _auth.setNeteaseCredentials(resp.body['cookie']);
setState(() => _isLogging = false);
GoRouter.of(context).goNamed('settings');
}
@override
void dispose() {
_phoneController.dispose();
_phoneRegionController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Connect with Netease Cloud Music'),
),
body: CenteredContainer(
maxWidth: 320,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
SizedBox(
width: 64,
child: TextField(
controller: _phoneRegionController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: '86',
isDense: true,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
const Gap(8),
Expanded(
child: TextField(
controller: _phoneController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text('Phone Number'),
isDense: true,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
],
),
const Gap(8),
Row(
children: [
Expanded(
child: TextField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
label: Text('Captcha Code'),
isDense: true,
),
onTapOutside: (_) =>
FocusManager.instance.primaryFocus?.unfocus(),
),
),
const Gap(8),
IconButton(
onPressed: _isLogging ? null : () => _sentCaptcha(),
icon: const Icon(Icons.sms),
tooltip: 'Get Captcha',
),
],
),
const Gap(8),
TextButton(
onPressed: _isLogging ? null : () => _performLogin(),
child: const Text('Login'),
)
],
),
),
);
}
}

View File

@ -72,8 +72,8 @@ class _ExploreScreenState extends State<ExploreScreen> {
} }
if (_auth.auth.value != null) { if (_auth.auth.value != null) {
final customEndpoint = final customEndpoint = CustomSpotifyEndpoints(
CustomSpotifyEndpoints(_auth.auth.value?.accessToken.value ?? ''); _auth.auth.value?.spotifyAccessToken.value ?? '');
final forYouView = await customEndpoint.getView( final forYouView = await customEndpoint.getView(
'made-for-x-hub', 'made-for-x-hub',
market: market, market: market,

View File

@ -1,15 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:rhythm_box/services/server/active_sourced_track.dart'; import 'package:rhythm_box/services/server/active_sourced_track.dart';
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
import 'package:rhythm_box/widgets/player/track_source_details.dart'; import 'package:rhythm_box/widgets/player/track_source_details.dart';
class SourceDetailsPopup extends StatelessWidget { class SourceDetailsPopup extends StatelessWidget {
const SourceDetailsPopup({super.key}); const SourceDetailsPopup({super.key});
Future<SourcedTrack?> _pullActiveTrack() async {
final ActiveSourcedTrackProvider activeSourcedTrack = Get.find();
return activeSourcedTrack.state.value;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ActiveSourcedTrackProvider activeTrack = Get.find();
return SizedBox( return SizedBox(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -20,8 +24,19 @@ class SourceDetailsPopup extends StatelessWidget {
).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16),
Expanded( Expanded(
child: Obx( child: Obx(
() => TrackSourceDetails( () => FutureBuilder(
track: activeTrack.state.value!, future: _pullActiveTrack(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return TrackSourceDetails(
track: snapshot.data!,
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
).paddingSymmetric(horizontal: 24), ).paddingSymmetric(horizontal: 24),
), ),
) )

View File

@ -82,7 +82,8 @@ class _PlayerScreenState extends State<PlayerScreen> {
padding: const EdgeInsets.symmetric(vertical: 24), padding: const EdgeInsets.symmetric(vertical: 24),
children: [ children: [
Obx( Obx(
() => LimitedBox( () => Center(
child: LimitedBox(
maxHeight: maxAlbumSize, maxHeight: maxAlbumSize,
maxWidth: maxAlbumSize, maxWidth: maxAlbumSize,
child: Hero( child: Hero(
@ -90,13 +91,15 @@ class _PlayerScreenState extends State<PlayerScreen> {
child: AspectRatio( child: AspectRatio(
aspectRatio: 1, aspectRatio: 1,
child: ClipRRect( child: ClipRRect(
borderRadius: borderRadius: const BorderRadius.all(
const BorderRadius.all(Radius.circular(16)), Radius.circular(16),
),
child: _albumArt != null child: _albumArt != null
? AutoCacheImage( ? AutoCacheImage(
_albumArt!, _albumArt!,
width: albumSize, width: albumSize,
height: albumSize, height: albumSize,
fit: BoxFit.cover,
) )
: Container( : Container(
color: Theme.of(context) color: Theme.of(context)
@ -113,6 +116,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
), ),
), ),
), ),
),
const Gap(24), const Gap(24),
Obx( Obx(
() => Row( () => Row(
@ -309,10 +313,12 @@ class _PlayerScreenState extends State<PlayerScreen> {
], ],
), ),
const Gap(20), const Gap(20),
SizedBox( Center(
child: SizedBox(
height: 40, height: 40,
child: ListView( child: ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
shrinkWrap: true,
children: [ children: [
TextButton.icon( TextButton.icon(
icon: const Icon(Icons.queue_music), icon: const Icon(Icons.queue_music),
@ -391,6 +397,7 @@ class _PlayerScreenState extends State<PlayerScreen> {
], ],
), ),
), ),
),
], ],
), ),
), ),

View File

@ -7,6 +7,7 @@ import 'package:rhythm_box/providers/spotify.dart';
import 'package:rhythm_box/providers/user_preferences.dart'; import 'package:rhythm_box/providers/user_preferences.dart';
import 'package:rhythm_box/screens/auth/login.dart'; import 'package:rhythm_box/screens/auth/login.dart';
import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/database/database.dart';
import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
import 'package:rhythm_box/widgets/auto_cache_image.dart'; import 'package:rhythm_box/widgets/auto_cache_image.dart';
import 'package:rhythm_box/widgets/sized_container.dart'; import 'package:rhythm_box/widgets/sized_container.dart';
@ -86,6 +87,75 @@ class _SettingsScreenState extends State<SettingsScreen> {
}, },
); );
}), }),
Obx(() {
if (_authenticate.auth.value == null) {
return const SizedBox();
}
if (_authenticate.auth.value?.neteaseCookie == null) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.cloud_outlined),
title: const Text('Connect with Netease Cloud Music'),
subtitle: const Text(
'Make us able to play more music from Netease Cloud Music'),
trailing: const Icon(Icons.chevron_right),
enabled: !_isLoggingIn,
onTap: () async {
setState(() => _isLoggingIn = true);
await GoRouter.of(context)
.pushNamed('authMobileLoginNetease');
setState(() => _isLoggingIn = false);
},
);
}
return FutureBuilder(
future: NeteaseSourcedTrack.getClient().get('/user/account'),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
leading: const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
),
),
title: const Text('Loading...'),
trailing: IconButton(
onPressed: () {
_authenticate.logoutNetease();
},
icon: const Icon(Icons.link_off),
),
);
}
return ListTile(
leading:
snapshot.data!.body['profile']['avatarUrl'] != null
? CircleAvatar(
backgroundImage: AutoCacheImage.provider(
snapshot.data!.body['profile']['avatarUrl'],
),
)
: const Icon(Icons.account_circle),
title: Text(snapshot.data!.body['profile']['nickname']),
subtitle: const Text(
'Connected with your Netease Cloud Music account',
),
trailing: IconButton(
onPressed: () {
_authenticate.logoutNetease();
},
icon: const Icon(Icons.link_off),
),
);
},
);
}),
Obx(() { Obx(() {
if (_authenticate.auth.value == null) { if (_authenticate.auth.value == null) {
return const SizedBox(); return const SizedBox();
@ -95,7 +165,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),
leading: const Icon(Icons.logout), leading: const Icon(Icons.logout),
title: const Text('Log out'), title: const Text('Log out'),
subtitle: const Text('Disconnect with this Spotify account'), subtitle: const Text('Disconnect with every account'),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () async { onTap: () async {
_authenticate.logout(); _authenticate.logout();

View File

@ -55,7 +55,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());
@override @override
int get schemaVersion => 2; int get schemaVersion => 1;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@ -63,11 +63,7 @@ class AppDatabase extends _$AppDatabase {
onCreate: (Migrator m) async { onCreate: (Migrator m) async {
await m.createAll(); await m.createAll();
}, },
onUpgrade: (Migrator m, int from, int to) async { onUpgrade: (Migrator m, int from, int to) async {},
if (from < 2) {
await m.addColumn(preferencesTable, preferencesTable.playerWakelock);
}
},
); );
} }
} }

View File

@ -18,29 +18,54 @@ class $AuthenticationTableTable extends AuthenticationTable
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const VerificationMeta _cookieMeta = const VerificationMeta('cookie'); static const VerificationMeta _spotifyCookieMeta =
@override const VerificationMeta('spotifyCookie');
late final GeneratedColumnWithTypeConverter<DecryptedText, String> cookie =
GeneratedColumn<String>('cookie', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<DecryptedText>(
$AuthenticationTableTable.$convertercookie);
static const VerificationMeta _accessTokenMeta =
const VerificationMeta('accessToken');
@override @override
late final GeneratedColumnWithTypeConverter<DecryptedText, String> late final GeneratedColumnWithTypeConverter<DecryptedText, String>
accessToken = GeneratedColumn<String>('access_token', aliasedName, false, spotifyCookie = GeneratedColumn<String>(
'spotify_cookie', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true) type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<DecryptedText>( .withConverter<DecryptedText>(
$AuthenticationTableTable.$converteraccessToken); $AuthenticationTableTable.$converterspotifyCookie);
static const VerificationMeta _expirationMeta = static const VerificationMeta _spotifyAccessTokenMeta =
const VerificationMeta('expiration'); const VerificationMeta('spotifyAccessToken');
@override @override
late final GeneratedColumn<DateTime> expiration = GeneratedColumn<DateTime>( late final GeneratedColumnWithTypeConverter<DecryptedText, String>
'expiration', aliasedName, false, spotifyAccessToken = GeneratedColumn<String>(
'spotify_access_token', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<DecryptedText>(
$AuthenticationTableTable.$converterspotifyAccessToken);
static const VerificationMeta _spotifyExpirationMeta =
const VerificationMeta('spotifyExpiration');
@override
late final GeneratedColumn<DateTime> spotifyExpiration =
GeneratedColumn<DateTime>('spotify_expiration', aliasedName, false,
type: DriftSqlType.dateTime, requiredDuringInsert: true); type: DriftSqlType.dateTime, requiredDuringInsert: true);
static const VerificationMeta _neteaseCookieMeta =
const VerificationMeta('neteaseCookie');
@override @override
List<GeneratedColumn> get $columns => [id, cookie, accessToken, expiration]; late final GeneratedColumnWithTypeConverter<DecryptedText?, String>
neteaseCookie = GeneratedColumn<String>(
'netease_cookie', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false)
.withConverter<DecryptedText?>(
$AuthenticationTableTable.$converterneteaseCookien);
static const VerificationMeta _neteaseExpirationMeta =
const VerificationMeta('neteaseExpiration');
@override
late final GeneratedColumn<DateTime> neteaseExpiration =
GeneratedColumn<DateTime>('netease_expiration', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false);
@override
List<GeneratedColumn> get $columns => [
id,
spotifyCookie,
spotifyAccessToken,
spotifyExpiration,
neteaseCookie,
neteaseExpiration
];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
@ -55,15 +80,22 @@ class $AuthenticationTableTable extends AuthenticationTable
if (data.containsKey('id')) { if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
} }
context.handle(_cookieMeta, const VerificationResult.success()); context.handle(_spotifyCookieMeta, const VerificationResult.success());
context.handle(_accessTokenMeta, const VerificationResult.success()); context.handle(_spotifyAccessTokenMeta, const VerificationResult.success());
if (data.containsKey('expiration')) { if (data.containsKey('spotify_expiration')) {
context.handle( context.handle(
_expirationMeta, _spotifyExpirationMeta,
expiration.isAcceptableOrUnknown( spotifyExpiration.isAcceptableOrUnknown(
data['expiration']!, _expirationMeta)); data['spotify_expiration']!, _spotifyExpirationMeta));
} else if (isInserting) { } else if (isInserting) {
context.missing(_expirationMeta); context.missing(_spotifyExpirationMeta);
}
context.handle(_neteaseCookieMeta, const VerificationResult.success());
if (data.containsKey('netease_expiration')) {
context.handle(
_neteaseExpirationMeta,
neteaseExpiration.isAcceptableOrUnknown(
data['netease_expiration']!, _neteaseExpirationMeta));
} }
return context; return context;
} }
@ -77,14 +109,19 @@ class $AuthenticationTableTable extends AuthenticationTable
return AuthenticationTableData( return AuthenticationTableData(
id: attachedDatabase.typeMapping id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!, .read(DriftSqlType.int, data['${effectivePrefix}id'])!,
cookie: $AuthenticationTableTable.$convertercookie.fromSql( spotifyCookie: $AuthenticationTableTable.$converterspotifyCookie.fromSql(
attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}cookie'])!),
accessToken: $AuthenticationTableTable.$converteraccessToken.fromSql(
attachedDatabase.typeMapping.read( attachedDatabase.typeMapping.read(
DriftSqlType.string, data['${effectivePrefix}access_token'])!), DriftSqlType.string, data['${effectivePrefix}spotify_cookie'])!),
expiration: attachedDatabase.typeMapping spotifyAccessToken: $AuthenticationTableTable.$converterspotifyAccessToken
.read(DriftSqlType.dateTime, data['${effectivePrefix}expiration'])!, .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string,
data['${effectivePrefix}spotify_access_token'])!),
spotifyExpiration: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, data['${effectivePrefix}spotify_expiration'])!,
neteaseCookie: $AuthenticationTableTable.$converterneteaseCookien.fromSql(
attachedDatabase.typeMapping.read(
DriftSqlType.string, data['${effectivePrefix}netease_cookie'])),
neteaseExpiration: attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, data['${effectivePrefix}netease_expiration']),
); );
} }
@ -93,45 +130,69 @@ class $AuthenticationTableTable extends AuthenticationTable
return $AuthenticationTableTable(attachedDatabase, alias); return $AuthenticationTableTable(attachedDatabase, alias);
} }
static TypeConverter<DecryptedText, String> $convertercookie = static TypeConverter<DecryptedText, String> $converterspotifyCookie =
EncryptedTextConverter(); EncryptedTextConverter();
static TypeConverter<DecryptedText, String> $converteraccessToken = static TypeConverter<DecryptedText, String> $converterspotifyAccessToken =
EncryptedTextConverter(); EncryptedTextConverter();
static TypeConverter<DecryptedText, String> $converterneteaseCookie =
EncryptedTextConverter();
static TypeConverter<DecryptedText?, String?> $converterneteaseCookien =
NullAwareTypeConverter.wrap($converterneteaseCookie);
} }
class AuthenticationTableData extends DataClass class AuthenticationTableData extends DataClass
implements Insertable<AuthenticationTableData> { implements Insertable<AuthenticationTableData> {
final int id; final int id;
final DecryptedText cookie; final DecryptedText spotifyCookie;
final DecryptedText accessToken; final DecryptedText spotifyAccessToken;
final DateTime expiration; final DateTime spotifyExpiration;
final DecryptedText? neteaseCookie;
final DateTime? neteaseExpiration;
const AuthenticationTableData( const AuthenticationTableData(
{required this.id, {required this.id,
required this.cookie, required this.spotifyCookie,
required this.accessToken, required this.spotifyAccessToken,
required this.expiration}); required this.spotifyExpiration,
this.neteaseCookie,
this.neteaseExpiration});
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
map['id'] = Variable<int>(id); map['id'] = Variable<int>(id);
{ {
map['cookie'] = Variable<String>( map['spotify_cookie'] = Variable<String>($AuthenticationTableTable
$AuthenticationTableTable.$convertercookie.toSql(cookie)); .$converterspotifyCookie
.toSql(spotifyCookie));
} }
{ {
map['access_token'] = Variable<String>( map['spotify_access_token'] = Variable<String>($AuthenticationTableTable
$AuthenticationTableTable.$converteraccessToken.toSql(accessToken)); .$converterspotifyAccessToken
.toSql(spotifyAccessToken));
}
map['spotify_expiration'] = Variable<DateTime>(spotifyExpiration);
if (!nullToAbsent || neteaseCookie != null) {
map['netease_cookie'] = Variable<String>($AuthenticationTableTable
.$converterneteaseCookien
.toSql(neteaseCookie));
}
if (!nullToAbsent || neteaseExpiration != null) {
map['netease_expiration'] = Variable<DateTime>(neteaseExpiration);
} }
map['expiration'] = Variable<DateTime>(expiration);
return map; return map;
} }
AuthenticationTableCompanion toCompanion(bool nullToAbsent) { AuthenticationTableCompanion toCompanion(bool nullToAbsent) {
return AuthenticationTableCompanion( return AuthenticationTableCompanion(
id: Value(id), id: Value(id),
cookie: Value(cookie), spotifyCookie: Value(spotifyCookie),
accessToken: Value(accessToken), spotifyAccessToken: Value(spotifyAccessToken),
expiration: Value(expiration), spotifyExpiration: Value(spotifyExpiration),
neteaseCookie: neteaseCookie == null && nullToAbsent
? const Value.absent()
: Value(neteaseCookie),
neteaseExpiration: neteaseExpiration == null && nullToAbsent
? const Value.absent()
: Value(neteaseExpiration),
); );
} }
@ -140,9 +201,14 @@ class AuthenticationTableData extends DataClass
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return AuthenticationTableData( return AuthenticationTableData(
id: serializer.fromJson<int>(json['id']), id: serializer.fromJson<int>(json['id']),
cookie: serializer.fromJson<DecryptedText>(json['cookie']), spotifyCookie: serializer.fromJson<DecryptedText>(json['spotifyCookie']),
accessToken: serializer.fromJson<DecryptedText>(json['accessToken']), spotifyAccessToken:
expiration: serializer.fromJson<DateTime>(json['expiration']), serializer.fromJson<DecryptedText>(json['spotifyAccessToken']),
spotifyExpiration:
serializer.fromJson<DateTime>(json['spotifyExpiration']),
neteaseCookie: serializer.fromJson<DecryptedText?>(json['neteaseCookie']),
neteaseExpiration:
serializer.fromJson<DateTime?>(json['neteaseExpiration']),
); );
} }
@override @override
@ -150,31 +216,51 @@ class AuthenticationTableData extends DataClass
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{ return <String, dynamic>{
'id': serializer.toJson<int>(id), 'id': serializer.toJson<int>(id),
'cookie': serializer.toJson<DecryptedText>(cookie), 'spotifyCookie': serializer.toJson<DecryptedText>(spotifyCookie),
'accessToken': serializer.toJson<DecryptedText>(accessToken), 'spotifyAccessToken':
'expiration': serializer.toJson<DateTime>(expiration), serializer.toJson<DecryptedText>(spotifyAccessToken),
'spotifyExpiration': serializer.toJson<DateTime>(spotifyExpiration),
'neteaseCookie': serializer.toJson<DecryptedText?>(neteaseCookie),
'neteaseExpiration': serializer.toJson<DateTime?>(neteaseExpiration),
}; };
} }
AuthenticationTableData copyWith( AuthenticationTableData copyWith(
{int? id, {int? id,
DecryptedText? cookie, DecryptedText? spotifyCookie,
DecryptedText? accessToken, DecryptedText? spotifyAccessToken,
DateTime? expiration}) => DateTime? spotifyExpiration,
Value<DecryptedText?> neteaseCookie = const Value.absent(),
Value<DateTime?> neteaseExpiration = const Value.absent()}) =>
AuthenticationTableData( AuthenticationTableData(
id: id ?? this.id, id: id ?? this.id,
cookie: cookie ?? this.cookie, spotifyCookie: spotifyCookie ?? this.spotifyCookie,
accessToken: accessToken ?? this.accessToken, spotifyAccessToken: spotifyAccessToken ?? this.spotifyAccessToken,
expiration: expiration ?? this.expiration, spotifyExpiration: spotifyExpiration ?? this.spotifyExpiration,
neteaseCookie:
neteaseCookie.present ? neteaseCookie.value : this.neteaseCookie,
neteaseExpiration: neteaseExpiration.present
? neteaseExpiration.value
: this.neteaseExpiration,
); );
AuthenticationTableData copyWithCompanion(AuthenticationTableCompanion data) { AuthenticationTableData copyWithCompanion(AuthenticationTableCompanion data) {
return AuthenticationTableData( return AuthenticationTableData(
id: data.id.present ? data.id.value : this.id, id: data.id.present ? data.id.value : this.id,
cookie: data.cookie.present ? data.cookie.value : this.cookie, spotifyCookie: data.spotifyCookie.present
accessToken: ? data.spotifyCookie.value
data.accessToken.present ? data.accessToken.value : this.accessToken, : this.spotifyCookie,
expiration: spotifyAccessToken: data.spotifyAccessToken.present
data.expiration.present ? data.expiration.value : this.expiration, ? data.spotifyAccessToken.value
: this.spotifyAccessToken,
spotifyExpiration: data.spotifyExpiration.present
? data.spotifyExpiration.value
: this.spotifyExpiration,
neteaseCookie: data.neteaseCookie.present
? data.neteaseCookie.value
: this.neteaseCookie,
neteaseExpiration: data.neteaseExpiration.present
? data.neteaseExpiration.value
: this.neteaseExpiration,
); );
} }
@ -182,69 +268,89 @@ class AuthenticationTableData extends DataClass
String toString() { String toString() {
return (StringBuffer('AuthenticationTableData(') return (StringBuffer('AuthenticationTableData(')
..write('id: $id, ') ..write('id: $id, ')
..write('cookie: $cookie, ') ..write('spotifyCookie: $spotifyCookie, ')
..write('accessToken: $accessToken, ') ..write('spotifyAccessToken: $spotifyAccessToken, ')
..write('expiration: $expiration') ..write('spotifyExpiration: $spotifyExpiration, ')
..write('neteaseCookie: $neteaseCookie, ')
..write('neteaseExpiration: $neteaseExpiration')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => Object.hash(id, cookie, accessToken, expiration); int get hashCode => Object.hash(id, spotifyCookie, spotifyAccessToken,
spotifyExpiration, neteaseCookie, neteaseExpiration);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
(other is AuthenticationTableData && (other is AuthenticationTableData &&
other.id == this.id && other.id == this.id &&
other.cookie == this.cookie && other.spotifyCookie == this.spotifyCookie &&
other.accessToken == this.accessToken && other.spotifyAccessToken == this.spotifyAccessToken &&
other.expiration == this.expiration); other.spotifyExpiration == this.spotifyExpiration &&
other.neteaseCookie == this.neteaseCookie &&
other.neteaseExpiration == this.neteaseExpiration);
} }
class AuthenticationTableCompanion class AuthenticationTableCompanion
extends UpdateCompanion<AuthenticationTableData> { extends UpdateCompanion<AuthenticationTableData> {
final Value<int> id; final Value<int> id;
final Value<DecryptedText> cookie; final Value<DecryptedText> spotifyCookie;
final Value<DecryptedText> accessToken; final Value<DecryptedText> spotifyAccessToken;
final Value<DateTime> expiration; final Value<DateTime> spotifyExpiration;
final Value<DecryptedText?> neteaseCookie;
final Value<DateTime?> neteaseExpiration;
const AuthenticationTableCompanion({ const AuthenticationTableCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
this.cookie = const Value.absent(), this.spotifyCookie = const Value.absent(),
this.accessToken = const Value.absent(), this.spotifyAccessToken = const Value.absent(),
this.expiration = const Value.absent(), this.spotifyExpiration = const Value.absent(),
this.neteaseCookie = const Value.absent(),
this.neteaseExpiration = const Value.absent(),
}); });
AuthenticationTableCompanion.insert({ AuthenticationTableCompanion.insert({
this.id = const Value.absent(), this.id = const Value.absent(),
required DecryptedText cookie, required DecryptedText spotifyCookie,
required DecryptedText accessToken, required DecryptedText spotifyAccessToken,
required DateTime expiration, required DateTime spotifyExpiration,
}) : cookie = Value(cookie), this.neteaseCookie = const Value.absent(),
accessToken = Value(accessToken), this.neteaseExpiration = const Value.absent(),
expiration = Value(expiration); }) : spotifyCookie = Value(spotifyCookie),
spotifyAccessToken = Value(spotifyAccessToken),
spotifyExpiration = Value(spotifyExpiration);
static Insertable<AuthenticationTableData> custom({ static Insertable<AuthenticationTableData> custom({
Expression<int>? id, Expression<int>? id,
Expression<String>? cookie, Expression<String>? spotifyCookie,
Expression<String>? accessToken, Expression<String>? spotifyAccessToken,
Expression<DateTime>? expiration, Expression<DateTime>? spotifyExpiration,
Expression<String>? neteaseCookie,
Expression<DateTime>? neteaseExpiration,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
if (id != null) 'id': id, if (id != null) 'id': id,
if (cookie != null) 'cookie': cookie, if (spotifyCookie != null) 'spotify_cookie': spotifyCookie,
if (accessToken != null) 'access_token': accessToken, if (spotifyAccessToken != null)
if (expiration != null) 'expiration': expiration, 'spotify_access_token': spotifyAccessToken,
if (spotifyExpiration != null) 'spotify_expiration': spotifyExpiration,
if (neteaseCookie != null) 'netease_cookie': neteaseCookie,
if (neteaseExpiration != null) 'netease_expiration': neteaseExpiration,
}); });
} }
AuthenticationTableCompanion copyWith( AuthenticationTableCompanion copyWith(
{Value<int>? id, {Value<int>? id,
Value<DecryptedText>? cookie, Value<DecryptedText>? spotifyCookie,
Value<DecryptedText>? accessToken, Value<DecryptedText>? spotifyAccessToken,
Value<DateTime>? expiration}) { Value<DateTime>? spotifyExpiration,
Value<DecryptedText?>? neteaseCookie,
Value<DateTime?>? neteaseExpiration}) {
return AuthenticationTableCompanion( return AuthenticationTableCompanion(
id: id ?? this.id, id: id ?? this.id,
cookie: cookie ?? this.cookie, spotifyCookie: spotifyCookie ?? this.spotifyCookie,
accessToken: accessToken ?? this.accessToken, spotifyAccessToken: spotifyAccessToken ?? this.spotifyAccessToken,
expiration: expiration ?? this.expiration, spotifyExpiration: spotifyExpiration ?? this.spotifyExpiration,
neteaseCookie: neteaseCookie ?? this.neteaseCookie,
neteaseExpiration: neteaseExpiration ?? this.neteaseExpiration,
); );
} }
@ -254,17 +360,26 @@ class AuthenticationTableCompanion
if (id.present) { if (id.present) {
map['id'] = Variable<int>(id.value); map['id'] = Variable<int>(id.value);
} }
if (cookie.present) { if (spotifyCookie.present) {
map['cookie'] = Variable<String>( map['spotify_cookie'] = Variable<String>($AuthenticationTableTable
$AuthenticationTableTable.$convertercookie.toSql(cookie.value)); .$converterspotifyCookie
.toSql(spotifyCookie.value));
} }
if (accessToken.present) { if (spotifyAccessToken.present) {
map['access_token'] = Variable<String>($AuthenticationTableTable map['spotify_access_token'] = Variable<String>($AuthenticationTableTable
.$converteraccessToken .$converterspotifyAccessToken
.toSql(accessToken.value)); .toSql(spotifyAccessToken.value));
} }
if (expiration.present) { if (spotifyExpiration.present) {
map['expiration'] = Variable<DateTime>(expiration.value); map['spotify_expiration'] = Variable<DateTime>(spotifyExpiration.value);
}
if (neteaseCookie.present) {
map['netease_cookie'] = Variable<String>($AuthenticationTableTable
.$converterneteaseCookien
.toSql(neteaseCookie.value));
}
if (neteaseExpiration.present) {
map['netease_expiration'] = Variable<DateTime>(neteaseExpiration.value);
} }
return map; return map;
} }
@ -273,9 +388,11 @@ class AuthenticationTableCompanion
String toString() { String toString() {
return (StringBuffer('AuthenticationTableCompanion(') return (StringBuffer('AuthenticationTableCompanion(')
..write('id: $id, ') ..write('id: $id, ')
..write('cookie: $cookie, ') ..write('spotifyCookie: $spotifyCookie, ')
..write('accessToken: $accessToken, ') ..write('spotifyAccessToken: $spotifyAccessToken, ')
..write('expiration: $expiration') ..write('spotifyExpiration: $spotifyExpiration, ')
..write('neteaseCookie: $neteaseCookie, ')
..write('neteaseExpiration: $neteaseExpiration')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@ -3824,16 +3941,20 @@ abstract class _$AppDatabase extends GeneratedDatabase {
typedef $$AuthenticationTableTableCreateCompanionBuilder typedef $$AuthenticationTableTableCreateCompanionBuilder
= AuthenticationTableCompanion Function({ = AuthenticationTableCompanion Function({
Value<int> id, Value<int> id,
required DecryptedText cookie, required DecryptedText spotifyCookie,
required DecryptedText accessToken, required DecryptedText spotifyAccessToken,
required DateTime expiration, required DateTime spotifyExpiration,
Value<DecryptedText?> neteaseCookie,
Value<DateTime?> neteaseExpiration,
}); });
typedef $$AuthenticationTableTableUpdateCompanionBuilder typedef $$AuthenticationTableTableUpdateCompanionBuilder
= AuthenticationTableCompanion Function({ = AuthenticationTableCompanion Function({
Value<int> id, Value<int> id,
Value<DecryptedText> cookie, Value<DecryptedText> spotifyCookie,
Value<DecryptedText> accessToken, Value<DecryptedText> spotifyAccessToken,
Value<DateTime> expiration, Value<DateTime> spotifyExpiration,
Value<DecryptedText?> neteaseCookie,
Value<DateTime?> neteaseExpiration,
}); });
class $$AuthenticationTableTableTableManager extends RootTableManager< class $$AuthenticationTableTableTableManager extends RootTableManager<
@ -3855,27 +3976,35 @@ class $$AuthenticationTableTableTableManager extends RootTableManager<
ComposerState(db, table)), ComposerState(db, table)),
updateCompanionCallback: ({ updateCompanionCallback: ({
Value<int> id = const Value.absent(), Value<int> id = const Value.absent(),
Value<DecryptedText> cookie = const Value.absent(), Value<DecryptedText> spotifyCookie = const Value.absent(),
Value<DecryptedText> accessToken = const Value.absent(), Value<DecryptedText> spotifyAccessToken = const Value.absent(),
Value<DateTime> expiration = const Value.absent(), Value<DateTime> spotifyExpiration = const Value.absent(),
Value<DecryptedText?> neteaseCookie = const Value.absent(),
Value<DateTime?> neteaseExpiration = const Value.absent(),
}) => }) =>
AuthenticationTableCompanion( AuthenticationTableCompanion(
id: id, id: id,
cookie: cookie, spotifyCookie: spotifyCookie,
accessToken: accessToken, spotifyAccessToken: spotifyAccessToken,
expiration: expiration, spotifyExpiration: spotifyExpiration,
neteaseCookie: neteaseCookie,
neteaseExpiration: neteaseExpiration,
), ),
createCompanionCallback: ({ createCompanionCallback: ({
Value<int> id = const Value.absent(), Value<int> id = const Value.absent(),
required DecryptedText cookie, required DecryptedText spotifyCookie,
required DecryptedText accessToken, required DecryptedText spotifyAccessToken,
required DateTime expiration, required DateTime spotifyExpiration,
Value<DecryptedText?> neteaseCookie = const Value.absent(),
Value<DateTime?> neteaseExpiration = const Value.absent(),
}) => }) =>
AuthenticationTableCompanion.insert( AuthenticationTableCompanion.insert(
id: id, id: id,
cookie: cookie, spotifyCookie: spotifyCookie,
accessToken: accessToken, spotifyAccessToken: spotifyAccessToken,
expiration: expiration, spotifyExpiration: spotifyExpiration,
neteaseCookie: neteaseCookie,
neteaseExpiration: neteaseExpiration,
), ),
)); ));
} }
@ -3889,21 +4018,33 @@ class $$AuthenticationTableTableFilterComposer
ColumnFilters(column, joinBuilders: joinBuilders)); ColumnFilters(column, joinBuilders: joinBuilders));
ColumnWithTypeConverterFilters<DecryptedText, DecryptedText, String> ColumnWithTypeConverterFilters<DecryptedText, DecryptedText, String>
get cookie => $state.composableBuilder( get spotifyCookie => $state.composableBuilder(
column: $state.table.cookie, column: $state.table.spotifyCookie,
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
column, column,
joinBuilders: joinBuilders)); joinBuilders: joinBuilders));
ColumnWithTypeConverterFilters<DecryptedText, DecryptedText, String> ColumnWithTypeConverterFilters<DecryptedText, DecryptedText, String>
get accessToken => $state.composableBuilder( get spotifyAccessToken => $state.composableBuilder(
column: $state.table.accessToken, column: $state.table.spotifyAccessToken,
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
column, column,
joinBuilders: joinBuilders)); joinBuilders: joinBuilders));
ColumnFilters<DateTime> get expiration => $state.composableBuilder( ColumnFilters<DateTime> get spotifyExpiration => $state.composableBuilder(
column: $state.table.expiration, column: $state.table.spotifyExpiration,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnWithTypeConverterFilters<DecryptedText?, DecryptedText, String>
get neteaseCookie => $state.composableBuilder(
column: $state.table.neteaseCookie,
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
column,
joinBuilders: joinBuilders));
ColumnFilters<DateTime> get neteaseExpiration => $state.composableBuilder(
column: $state.table.neteaseExpiration,
builder: (column, joinBuilders) => builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders)); ColumnFilters(column, joinBuilders: joinBuilders));
} }
@ -3916,18 +4057,28 @@ class $$AuthenticationTableTableOrderingComposer
builder: (column, joinBuilders) => builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders)); ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get cookie => $state.composableBuilder( ColumnOrderings<String> get spotifyCookie => $state.composableBuilder(
column: $state.table.cookie, column: $state.table.spotifyCookie,
builder: (column, joinBuilders) => builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders)); ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get accessToken => $state.composableBuilder( ColumnOrderings<String> get spotifyAccessToken => $state.composableBuilder(
column: $state.table.accessToken, column: $state.table.spotifyAccessToken,
builder: (column, joinBuilders) => builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders)); ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<DateTime> get expiration => $state.composableBuilder( ColumnOrderings<DateTime> get spotifyExpiration => $state.composableBuilder(
column: $state.table.expiration, column: $state.table.spotifyExpiration,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get neteaseCookie => $state.composableBuilder(
column: $state.table.neteaseCookie,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<DateTime> get neteaseExpiration => $state.composableBuilder(
column: $state.table.neteaseExpiration,
builder: (column, joinBuilders) => builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders)); ColumnOrderings(column, joinBuilders: joinBuilders));
} }

View File

@ -2,7 +2,10 @@ part of '../database.dart';
class AuthenticationTable extends Table { class AuthenticationTable extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
TextColumn get cookie => text().map(EncryptedTextConverter())(); TextColumn get spotifyCookie => text().map(EncryptedTextConverter())();
TextColumn get accessToken => text().map(EncryptedTextConverter())(); TextColumn get spotifyAccessToken => text().map(EncryptedTextConverter())();
DateTimeColumn get expiration => dateTime()(); DateTimeColumn get spotifyExpiration => dateTime()();
TextColumn get neteaseCookie =>
text().map(EncryptedTextConverter()).nullable()();
DateTimeColumn get neteaseExpiration => dateTime().nullable()();
} }

View File

@ -14,7 +14,8 @@ enum CloseBehavior {
enum AudioSource { enum AudioSource {
youtube, youtube,
piped, piped,
netease; netease,
kugou;
String get label => name[0].toUpperCase() + name.substring(1); String get label => name[0].toUpperCase() + name.substring(1);
} }

View File

@ -3,7 +3,8 @@ part of '../database.dart';
enum SourceType { enum SourceType {
youtube._('YouTube'), youtube._('YouTube'),
youtubeMusic._('YouTube Music'), youtubeMusic._('YouTube Music'),
netease._('Netease Music'); netease._('Netease Music'),
kugou._('Kugou Music');
final String label; final String label;

View File

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:dio/dio.dart' hide Response; import 'package:dio/dio.dart' hide Response;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:get/get.dart' hide Response; import 'package:get/get.dart' hide Response;
@ -6,6 +8,7 @@ import 'package:rhythm_box/providers/error_notifier.dart';
import 'package:rhythm_box/services/audio_player/audio_player.dart'; import 'package:rhythm_box/services/audio_player/audio_player.dart';
import 'package:rhythm_box/services/server/active_sourced_track.dart'; import 'package:rhythm_box/services/server/active_sourced_track.dart';
import 'package:rhythm_box/services/server/sourced_track.dart'; import 'package:rhythm_box/services/server/sourced_track.dart';
import 'package:rhythm_box/services/sourced_track/sources/kugou.dart';
import 'package:rhythm_box/services/sourced_track/sources/netease.dart'; import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
@ -34,6 +37,13 @@ class ServerPlaybackRoutesProvider {
); );
final realUrl = resp.body['data'][0]['url']; final realUrl = resp.body['data'][0]['url'];
url = realUrl; url = realUrl;
} else if (sourcedTrack is KugouSourcedTrack) {
// Special processing for kugou to get real assets url
final resp = await GetConnect(timeout: const Duration(seconds: 30))
.get(sourcedTrack.url);
final realUrl =
KugouSourcedTrack.unescapeUrl(jsonDecode(resp.body)['url'][0]);
url = realUrl;
} }
final res = await Dio().get( final res = await Dio().get(

View File

@ -1,7 +1,9 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:rhythm_box/providers/error_notifier.dart';
import 'package:rhythm_box/providers/user_preferences.dart'; import 'package:rhythm_box/providers/user_preferences.dart';
import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/database/database.dart';
import 'package:rhythm_box/services/sourced_track/sources/kugou.dart';
import 'package:rhythm_box/services/sourced_track/sources/netease.dart'; import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
import 'package:rhythm_box/services/utils.dart'; import 'package:rhythm_box/services/utils.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -103,16 +105,23 @@ abstract class SourcedTrack extends Track {
return switch (audioSource) { return switch (audioSource) {
AudioSource.netease => AudioSource.netease =>
await NeteaseSourcedTrack.fetchFromTrack(track: track), await NeteaseSourcedTrack.fetchFromTrack(track: track),
AudioSource.kugou =>
await KugouSourcedTrack.fetchFromTrack(track: track),
AudioSource.piped => AudioSource.piped =>
await PipedSourcedTrack.fetchFromTrack(track: track), await PipedSourcedTrack.fetchFromTrack(track: track),
_ => await YoutubeSourcedTrack.fetchFromTrack(track: track), _ => await YoutubeSourcedTrack.fetchFromTrack(track: track),
}; };
} on TrackNotFoundError catch (_) { } on TrackNotFoundError catch (err) {
Get.find<ErrorNotifier>().showError(
'${err.toString()} via ${preferences.audioSource.label}, querying in fallback sources...',
);
return switch (preferences.audioSource) { return switch (preferences.audioSource) {
AudioSource.piped || AudioSource.piped ||
AudioSource.youtube => AudioSource.youtube =>
await NeteaseSourcedTrack.fetchFromTrack(track: track), await NeteaseSourcedTrack.fetchFromTrack(track: track),
AudioSource.netease => AudioSource.netease =>
await KugouSourcedTrack.fetchFromTrack(track: track),
AudioSource.kugou =>
await YoutubeSourcedTrack.fetchFromTrack(track: track), await YoutubeSourcedTrack.fetchFromTrack(track: track),
}; };
} on HttpClientClosedException catch (_) { } on HttpClientClosedException catch (_) {

View File

@ -0,0 +1,229 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:crypto/crypto.dart';
import 'package:get/get.dart' hide Value;
import 'package:rhythm_box/providers/database.dart';
import 'package:rhythm_box/services/database/database.dart';
import 'package:spotify/spotify.dart';
import 'package:rhythm_box/services/sourced_track/enums.dart';
import 'package:rhythm_box/services/sourced_track/exceptions.dart';
import 'package:rhythm_box/services/sourced_track/models/source_info.dart';
import 'package:rhythm_box/services/sourced_track/models/source_map.dart';
import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
class KugouSourceInfo extends SourceInfo {
KugouSourceInfo({
required super.id,
required super.title,
required super.artist,
required super.thumbnail,
required super.pageUrl,
required super.duration,
required super.artistUrl,
required super.album,
});
}
class KugouSourcedTrack extends SourcedTrack {
KugouSourcedTrack({
required super.source,
required super.siblings,
required super.sourceInfo,
required super.track,
});
static String unescapeUrl(String src) {
return src.replaceAll('\\/', '/');
}
static String getBaseUrl() {
return 'http://mobilecdn.kugou.com';
}
static GetConnect getClient() {
final client = GetConnect(
withCredentials: true,
timeout: const Duration(seconds: 30),
);
client.baseUrl = getBaseUrl();
return client;
}
static Future<KugouSourcedTrack> fetchFromTrack({
required Track track,
}) async {
final DatabaseProvider db = Get.find();
final cachedSource = await (db.database.select(db.database.sourceMatchTable)
..where((s) => s.trackId.equals(track.id!))
..limit(1)
..orderBy([
(s) =>
OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc),
]))
.get()
.then((s) => s.firstOrNull);
if (cachedSource == null || cachedSource.sourceType != SourceType.kugou) {
final siblings = await fetchSiblings(track: track);
if (siblings.isEmpty) {
throw TrackNotFoundError(track);
}
await db.database.into(db.database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: track.id!,
sourceId: siblings.first.info.id,
sourceType: const Value(SourceType.kugou),
),
);
return KugouSourcedTrack(
siblings: siblings.map((s) => s.info).skip(1).toList(),
source: siblings.first.source as SourceMap,
sourceInfo: siblings.first.info,
track: track,
);
}
return KugouSourcedTrack(
siblings: [],
source: toSourceMap(cachedSource),
sourceInfo: KugouSourceInfo(
id: cachedSource.sourceId,
artist: 'unknown',
artistUrl: '#',
pageUrl: '#',
thumbnail: '#',
title: 'unknown',
duration: Duration.zero,
album: 'unknown',
),
track: track,
);
}
static SourceMap toSourceMap(dynamic manifest) {
const baseUrl = 'http://trackercdn.kugou.com/i/v2';
final hash = manifest is SourceMatchTableData
? manifest.sourceId
: manifest?['hash'];
final key = md5.convert(utf8.encode('${hash}kgcloudv2')).toString();
final url =
'$baseUrl/song/url?key=$key&hash=$hash&appid=1005&pid=2&cmd=25&behavior=play';
return SourceMap(
m4a: SourceQualityMap(
high: url,
medium: url,
low: url,
),
weba: SourceQualityMap(
high: url,
medium: url,
low: url,
),
);
}
static Future<List<SiblingType>> fetchSiblings({
required Track track,
}) async {
final query = SourcedTrack.getSearchTerm(track);
final client = getClient();
final resp = await client.get(
'/api/v3/search/song?keyword=${Uri.encodeComponent(query)}&page=1&pagesize=10',
);
final results = jsonDecode(resp.body)['data']['info'];
// We can just trust kugou music for now
// If we need to check is the result correct, refer to this code
// https://github.com/KRTirtho/spotube/blob/9b024120601c0d381edeab4460cb22f87149d0f8/lib/services/sourced_track/sources/jiosaavn.dart#L129
final matchedResults = results.map(toSiblingType).toList();
return matchedResults.cast<SiblingType>();
}
@override
Future<KugouSourcedTrack> copyWithSibling() async {
if (siblings.isNotEmpty) {
return this;
}
final fetchedSiblings = await fetchSiblings(track: this);
return KugouSourcedTrack(
siblings: fetchedSiblings
.where((s) => s.info.id != sourceInfo.id)
.map((s) => s.info)
.toList(),
source: source,
sourceInfo: sourceInfo,
track: this,
);
}
@override
Future<KugouSourcedTrack?> swapWithSibling(SourceInfo sibling) async {
if (sibling.id == sourceInfo.id) {
return null;
}
// a sibling source that was fetched from the search results
final isStepSibling = siblings.none((s) => s.id == sibling.id);
final newSourceInfo = isStepSibling
? sibling
: siblings.firstWhere((s) => s.id == sibling.id);
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
..insert(0, sourceInfo);
final info = newSourceInfo as KugouSourceInfo;
final source = toSourceMap(newSourceInfo.id);
final db = Get.find<DatabaseProvider>();
await db.database.into(db.database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: id!,
sourceId: info.id,
sourceType: const Value(SourceType.kugou),
// Because we're sorting by createdAt in the query
// we have to update it to indicate priority
createdAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
return KugouSourcedTrack(
siblings: newSiblings,
source: source,
sourceInfo: info,
track: this,
);
}
static KugouSourceInfo toSourceInfo(dynamic item) {
return KugouSourceInfo(
id: item['hash'],
artist: item['singername'],
artistUrl: '#',
pageUrl: '#',
thumbnail: unescapeUrl(item['trans_param']['union_cover'])
.replaceFirst('/{size}', ''),
title: item['songname'],
duration: Duration(seconds: item['duration']),
album: item['album_name'],
);
}
static SiblingType toSiblingType(dynamic item) {
final SiblingType sibling = (
info: toSourceInfo(item),
source: toSourceMap(item),
);
return sibling;
}
}

View File

@ -1,6 +1,8 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:get/get.dart' hide Value; import 'package:get/get.dart' hide Value;
import 'package:get/get_connect/http/src/request/request.dart';
import 'package:rhythm_box/providers/auth.dart';
import 'package:rhythm_box/providers/database.dart'; import 'package:rhythm_box/providers/database.dart';
import 'package:rhythm_box/providers/user_preferences.dart'; import 'package:rhythm_box/providers/user_preferences.dart';
import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/database/database.dart';
@ -38,9 +40,25 @@ class NeteaseSourcedTrack extends SourcedTrack {
} }
static GetConnect getClient() { static GetConnect getClient() {
final client = GetConnect(); final client = GetConnect(
withCredentials: true,
timeout: const Duration(seconds: 30),
);
client.baseUrl = getBaseUrl(); client.baseUrl = getBaseUrl();
client.timeout = const Duration(seconds: 30); client.httpClient.addRequestModifier((Request request) async {
final AuthenticationProvider auth = Get.find();
if (auth.auth.value?.neteaseCookie != null) {
final cookie =
'MUSIC_U=${auth.auth.value!.getNeteaseCookie('MUSIC_U')}';
if (request.headers['Cookie'] == null) {
request.headers['Cookie'] = cookie;
} else {
request.headers['Cookie'] = request.headers['Cookie']! + cookie;
}
}
return request;
});
return client; return client;
} }
@ -69,12 +87,18 @@ class NeteaseSourcedTrack extends SourcedTrack {
.get() .get()
.then((s) => s.firstOrNull); .then((s) => s.firstOrNull);
if (cachedSource == null) { if (cachedSource == null || cachedSource.sourceType != SourceType.netease) {
final siblings = await fetchSiblings(track: track); final siblings = await fetchSiblings(track: track);
if (siblings.isEmpty) { if (siblings.isEmpty) {
throw TrackNotFoundError(track); throw TrackNotFoundError(track);
} }
final client = getClient();
final checkResp = await client.get(
'/check/music?id=${siblings.first.info.id}&realIP=${await lookupRealIp()}',
);
if (checkResp.body['success'] != true) throw TrackNotFoundError(track);
await db.database.into(db.database.sourceMatchTable).insert( await db.database.into(db.database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert( SourceMatchTableCompanion.insert(
trackId: track.id!, trackId: track.id!,
@ -93,7 +117,14 @@ class NeteaseSourcedTrack extends SourcedTrack {
final client = getClient(); final client = getClient();
final resp = await client.get('/song/detail?ids=${cachedSource.sourceId}'); final resp = await client.get('/song/detail?ids=${cachedSource.sourceId}');
final item = resp.body['songs'][0]; final item = (resp.body['songs'] as List<dynamic>).firstOrNull;
if (item == null) throw TrackNotFoundError(track);
final checkResp = await client.get(
'/check/music?id=${item['id']}&realIP=${await lookupRealIp()}',
);
if (checkResp.body['success'] != true) throw TrackNotFoundError(track);
return NeteaseSourcedTrack( return NeteaseSourcedTrack(
siblings: [], siblings: [],
@ -136,8 +167,10 @@ class NeteaseSourcedTrack extends SourcedTrack {
final query = SourcedTrack.getSearchTerm(track); final query = SourcedTrack.getSearchTerm(track);
final client = getClient(); final client = getClient();
final resp = final resp = await client.get(
await client.get('/search?keywords=${Uri.encodeComponent(query)}'); '/search?keywords=${Uri.encodeComponent(query)}&realIP=${NeteaseSourcedTrack.lookupRealIp()}',
);
if (resp.body?['code'] == 405) throw TrackNotFoundError(track);
final results = resp.body['result']['songs']; final results = resp.body['result']['songs'];
// We can just trust netease music for now // We can just trust netease music for now
@ -183,7 +216,7 @@ class NeteaseSourcedTrack extends SourcedTrack {
final client = getClient(); final client = getClient();
final resp = await client.get('/song/detail?ids=${newSourceInfo.id}'); final resp = await client.get('/song/detail?ids=${newSourceInfo.id}');
final item = resp.body['songs'][0]; final item = (resp.body['songs'] as List<dynamic>).first;
final (:info, :source) = toSiblingType(item); final (:info, :source) = toSiblingType(item);
@ -208,11 +241,10 @@ class NeteaseSourcedTrack extends SourcedTrack {
); );
} }
static SiblingType toSiblingType(dynamic item) { static NeteaseSourceInfo toSourceInfo(dynamic item) {
final firstArtist = item['ar'] != null ? item['ar'][0] : item['artists'][0]; final firstArtist = item['ar'] != null ? item['ar'][0] : item['artists'][0];
final SiblingType sibling = ( return NeteaseSourceInfo(
info: NeteaseSourceInfo(
id: item['id'].toString(), id: item['id'].toString(),
artist: item['ar'] != null artist: item['ar'] != null
? item['ar'].map((x) => x['name']).join(',') ? item['ar'].map((x) => x['name']).join(',')
@ -224,9 +256,14 @@ class NeteaseSourcedTrack extends SourcedTrack {
title: item['name'], title: item['name'],
duration: item['dt'] != null duration: item['dt'] != null
? Duration(milliseconds: item['dt']) ? Duration(milliseconds: item['dt'])
: Duration.zero, : Duration(milliseconds: item['duration']),
album: item['al']?['name'], album: item['al']?['name'],
), );
}
static SiblingType toSiblingType(dynamic item) {
final SiblingType sibling = (
info: toSourceInfo(item),
source: toSourceMap(item), source: toSourceMap(item),
); );

View File

@ -69,6 +69,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
sourceId: siblings.first.info.id, sourceId: siblings.first.info.id,
sourceType: const Value(SourceType.youtube), sourceType: const Value(SourceType.youtube),
), ),
mode: InsertMode.insertOrReplace,
); );
return YoutubeSourcedTrack( return YoutubeSourcedTrack(

View File

@ -5,8 +5,10 @@ import 'package:rhythm_box/platform.dart';
class AutoCacheImage extends StatelessWidget { class AutoCacheImage extends StatelessWidget {
final String url; final String url;
final double? width, height; final double? width, height;
final BoxFit? fit;
const AutoCacheImage(this.url, {super.key, this.width, this.height}); const AutoCacheImage(this.url,
{super.key, this.width, this.height, this.fit});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -15,12 +17,14 @@ class AutoCacheImage extends StatelessWidget {
imageUrl: url, imageUrl: url,
width: width, width: width,
height: height, height: height,
fit: fit,
); );
} }
return Image.network( return Image.network(
url, url,
width: width, width: width,
height: height, height: height,
fit: fit,
); );
} }

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:rhythm_box/providers/audio_player.dart'; import 'package:rhythm_box/providers/audio_player.dart';
import 'package:rhythm_box/providers/error_notifier.dart';
import 'package:rhythm_box/providers/user_preferences.dart'; import 'package:rhythm_box/providers/user_preferences.dart';
import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/database/database.dart';
import 'package:rhythm_box/services/duration.dart'; import 'package:rhythm_box/services/duration.dart';
@ -12,6 +13,7 @@ import 'package:rhythm_box/services/server/active_sourced_track.dart';
import 'package:rhythm_box/services/sourced_track/models/source_info.dart'; import 'package:rhythm_box/services/sourced_track/models/source_info.dart';
import 'package:rhythm_box/services/sourced_track/models/video_info.dart'; import 'package:rhythm_box/services/sourced_track/models/video_info.dart';
import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
import 'package:rhythm_box/services/sourced_track/sources/kugou.dart';
import 'package:rhythm_box/services/sourced_track/sources/netease.dart'; import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
import 'package:rhythm_box/services/sourced_track/sources/piped.dart'; import 'package:rhythm_box/services/sourced_track/sources/piped.dart';
import 'package:rhythm_box/services/sourced_track/sources/youtube.dart'; import 'package:rhythm_box/services/sourced_track/sources/youtube.dart';
@ -44,6 +46,7 @@ class _SiblingTracksState extends State<SiblingTracks> {
YoutubeSourceInfo: 'YouTube', YoutubeSourceInfo: 'YouTube',
PipedSourceInfo: 'Piped', PipedSourceInfo: 'Piped',
NeteaseSourceInfo: 'Netease', NeteaseSourceInfo: 'Netease',
KugouSourceInfo: 'Kugou',
}; };
List<StreamSubscription>? _subscriptions; List<StreamSubscription>? _subscriptions;
@ -89,13 +92,17 @@ class _SiblingTracksState extends State<SiblingTracks> {
final preferences = Get.find<UserPreferencesProvider>().state.value; final preferences = Get.find<UserPreferencesProvider>().state.value;
final searchTerm = _searchTermController.text.trim(); final searchTerm = _searchTermController.text.trim();
try {
if (preferences.audioSource == AudioSource.youtube || if (preferences.audioSource == AudioSource.youtube ||
preferences.audioSource == AudioSource.piped) { preferences.audioSource == AudioSource.piped) {
final resultsYt = await youtubeClient.search.search(searchTerm.trim()); final resultsYt = await youtubeClient.search.search(searchTerm.trim());
final searchResults = await Future.wait( final searchResults = await Future.wait(
resultsYt.map(YoutubeVideoInfo.fromVideo).mapIndexed((i, video) async { resultsYt
final siblingType = await YoutubeSourcedTrack.toSiblingType(i, video); .map(YoutubeVideoInfo.fromVideo)
.mapIndexed((i, video) async {
final siblingType =
await YoutubeSourcedTrack.toSiblingType(i, video);
return siblingType.info; return siblingType.info;
}), }),
); );
@ -111,11 +118,10 @@ class _SiblingTracksState extends State<SiblingTracks> {
); );
} else if (preferences.audioSource == AudioSource.netease) { } else if (preferences.audioSource == AudioSource.netease) {
final client = NeteaseSourcedTrack.getClient(); final client = NeteaseSourcedTrack.getClient();
final resp = await client final resp = await client.get(
.get('/search?keywords=${Uri.encodeComponent(searchTerm)}'); '/search?keywords=${Uri.encodeComponent(searchTerm)}&realIP=${NeteaseSourcedTrack.lookupRealIp()}');
final searchResults = resp.body['result']['songs'] final searchResults = resp.body['result']['songs']
.map(NeteaseSourcedTrack.toSiblingType) .map(NeteaseSourcedTrack.toSourceInfo)
.map((x) => x.info)
.toList(); .toList();
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo; final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
@ -129,9 +135,12 @@ class _SiblingTracksState extends State<SiblingTracks> {
growable: true, growable: true,
); );
} }
} catch (err) {
Get.find<ErrorNotifier>().showError(err.toString());
} finally {
setState(() => _isSearching = false); setState(() => _isSearching = false);
} }
}
@override @override
void initState() { void initState() {

View File

@ -1,13 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rhythm_box/services/duration.dart'; import 'package:rhythm_box/services/duration.dart';
import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
import 'package:rhythm_box/services/sourced_track/sources/kugou.dart';
import 'package:rhythm_box/services/sourced_track/sources/netease.dart'; import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
import 'package:rhythm_box/services/sourced_track/sources/piped.dart'; import 'package:rhythm_box/services/sourced_track/sources/piped.dart';
import 'package:rhythm_box/services/sourced_track/sources/youtube.dart'; import 'package:rhythm_box/services/sourced_track/sources/youtube.dart';
import 'package:spotify/spotify.dart';
class TrackSourceDetails extends StatelessWidget { class TrackSourceDetails extends StatelessWidget {
final Track track; final SourcedTrack track;
const TrackSourceDetails({super.key, required this.track}); const TrackSourceDetails({super.key, required this.track});
@ -15,6 +15,7 @@ class TrackSourceDetails extends StatelessWidget {
YoutubeSourceInfo: 'YouTube', YoutubeSourceInfo: 'YouTube',
PipedSourceInfo: 'Piped', PipedSourceInfo: 'Piped',
NeteaseSourceInfo: 'Netease', NeteaseSourceInfo: 'Netease',
KugouSourceInfo: 'Kugou',
}; };
@override @override
@ -25,14 +26,11 @@ class TrackSourceDetails extends StatelessWidget {
'Title': track.name!, 'Title': track.name!,
'Artist': track.artists?.map((x) => x.name).join(', '), 'Artist': track.artists?.map((x) => x.name).join(', '),
'Album': track.album!.name!, 'Album': track.album!.name!,
'Duration': (track is SourcedTrack 'Duration': track.sourceInfo.duration.toHumanReadableString(),
? (track as SourcedTrack).sourceInfo.duration
: track.duration!)
.toHumanReadableString(),
if (track.album!.releaseDate != null) if (track.album!.releaseDate != null)
'Released': track.album!.releaseDate, 'Released': track.album!.releaseDate,
'Popularity': track.popularity?.toString() ?? '0', 'Popularity': track.popularity?.toString() ?? '0',
'Provider': sourceInfoToLabelMap[track.runtimeType], 'Provider': sourceInfoToLabelMap[track.sourceInfo.runtimeType],
}; };
return Table( return Table(

View File

@ -28,6 +28,8 @@
<string>MainMenu</string> <string>MainMenu</string>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string>public.app-category.music</string> <string>public.app-category.music</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
</dict> </dict>

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+8 version: 1.0.0+12
environment: environment:
sdk: ^3.5.0 sdk: ^3.5.0