8 Commits

Author SHA1 Message Date
b69bee7e59 🚀 Launch 1.0.0+13 2024-09-07 18:58:33 +08:00
3d23152802 💄 Auto dismiss error 2024-09-07 18:54:01 +08:00
90dc3f43a7 Allow player keep original cache provider
 Support chain fallback
2024-09-07 18:49:05 +08:00
3df93e47d2 🚀 Launch 1.2.1+12 2024-09-06 22:44:39 +08:00
6d2a027d9b Kugou music source 2024-09-06 22:19:26 +08:00
222d50d80d 🐛 Bug fixes of querying backend 2024-09-06 18:10:12 +08:00
499bca5b1c Better netease music check 2024-09-06 16:37:49 +08:00
252e4619f7 🐛 Fix player view layout issue 2024-09-06 16:22:39 +08:00
19 changed files with 671 additions and 236 deletions

View File

@@ -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>

View File

@@ -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;
});
} }
} }

View File

@@ -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)));
}
} }

View File

@@ -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,87 +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(
shrinkWrap: true, scrollDirection: Axis.horizontal,
children: [ shrinkWrap: true,
TextButton.icon( children: [
icon: const Icon(Icons.queue_music),
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(() {});
}
});
},
),
],
), ),
), ),
], ],

View File

