Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
a6b40e81a7 | |||
1913a7e909 | |||
d860936010 | |||
873ad1cf8c | |||
e0c9edad78 | |||
70ea02962f | |||
59783c48f7 | |||
b099f63f61 | |||
b69bee7e59 | |||
3d23152802 | |||
90dc3f43a7 |
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,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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,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),
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -90,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() {
|
||||||
@ -118,6 +120,7 @@ class PreferencesTable extends Table {
|
|||||||
downloadMusicCodec: SourceCodecs.m4a,
|
downloadMusicCodec: SourceCodecs.m4a,
|
||||||
endlessPlayback: true,
|
endlessPlayback: true,
|
||||||
playerWakelock: true,
|
playerWakelock: true,
|
||||||
|
overrideCacheProvider: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
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';
|
||||||
@ -41,8 +42,17 @@ class ServerPlaybackRoutesProvider {
|
|||||||
// Special processing for kugou to get real assets url
|
// Special processing for kugou to get real assets url
|
||||||
final resp = await GetConnect(timeout: const Duration(seconds: 30))
|
final resp = await GetConnect(timeout: const Duration(seconds: 30))
|
||||||
.get(sourcedTrack.url);
|
.get(sourcedTrack.url);
|
||||||
final realUrl =
|
final urls = jsonDecode(resp.body)['url'];
|
||||||
KugouSourcedTrack.unescapeUrl(jsonDecode(resp.body)['url'][0]);
|
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;
|
url = realUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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/server/active_sourced_track.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/sources/kugou.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';
|
||||||
@ -79,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)
|
||||||
@ -97,9 +127,43 @@ 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) {
|
||||||
@ -115,14 +179,20 @@ abstract class SourcedTrack extends Track {
|
|||||||
Get.find<ErrorNotifier>().showError(
|
Get.find<ErrorNotifier>().showError(
|
||||||
'${err.toString()} via ${preferences.audioSource.label}, querying in fallback sources...',
|
'${err.toString()} via ${preferences.audioSource.label}, querying in fallback sources...',
|
||||||
);
|
);
|
||||||
return switch (preferences.audioSource) {
|
|
||||||
AudioSource.piped ||
|
if (fallbackTo != null) {
|
||||||
AudioSource.youtube =>
|
// Prevent infinite fallback
|
||||||
await NeteaseSourcedTrack.fetchFromTrack(track: track),
|
if (audioSource == AudioSource.youtube ||
|
||||||
|
audioSource == AudioSource.piped) rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (audioSource) {
|
||||||
AudioSource.netease =>
|
AudioSource.netease =>
|
||||||
await KugouSourcedTrack.fetchFromTrack(track: track),
|
await fetchFromTrack(track: track, fallbackTo: AudioSource.youtube),
|
||||||
AudioSource.kugou =>
|
AudioSource.kugou =>
|
||||||
await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
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);
|
||||||
|
@ -51,7 +51,7 @@ class KugouSourcedTrack extends SourcedTrack {
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<KugouSourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required Track track,
|
||||||
}) async {
|
}) async {
|
||||||
final DatabaseProvider db = Get.find();
|
final DatabaseProvider db = Get.find();
|
||||||
@ -77,6 +77,7 @@ class KugouSourcedTrack extends SourcedTrack {
|
|||||||
sourceId: siblings.first.info.id,
|
sourceId: siblings.first.info.id,
|
||||||
sourceType: const Value(SourceType.kugou),
|
sourceType: const Value(SourceType.kugou),
|
||||||
),
|
),
|
||||||
|
mode: InsertMode.insertOrReplace,
|
||||||
);
|
);
|
||||||
|
|
||||||
return KugouSourcedTrack(
|
return KugouSourcedTrack(
|
||||||
@ -85,6 +86,11 @@ class KugouSourcedTrack extends SourcedTrack {
|
|||||||
sourceInfo: siblings.first.info,
|
sourceInfo: siblings.first.info,
|
||||||
track: track,
|
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(
|
return KugouSourcedTrack(
|
||||||
@ -109,7 +115,9 @@ class KugouSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
final hash = manifest is SourceMatchTableData
|
final hash = manifest is SourceMatchTableData
|
||||||
? manifest.sourceId
|
? manifest.sourceId
|
||||||
: manifest?['hash'];
|
: manifest is KugouSourceInfo
|
||||||
|
? manifest.id
|
||||||
|
: manifest?['hash'];
|
||||||
final key = md5.convert(utf8.encode('${hash}kgcloudv2')).toString();
|
final key = md5.convert(utf8.encode('${hash}kgcloudv2')).toString();
|
||||||
final url =
|
final url =
|
||||||
'$baseUrl/song/url?key=$key&hash=$hash&appid=1005&pid=2&cmd=25&behavior=play';
|
'$baseUrl/song/url?key=$key&hash=$hash&appid=1005&pid=2&cmd=25&behavior=play';
|
||||||
@ -142,7 +150,8 @@ class KugouSourcedTrack extends SourcedTrack {
|
|||||||
// We can just trust kugou music for now
|
// We can just trust kugou music for now
|
||||||
// If we need to check is the result correct, refer to this code
|
// 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
|
// https://github.com/KRTirtho/spotube/blob/9b024120601c0d381edeab4460cb22f87149d0f8/lib/services/sourced_track/sources/jiosaavn.dart#L129
|
||||||
final matchedResults = results.map(toSiblingType).toList();
|
final matchedResults =
|
||||||
|
results.where((x) => x['pay_type'] == 0).map(toSiblingType).toList();
|
||||||
|
|
||||||
return matchedResults.cast<SiblingType>();
|
return matchedResults.cast<SiblingType>();
|
||||||
}
|
}
|
||||||
@ -166,7 +175,11 @@ class KugouSourcedTrack extends SourcedTrack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<KugouSourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
||||||
|
if (sibling is! KugouSourceInfo) {
|
||||||
|
return reRoutineSwapSiblings(sibling);
|
||||||
|
}
|
||||||
|
|
||||||
if (sibling.id == sourceInfo.id) {
|
if (sibling.id == sourceInfo.id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -181,7 +194,7 @@ class KugouSourcedTrack extends SourcedTrack {
|
|||||||
..insert(0, sourceInfo);
|
..insert(0, sourceInfo);
|
||||||
|
|
||||||
final info = newSourceInfo as KugouSourceInfo;
|
final info = newSourceInfo as KugouSourceInfo;
|
||||||
final source = toSourceMap(newSourceInfo.id);
|
final source = toSourceMap(newSourceInfo);
|
||||||
|
|
||||||
final db = Get.find<DatabaseProvider>();
|
final db = Get.find<DatabaseProvider>();
|
||||||
await db.database.into(db.database.sourceMatchTable).insert(
|
await db.database.into(db.database.sourceMatchTable).insert(
|
||||||
|
@ -73,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();
|
||||||
@ -105,6 +105,7 @@ class NeteaseSourcedTrack extends SourcedTrack {
|
|||||||
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(
|
||||||
@ -113,10 +114,16 @@ 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}');
|
||||||
|
if (resp.body?['songs'] == null) throw TrackNotFoundError(track);
|
||||||
final item = (resp.body['songs'] as List<dynamic>).firstOrNull;
|
final item = (resp.body['songs'] as List<dynamic>).firstOrNull;
|
||||||
|
|
||||||
if (item == null) throw TrackNotFoundError(track);
|
if (item == null) throw TrackNotFoundError(track);
|
||||||
@ -168,7 +175,7 @@ class NeteaseSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
final client = getClient();
|
final client = getClient();
|
||||||
final resp = await client.get(
|
final resp = await client.get(
|
||||||
'/search?keywords=${Uri.encodeComponent(query)}&realIP=${NeteaseSourcedTrack.lookupRealIp()}',
|
'/search?keywords=${Uri.encodeComponent(query)}&realIP=${await NeteaseSourcedTrack.lookupRealIp()}',
|
||||||
);
|
);
|
||||||
if (resp.body?['code'] == 405) throw TrackNotFoundError(track);
|
if (resp.body?['code'] == 405) throw TrackNotFoundError(track);
|
||||||
final results = resp.body['result']['songs'];
|
final results = resp.body['result']['songs'];
|
||||||
@ -200,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;
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
@ -78,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);
|
||||||
@ -269,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;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
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';
|
||||||
@ -119,11 +120,32 @@ class _SiblingTracksState extends State<SiblingTracks> {
|
|||||||
} else if (preferences.audioSource == AudioSource.netease) {
|
} else if (preferences.audioSource == AudioSource.netease) {
|
||||||
final client = NeteaseSourcedTrack.getClient();
|
final client = NeteaseSourcedTrack.getClient();
|
||||||
final resp = await client.get(
|
final resp = await client.get(
|
||||||
'/search?keywords=${Uri.encodeComponent(searchTerm)}&realIP=${NeteaseSourcedTrack.lookupRealIp()}');
|
'/search?keywords=${Uri.encodeComponent(searchTerm)}&realIP=${await NeteaseSourcedTrack.lookupRealIp()}');
|
||||||
final searchResults = resp.body['result']['songs']
|
final searchResults = resp.body['result']['songs']
|
||||||
.map(NeteaseSourcedTrack.toSourceInfo)
|
.map(NeteaseSourcedTrack.toSourceInfo)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
|
||||||
|
_siblings = List.from(
|
||||||
|
searchResults
|
||||||
|
..removeWhere((element) => element.id == activeSourceInfo.id)
|
||||||
|
..insert(
|
||||||
|
0,
|
||||||
|
activeSourceInfo,
|
||||||
|
),
|
||||||
|
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;
|
final activeSourceInfo = (_activeTrack! as SourcedTrack).sourceInfo;
|
||||||
_siblings = List.from(
|
_siblings = List.from(
|
||||||
searchResults
|
searchResults
|
||||||
|
@ -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+12
|
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