✨ Kugou music source
This commit is contained in:
parent
222d50d80d
commit
6d2a027d9b
@ -14,7 +14,8 @@ enum CloseBehavior {
|
||||
enum AudioSource {
|
||||
youtube,
|
||||
piped,
|
||||
netease;
|
||||
netease,
|
||||
kugou;
|
||||
|
||||
String get label => name[0].toUpperCase() + name.substring(1);
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ part of '../database.dart';
|
||||
enum SourceType {
|
||||
youtube._('YouTube'),
|
||||
youtubeMusic._('YouTube Music'),
|
||||
netease._('Netease Music');
|
||||
netease._('Netease Music'),
|
||||
kugou._('Kugou Music');
|
||||
|
||||
final String label;
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:dio/dio.dart' hide Response;
|
||||
import 'package:flutter/foundation.dart';
|
||||
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/server/active_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:shelf/shelf.dart';
|
||||
|
||||
@ -34,6 +37,13 @@ class ServerPlaybackRoutesProvider {
|
||||
);
|
||||
final realUrl = resp.body['data'][0]['url'];
|
||||
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(
|
||||
|
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||
import 'package:rhythm_box/providers/error_notifier.dart';
|
||||
import 'package:rhythm_box/providers/user_preferences.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/utils.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
@ -104,6 +105,8 @@ abstract class SourcedTrack extends Track {
|
||||
return switch (audioSource) {
|
||||
AudioSource.netease =>
|
||||
await NeteaseSourcedTrack.fetchFromTrack(track: track),
|
||||
AudioSource.kugou =>
|
||||
await KugouSourcedTrack.fetchFromTrack(track: track),
|
||||
AudioSource.piped =>
|
||||
await PipedSourcedTrack.fetchFromTrack(track: track),
|
||||
_ => await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
||||
@ -117,6 +120,8 @@ abstract class SourcedTrack extends Track {
|
||||
AudioSource.youtube =>
|
||||
await NeteaseSourcedTrack.fetchFromTrack(track: track),
|
||||
AudioSource.netease =>
|
||||
await KugouSourcedTrack.fetchFromTrack(track: track),
|
||||
AudioSource.kugou =>
|
||||
await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
||||
};
|
||||
} 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()}',
|
||||
);
|
||||
if (resp.body?['code'] == 405) throw TrackNotFoundError(track);
|
||||
print(resp.body);
|
||||
final results = resp.body['result']['songs'];
|
||||
|
||||
// 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/video_info.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/piped.dart';
|
||||
import 'package:rhythm_box/services/sourced_track/sources/youtube.dart';
|
||||
@ -45,6 +46,7 @@ class _SiblingTracksState extends State<SiblingTracks> {
|
||||
YoutubeSourceInfo: 'YouTube',
|
||||
PipedSourceInfo: 'Piped',
|
||||
NeteaseSourceInfo: 'Netease',
|
||||
KugouSourceInfo: 'Kugou',
|
||||
};
|
||||
|
||||
List<StreamSubscription>? _subscriptions;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.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/sources/kugou.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/youtube.dart';
|
||||
@ -14,6 +15,7 @@ class TrackSourceDetails extends StatelessWidget {
|
||||
YoutubeSourceInfo: 'YouTube',
|
||||
PipedSourceInfo: 'Piped',
|
||||
NeteaseSourceInfo: 'Netease',
|
||||
KugouSourceInfo: 'Kugou',
|
||||
};
|
||||
|
||||
@override
|
||||
|
Loading…
Reference in New Issue
Block a user