@@ -220,6 +220,19 @@ 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.all_inclusive),
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.endlessPlayback,
onChanged: (value) =>
_preferences.setOverrideCacheProvider(value ?? false),
),
),
const Divider(thickness: 0.3, height: 1),
Obx( Obx(
() => SwitchListTile( () => SwitchListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24), contentPadding: const EdgeInsets.symmetric(horizontal: 24),

View File

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

View File

@@ -640,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,
@@ -665,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;
@@ -760,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;
} }
@@ -830,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'])!,
); );
} }
@@ -894,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,
@@ -918,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>{};
@@ -986,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;
} }
@@ -1015,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),
); );
} }
@@ -1058,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
@@ -1100,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),
}; };
} }
@@ -1127,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,
@@ -1153,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(
@@ -1216,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,
); );
} }
@@ -1245,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();
} }
@@ -1275,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) =>
@@ -1304,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> {
@@ -1332,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(),
@@ -1357,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(),
@@ -1383,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,
@@ -1409,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,
@@ -1439,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,
}); });
} }
@@ -1466,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,
@@ -1492,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,
); );
} }
@@ -1589,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;
} }
@@ -1618,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();
} }
@@ -4109,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({
@@ -4136,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<
@@ -4180,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,
@@ -4206,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(),
@@ -4232,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,
@@ -4258,6 +4313,7 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
downloadMusicCodec: downloadMusicCodec, downloadMusicCodec: downloadMusicCodec,
endlessPlayback: endlessPlayback, endlessPlayback: endlessPlayback,
playerWakelock: playerWakelock, playerWakelock: playerWakelock,
overrideCacheProvider: overrideCacheProvider,
), ),
)); ));
} }
@@ -4408,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
@@ -4532,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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
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/error_notifier.dart';
import 'package:rhythm_box/providers/user_preferences.dart'; import 'package:rhythm_box/providers/user_preferences.dart';
import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/database/database.dart';
import 'package:rhythm_box/services/sourced_track/sources/kugou.dart';
import 'package:rhythm_box/services/sourced_track/sources/netease.dart'; import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
import 'package:rhythm_box/services/utils.dart'; import 'package:rhythm_box/services/utils.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@@ -96,27 +99,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 (err) { } on TrackNotFoundError catch (err) {
Get.find<ErrorNotifier>() Get.find<ErrorNotifier>().showError(
.showError('${err.toString()} via ${preferences.audioSource.label}'); '${err.toString()} via ${preferences.audioSource.label}, querying in fallback sources...',
return switch (preferences.audioSource) { );
AudioSource.piped ||
AudioSource.youtube => if (fallbackTo != null) {
await NeteaseSourcedTrack.fetchFromTrack(track: track), // 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.kugou),
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);

View File

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

View File

@@ -93,6 +93,12 @@ class NeteaseSourcedTrack extends SourcedTrack {
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!,
@@ -111,8 +117,14 @@ class NeteaseSourcedTrack extends SourcedTrack {
final client = getClient(); final client = getClient();
final resp = await client.get('/song/detail?ids=${cachedSource.sourceId}'); final resp = await client.get('/song/detail?ids=${cachedSource.sourceId}');
print(resp.body); final item = (resp.body['songs'] as List<dynamic>).firstOrNull;
final item = resp.body['songs'][0];
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: [],
@@ -155,8 +167,10 @@ class NeteaseSourcedTrack extends SourcedTrack {
final query = SourcedTrack.getSearchTerm(track); final query = SourcedTrack.getSearchTerm(track);
final client = getClient(); final client = getClient();
final resp = final resp = await client.get(
await client.get('/search?keywords=${Uri.encodeComponent(query)}'); '/search?keywords=${Uri.encodeComponent(query)}&realIP=${NeteaseSourcedTrack.lookupRealIp()}',
);
if (resp.body?['code'] == 405) throw TrackNotFoundError(track);
final results = resp.body['result']['songs']; final results = resp.body['result']['songs'];
// We can just trust netease music for now // We can just trust netease music for now
@@ -202,7 +216,7 @@ class NeteaseSourcedTrack extends SourcedTrack {
final client = getClient(); final client = getClient();
final resp = await client.get('/song/detail?ids=${newSourceInfo.id}'); final resp = await client.get('/song/detail?ids=${newSourceInfo.id}');
final item = resp.body['songs'][0]; final item = (resp.body['songs'] as List<dynamic>).first;
final (:info, :source) = toSiblingType(item); final (:info, :source) = toSiblingType(item);
@@ -227,25 +241,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(milliseconds: item['duration']),
album: item['al']?['name'],
),
source: toSourceMap(item), source: toSourceMap(item),
); );

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:rhythm_box/providers/audio_player.dart'; import 'package:rhythm_box/providers/audio_player.dart';
import 'package:rhythm_box/providers/error_notifier.dart';
import 'package:rhythm_box/providers/user_preferences.dart'; import 'package:rhythm_box/providers/user_preferences.dart';
import 'package:rhythm_box/services/database/database.dart'; import 'package:rhythm_box/services/database/database.dart';
import 'package:rhythm_box/services/duration.dart'; import 'package:rhythm_box/services/duration.dart';
@@ -12,6 +13,7 @@ import 'package:rhythm_box/services/server/active_sourced_track.dart';
import 'package:rhythm_box/services/sourced_track/models/source_info.dart'; import 'package:rhythm_box/services/sourced_track/models/source_info.dart';
import 'package:rhythm_box/services/sourced_track/models/video_info.dart'; import 'package:rhythm_box/services/sourced_track/models/video_info.dart';
import 'package:rhythm_box/services/sourced_track/sourced_track.dart'; import 'package:rhythm_box/services/sourced_track/sourced_track.dart';
import 'package:rhythm_box/services/sourced_track/sources/kugou.dart';
import 'package:rhythm_box/services/sourced_track/sources/netease.dart'; import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
import 'package:rhythm_box/services/sourced_track/sources/piped.dart'; import 'package:rhythm_box/services/sourced_track/sources/piped.dart';
import 'package:rhythm_box/services/sourced_track/sources/youtube.dart'; import 'package:rhythm_box/services/sourced_track/sources/youtube.dart';
@@ -44,6 +46,7 @@ class _SiblingTracksState extends State<SiblingTracks> {
YoutubeSourceInfo: 'YouTube', YoutubeSourceInfo: 'YouTube',
PipedSourceInfo: 'Piped', PipedSourceInfo: 'Piped',
NeteaseSourceInfo: 'Netease', NeteaseSourceInfo: 'Netease',
KugouSourceInfo: 'Kugou',
}; };
List<StreamSubscription>? _subscriptions; List<StreamSubscription>? _subscriptions;
@@ -89,48 +92,54 @@ 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=${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,
); );
}
} catch (err) {
Get.find<ErrorNotifier>().showError(err.toString());
} finally {
setState(() => _isSearching = false);
} }
setState(() => _isSearching = false);
} }
@override @override

View File

@@ -1,6 +1,7 @@
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';
@@ -14,6 +15,7 @@ class TrackSourceDetails extends StatelessWidget {
YoutubeSourceInfo: 'YouTube', YoutubeSourceInfo: 'YouTube',
PipedSourceInfo: 'Piped', PipedSourceInfo: 'Piped',
NeteaseSourceInfo: 'Netease', NeteaseSourceInfo: 'Netease',
KugouSourceInfo: 'Kugou',
}; };
@override @override

View File

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

View File

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