2024-08-26 15:21:22 +00:00
|
|
|
import 'package:collection/collection.dart';
|
2024-09-07 10:49:05 +00:00
|
|
|
import 'package:drift/drift.dart';
|
2024-08-27 08:37:31 +00:00
|
|
|
import 'package:get/get.dart';
|
2024-09-07 10:49:05 +00:00
|
|
|
import 'package:rhythm_box/providers/database.dart';
|
2024-09-06 05:26:20 +00:00
|
|
|
import 'package:rhythm_box/providers/error_notifier.dart';
|
2024-08-27 08:37:31 +00:00
|
|
|
import 'package:rhythm_box/providers/user_preferences.dart';
|
|
|
|
import 'package:rhythm_box/services/database/database.dart';
|
2024-09-10 15:06:15 +00:00
|
|
|
import 'package:rhythm_box/services/server/active_sourced_track.dart';
|
2024-09-06 14:19:26 +00:00
|
|
|
import 'package:rhythm_box/services/sourced_track/sources/kugou.dart';
|
2024-09-04 15:28:59 +00:00
|
|
|
import 'package:rhythm_box/services/sourced_track/sources/netease.dart';
|
2024-08-26 15:21:22 +00:00
|
|
|
import 'package:rhythm_box/services/utils.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/sources/piped.dart';
|
|
|
|
import 'package:rhythm_box/services/sourced_track/sources/youtube.dart';
|
|
|
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
|
|
|
|
|
|
|
abstract class SourcedTrack extends Track {
|
|
|
|
final SourceMap source;
|
|
|
|
final List<SourceInfo> siblings;
|
|
|
|
final SourceInfo sourceInfo;
|
|
|
|
|
|
|
|
SourcedTrack({
|
|
|
|
required this.source,
|
|
|
|
required this.siblings,
|
|
|
|
required this.sourceInfo,
|
|
|
|
required Track track,
|
|
|
|
}) {
|
|
|
|
id = track.id;
|
|
|
|
name = track.name;
|
|
|
|
artists = track.artists;
|
|
|
|
album = track.album;
|
|
|
|
durationMs = track.durationMs;
|
|
|
|
discNumber = track.discNumber;
|
|
|
|
explicit = track.explicit;
|
|
|
|
externalIds = track.externalIds;
|
|
|
|
href = track.href;
|
|
|
|
isPlayable = track.isPlayable;
|
|
|
|
linkedFrom = track.linkedFrom;
|
|
|
|
popularity = track.popularity;
|
|
|
|
previewUrl = track.previewUrl;
|
|
|
|
trackNumber = track.trackNumber;
|
|
|
|
type = track.type;
|
|
|
|
uri = track.uri;
|
|
|
|
}
|
|
|
|
|
|
|
|
static SourcedTrack fromJson(Map<String, dynamic> json) {
|
2024-08-27 08:37:31 +00:00
|
|
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
|
|
|
final audioSource = preferences.audioSource;
|
2024-08-26 15:21:22 +00:00
|
|
|
|
|
|
|
final sourceInfo = SourceInfo.fromJson(json);
|
|
|
|
final source = SourceMap.fromJson(json);
|
|
|
|
final track = Track.fromJson(json);
|
2024-08-27 06:48:31 +00:00
|
|
|
final siblings = (json['siblings'] as List)
|
2024-08-26 15:21:22 +00:00
|
|
|
.map((sibling) => SourceInfo.fromJson(sibling))
|
|
|
|
.toList()
|
|
|
|
.cast<SourceInfo>();
|
|
|
|
|
|
|
|
return switch (audioSource) {
|
2024-09-04 15:28:59 +00:00
|
|
|
AudioSource.netease => NeteaseSourcedTrack(
|
|
|
|
source: source,
|
|
|
|
siblings: siblings,
|
|
|
|
sourceInfo: sourceInfo,
|
|
|
|
track: track,
|
|
|
|
),
|
2024-08-27 08:37:31 +00:00
|
|
|
AudioSource.piped => PipedSourcedTrack(
|
2024-08-26 15:21:22 +00:00
|
|
|
source: source,
|
|
|
|
siblings: siblings,
|
|
|
|
sourceInfo: sourceInfo,
|
|
|
|
track: track,
|
|
|
|
),
|
|
|
|
_ => YoutubeSourcedTrack(
|
|
|
|
source: source,
|
|
|
|
siblings: siblings,
|
|
|
|
sourceInfo: sourceInfo,
|
|
|
|
track: track,
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-09-11 12:18:03 +00:00
|
|
|
static Future<SourcedTrack?> reRoutineFetchFromTrack(
|
|
|
|
Track track, SourceMatchTableData cachedSource) {
|
|
|
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
|
|
|
final ytOrPiped = preferences.audioSource == AudioSource.piped
|
|
|
|
? PipedSourcedTrack.fetchFromTrack
|
|
|
|
: YoutubeSourcedTrack.fetchFromTrack;
|
|
|
|
final sourceInfoTrackMap = {
|
|
|
|
SourceType.youtube: ytOrPiped,
|
|
|
|
SourceType.youtubeMusic: ytOrPiped,
|
|
|
|
SourceType.netease: NeteaseSourcedTrack.fetchFromTrack,
|
|
|
|
SourceType.kugou: KugouSourcedTrack.fetchFromTrack,
|
|
|
|
};
|
|
|
|
return sourceInfoTrackMap[cachedSource.sourceType]!(track: track);
|
|
|
|
}
|
|
|
|
|
2024-09-10 15:06:15 +00:00
|
|
|
Future<SourcedTrack?> reRoutineSwapSiblings(SourceInfo info) {
|
2024-09-07 17:20:46 +00:00
|
|
|
final sourceInfoTrackMap = {
|
2024-09-10 15:06:15 +00:00
|
|
|
YoutubeSourceInfo: YoutubeSourcedTrack.fetchFromTrack,
|
|
|
|
PipedSourceInfo: PipedSourcedTrack.fetchFromTrack,
|
|
|
|
NeteaseSourceInfo: NeteaseSourcedTrack.fetchFromTrack,
|
|
|
|
KugouSourceInfo: KugouSourcedTrack.fetchFromTrack,
|
2024-09-07 17:20:46 +00:00
|
|
|
};
|
2024-09-10 15:06:15 +00:00
|
|
|
return sourceInfoTrackMap[info.runtimeType]!(
|
|
|
|
track: Get.find<ActiveSourcedTrackProvider>().state.value!,
|
|
|
|
);
|
2024-09-07 17:20:46 +00:00
|
|
|
}
|
|
|
|
|
2024-08-26 15:21:22 +00:00
|
|
|
static String getSearchTerm(Track track) {
|
|
|
|
final artists = (track.artists ?? [])
|
|
|
|
.map((ar) => ar.name)
|
|
|
|
.toList()
|
|
|
|
.whereNotNull()
|
|
|
|
.toList();
|
|
|
|
|
|
|
|
final title = ServiceUtils.getTitle(
|
|
|
|
track.name!,
|
|
|
|
artists: artists,
|
|
|
|
onlyCleanArtist: true,
|
|
|
|
).trim();
|
|
|
|
|
|
|
|
return "$title - ${artists.join(", ")}";
|
|
|
|
}
|
|
|
|
|
|
|
|
static Future<SourcedTrack> fetchFromTrack({
|
|
|
|
required Track track,
|
2024-09-07 10:49:05 +00:00
|
|
|
AudioSource? fallbackTo,
|
2024-08-26 15:21:22 +00:00
|
|
|
}) async {
|
2024-08-27 08:37:31 +00:00
|
|
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
2024-09-07 10:49:05 +00:00
|
|
|
var audioSource = preferences.audioSource;
|
|
|
|
|
|
|
|
if (!preferences.overrideCacheProvider && fallbackTo == null) {
|
|
|
|
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);
|
|
|
|
|
|
|
|
final ytOrPiped = preferences.audioSource == AudioSource.youtube
|
|
|
|
? AudioSource.youtube
|
|
|
|
: AudioSource.piped;
|
|
|
|
final sourceTypeTrackMap = {
|
|
|
|
SourceType.youtube: ytOrPiped,
|
|
|
|
SourceType.youtubeMusic: ytOrPiped,
|
|
|
|
SourceType.netease: AudioSource.netease,
|
|
|
|
SourceType.kugou: AudioSource.kugou,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (cachedSource != null) {
|
|
|
|
final cachedAudioSource = sourceTypeTrackMap[cachedSource.sourceType]!;
|
|
|
|
audioSource = cachedAudioSource;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fallbackTo != null) {
|
|
|
|
audioSource = fallbackTo;
|
|
|
|
}
|
2024-08-26 15:21:22 +00:00
|
|
|
|
|
|
|
try {
|
|
|
|
return switch (audioSource) {
|
2024-09-04 15:28:59 +00:00
|
|
|
AudioSource.netease =>
|
|
|
|
await NeteaseSourcedTrack.fetchFromTrack(track: track),
|
2024-09-06 14:19:26 +00:00
|
|
|
AudioSource.kugou =>
|
|
|
|
await KugouSourcedTrack.fetchFromTrack(track: track),
|
2024-08-27 08:37:31 +00:00
|
|
|
AudioSource.piped =>
|
|
|
|
await PipedSourcedTrack.fetchFromTrack(track: track),
|
2024-08-26 15:21:22 +00:00
|
|
|
_ => await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
|
|
|
};
|
2024-09-06 05:26:20 +00:00
|
|
|
} on TrackNotFoundError catch (err) {
|
2024-09-06 08:37:49 +00:00
|
|
|
Get.find<ErrorNotifier>().showError(
|
|
|
|
'${err.toString()} via ${preferences.audioSource.label}, querying in fallback sources...',
|
|
|
|
);
|
2024-09-07 10:49:05 +00:00
|
|
|
|
|
|
|
if (fallbackTo != null) {
|
|
|
|
// Prevent infinite fallback
|
|
|
|
if (audioSource == AudioSource.youtube ||
|
|
|
|
audioSource == AudioSource.piped) rethrow;
|
|
|
|
}
|
|
|
|
|
|
|
|
return switch (audioSource) {
|
2024-09-04 15:28:59 +00:00
|
|
|
AudioSource.netease =>
|
2024-09-08 14:24:10 +00:00
|
|
|
await fetchFromTrack(track: track, fallbackTo: AudioSource.youtube),
|
2024-09-06 14:19:26 +00:00
|
|
|
AudioSource.kugou =>
|
2024-09-07 10:49:05 +00:00
|
|
|
await fetchFromTrack(track: track, fallbackTo: AudioSource.youtube),
|
|
|
|
_ =>
|
|
|
|
await fetchFromTrack(track: track, fallbackTo: AudioSource.netease),
|
2024-09-04 15:28:59 +00:00
|
|
|
};
|
2024-08-26 15:21:22 +00:00
|
|
|
} on HttpClientClosedException catch (_) {
|
|
|
|
return await PipedSourcedTrack.fetchFromTrack(track: track);
|
|
|
|
} on VideoUnplayableException catch (_) {
|
|
|
|
return await PipedSourcedTrack.fetchFromTrack(track: track);
|
|
|
|
} catch (e) {
|
|
|
|
rethrow;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static Future<List<SiblingType>> fetchSiblings({
|
|
|
|
required Track track,
|
|
|
|
}) {
|
2024-08-27 08:37:31 +00:00
|
|
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
|
|
|
final audioSource = preferences.audioSource;
|
2024-08-26 15:21:22 +00:00
|
|
|
|
|
|
|
return switch (audioSource) {
|
2024-08-27 08:37:31 +00:00
|
|
|
AudioSource.piped => PipedSourcedTrack.fetchSiblings(track: track),
|
2024-08-26 15:21:22 +00:00
|
|
|
_ => YoutubeSourcedTrack.fetchSiblings(track: track),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<SourcedTrack> copyWithSibling();
|
|
|
|
|
|
|
|
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling);
|
|
|
|
|
|
|
|
Future<SourcedTrack?> swapWithSiblingOfIndex(int index) {
|
|
|
|
return swapWithSibling(siblings[index]);
|
|
|
|
}
|
|
|
|
|
|
|
|
String get url {
|
2024-08-27 08:37:31 +00:00
|
|
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
|
|
|
final streamMusicCodec = preferences.streamMusicCodec;
|
2024-08-26 15:21:22 +00:00
|
|
|
|
|
|
|
return getUrlOfCodec(streamMusicCodec);
|
|
|
|
}
|
|
|
|
|
|
|
|
String getUrlOfCodec(SourceCodecs codec) {
|
2024-08-27 08:37:31 +00:00
|
|
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
|
|
|
final audioQuality = preferences.audioQuality;
|
2024-08-26 15:21:22 +00:00
|
|
|
|
|
|
|
return source[codec]?[audioQuality] ??
|
|
|
|
// this will ensure playback doesn't break
|
|
|
|
source[codec == SourceCodecs.m4a ? SourceCodecs.weba : SourceCodecs.m4a]
|
|
|
|
[audioQuality];
|
|
|
|
}
|
|
|
|
|
|
|
|
SourceCodecs get codec {
|
2024-08-27 08:37:31 +00:00
|
|
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
|
|
|
final streamMusicCodec = preferences.streamMusicCodec;
|
2024-08-26 15:21:22 +00:00
|
|
|
|
|
|
|
return streamMusicCodec;
|
|
|
|
}
|
|
|
|
}
|