RhythmBox/lib/services/sourced_track/sources/netease.dart

236 lines
7.6 KiB
Dart
Raw Normal View History

2024-09-04 15:28:59 +00:00
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:get/get.dart' hide Value;
import 'package:rhythm_box/providers/database.dart';
import 'package:rhythm_box/providers/user_preferences.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 NeteaseSourceInfo extends SourceInfo {
NeteaseSourceInfo({
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 NeteaseSourcedTrack extends SourcedTrack {
NeteaseSourcedTrack({
required super.source,
required super.siblings,
required super.sourceInfo,
required super.track,
});
static String _getBaseUrl() {
final preferences = Get.find<UserPreferencesProvider>().state.value;
return preferences.neteaseApiInstance;
}
static GetConnect _getClient() {
final client = GetConnect();
client.baseUrl = _getBaseUrl();
client.timeout = const Duration(seconds: 30);
return client;
}
static String? _lookedUpRealIp;
static Future<String> lookupRealIp() async {
if (_lookedUpRealIp != null) return _lookedUpRealIp!;
const ipCheckUrl = 'https://api.ipify.org';
final client = GetConnect(timeout: const Duration(seconds: 30));
final resp = await client.get(ipCheckUrl);
_lookedUpRealIp = resp.body;
return _lookedUpRealIp!;
}
static Future<NeteaseSourcedTrack> 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) {
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.netease),
),
);
return NeteaseSourcedTrack(
siblings: siblings.map((s) => s.info).skip(1).toList(),
source: siblings.first.source as SourceMap,
sourceInfo: siblings.first.info,
track: track,
);
}
final client = _getClient();
final resp = await client.get('/song/detail?ids=${cachedSource.sourceId}');
final item = resp.body['songs'][0];
return NeteaseSourcedTrack(
siblings: [],
source: toSourceMap(item),
sourceInfo: NeteaseSourceInfo(
id: item['id'].toString(),
artist: item['ar'].map((x) => x['name']).join(','),
artistUrl: 'https://music.163.com/#/artist?id=${item['ar'][0]['id']}',
pageUrl: 'https://music.163.com/#/song?id=${item['id']}',
thumbnail: item['al']['picUrl'],
title: item['name'],
duration: Duration(milliseconds: item['dt']),
album: item['al']['name'],
),
track: track,
);
}
static SourceMap toSourceMap(dynamic manifest) {
final baseUrl = _getBaseUrl();
// Due to netease may provide m4a, mp3 and others, we cannot decide this so mock this data.
return SourceMap(
m4a: SourceQualityMap(
high: '$baseUrl/song/url?id=${manifest['id']}',
medium: '$baseUrl/song/url?id=${manifest['id']}&br=192000',
low: '$baseUrl/song/url?id=${manifest['id']}&br=128000',
),
weba: SourceQualityMap(
high: '$baseUrl/song/url?id=${manifest['id']}',
medium: '$baseUrl/song/url?id=${manifest['id']}&br=192000',
low: '$baseUrl/song/url?id=${manifest['id']}&br=128000',
),
);
}
static Future<List<SiblingType>> fetchSiblings({
required Track track,
}) async {
final query = SourcedTrack.getSearchTerm(track);
final client = _getClient();
final resp =
await client.get('/search?keywords=${Uri.encodeComponent(query)}');
final results = resp.body['result']['songs'];
// We can just trust netease 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<NeteaseSourcedTrack> copyWithSibling() async {
if (siblings.isNotEmpty) {
return this;
}
final fetchedSiblings = await fetchSiblings(track: this);
return NeteaseSourcedTrack(
siblings: fetchedSiblings
.where((s) => s.info.id != sourceInfo.id)
.map((s) => s.info)
.toList(),
source: source,
sourceInfo: sourceInfo,
track: this,
);
}
@override
Future<NeteaseSourcedTrack?> 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 client = _getClient();
final resp = await client.get('/song/detail?ids=${newSourceInfo.id}');
final item = resp.body['songs'][0];
final (:info, :source) = toSiblingType(item);
final db = Get.find<DatabaseProvider>();
await db.database.into(db.database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: id!,
sourceId: info.id,
sourceType: const Value(SourceType.netease),
// 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 NeteaseSourcedTrack(
siblings: newSiblings,
source: source!,
sourceInfo: info,
track: this,
);
}
static SiblingType toSiblingType(dynamic item) {
final firstArtist = item['ar'] != null ? item['ar'][0] : item['artists'][0];
final SiblingType sibling = (
info: NeteaseSourceInfo(
id: item['id'].toString(),
artist: item['ar'] != null
? item['ar'].map((x) => x['name']).join(',')
: item['artists'].map((x) => x['name']).toString(),
artistUrl: 'https://music.163.com/#/artist?id=${firstArtist['id']}',
pageUrl: 'https://music.163.com/#/song?id=${item['id']}',
thumbnail: item['al']?['picUrl'] ??
'https://p1.music.126.net/6y-UleORITEDbvrOLV0Q8A==/5639395138885805.jpg',
title: item['name'],
duration: item['dt'] != null
? Duration(milliseconds: item['dt'])
: Duration.zero,
album: item['al']?['name'],
),
source: toSourceMap(item),
);
return sibling;
}
}