3 Commits

Author SHA1 Message Date
70ea02962f 📝 Update README.md 2024-09-08 01:27:44 +08:00
59783c48f7 🐛 Fix & optimize kugou audio source
🐛 Fix fallback source switch causing error
2024-09-08 01:20:46 +08:00
b099f63f61 🐛 Fix settings page issue 2024-09-07 19:57:26 +08:00
10 changed files with 119 additions and 18 deletions

View File

@ -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).
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
- [x] Playing music
- [x] Add netease music as source
- [ ] Add bilibili as source
- [ ] Add kuwo music as source
- [ ] Add kugo music as source
- [x] Add netease music as source
- [ ] Add bilibili as source
- [ ] Add kuwo music as source
- [x] Add kugou music as source
- [x] Optimize fallback strategy
- [x] Re-design user interface
- [x] Simplified UI and UX
- [x] Support for large screen device
- [x] Simplified UI and UX
- [x] Support for large screen device
## 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 all rights reversed by LittleSheep and Solsynth LLC.
This project is all rights reversed by LittleSheep and Solsynth LLC.

View File

@ -223,15 +223,40 @@ class _SettingsScreenState extends State<SettingsScreen> {
Obx(
() => CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
secondary: const Icon(Icons.all_inclusive),
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.endlessPlayback,
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(
() => SwitchListTile(

View File

@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart' hide Response;
import 'package:flutter/foundation.dart';
@ -41,8 +42,17 @@ class ServerPlaybackRoutesProvider {
// 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]);
final urls = jsonDecode(resp.body)['url'];
if (urls?.isEmpty ?? true) {
Get.find<ErrorNotifier>().showError(
'[PlaybackServer] Unable get audio source via Kugou, probably cause by paid needed resources.',
);
return Response(
HttpStatus.notFound,
body: 'Unable get audio source via Kugou',
);
}
final realUrl = KugouSourcedTrack.unescapeUrl(urls.first);
url = realUrl;
}

View File

@ -81,6 +81,16 @@ abstract class SourcedTrack extends Track {
};
}
static Type getTrackBySourceInfo(SourceInfo info) {
final sourceInfoTrackMap = {
YoutubeSourceInfo: YoutubeSourcedTrack,
PipedSourceInfo: PipedSourcedTrack,
NeteaseSourceInfo: NeteaseSourcedTrack,
KugouSourceInfo: KugouSourcedTrack,
};
return sourceInfoTrackMap[info.runtimeType]!;
}
static String getSearchTerm(Track track) {
final artists = (track.artists ?? [])
.map((ar) => ar.name)

View File

@ -109,7 +109,9 @@ class KugouSourcedTrack extends SourcedTrack {
final hash = manifest is SourceMatchTableData
? manifest.sourceId
: manifest?['hash'];
: manifest is KugouSourceInfo
? manifest.id
: manifest?['hash'];
final key = md5.convert(utf8.encode('${hash}kgcloudv2')).toString();
final url =
'$baseUrl/song/url?key=$key&hash=$hash&appid=1005&pid=2&cmd=25&behavior=play';
@ -142,7 +144,8 @@ class KugouSourcedTrack extends SourcedTrack {
// 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();
final matchedResults =
results.where((x) => x['pay_type'] == 0).map(toSiblingType).toList();
return matchedResults.cast<SiblingType>();
}
@ -166,7 +169,12 @@ class KugouSourcedTrack extends SourcedTrack {
}
@override
Future<KugouSourcedTrack?> swapWithSibling(SourceInfo sibling) async {
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
if (sibling is! KugouSourceInfo) {
return (SourcedTrack.getTrackBySourceInfo(sibling) as SourcedTrack)
.swapWithSibling(sibling);
}
if (sibling.id == sourceInfo.id) {
return null;
}
@ -181,7 +189,7 @@ class KugouSourcedTrack extends SourcedTrack {
..insert(0, sourceInfo);
final info = newSourceInfo as KugouSourceInfo;
final source = toSourceMap(newSourceInfo.id);
final source = toSourceMap(newSourceInfo);
final db = Get.find<DatabaseProvider>();
await db.database.into(db.database.sourceMatchTable).insert(

View File

@ -200,7 +200,12 @@ class NeteaseSourcedTrack extends SourcedTrack {
}
@override
Future<NeteaseSourcedTrack?> swapWithSibling(SourceInfo sibling) async {
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
if (sibling is! NeteaseSourceInfo) {
return (SourcedTrack.getTrackBySourceInfo(sibling) as SourcedTrack)
.swapWithSibling(sibling);
}
if (sibling.id == sourceInfo.id) {
return null;
}

View File

@ -255,6 +255,11 @@ class PipedSourcedTrack extends SourcedTrack {
@override
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
if (sibling is! PipedSourceInfo) {
return (SourcedTrack.getTrackBySourceInfo(sibling) as SourcedTrack)
.swapWithSibling(sibling);
}
if (sibling.id == sourceInfo.id) {
return null;
}

View File

@ -269,7 +269,12 @@ class YoutubeSourcedTrack extends SourcedTrack {
}
@override
Future<YoutubeSourcedTrack?> swapWithSibling(SourceInfo sibling) async {
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
if (sibling is! YoutubeSourceInfo) {
return (SourcedTrack.getTrackBySourceInfo(sibling) as SourcedTrack)
.swapWithSibling(sibling);
}
if (sibling.id == sourceInfo.id) {
return null;
}

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
@ -124,6 +125,27 @@ class _SiblingTracksState extends State<SiblingTracks> {
.map(NeteaseSourcedTrack.toSourceInfo)
.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;
_siblings = List.from(
searchResults

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
# 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.
version: 1.0.0+13
version: 1.0.0+15
environment:
sdk: ^3.5.0