✨ Kugou music source
This commit is contained in:
parent
222d50d80d
commit
6d2a027d9b
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ part of '../database.dart';
|
|||||||
enum SourceType {
|
enum SourceType {
|
||||||
youtube._('YouTube'),
|
youtube._('YouTube'),
|
||||||
youtubeMusic._('YouTube Music'),
|
youtubeMusic._('YouTube Music'),
|
||||||
netease._('Netease Music');
|
netease._('Netease Music'),
|
||||||
|
kugou._('Kugou Music');
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
|
@ -1,3 +1,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(
|
||||||
|
@ -3,6 +3,7 @@ import 'package:get/get.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';
|
||||||
@ -104,6 +105,8 @@ abstract class SourcedTrack extends Track {
|
|||||||
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),
|
||||||
@ -117,6 +120,8 @@ abstract class SourcedTrack extends Track {
|
|||||||
AudioSource.youtube =>
|
AudioSource.youtube =>
|
||||||
await NeteaseSourcedTrack.fetchFromTrack(track: track),
|
await NeteaseSourcedTrack.fetchFromTrack(track: track),
|
||||||
AudioSource.netease =>
|
AudioSource.netease =>
|
||||||
|
await KugouSourcedTrack.fetchFromTrack(track: track),
|
||||||
|
AudioSource.kugou =>
|
||||||
await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
||||||
};
|
};
|
||||||
} on HttpClientClosedException catch (_) {
|
} on HttpClientClosedException catch (_) {
|
||||||
|
229
lib/services/sourced_track/sources/kugou.dart
Normal file
229
lib/services/sourced_track/sources/kugou.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -171,7 +171,6 @@ class NeteaseSourcedTrack extends SourcedTrack {
|
|||||||
'/search?keywords=${Uri.encodeComponent(query)}&realIP=${NeteaseSourcedTrack.lookupRealIp()}',
|
'/search?keywords=${Uri.encodeComponent(query)}&realIP=${NeteaseSourcedTrack.lookupRealIp()}',
|
||||||
);
|
);
|
||||||
if (resp.body?['code'] == 405) throw TrackNotFoundError(track);
|
if (resp.body?['code'] == 405) throw TrackNotFoundError(track);
|
||||||
print(resp.body);
|
|
||||||
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
|
||||||
|
@ -13,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';
|
||||||
@ -45,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;
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user