Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
70ea02962f | |||
59783c48f7 | |||
b099f63f61 |
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).
|
||||
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.
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user