Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
a6b40e81a7 | |||
1913a7e909 | |||
d860936010 | |||
873ad1cf8c | |||
e0c9edad78 | |||
70ea02962f | |||
59783c48f7 | |||
b099f63f61 | |||
b69bee7e59 | |||
3d23152802 | |||
90dc3f43a7 | |||
3df93e47d2 | |||
6d2a027d9b | |||
222d50d80d | |||
499bca5b1c | |||
252e4619f7 | |||
463cb9870f | |||
2cee7ee958 | |||
e30e7a5c24 | |||
6509cd2511 | |||
6cdc025c40 | |||
de3ad4b21e |
README.md
ios/Runner
lib
macos/Runner
pubspec.yaml
25
README.md
25
README.md
@ -5,19 +5,30 @@ Yet another spotify third-party client. Support multi-platform because built wit
|
|||||||
This project is inspired by and taken supported by [spotube](https://spotube.krtirtho.dev).
|
This project is inspired by and taken supported by [spotube](https://spotube.krtirtho.dev).
|
||||||
Their original app is good enough. But I just want to redesign the user interface and make it ready add to more features and more backend support.
|
Their original app is good enough. But I just want to redesign the user interface and make it ready add to more features and more backend support.
|
||||||
|
|
||||||
|
## Highlight
|
||||||
|
|
||||||
|
Compare to original spotube. This project added more audio source e.g. netease cloud music, kugou and provide the ability to use in China Mainland.
|
||||||
|
|
||||||
|
At the same time, this project also focus on playing experience of VOCALOID songs.
|
||||||
|
We improve the search and rank algorithm to make the querying will less pick the cover version instead of original ones.
|
||||||
|
|
||||||
|
Due to the end service of jiosaavn in Asian region (maybe other regions also affected). We removed the jiosaavn audio source.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [x] Playing music
|
- [x] Playing music
|
||||||
- [x] Add netease music as source
|
- [x] Add netease music as source
|
||||||
- [ ] Add bilibili as source
|
- [ ] Add bilibili as source
|
||||||
- [ ] Add kuwo music as source
|
- [ ] Add kuwo music as source
|
||||||
- [ ] Add kugo music as source
|
- [x] Add kugou music as source
|
||||||
|
- [x] Optimize fallback strategy
|
||||||
- [x] Re-design user interface
|
- [x] Re-design user interface
|
||||||
- [x] Simplified UI and UX
|
- [x] Simplified UI and UX
|
||||||
- [x] Support for large screen device
|
- [x] Support for large screen device
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is open-sourced under APGLv3 license. The original spotube project is open-sourced under license BSD-Clause4 and copyright by Kingkor Roy Tirtho.
|
This project is open-sourced under APGLv3 license. The original spotube project is open-sourced under license BSD-Clause4 and copyright by Kingkor Roy Tirtho.
|
||||||
|
|
||||||
This project is all rights reversed by LittleSheep and Solsynth LLC.
|
This project is all rights reversed by LittleSheep and Solsynth LLC.
|
||||||
|
|
||||||
|
@ -1,59 +1,61 @@
|
|||||||
<?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>CADisableMinimumFrameDurationOnPhone</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<true/>
|
<false/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<true/>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>Groovy Box</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>Groovy Box</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>6.0</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>Groovy Box</string>
|
<string>6.0</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundleName</key>
|
||||||
<string>APPL</string>
|
<string>Groovy Box</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>????</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>????</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>public.app-category.music</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<true/>
|
<string>public.app-category.music</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<string>To provide information for RhythmBox to normalize the output audio</string>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<true/>
|
<string>To provide information for RhythmBox to normalize the output audio</string>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<array>
|
<true/>
|
||||||
<string>audio</string>
|
<key>UIBackgroundModes</key>
|
||||||
</array>
|
<array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<string>audio</string>
|
||||||
<string>LaunchScreen</string>
|
</array>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>Main</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<false/>
|
<string>Main</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<array>
|
<false/>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
</array>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<array>
|
</array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<array>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
</array>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</dict>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -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();
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -6,12 +7,16 @@ import 'package:get/get.dart';
|
|||||||
class ErrorNotifier extends GetxController {
|
class ErrorNotifier extends GetxController {
|
||||||
Rx<MaterialBanner?> showing = Rx(null);
|
Rx<MaterialBanner?> showing = Rx(null);
|
||||||
|
|
||||||
|
Timer? _autoDismissTimer;
|
||||||
|
|
||||||
void logError(String msg, {StackTrace? trace}) {
|
void logError(String msg, {StackTrace? trace}) {
|
||||||
log('$msg${trace != null ? '\nTrace:\n$trace' : ''}');
|
log('$msg${trace != null ? '\nTrace:\n$trace' : ''}');
|
||||||
showError(msg);
|
showError(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showError(String msg) {
|
void showError(String msg) {
|
||||||
|
_autoDismissTimer?.cancel();
|
||||||
|
|
||||||
showing.value = MaterialBanner(
|
showing.value = MaterialBanner(
|
||||||
dividerColor: Colors.transparent,
|
dividerColor: Colors.transparent,
|
||||||
leading: const Icon(Icons.error),
|
leading: const Icon(Icons.error),
|
||||||
@ -34,5 +39,9 @@ class ErrorNotifier extends GetxController {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_autoDismissTimer = Timer(const Duration(seconds: 3), () {
|
||||||
|
showing.value = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -178,4 +178,8 @@ class UserPreferencesProvider extends GetxController {
|
|||||||
setData(PreferencesTableCompanion(playerWakelock: Value(wakelock)));
|
setData(PreferencesTableCompanion(playerWakelock: Value(wakelock)));
|
||||||
WakelockPlus.toggle(enable: wakelock);
|
WakelockPlus.toggle(enable: wakelock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setOverrideCacheProvider(bool override) {
|
||||||
|
setData(PreferencesTableCompanion(overrideCacheProvider: Value(override)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
156
lib/screens/auth/login_netease.dart
Normal file
156
lib/screens/auth/login_netease.dart
Normal 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×tamp=${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×tamp=${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'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -82,34 +82,38 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
children: [
|
children: [
|
||||||
Obx(
|
Obx(
|
||||||
() => LimitedBox(
|
() => Center(
|
||||||
maxHeight: maxAlbumSize,
|
child: LimitedBox(
|
||||||
maxWidth: maxAlbumSize,
|
maxHeight: maxAlbumSize,
|
||||||
child: Hero(
|
maxWidth: maxAlbumSize,
|
||||||
tag: const Key('current-active-track-album-art'),
|
child: Hero(
|
||||||
child: AspectRatio(
|
tag: const Key('current-active-track-album-art'),
|
||||||
aspectRatio: 1,
|
child: AspectRatio(
|
||||||
child: ClipRRect(
|
aspectRatio: 1,
|
||||||
borderRadius:
|
child: ClipRRect(
|
||||||
const BorderRadius.all(Radius.circular(16)),
|
borderRadius: const BorderRadius.all(
|
||||||
child: _albumArt != null
|
Radius.circular(16),
|
||||||
? AutoCacheImage(
|
),
|
||||||
_albumArt!,
|
child: _albumArt != null
|
||||||
width: albumSize,
|
? AutoCacheImage(
|
||||||
height: albumSize,
|
_albumArt!,
|
||||||
)
|
width: albumSize,
|
||||||
: Container(
|
height: albumSize,
|
||||||
color: Theme.of(context)
|
fit: BoxFit.cover,
|
||||||
.colorScheme
|
)
|
||||||
.surfaceContainerHigh,
|
: Container(
|
||||||
width: 64,
|
color: Theme.of(context)
|
||||||
height: 64,
|
.colorScheme
|
||||||
child: const Center(
|
.surfaceContainerHigh,
|
||||||
child: Icon(Icons.image),
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(Icons.image),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
).marginSymmetric(horizontal: 24),
|
||||||
).marginSymmetric(horizontal: 24),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -309,86 +313,89 @@ class _PlayerScreenState extends State<PlayerScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Gap(20),
|
const Gap(20),
|
||||||
SizedBox(
|
Center(
|
||||||
height: 40,
|
child: SizedBox(
|
||||||
child: ListView(
|
height: 40,
|
||||||
scrollDirection: Axis.horizontal,
|
child: ListView(
|
||||||
children: [
|
scrollDirection: Axis.horizontal,
|
||||||
TextButton.icon(
|
shrinkWrap: true,
|
||||||
icon: const Icon(Icons.queue_music),
|
children: [
|
||||||
label: const Text(
|
|
||||||
'Queue',
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
isScrollControlled: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) =>
|
|
||||||
const PlayerQueuePopup(),
|
|
||||||
).then((_) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!isLargeScreen) const Gap(4),
|
|
||||||
if (!isLargeScreen)
|
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(Icons.lyrics),
|
icon: const Icon(Icons.queue_music),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
'Lyrics',
|
'Queue',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.fade,
|
overflow: TextOverflow.fade,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouter.of(context)
|
showModalBottomSheet(
|
||||||
.pushNamed('playerLyrics');
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
const PlayerQueuePopup(),
|
||||||
|
).then((_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Gap(4),
|
if (!isLargeScreen) const Gap(4),
|
||||||
TextButton.icon(
|
if (!isLargeScreen)
|
||||||
icon: const Icon(Icons.merge),
|
TextButton.icon(
|
||||||
label: const Text(
|
icon: const Icon(Icons.lyrics),
|
||||||
'Sources',
|
label: const Text(
|
||||||
maxLines: 1,
|
'Lyrics',
|
||||||
overflow: TextOverflow.fade,
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
GoRouter.of(context)
|
||||||
|
.pushNamed('playerLyrics');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(4),
|
||||||
|
TextButton.icon(
|
||||||
|
icon: const Icon(Icons.merge),
|
||||||
|
label: const Text(
|
||||||
|
'Sources',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
const SiblingTracksPopup(),
|
||||||
|
).then((_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
onPressed: () {
|
const Gap(4),
|
||||||
showModalBottomSheet(
|
TextButton.icon(
|
||||||
useRootNavigator: true,
|
label: const Text('Info'),
|
||||||
isScrollControlled: true,
|
icon: const Icon(Icons.info),
|
||||||
context: context,
|
onPressed: () {
|
||||||
builder: (context) =>
|
showModalBottomSheet(
|
||||||
const SiblingTracksPopup(),
|
useRootNavigator: true,
|
||||||
).then((_) {
|
context: context,
|
||||||
if (mounted) {
|
builder: (context) =>
|
||||||
setState(() {});
|
const SourceDetailsPopup(),
|
||||||
}
|
).then((_) {
|
||||||
});
|
if (mounted) {
|
||||||
},
|
setState(() {});
|
||||||
),
|
}
|
||||||
const Gap(4),
|
});
|
||||||
TextButton.icon(
|
},
|
||||||
label: const Text('Info'),
|
),
|
||||||
icon: const Icon(Icons.info),
|
],
|
||||||
onPressed: () {
|
),
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
builder: (context) =>
|
|
||||||
const SourceDetailsPopup(),
|
|
||||||
).then((_) {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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();
|
||||||
@ -150,6 +220,44 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(thickness: 0.3, height: 1),
|
const Divider(thickness: 0.3, height: 1),
|
||||||
|
Obx(
|
||||||
|
() => CheckboxListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
secondary: const Icon(Icons.update),
|
||||||
|
title: const Text('Override Cache Provider'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Decide whether use original cached source or query a new one from current audio provider'),
|
||||||
|
value: _preferences.state.value.overrideCacheProvider,
|
||||||
|
onChanged: (value) =>
|
||||||
|
_preferences.setOverrideCacheProvider(value ?? false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Obx(
|
||||||
|
() => Column(
|
||||||
|
children: [
|
||||||
|
const ListTile(
|
||||||
|
contentPadding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
leading: Icon(Icons.cloud),
|
||||||
|
title: Text('Netease Cloud Music API'),
|
||||||
|
subtitle: Text(
|
||||||
|
'Use your own endpoint to prevent IP throttling and more'),
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: _preferences.state.value.neteaseApiInstance,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Endpoint URL',
|
||||||
|
isDense: true,
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_preferences.setNeteaseApiInstance(value);
|
||||||
|
},
|
||||||
|
onTapOutside: (_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
).paddingOnly(left: 24, right: 24, bottom: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(thickness: 0.3, height: 1),
|
||||||
Obx(
|
Obx(
|
||||||
() => SwitchListTile(
|
() => SwitchListTile(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
@ -65,7 +65,10 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
},
|
},
|
||||||
onUpgrade: (Migrator m, int from, int to) async {
|
onUpgrade: (Migrator m, int from, int to) async {
|
||||||
if (from < 2) {
|
if (from < 2) {
|
||||||
await m.addColumn(preferencesTable, preferencesTable.playerWakelock);
|
await m.addColumn(
|
||||||
|
preferencesTable,
|
||||||
|
preferencesTable.overrideCacheProvider,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -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>(
|
||||||
type: DriftSqlType.dateTime, requiredDuringInsert: true);
|
'spotify_access_token', aliasedName, false,
|
||||||
|
type: DriftSqlType.string, requiredDuringInsert: true)
|
||||||
|
.withConverter<DecryptedText>(
|
||||||
|
$AuthenticationTableTable.$converterspotifyAccessToken);
|
||||||
|
static const VerificationMeta _spotifyExpirationMeta =
|
||||||
|
const VerificationMeta('spotifyExpiration');
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [id, cookie, accessToken, expiration];
|
late final GeneratedColumn<DateTime> spotifyExpiration =
|
||||||
|
GeneratedColumn<DateTime>('spotify_expiration', aliasedName, false,
|
||||||
|
type: DriftSqlType.dateTime, requiredDuringInsert: true);
|
||||||
|
static const VerificationMeta _neteaseCookieMeta =
|
||||||
|
const VerificationMeta('neteaseCookie');
|
||||||
|
@override
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
@ -523,6 +640,16 @@ class $PreferencesTableTable extends PreferencesTable
|
|||||||
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
'CHECK ("player_wakelock" IN (0, 1))'),
|
'CHECK ("player_wakelock" IN (0, 1))'),
|
||||||
defaultValue: const Constant(true));
|
defaultValue: const Constant(true));
|
||||||
|
static const VerificationMeta _overrideCacheProviderMeta =
|
||||||
|
const VerificationMeta('overrideCacheProvider');
|
||||||
|
@override
|
||||||
|
late final GeneratedColumn<bool> overrideCacheProvider =
|
||||||
|
GeneratedColumn<bool>('override_cache_provider', aliasedName, false,
|
||||||
|
type: DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints: GeneratedColumn.constraintIsAlways(
|
||||||
|
'CHECK ("override_cache_provider" IN (0, 1))'),
|
||||||
|
defaultValue: const Constant(true));
|
||||||
@override
|
@override
|
||||||
List<GeneratedColumn> get $columns => [
|
List<GeneratedColumn> get $columns => [
|
||||||
id,
|
id,
|
||||||
@ -548,7 +675,8 @@ class $PreferencesTableTable extends PreferencesTable
|
|||||||
streamMusicCodec,
|
streamMusicCodec,
|
||||||
downloadMusicCodec,
|
downloadMusicCodec,
|
||||||
endlessPlayback,
|
endlessPlayback,
|
||||||
playerWakelock
|
playerWakelock,
|
||||||
|
overrideCacheProvider
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
@ -643,6 +771,12 @@ class $PreferencesTableTable extends PreferencesTable
|
|||||||
playerWakelock.isAcceptableOrUnknown(
|
playerWakelock.isAcceptableOrUnknown(
|
||||||
data['player_wakelock']!, _playerWakelockMeta));
|
data['player_wakelock']!, _playerWakelockMeta));
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('override_cache_provider')) {
|
||||||
|
context.handle(
|
||||||
|
_overrideCacheProviderMeta,
|
||||||
|
overrideCacheProvider.isAcceptableOrUnknown(
|
||||||
|
data['override_cache_provider']!, _overrideCacheProviderMeta));
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -713,6 +847,9 @@ class $PreferencesTableTable extends PreferencesTable
|
|||||||
.read(DriftSqlType.bool, data['${effectivePrefix}endless_playback'])!,
|
.read(DriftSqlType.bool, data['${effectivePrefix}endless_playback'])!,
|
||||||
playerWakelock: attachedDatabase.typeMapping
|
playerWakelock: attachedDatabase.typeMapping
|
||||||
.read(DriftSqlType.bool, data['${effectivePrefix}player_wakelock'])!,
|
.read(DriftSqlType.bool, data['${effectivePrefix}player_wakelock'])!,
|
||||||
|
overrideCacheProvider: attachedDatabase.typeMapping.read(
|
||||||
|
DriftSqlType.bool,
|
||||||
|
data['${effectivePrefix}override_cache_provider'])!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -777,6 +914,7 @@ class PreferencesTableData extends DataClass
|
|||||||
final SourceCodecs downloadMusicCodec;
|
final SourceCodecs downloadMusicCodec;
|
||||||
final bool endlessPlayback;
|
final bool endlessPlayback;
|
||||||
final bool playerWakelock;
|
final bool playerWakelock;
|
||||||
|
final bool overrideCacheProvider;
|
||||||
const PreferencesTableData(
|
const PreferencesTableData(
|
||||||
{required this.id,
|
{required this.id,
|
||||||
required this.audioQuality,
|
required this.audioQuality,
|
||||||
@ -801,7 +939,8 @@ class PreferencesTableData extends DataClass
|
|||||||
required this.streamMusicCodec,
|
required this.streamMusicCodec,
|
||||||
required this.downloadMusicCodec,
|
required this.downloadMusicCodec,
|
||||||
required this.endlessPlayback,
|
required this.endlessPlayback,
|
||||||
required this.playerWakelock});
|
required this.playerWakelock,
|
||||||
|
required this.overrideCacheProvider});
|
||||||
@override
|
@override
|
||||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||||
final map = <String, Expression>{};
|
final map = <String, Expression>{};
|
||||||
@ -869,6 +1008,7 @@ class PreferencesTableData extends DataClass
|
|||||||
}
|
}
|
||||||
map['endless_playback'] = Variable<bool>(endlessPlayback);
|
map['endless_playback'] = Variable<bool>(endlessPlayback);
|
||||||
map['player_wakelock'] = Variable<bool>(playerWakelock);
|
map['player_wakelock'] = Variable<bool>(playerWakelock);
|
||||||
|
map['override_cache_provider'] = Variable<bool>(overrideCacheProvider);
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -898,6 +1038,7 @@ class PreferencesTableData extends DataClass
|
|||||||
downloadMusicCodec: Value(downloadMusicCodec),
|
downloadMusicCodec: Value(downloadMusicCodec),
|
||||||
endlessPlayback: Value(endlessPlayback),
|
endlessPlayback: Value(endlessPlayback),
|
||||||
playerWakelock: Value(playerWakelock),
|
playerWakelock: Value(playerWakelock),
|
||||||
|
overrideCacheProvider: Value(overrideCacheProvider),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -941,6 +1082,8 @@ class PreferencesTableData extends DataClass
|
|||||||
.fromJson(serializer.fromJson<String>(json['downloadMusicCodec'])),
|
.fromJson(serializer.fromJson<String>(json['downloadMusicCodec'])),
|
||||||
endlessPlayback: serializer.fromJson<bool>(json['endlessPlayback']),
|
endlessPlayback: serializer.fromJson<bool>(json['endlessPlayback']),
|
||||||
playerWakelock: serializer.fromJson<bool>(json['playerWakelock']),
|
playerWakelock: serializer.fromJson<bool>(json['playerWakelock']),
|
||||||
|
overrideCacheProvider:
|
||||||
|
serializer.fromJson<bool>(json['overrideCacheProvider']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
@ -983,6 +1126,7 @@ class PreferencesTableData extends DataClass
|
|||||||
.toJson(downloadMusicCodec)),
|
.toJson(downloadMusicCodec)),
|
||||||
'endlessPlayback': serializer.toJson<bool>(endlessPlayback),
|
'endlessPlayback': serializer.toJson<bool>(endlessPlayback),
|
||||||
'playerWakelock': serializer.toJson<bool>(playerWakelock),
|
'playerWakelock': serializer.toJson<bool>(playerWakelock),
|
||||||
|
'overrideCacheProvider': serializer.toJson<bool>(overrideCacheProvider),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1010,7 +1154,8 @@ class PreferencesTableData extends DataClass
|
|||||||
SourceCodecs? streamMusicCodec,
|
SourceCodecs? streamMusicCodec,
|
||||||
SourceCodecs? downloadMusicCodec,
|
SourceCodecs? downloadMusicCodec,
|
||||||
bool? endlessPlayback,
|
bool? endlessPlayback,
|
||||||
bool? playerWakelock}) =>
|
bool? playerWakelock,
|
||||||
|
bool? overrideCacheProvider}) =>
|
||||||
PreferencesTableData(
|
PreferencesTableData(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
audioQuality: audioQuality ?? this.audioQuality,
|
audioQuality: audioQuality ?? this.audioQuality,
|
||||||
@ -1036,6 +1181,8 @@ class PreferencesTableData extends DataClass
|
|||||||
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
||||||
endlessPlayback: endlessPlayback ?? this.endlessPlayback,
|
endlessPlayback: endlessPlayback ?? this.endlessPlayback,
|
||||||
playerWakelock: playerWakelock ?? this.playerWakelock,
|
playerWakelock: playerWakelock ?? this.playerWakelock,
|
||||||
|
overrideCacheProvider:
|
||||||
|
overrideCacheProvider ?? this.overrideCacheProvider,
|
||||||
);
|
);
|
||||||
PreferencesTableData copyWithCompanion(PreferencesTableCompanion data) {
|
PreferencesTableData copyWithCompanion(PreferencesTableCompanion data) {
|
||||||
return PreferencesTableData(
|
return PreferencesTableData(
|
||||||
@ -1099,6 +1246,9 @@ class PreferencesTableData extends DataClass
|
|||||||
playerWakelock: data.playerWakelock.present
|
playerWakelock: data.playerWakelock.present
|
||||||
? data.playerWakelock.value
|
? data.playerWakelock.value
|
||||||
: this.playerWakelock,
|
: this.playerWakelock,
|
||||||
|
overrideCacheProvider: data.overrideCacheProvider.present
|
||||||
|
? data.overrideCacheProvider.value
|
||||||
|
: this.overrideCacheProvider,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1128,7 +1278,8 @@ class PreferencesTableData extends DataClass
|
|||||||
..write('streamMusicCodec: $streamMusicCodec, ')
|
..write('streamMusicCodec: $streamMusicCodec, ')
|
||||||
..write('downloadMusicCodec: $downloadMusicCodec, ')
|
..write('downloadMusicCodec: $downloadMusicCodec, ')
|
||||||
..write('endlessPlayback: $endlessPlayback, ')
|
..write('endlessPlayback: $endlessPlayback, ')
|
||||||
..write('playerWakelock: $playerWakelock')
|
..write('playerWakelock: $playerWakelock, ')
|
||||||
|
..write('overrideCacheProvider: $overrideCacheProvider')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
@ -1158,7 +1309,8 @@ class PreferencesTableData extends DataClass
|
|||||||
streamMusicCodec,
|
streamMusicCodec,
|
||||||
downloadMusicCodec,
|
downloadMusicCodec,
|
||||||
endlessPlayback,
|
endlessPlayback,
|
||||||
playerWakelock
|
playerWakelock,
|
||||||
|
overrideCacheProvider
|
||||||
]);
|
]);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@ -1187,7 +1339,8 @@ class PreferencesTableData extends DataClass
|
|||||||
other.streamMusicCodec == this.streamMusicCodec &&
|
other.streamMusicCodec == this.streamMusicCodec &&
|
||||||
other.downloadMusicCodec == this.downloadMusicCodec &&
|
other.downloadMusicCodec == this.downloadMusicCodec &&
|
||||||
other.endlessPlayback == this.endlessPlayback &&
|
other.endlessPlayback == this.endlessPlayback &&
|
||||||
other.playerWakelock == this.playerWakelock);
|
other.playerWakelock == this.playerWakelock &&
|
||||||
|
other.overrideCacheProvider == this.overrideCacheProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||||
@ -1215,6 +1368,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
final Value<SourceCodecs> downloadMusicCodec;
|
final Value<SourceCodecs> downloadMusicCodec;
|
||||||
final Value<bool> endlessPlayback;
|
final Value<bool> endlessPlayback;
|
||||||
final Value<bool> playerWakelock;
|
final Value<bool> playerWakelock;
|
||||||
|
final Value<bool> overrideCacheProvider;
|
||||||
const PreferencesTableCompanion({
|
const PreferencesTableCompanion({
|
||||||
this.id = const Value.absent(),
|
this.id = const Value.absent(),
|
||||||
this.audioQuality = const Value.absent(),
|
this.audioQuality = const Value.absent(),
|
||||||
@ -1240,6 +1394,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
this.downloadMusicCodec = const Value.absent(),
|
this.downloadMusicCodec = const Value.absent(),
|
||||||
this.endlessPlayback = const Value.absent(),
|
this.endlessPlayback = const Value.absent(),
|
||||||
this.playerWakelock = const Value.absent(),
|
this.playerWakelock = const Value.absent(),
|
||||||
|
this.overrideCacheProvider = const Value.absent(),
|
||||||
});
|
});
|
||||||
PreferencesTableCompanion.insert({
|
PreferencesTableCompanion.insert({
|
||||||
this.id = const Value.absent(),
|
this.id = const Value.absent(),
|
||||||
@ -1266,6 +1421,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
this.downloadMusicCodec = const Value.absent(),
|
this.downloadMusicCodec = const Value.absent(),
|
||||||
this.endlessPlayback = const Value.absent(),
|
this.endlessPlayback = const Value.absent(),
|
||||||
this.playerWakelock = const Value.absent(),
|
this.playerWakelock = const Value.absent(),
|
||||||
|
this.overrideCacheProvider = const Value.absent(),
|
||||||
});
|
});
|
||||||
static Insertable<PreferencesTableData> custom({
|
static Insertable<PreferencesTableData> custom({
|
||||||
Expression<int>? id,
|
Expression<int>? id,
|
||||||
@ -1292,6 +1448,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
Expression<String>? downloadMusicCodec,
|
Expression<String>? downloadMusicCodec,
|
||||||
Expression<bool>? endlessPlayback,
|
Expression<bool>? endlessPlayback,
|
||||||
Expression<bool>? playerWakelock,
|
Expression<bool>? playerWakelock,
|
||||||
|
Expression<bool>? overrideCacheProvider,
|
||||||
}) {
|
}) {
|
||||||
return RawValuesInsertable({
|
return RawValuesInsertable({
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
@ -1322,6 +1479,8 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
'download_music_codec': downloadMusicCodec,
|
'download_music_codec': downloadMusicCodec,
|
||||||
if (endlessPlayback != null) 'endless_playback': endlessPlayback,
|
if (endlessPlayback != null) 'endless_playback': endlessPlayback,
|
||||||
if (playerWakelock != null) 'player_wakelock': playerWakelock,
|
if (playerWakelock != null) 'player_wakelock': playerWakelock,
|
||||||
|
if (overrideCacheProvider != null)
|
||||||
|
'override_cache_provider': overrideCacheProvider,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1349,7 +1508,8 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
Value<SourceCodecs>? streamMusicCodec,
|
Value<SourceCodecs>? streamMusicCodec,
|
||||||
Value<SourceCodecs>? downloadMusicCodec,
|
Value<SourceCodecs>? downloadMusicCodec,
|
||||||
Value<bool>? endlessPlayback,
|
Value<bool>? endlessPlayback,
|
||||||
Value<bool>? playerWakelock}) {
|
Value<bool>? playerWakelock,
|
||||||
|
Value<bool>? overrideCacheProvider}) {
|
||||||
return PreferencesTableCompanion(
|
return PreferencesTableCompanion(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
audioQuality: audioQuality ?? this.audioQuality,
|
audioQuality: audioQuality ?? this.audioQuality,
|
||||||
@ -1375,6 +1535,8 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
||||||
endlessPlayback: endlessPlayback ?? this.endlessPlayback,
|
endlessPlayback: endlessPlayback ?? this.endlessPlayback,
|
||||||
playerWakelock: playerWakelock ?? this.playerWakelock,
|
playerWakelock: playerWakelock ?? this.playerWakelock,
|
||||||
|
overrideCacheProvider:
|
||||||
|
overrideCacheProvider ?? this.overrideCacheProvider,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1472,6 +1634,10 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
if (playerWakelock.present) {
|
if (playerWakelock.present) {
|
||||||
map['player_wakelock'] = Variable<bool>(playerWakelock.value);
|
map['player_wakelock'] = Variable<bool>(playerWakelock.value);
|
||||||
}
|
}
|
||||||
|
if (overrideCacheProvider.present) {
|
||||||
|
map['override_cache_provider'] =
|
||||||
|
Variable<bool>(overrideCacheProvider.value);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1501,7 +1667,8 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
..write('streamMusicCodec: $streamMusicCodec, ')
|
..write('streamMusicCodec: $streamMusicCodec, ')
|
||||||
..write('downloadMusicCodec: $downloadMusicCodec, ')
|
..write('downloadMusicCodec: $downloadMusicCodec, ')
|
||||||
..write('endlessPlayback: $endlessPlayback, ')
|
..write('endlessPlayback: $endlessPlayback, ')
|
||||||
..write('playerWakelock: $playerWakelock')
|
..write('playerWakelock: $playerWakelock, ')
|
||||||
|
..write('overrideCacheProvider: $overrideCacheProvider')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
@ -3824,16 +3991,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 +4026,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 +4068,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 +4107,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));
|
||||||
}
|
}
|
||||||
@ -3958,6 +4159,7 @@ typedef $$PreferencesTableTableCreateCompanionBuilder
|
|||||||
Value<SourceCodecs> downloadMusicCodec,
|
Value<SourceCodecs> downloadMusicCodec,
|
||||||
Value<bool> endlessPlayback,
|
Value<bool> endlessPlayback,
|
||||||
Value<bool> playerWakelock,
|
Value<bool> playerWakelock,
|
||||||
|
Value<bool> overrideCacheProvider,
|
||||||
});
|
});
|
||||||
typedef $$PreferencesTableTableUpdateCompanionBuilder
|
typedef $$PreferencesTableTableUpdateCompanionBuilder
|
||||||
= PreferencesTableCompanion Function({
|
= PreferencesTableCompanion Function({
|
||||||
@ -3985,6 +4187,7 @@ typedef $$PreferencesTableTableUpdateCompanionBuilder
|
|||||||
Value<SourceCodecs> downloadMusicCodec,
|
Value<SourceCodecs> downloadMusicCodec,
|
||||||
Value<bool> endlessPlayback,
|
Value<bool> endlessPlayback,
|
||||||
Value<bool> playerWakelock,
|
Value<bool> playerWakelock,
|
||||||
|
Value<bool> overrideCacheProvider,
|
||||||
});
|
});
|
||||||
|
|
||||||
class $$PreferencesTableTableTableManager extends RootTableManager<
|
class $$PreferencesTableTableTableManager extends RootTableManager<
|
||||||
@ -4029,6 +4232,7 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
|||||||
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
||||||
Value<bool> endlessPlayback = const Value.absent(),
|
Value<bool> endlessPlayback = const Value.absent(),
|
||||||
Value<bool> playerWakelock = const Value.absent(),
|
Value<bool> playerWakelock = const Value.absent(),
|
||||||
|
Value<bool> overrideCacheProvider = const Value.absent(),
|
||||||
}) =>
|
}) =>
|
||||||
PreferencesTableCompanion(
|
PreferencesTableCompanion(
|
||||||
id: id,
|
id: id,
|
||||||
@ -4055,6 +4259,7 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
|||||||
downloadMusicCodec: downloadMusicCodec,
|
downloadMusicCodec: downloadMusicCodec,
|
||||||
endlessPlayback: endlessPlayback,
|
endlessPlayback: endlessPlayback,
|
||||||
playerWakelock: playerWakelock,
|
playerWakelock: playerWakelock,
|
||||||
|
overrideCacheProvider: overrideCacheProvider,
|
||||||
),
|
),
|
||||||
createCompanionCallback: ({
|
createCompanionCallback: ({
|
||||||
Value<int> id = const Value.absent(),
|
Value<int> id = const Value.absent(),
|
||||||
@ -4081,6 +4286,7 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
|||||||
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
||||||
Value<bool> endlessPlayback = const Value.absent(),
|
Value<bool> endlessPlayback = const Value.absent(),
|
||||||
Value<bool> playerWakelock = const Value.absent(),
|
Value<bool> playerWakelock = const Value.absent(),
|
||||||
|
Value<bool> overrideCacheProvider = const Value.absent(),
|
||||||
}) =>
|
}) =>
|
||||||
PreferencesTableCompanion.insert(
|
PreferencesTableCompanion.insert(
|
||||||
id: id,
|
id: id,
|
||||||
@ -4107,6 +4313,7 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
|||||||
downloadMusicCodec: downloadMusicCodec,
|
downloadMusicCodec: downloadMusicCodec,
|
||||||
endlessPlayback: endlessPlayback,
|
endlessPlayback: endlessPlayback,
|
||||||
playerWakelock: playerWakelock,
|
playerWakelock: playerWakelock,
|
||||||
|
overrideCacheProvider: overrideCacheProvider,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -4257,6 +4464,11 @@ class $$PreferencesTableTableFilterComposer
|
|||||||
column: $state.table.playerWakelock,
|
column: $state.table.playerWakelock,
|
||||||
builder: (column, joinBuilders) =>
|
builder: (column, joinBuilders) =>
|
||||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||||
|
|
||||||
|
ColumnFilters<bool> get overrideCacheProvider => $state.composableBuilder(
|
||||||
|
column: $state.table.overrideCacheProvider,
|
||||||
|
builder: (column, joinBuilders) =>
|
||||||
|
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$PreferencesTableTableOrderingComposer
|
class $$PreferencesTableTableOrderingComposer
|
||||||
@ -4381,6 +4593,11 @@ class $$PreferencesTableTableOrderingComposer
|
|||||||
column: $state.table.playerWakelock,
|
column: $state.table.playerWakelock,
|
||||||
builder: (column, joinBuilders) =>
|
builder: (column, joinBuilders) =>
|
||||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||||
|
|
||||||
|
ColumnOrderings<bool> get overrideCacheProvider => $state.composableBuilder(
|
||||||
|
column: $state.table.overrideCacheProvider,
|
||||||
|
builder: (column, joinBuilders) =>
|
||||||
|
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef $$ScrobblerTableTableCreateCompanionBuilder = ScrobblerTableCompanion
|
typedef $$ScrobblerTableTableCreateCompanionBuilder = ScrobblerTableCompanion
|
||||||
|
@ -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()();
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
@ -89,6 +90,8 @@ class PreferencesTable extends Table {
|
|||||||
boolean().withDefault(const Constant(true))();
|
boolean().withDefault(const Constant(true))();
|
||||||
BoolColumn get playerWakelock =>
|
BoolColumn get playerWakelock =>
|
||||||
boolean().withDefault(const Constant(true))();
|
boolean().withDefault(const Constant(true))();
|
||||||
|
BoolColumn get overrideCacheProvider =>
|
||||||
|
boolean().withDefault(const Constant(true))();
|
||||||
|
|
||||||
// Default values as PreferencesTableData
|
// Default values as PreferencesTableData
|
||||||
static PreferencesTableData defaults() {
|
static PreferencesTableData defaults() {
|
||||||
@ -117,6 +120,7 @@ class PreferencesTable extends Table {
|
|||||||
downloadMusicCodec: SourceCodecs.m4a,
|
downloadMusicCodec: SourceCodecs.m4a,
|
||||||
endlessPlayback: true,
|
endlessPlayback: true,
|
||||||
playerWakelock: true,
|
playerWakelock: true,
|
||||||
|
overrideCacheProvider: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
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 +9,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 +38,22 @@ 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 urls = jsonDecode(resp.body)['url'];
|
||||||
|
if (urls?.isEmpty ?? true) {
|
||||||
|
Get.find<ErrorNotifier>().showError(
|
||||||
|
'[PlaybackServer] Unable get audio source via Kugou, probably cause by paid needed resources.',
|
||||||
|
);
|
||||||
|
return Response(
|
||||||
|
HttpStatus.notFound,
|
||||||
|
body: 'Unable get audio source via Kugou',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final realUrl = KugouSourcedTrack.unescapeUrl(urls.first);
|
||||||
|
url = realUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
final res = await Dio().get(
|
final res = await Dio().get(
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:rhythm_box/providers/database.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/server/active_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/utils.dart';
|
import 'package:rhythm_box/services/utils.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
@ -77,6 +82,33 @@ abstract class SourcedTrack extends Track {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<SourcedTrack?> reRoutineFetchFromTrack(
|
||||||
|
Track track, SourceMatchTableData cachedSource) {
|
||||||
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
|
final ytOrPiped = preferences.audioSource == AudioSource.piped
|
||||||
|
? PipedSourcedTrack.fetchFromTrack
|
||||||
|
: YoutubeSourcedTrack.fetchFromTrack;
|
||||||
|
final sourceInfoTrackMap = {
|
||||||
|
SourceType.youtube: ytOrPiped,
|
||||||
|
SourceType.youtubeMusic: ytOrPiped,
|
||||||
|
SourceType.netease: NeteaseSourcedTrack.fetchFromTrack,
|
||||||
|
SourceType.kugou: KugouSourcedTrack.fetchFromTrack,
|
||||||
|
};
|
||||||
|
return sourceInfoTrackMap[cachedSource.sourceType]!(track: track);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SourcedTrack?> reRoutineSwapSiblings(SourceInfo info) {
|
||||||
|
final sourceInfoTrackMap = {
|
||||||
|
YoutubeSourceInfo: YoutubeSourcedTrack.fetchFromTrack,
|
||||||
|
PipedSourceInfo: PipedSourcedTrack.fetchFromTrack,
|
||||||
|
NeteaseSourceInfo: NeteaseSourcedTrack.fetchFromTrack,
|
||||||
|
KugouSourceInfo: KugouSourcedTrack.fetchFromTrack,
|
||||||
|
};
|
||||||
|
return sourceInfoTrackMap[info.runtimeType]!(
|
||||||
|
track: Get.find<ActiveSourcedTrackProvider>().state.value!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static String getSearchTerm(Track track) {
|
static String getSearchTerm(Track track) {
|
||||||
final artists = (track.artists ?? [])
|
final artists = (track.artists ?? [])
|
||||||
.map((ar) => ar.name)
|
.map((ar) => ar.name)
|
||||||
@ -95,25 +127,72 @@ abstract class SourcedTrack extends Track {
|
|||||||
|
|
||||||
static Future<SourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required Track track,
|
||||||
|
AudioSource? fallbackTo,
|
||||||
}) async {
|
}) async {
|
||||||
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
final audioSource = preferences.audioSource;
|
var audioSource = preferences.audioSource;
|
||||||
|
|
||||||
|
if (!preferences.overrideCacheProvider && fallbackTo == null) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
final ytOrPiped = preferences.audioSource == AudioSource.youtube
|
||||||
|
? AudioSource.youtube
|
||||||
|
: AudioSource.piped;
|
||||||
|
final sourceTypeTrackMap = {
|
||||||
|
SourceType.youtube: ytOrPiped,
|
||||||
|
SourceType.youtubeMusic: ytOrPiped,
|
||||||
|
SourceType.netease: AudioSource.netease,
|
||||||
|
SourceType.kugou: AudioSource.kugou,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cachedSource != null) {
|
||||||
|
final cachedAudioSource = sourceTypeTrackMap[cachedSource.sourceType]!;
|
||||||
|
audioSource = cachedAudioSource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallbackTo != null) {
|
||||||
|
audioSource = fallbackTo;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
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) {
|
||||||
return switch (preferences.audioSource) {
|
Get.find<ErrorNotifier>().showError(
|
||||||
AudioSource.piped ||
|
'${err.toString()} via ${preferences.audioSource.label}, querying in fallback sources...',
|
||||||
AudioSource.youtube =>
|
);
|
||||||
await NeteaseSourcedTrack.fetchFromTrack(track: track),
|
|
||||||
|
if (fallbackTo != null) {
|
||||||
|
// Prevent infinite fallback
|
||||||
|
if (audioSource == AudioSource.youtube ||
|
||||||
|
audioSource == AudioSource.piped) rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (audioSource) {
|
||||||
AudioSource.netease =>
|
AudioSource.netease =>
|
||||||
await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
await fetchFromTrack(track: track, fallbackTo: AudioSource.youtube),
|
||||||
|
AudioSource.kugou =>
|
||||||
|
await fetchFromTrack(track: track, fallbackTo: AudioSource.youtube),
|
||||||
|
_ =>
|
||||||
|
await fetchFromTrack(track: track, fallbackTo: AudioSource.netease),
|
||||||
};
|
};
|
||||||
} on HttpClientClosedException catch (_) {
|
} on HttpClientClosedException catch (_) {
|
||||||
return await PipedSourcedTrack.fetchFromTrack(track: track);
|
return await PipedSourcedTrack.fetchFromTrack(track: track);
|
||||||
|
242
lib/services/sourced_track/sources/kugou.dart
Normal file
242
lib/services/sourced_track/sources/kugou.dart
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
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<SourcedTrack> 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),
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
|
);
|
||||||
|
|
||||||
|
return KugouSourcedTrack(
|
||||||
|
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
||||||
|
source: siblings.first.source as SourceMap,
|
||||||
|
sourceInfo: siblings.first.info,
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
} else if (cachedSource.sourceType != SourceType.kugou) {
|
||||||
|
final out =
|
||||||
|
await SourcedTrack.reRoutineFetchFromTrack(track, cachedSource);
|
||||||
|
if (out == null) throw TrackNotFoundError(track);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 is KugouSourceInfo
|
||||||
|
? manifest.id
|
||||||
|
: 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.where((x) => x['pay_type'] == 0).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<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
||||||
|
if (sibling is! KugouSourceInfo) {
|
||||||
|
return reRoutineSwapSiblings(sibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +73,7 @@ class NeteaseSourcedTrack extends SourcedTrack {
|
|||||||
return _lookedUpRealIp!;
|
return _lookedUpRealIp!;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<NeteaseSourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required Track track,
|
||||||
}) async {
|
}) async {
|
||||||
final DatabaseProvider db = Get.find();
|
final DatabaseProvider db = Get.find();
|
||||||
@ -69,18 +87,25 @@ 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!,
|
||||||
sourceId: siblings.first.info.id,
|
sourceId: siblings.first.info.id,
|
||||||
sourceType: const Value(SourceType.netease),
|
sourceType: const Value(SourceType.netease),
|
||||||
),
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
);
|
);
|
||||||
|
|
||||||
return NeteaseSourcedTrack(
|
return NeteaseSourcedTrack(
|
||||||
@ -89,11 +114,24 @@ class NeteaseSourcedTrack extends SourcedTrack {
|
|||||||
sourceInfo: siblings.first.info,
|
sourceInfo: siblings.first.info,
|
||||||
track: track,
|
track: track,
|
||||||
);
|
);
|
||||||
|
} else if (cachedSource.sourceType != SourceType.netease) {
|
||||||
|
final out =
|
||||||
|
await SourcedTrack.reRoutineFetchFromTrack(track, cachedSource);
|
||||||
|
if (out == null) throw TrackNotFoundError(track);
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
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];
|
if (resp.body?['songs'] == null) throw TrackNotFoundError(track);
|
||||||
|
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 +174,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=${await 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
|
||||||
@ -167,7 +207,11 @@ class NeteaseSourcedTrack extends SourcedTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<NeteaseSourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
||||||
|
if (sibling is! NeteaseSourceInfo) {
|
||||||
|
return reRoutineSwapSiblings(sibling);
|
||||||
|
}
|
||||||
|
|
||||||
if (sibling.id == sourceInfo.id) {
|
if (sibling.id == sourceInfo.id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -183,7 +227,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,25 +252,29 @@ 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];
|
||||||
|
|
||||||
|
return NeteaseSourceInfo(
|
||||||
|
id: item['id'].toString(),
|
||||||
|
artist: item['ar'] != null
|
||||||
|
? item['ar'].map((x) => x['name']).join(',')
|
||||||
|
: item['artists'].map((x) => x['name']).toString(),
|
||||||
|
artistUrl: 'https://music.163.com/#/artist?id=${firstArtist['id']}',
|
||||||
|
pageUrl: 'https://music.163.com/#/song?id=${item['id']}',
|
||||||
|
thumbnail: item['al']?['picUrl'] ??
|
||||||
|
'https://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg',
|
||||||
|
title: item['name'],
|
||||||
|
duration: item['dt'] != null
|
||||||
|
? Duration(milliseconds: item['dt'])
|
||||||
|
: Duration(milliseconds: item['duration']),
|
||||||
|
album: item['al']?['name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SiblingType toSiblingType(dynamic item) {
|
||||||
final SiblingType sibling = (
|
final SiblingType sibling = (
|
||||||
info: NeteaseSourceInfo(
|
info: toSourceInfo(item),
|
||||||
id: item['id'].toString(),
|
|
||||||
artist: item['ar'] != null
|
|
||||||
? item['ar'].map((x) => x['name']).join(',')
|
|
||||||
: item['artists'].map((x) => x['name']).toString(),
|
|
||||||
artistUrl: 'https://music.163.com/#/artist?id=${firstArtist['id']}',
|
|
||||||
pageUrl: 'https://music.163.com/#/song?id=${item['id']}',
|
|
||||||
thumbnail: item['al']?['picUrl'] ??
|
|
||||||
'https://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg',
|
|
||||||
title: item['name'],
|
|
||||||
duration: item['dt'] != null
|
|
||||||
? Duration(milliseconds: item['dt'])
|
|
||||||
: Duration.zero,
|
|
||||||
album: item['al']?['name'],
|
|
||||||
),
|
|
||||||
source: toSourceMap(item),
|
source: toSourceMap(item),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -57,6 +57,14 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
|
|
||||||
|
if (cachedSource?.sourceType != SourceType.youtube &&
|
||||||
|
cachedSource?.sourceType != SourceType.youtubeMusic) {
|
||||||
|
final out =
|
||||||
|
await SourcedTrack.reRoutineFetchFromTrack(track, cachedSource!);
|
||||||
|
if (out == null) throw TrackNotFoundError(track);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
if (cachedSource == null) {
|
if (cachedSource == null) {
|
||||||
final siblings = await fetchSiblings(track: track);
|
final siblings = await fetchSiblings(track: track);
|
||||||
if (siblings.isEmpty) {
|
if (siblings.isEmpty) {
|
||||||
@ -73,6 +81,7 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
: SourceType.youtubeMusic,
|
: SourceType.youtubeMusic,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
);
|
);
|
||||||
|
|
||||||
return PipedSourcedTrack(
|
return PipedSourcedTrack(
|
||||||
@ -255,6 +264,10 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
||||||
|
if (sibling is! PipedSourceInfo) {
|
||||||
|
return reRoutineSwapSiblings(sibling);
|
||||||
|
}
|
||||||
|
|
||||||
if (sibling.id == sourceInfo.id) {
|
if (sibling.id == sourceInfo.id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
required super.track,
|
required super.track,
|
||||||
});
|
});
|
||||||
|
|
||||||
static Future<YoutubeSourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required Track track,
|
||||||
}) async {
|
}) async {
|
||||||
final DatabaseProvider db = Get.find();
|
final DatabaseProvider db = Get.find();
|
||||||
@ -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(
|
||||||
@ -77,6 +78,11 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
sourceInfo: siblings.first.info,
|
sourceInfo: siblings.first.info,
|
||||||
track: track,
|
track: track,
|
||||||
);
|
);
|
||||||
|
} else if (cachedSource.sourceType != SourceType.youtube) {
|
||||||
|
final out =
|
||||||
|
await SourcedTrack.reRoutineFetchFromTrack(track, cachedSource);
|
||||||
|
if (out == null) throw TrackNotFoundError(track);
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
final item = await youtubeClient.videos.get(cachedSource.sourceId);
|
final item = await youtubeClient.videos.get(cachedSource.sourceId);
|
||||||
@ -268,7 +274,11 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<YoutubeSourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
||||||
|
if (sibling is! YoutubeSourceInfo) {
|
||||||
|
return reRoutineSwapSiblings(sibling);
|
||||||
|
}
|
||||||
|
|
||||||
if (sibling.id == sourceInfo.id) {
|
if (sibling.id == sourceInfo.id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
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 +14,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 +47,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,48 +93,75 @@ 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();
|
||||||
|
|
||||||
if (preferences.audioSource == AudioSource.youtube ||
|
try {
|
||||||
preferences.audioSource == AudioSource.piped) {
|
if (preferences.audioSource == AudioSource.youtube ||
|
||||||
final resultsYt = await youtubeClient.search.search(searchTerm.trim());
|
preferences.audioSource == AudioSource.piped) {
|
||||||
|
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)
|
||||||
return siblingType.info;
|
.mapIndexed((i, video) async {
|
||||||
}),
|
final siblingType =
|
||||||
);
|
await YoutubeSourcedTrack.toSiblingType(i, video);
|
||||||
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
|
return siblingType.info;
|
||||||
_siblings = List.from(
|
}),
|
||||||
searchResults
|
);
|
||||||
..removeWhere((element) => element.id == activeSourceInfo.id)
|
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
|
||||||
..insert(
|
_siblings = List.from(
|
||||||
0,
|
searchResults
|
||||||
activeSourceInfo,
|
..removeWhere((element) => element.id == activeSourceInfo.id)
|
||||||
),
|
..insert(
|
||||||
growable: true,
|
0,
|
||||||
);
|
activeSourceInfo,
|
||||||
} else if (preferences.audioSource == AudioSource.netease) {
|
),
|
||||||
final client = NeteaseSourcedTrack.getClient();
|
growable: true,
|
||||||
final resp = await client
|
);
|
||||||
.get('/search?keywords=${Uri.encodeComponent(searchTerm)}');
|
} else if (preferences.audioSource == AudioSource.netease) {
|
||||||
final searchResults = resp.body['result']['songs']
|
final client = NeteaseSourcedTrack.getClient();
|
||||||
.map(NeteaseSourcedTrack.toSiblingType)
|
final resp = await client.get(
|
||||||
.map((x) => x.info)
|
'/search?keywords=${Uri.encodeComponent(searchTerm)}&realIP=${await NeteaseSourcedTrack.lookupRealIp()}');
|
||||||
.toList();
|
final searchResults = resp.body['result']['songs']
|
||||||
|
.map(NeteaseSourcedTrack.toSourceInfo)
|
||||||
|
.toList();
|
||||||
|
|
||||||
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
|
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
|
||||||
_siblings = List.from(
|
_siblings = List.from(
|
||||||
searchResults
|
searchResults
|
||||||
..removeWhere((element) => element.id == activeSourceInfo.id)
|
..removeWhere((element) => element.id == activeSourceInfo.id)
|
||||||
..insert(
|
..insert(
|
||||||
0,
|
0,
|
||||||
activeSourceInfo,
|
activeSourceInfo,
|
||||||
),
|
),
|
||||||
growable: true,
|
growable: true,
|
||||||
);
|
);
|
||||||
|
} else if (preferences.audioSource == AudioSource.kugou) {
|
||||||
|
final client = KugouSourcedTrack.getClient();
|
||||||
|
final resp = await client.get(
|
||||||
|
'/api/v3/search/song?keyword=${Uri.encodeComponent(searchTerm)}&page=1&pagesize=10',
|
||||||
|
);
|
||||||
|
final results = jsonDecode(resp.body)['data']['info'];
|
||||||
|
final searchResults = results
|
||||||
|
.where((x) => x['pay_type'] == 0)
|
||||||
|
.map(KugouSourcedTrack.toSourceInfo)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
|
||||||
|
_siblings = List.from(
|
||||||
|
searchResults
|
||||||
|
..removeWhere((element) => element.id == activeSourceInfo.id)
|
||||||
|
..insert(
|
||||||
|
0,
|
||||||
|
activeSourceInfo,
|
||||||
|
),
|
||||||
|
growable: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Get.find<ErrorNotifier>().showError(err.toString());
|
||||||
|
} finally {
|
||||||
|
setState(() => _isSearching = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _isSearching = false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -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(
|
||||||
|
@ -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>
|
||||||
|
@ -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+18
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.0
|
sdk: ^3.5.0
|
||||||
@ -180,4 +180,3 @@ flutter_native_splash:
|
|||||||
color: "#fef8f5"
|
color: "#fef8f5"
|
||||||
color_dark: "#18120d"
|
color_dark: "#18120d"
|
||||||
image: assets/icon-w-shadow.png
|
image: assets/icon-w-shadow.png
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user