✨ Impl more features (clean up 20+ todo)
⚡ Add query cache
This commit is contained in:
parent
a162619a88
commit
2e17078fea
@ -2,7 +2,14 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
import 'package:rhythm_box/providers/audio_player.dart';
|
import 'package:rhythm_box/providers/audio_player.dart';
|
||||||
|
import 'package:rhythm_box/providers/audio_player_stream.dart';
|
||||||
|
import 'package:rhythm_box/providers/database.dart';
|
||||||
|
import 'package:rhythm_box/providers/history.dart';
|
||||||
|
import 'package:rhythm_box/providers/palette.dart';
|
||||||
|
import 'package:rhythm_box/providers/scrobbler.dart';
|
||||||
|
import 'package:rhythm_box/providers/skip_segments.dart';
|
||||||
import 'package:rhythm_box/providers/spotify.dart';
|
import 'package:rhythm_box/providers/spotify.dart';
|
||||||
|
import 'package:rhythm_box/providers/user_preferences.dart';
|
||||||
import 'package:rhythm_box/router.dart';
|
import 'package:rhythm_box/router.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/routes/playback.dart';
|
import 'package:rhythm_box/services/server/routes/playback.dart';
|
||||||
@ -51,7 +58,16 @@ class MyApp extends StatelessWidget {
|
|||||||
|
|
||||||
void _initializeProviders(BuildContext context) async {
|
void _initializeProviders(BuildContext context) async {
|
||||||
Get.lazyPut(() => SpotifyProvider());
|
Get.lazyPut(() => SpotifyProvider());
|
||||||
Get.lazyPut(() => ActiveSourcedTrackProvider());
|
|
||||||
|
Get.put(ActiveSourcedTrackProvider());
|
||||||
|
Get.put(AudioPlayerStreamProvider());
|
||||||
|
|
||||||
|
Get.put(DatabaseProvider());
|
||||||
|
Get.put(PlaybackHistoryProvider());
|
||||||
|
Get.put(SegmentsProvider());
|
||||||
|
Get.put(PaletteProvider());
|
||||||
|
Get.put(ScrobblerProvider());
|
||||||
|
Get.put(UserPreferencesProvider());
|
||||||
|
|
||||||
Get.put(AudioPlayerProvider());
|
Get.put(AudioPlayerProvider());
|
||||||
Get.put(QueryingTrackInfoProvider());
|
Get.put(QueryingTrackInfoProvider());
|
||||||
|
@ -9,7 +9,7 @@ abstract class PlatformInfo {
|
|||||||
static bool get isLinux => !kIsWeb && Platform.isLinux;
|
static bool get isLinux => !kIsWeb && Platform.isLinux;
|
||||||
|
|
||||||
static bool get isInFlatpak =>
|
static bool get isInFlatpak =>
|
||||||
kIsWeb ? false : Platform.environment["FLATPAK_ID"] != null;
|
kIsWeb ? false : Platform.environment['FLATPAK_ID'] != null;
|
||||||
|
|
||||||
static bool get isWindows => !kIsWeb && Platform.isWindows;
|
static bool get isWindows => !kIsWeb && Platform.isWindows;
|
||||||
|
|
||||||
|
157
lib/providers/audio_player_stream.dart
Normal file
157
lib/providers/audio_player_stream.dart
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
|
import 'package:rhythm_box/providers/audio_player.dart';
|
||||||
|
import 'package:rhythm_box/providers/history.dart';
|
||||||
|
import 'package:rhythm_box/providers/scrobbler.dart';
|
||||||
|
import 'package:rhythm_box/providers/skip_segments.dart';
|
||||||
|
import 'package:rhythm_box/providers/user_preferences.dart';
|
||||||
|
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||||
|
import 'package:rhythm_box/services/audio_services/audio_services.dart';
|
||||||
|
import 'package:rhythm_box/services/audio_services/image.dart';
|
||||||
|
import 'package:rhythm_box/services/local_track.dart';
|
||||||
|
import 'package:rhythm_box/services/server/sourced_track.dart';
|
||||||
|
import 'package:rhythm_box/widgets/auto_cache_image.dart';
|
||||||
|
|
||||||
|
class AudioPlayerStreamProvider extends GetxController {
|
||||||
|
late final AudioServices notificationService;
|
||||||
|
final Rxn<PaletteGenerator?> palette = Rxn<PaletteGenerator?>();
|
||||||
|
|
||||||
|
List<StreamSubscription>? _subscriptions;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
AudioServices.create().then(
|
||||||
|
(value) => notificationService = value,
|
||||||
|
);
|
||||||
|
|
||||||
|
_subscriptions = [
|
||||||
|
subscribeToPlaylist(),
|
||||||
|
subscribeToSkipSponsor(),
|
||||||
|
subscribeToScrobbleChanged(),
|
||||||
|
subscribeToPosition(),
|
||||||
|
subscribeToPlayerError(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (_subscriptions != null) {
|
||||||
|
for (final subscription in _subscriptions!) {
|
||||||
|
subscription.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updatePalette() async {
|
||||||
|
if (!Get.find<UserPreferences>().albumColorSync) {
|
||||||
|
if (palette.value != null) {
|
||||||
|
palette.value = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final AudioPlayerProvider playback = Get.find();
|
||||||
|
final activeTrack = playback.state.value.activeTrack;
|
||||||
|
if (activeTrack == null) return;
|
||||||
|
|
||||||
|
if (activeTrack.album?.images != null) {
|
||||||
|
final newPalette = await PaletteGenerator.fromImageProvider(
|
||||||
|
AutoCacheImage.provider(
|
||||||
|
(activeTrack.album?.images).asUrlString()!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
palette.value = newPalette;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription subscribeToPlaylist() {
|
||||||
|
final AudioPlayerProvider playback = Get.find();
|
||||||
|
return audioPlayer.playlistStream.listen((mpvPlaylist) {
|
||||||
|
final activeTrack = playback.state.value.activeTrack;
|
||||||
|
if (activeTrack != null) {
|
||||||
|
notificationService.addTrack(activeTrack);
|
||||||
|
updatePalette();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription subscribeToSkipSponsor() {
|
||||||
|
return audioPlayer.positionStream.listen((position) async {
|
||||||
|
final currentSegments =
|
||||||
|
await Get.find<SegmentsProvider>().fetchSegments();
|
||||||
|
|
||||||
|
if (currentSegments?.segments.isNotEmpty != true ||
|
||||||
|
position < const Duration(seconds: 3)) return;
|
||||||
|
|
||||||
|
for (final segment in currentSegments!.segments) {
|
||||||
|
final seconds = position.inSeconds;
|
||||||
|
|
||||||
|
if (seconds < segment.start || seconds >= segment.end) continue;
|
||||||
|
|
||||||
|
await audioPlayer.seek(Duration(seconds: segment.end + 1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription subscribeToScrobbleChanged() {
|
||||||
|
String? lastScrobbled;
|
||||||
|
return audioPlayer.positionStream.listen((position) {
|
||||||
|
try {
|
||||||
|
final AudioPlayerProvider playback = Get.find();
|
||||||
|
final uid = playback.state.value.activeTrack is LocalTrack
|
||||||
|
? (playback.state.value.activeTrack as LocalTrack).path
|
||||||
|
: playback.state.value.activeTrack?.id;
|
||||||
|
|
||||||
|
if (playback.state.value.activeTrack == null ||
|
||||||
|
lastScrobbled == uid ||
|
||||||
|
position.inSeconds < 30) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.find<ScrobblerProvider>()
|
||||||
|
.scrobble(playback.state.value.activeTrack!);
|
||||||
|
Get.find<PlaybackHistoryProvider>()
|
||||||
|
.addTrack(playback.state.value.activeTrack!);
|
||||||
|
lastScrobbled = uid;
|
||||||
|
} catch (e, stack) {
|
||||||
|
log('[Scrobbler] Error: $e; Trace:\n$stack');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription subscribeToPosition() {
|
||||||
|
String lastTrack = ''; // used to prevent multiple calls to the same track
|
||||||
|
final AudioPlayerProvider playback = Get.find();
|
||||||
|
return audioPlayer.positionStream.listen((event) async {
|
||||||
|
if (event < const Duration(seconds: 3) ||
|
||||||
|
audioPlayer.playlist.index == -1 ||
|
||||||
|
audioPlayer.playlist.index ==
|
||||||
|
playback.state.value.tracks.length - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final nextTrack = RhythmMedia.fromMedia(
|
||||||
|
audioPlayer.playlist.medias.elementAt(audioPlayer.playlist.index + 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastTrack == nextTrack.track.id || nextTrack.track is LocalTrack) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Get.find<SourcedTrackProvider>().fetch(nextTrack);
|
||||||
|
} finally {
|
||||||
|
lastTrack = nextTrack.track.id!;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription subscribeToPlayerError() {
|
||||||
|
return audioPlayer.errorStream.listen((event) {
|
||||||
|
// Handle player error events here
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
6
lib/providers/database.dart
Normal file
6
lib/providers/database.dart
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:rhythm_box/services/database/database.dart';
|
||||||
|
|
||||||
|
class DatabaseProvider extends GetxController {
|
||||||
|
late final AppDatabase database = AppDatabase();
|
||||||
|
}
|
62
lib/providers/history.dart
Normal file
62
lib/providers/history.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:rhythm_box/providers/database.dart';
|
||||||
|
import 'package:rhythm_box/services/database/database.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
|
class PlaybackHistoryProvider extends GetxController {
|
||||||
|
final AppDatabase _db = Get.find<DatabaseProvider>().database;
|
||||||
|
|
||||||
|
Future<void> _batchInsertHistoryEntries(
|
||||||
|
List<HistoryTableCompanion> entries) async {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
batch.insertAll(_db.historyTable, entries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addPlaylists(List<PlaylistSimple> playlists) async {
|
||||||
|
await _batchInsertHistoryEntries([
|
||||||
|
for (final playlist in playlists)
|
||||||
|
HistoryTableCompanion.insert(
|
||||||
|
type: HistoryEntryType.playlist,
|
||||||
|
itemId: playlist.id!,
|
||||||
|
data: playlist.toJson(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addAlbums(List<AlbumSimple> albums) async {
|
||||||
|
await _batchInsertHistoryEntries([
|
||||||
|
for (final album in albums)
|
||||||
|
HistoryTableCompanion.insert(
|
||||||
|
type: HistoryEntryType.album,
|
||||||
|
itemId: album.id!,
|
||||||
|
data: album.toJson(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addTracks(List<Track> tracks) async {
|
||||||
|
await _batchInsertHistoryEntries([
|
||||||
|
for (final track in tracks)
|
||||||
|
HistoryTableCompanion.insert(
|
||||||
|
type: HistoryEntryType.track,
|
||||||
|
itemId: track.id!,
|
||||||
|
data: track.toJson(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addTrack(Track track) async {
|
||||||
|
await _db.into(_db.historyTable).insert(
|
||||||
|
HistoryTableCompanion.insert(
|
||||||
|
type: HistoryEntryType.track,
|
||||||
|
itemId: track.id!,
|
||||||
|
data: track.toJson(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clear() async {
|
||||||
|
await _db.delete(_db.historyTable).go();
|
||||||
|
}
|
||||||
|
}
|
14
lib/providers/palette.dart
Normal file
14
lib/providers/palette.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:palette_generator/palette_generator.dart';
|
||||||
|
|
||||||
|
class PaletteProvider extends GetxController {
|
||||||
|
final Rx<PaletteGenerator?> palette = Rx<PaletteGenerator?>(null);
|
||||||
|
|
||||||
|
void updatePalette(PaletteGenerator? newPalette) {
|
||||||
|
palette.value = newPalette;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
palette.value = null;
|
||||||
|
}
|
||||||
|
}
|
140
lib/providers/scrobbler.dart
Normal file
140
lib/providers/scrobbler.dart
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:developer';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:get/get.dart' hide Value;
|
||||||
|
import 'package:rhythm_box/providers/database.dart';
|
||||||
|
import 'package:rhythm_box/services/artist.dart';
|
||||||
|
import 'package:rhythm_box/services/database/database.dart';
|
||||||
|
import 'package:scrobblenaut/scrobblenaut.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
|
class ScrobblerProvider extends GetxController {
|
||||||
|
final StreamController<Track> _scrobbleController =
|
||||||
|
StreamController<Track>.broadcast();
|
||||||
|
final Rxn<Scrobblenaut?> scrobbler = Rxn<Scrobblenaut?>(null);
|
||||||
|
late StreamSubscription _databaseSubscription;
|
||||||
|
late StreamSubscription _scrobbleSubscription;
|
||||||
|
|
||||||
|
static String apiKey = 'd2a75393e1141d0c9486eb77cc7b8892';
|
||||||
|
static String apiSecret = '3ac3a5231a2e8a0dc98577c246101b78';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
_initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initialize() async {
|
||||||
|
final database = Get.find<DatabaseProvider>().database;
|
||||||
|
|
||||||
|
final loginInfo = await (database.select(database.scrobblerTable)
|
||||||
|
..where((t) => t.id.equals(0)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
|
||||||
|
_databaseSubscription =
|
||||||
|
database.select(database.scrobblerTable).watch().listen((event) async {
|
||||||
|
if (event.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
scrobbler.value = Scrobblenaut(
|
||||||
|
lastFM: await LastFM.authenticateWithPasswordHash(
|
||||||
|
apiKey: apiKey,
|
||||||
|
apiSecret: apiSecret,
|
||||||
|
username: event.first.username,
|
||||||
|
passwordHash: event.first.passwordHash.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
log('[Scrobble] Error: $e; Trace:\n$stack');
|
||||||
|
scrobbler.value = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
scrobbler.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_scrobbleSubscription = _scrobbleController.stream.listen((track) async {
|
||||||
|
try {
|
||||||
|
await scrobbler.value?.track.scrobble(
|
||||||
|
artist: track.artists!.first.name!,
|
||||||
|
track: track.name!,
|
||||||
|
album: track.album!.name!,
|
||||||
|
chosenByUser: true,
|
||||||
|
duration: track.duration,
|
||||||
|
timestamp: DateTime.now().toUtc(),
|
||||||
|
trackNumber: track.trackNumber,
|
||||||
|
);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
log('[Scrobble] Error: $e; Trace:\n$stackTrace');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loginInfo == null) {
|
||||||
|
scrobbler.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrobbler.value = Scrobblenaut(
|
||||||
|
lastFM: await LastFM.authenticateWithPasswordHash(
|
||||||
|
apiKey: apiKey,
|
||||||
|
apiSecret: apiSecret,
|
||||||
|
username: loginInfo.username,
|
||||||
|
passwordHash: loginInfo.passwordHash.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> login(String username, String password) async {
|
||||||
|
final database = Get.find<DatabaseProvider>().database;
|
||||||
|
|
||||||
|
final lastFm = await LastFM.authenticate(
|
||||||
|
apiKey: apiKey,
|
||||||
|
apiSecret: apiSecret,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!lastFm.isAuth) throw Exception('Invalid credentials');
|
||||||
|
|
||||||
|
await database.into(database.scrobblerTable).insert(
|
||||||
|
ScrobblerTableCompanion.insert(
|
||||||
|
id: const Value(0),
|
||||||
|
username: username,
|
||||||
|
passwordHash: DecryptedText(lastFm.passwordHash!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
scrobbler.value = Scrobblenaut(lastFM: lastFm);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> logout() async {
|
||||||
|
scrobbler.value = null;
|
||||||
|
final database = Get.find<DatabaseProvider>().database;
|
||||||
|
await database.delete(database.scrobblerTable).go();
|
||||||
|
}
|
||||||
|
|
||||||
|
void scrobble(Track track) {
|
||||||
|
_scrobbleController.add(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> love(Track track) async {
|
||||||
|
await scrobbler.value?.track.love(
|
||||||
|
artist: track.artists!.asString(),
|
||||||
|
track: track.name!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> unlove(Track track) async {
|
||||||
|
await scrobbler.value?.track.unLove(
|
||||||
|
artist: track.artists!.asString(),
|
||||||
|
track: track.name!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
_databaseSubscription.cancel();
|
||||||
|
_scrobbleSubscription.cancel();
|
||||||
|
_scrobbleController.close();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
}
|
117
lib/providers/skip_segments.dart
Normal file
117
lib/providers/skip_segments.dart
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
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:rhythm_box/services/server/active_sourced_track.dart';
|
||||||
|
|
||||||
|
class SourcedSegments {
|
||||||
|
final String source;
|
||||||
|
final List<SkipSegmentTableData> segments;
|
||||||
|
|
||||||
|
SourcedSegments({required this.source, required this.segments});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<SkipSegmentTableData>> getAndCacheSkipSegments(String id) async {
|
||||||
|
final database = Get.find<DatabaseProvider>().database;
|
||||||
|
try {
|
||||||
|
final cached = await (database.select(database.skipSegmentTable)
|
||||||
|
..where((s) => s.trackId.equals(id)))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (cached.isNotEmpty) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
final res = await Dio().getUri(
|
||||||
|
Uri(
|
||||||
|
scheme: 'https',
|
||||||
|
host: 'sponsor.ajay.app',
|
||||||
|
path: '/api/skipSegments',
|
||||||
|
queryParameters: {
|
||||||
|
'videoID': id,
|
||||||
|
'category': [
|
||||||
|
'sponsor',
|
||||||
|
'selfpromo',
|
||||||
|
'interaction',
|
||||||
|
'intro',
|
||||||
|
'outro',
|
||||||
|
'music_offtopic'
|
||||||
|
],
|
||||||
|
'actionType': 'skip'
|
||||||
|
},
|
||||||
|
),
|
||||||
|
options: Options(
|
||||||
|
responseType: ResponseType.json,
|
||||||
|
validateStatus: (status) => (status ?? 0) < 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data == 'Not Found') {
|
||||||
|
return List.castFrom<dynamic, SkipSegmentTableData>([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = res.data as List;
|
||||||
|
final segments = data.map((obj) {
|
||||||
|
final start = obj['segment'].first.toInt();
|
||||||
|
final end = obj['segment'].last.toInt();
|
||||||
|
return SkipSegmentTableCompanion.insert(
|
||||||
|
trackId: id,
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
await database.batch((b) {
|
||||||
|
b.insertAll(database.skipSegmentTable, segments);
|
||||||
|
});
|
||||||
|
|
||||||
|
return await (database.select(database.skipSegmentTable)
|
||||||
|
..where((s) => s.trackId.equals(id)))
|
||||||
|
.get();
|
||||||
|
} catch (e, stack) {
|
||||||
|
log('[SkipSegment] Error: $e; Trace:\n$stack');
|
||||||
|
return List.castFrom<dynamic, SkipSegmentTableData>([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SegmentsProvider extends GetxController {
|
||||||
|
final Rx<SourcedSegments?> segments = Rx<SourcedSegments?>(null);
|
||||||
|
|
||||||
|
Future<SourcedSegments?> fetchSegments() async {
|
||||||
|
final track = Get.find<ActiveSourcedTrackProvider>().state.value;
|
||||||
|
if (track == null) {
|
||||||
|
segments.value = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final userPreferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
|
final skipNonMusic = userPreferences.skipNonMusic &&
|
||||||
|
!(userPreferences.audioSource == AudioSource.piped &&
|
||||||
|
userPreferences.searchMode == SearchMode.youtubeMusic);
|
||||||
|
|
||||||
|
if (!skipNonMusic) {
|
||||||
|
segments.value = SourcedSegments(
|
||||||
|
segments: [],
|
||||||
|
source: track.sourceInfo.id,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final fetchedSegments = await getAndCacheSkipSegments(track.sourceInfo.id);
|
||||||
|
segments.value = SourcedSegments(
|
||||||
|
source: track.sourceInfo.id,
|
||||||
|
segments: fetchedSegments,
|
||||||
|
);
|
||||||
|
|
||||||
|
return segments.value!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchSegments(); // Automatically load segments when controller is initialized
|
||||||
|
}
|
||||||
|
}
|
190
lib/providers/user_preferences.dart
Normal file
190
lib/providers/user_preferences.dart
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import 'package:get/get.dart' hide Value;
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:rhythm_box/platform.dart';
|
||||||
|
import 'package:rhythm_box/providers/audio_player_stream.dart';
|
||||||
|
import 'package:rhythm_box/providers/database.dart';
|
||||||
|
import 'package:rhythm_box/providers/palette.dart';
|
||||||
|
import 'package:rhythm_box/services/audio_player/audio_player.dart';
|
||||||
|
import 'package:rhythm_box/services/color.dart';
|
||||||
|
import 'package:rhythm_box/services/database/database.dart';
|
||||||
|
import 'package:rhythm_box/services/sourced_track/enums.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
typedef UserPreferences = PreferencesTableData;
|
||||||
|
|
||||||
|
class UserPreferencesProvider extends GetxController {
|
||||||
|
final Rx<UserPreferences> state = PreferencesTable.defaults().obs;
|
||||||
|
late final AppDatabase db;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
db = Get.find<DatabaseProvider>().database;
|
||||||
|
_initializePreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initializePreferences() async {
|
||||||
|
var result = await (db.select(db.preferencesTable)
|
||||||
|
..where((tbl) => tbl.id.equals(0)))
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (result == null) {
|
||||||
|
await db.into(db.preferencesTable).insert(
|
||||||
|
PreferencesTableCompanion.insert(
|
||||||
|
id: const Value(0),
|
||||||
|
downloadLocation: Value(await _getDefaultDownloadDirectory()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
state.value = await (db.select(db.preferencesTable)
|
||||||
|
..where((tbl) => tbl.id.equals(0)))
|
||||||
|
.getSingle();
|
||||||
|
|
||||||
|
// Subscribe to updates
|
||||||
|
(db.select(db.preferencesTable)..where((tbl) => tbl.id.equals(0)))
|
||||||
|
.watchSingle()
|
||||||
|
.listen((event) async {
|
||||||
|
state.value = event;
|
||||||
|
|
||||||
|
if (PlatformInfo.isDesktop) {
|
||||||
|
await windowManager.setTitleBarStyle(
|
||||||
|
state.value.systemTitleBar
|
||||||
|
? TitleBarStyle.normal
|
||||||
|
: TitleBarStyle.hidden,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await audioPlayer.setAudioNormalization(state.value.normalizeAudio);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _getDefaultDownloadDirectory() async {
|
||||||
|
if (PlatformInfo.isAndroid) return '/storage/emulated/0/Download/RhythmBox';
|
||||||
|
|
||||||
|
if (PlatformInfo.isMacOS) {
|
||||||
|
return join((await getLibraryDirectory()).path, 'Caches');
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDownloadsDirectory().then((dir) {
|
||||||
|
return join(dir!.path, 'RhythmBox');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setData(PreferencesTableCompanion data) async {
|
||||||
|
await (db.update(db.preferencesTable)..where((t) => t.id.equals(0)))
|
||||||
|
.write(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reset() async {
|
||||||
|
await (db.update(db.preferencesTable)..where((t) => t.id.equals(0)))
|
||||||
|
.replace(PreferencesTableCompanion.insert());
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStreamMusicCodec(SourceCodecs codec) {
|
||||||
|
setData(PreferencesTableCompanion(streamMusicCodec: Value(codec)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDownloadMusicCodec(SourceCodecs codec) {
|
||||||
|
setData(PreferencesTableCompanion(downloadMusicCodec: Value(codec)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setThemeMode(ThemeMode mode) {
|
||||||
|
setData(PreferencesTableCompanion(themeMode: Value(mode)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRecommendationMarket(Market country) {
|
||||||
|
setData(PreferencesTableCompanion(market: Value(country)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAccentColorScheme(RhythmColor color) {
|
||||||
|
setData(PreferencesTableCompanion(accentColorScheme: Value(color)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAlbumColorSync(bool sync) {
|
||||||
|
setData(PreferencesTableCompanion(albumColorSync: Value(sync)));
|
||||||
|
|
||||||
|
if (!sync) {
|
||||||
|
Get.find<PaletteProvider>().clear();
|
||||||
|
} else {
|
||||||
|
Get.find<AudioPlayerStreamProvider>().updatePalette();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCheckUpdate(bool check) {
|
||||||
|
setData(PreferencesTableCompanion(checkUpdate: Value(check)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAudioQuality(SourceQualities quality) {
|
||||||
|
setData(PreferencesTableCompanion(audioQuality: Value(quality)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDownloadLocation(String downloadDir) {
|
||||||
|
if (downloadDir.isEmpty) return;
|
||||||
|
setData(PreferencesTableCompanion(downloadLocation: Value(downloadDir)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setLocalLibraryLocation(List<String> localLibraryDirs) {
|
||||||
|
setData(PreferencesTableCompanion(
|
||||||
|
localLibraryLocation: Value(localLibraryDirs)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setLayoutMode(LayoutMode mode) {
|
||||||
|
setData(PreferencesTableCompanion(layoutMode: Value(mode)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCloseBehavior(CloseBehavior behavior) {
|
||||||
|
setData(PreferencesTableCompanion(closeBehavior: Value(behavior)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setShowSystemTrayIcon(bool show) {
|
||||||
|
setData(PreferencesTableCompanion(showSystemTrayIcon: Value(show)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setLocale(Locale locale) {
|
||||||
|
setData(PreferencesTableCompanion(locale: Value(locale)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPipedInstance(String instance) {
|
||||||
|
setData(PreferencesTableCompanion(pipedInstance: Value(instance)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSearchMode(SearchMode mode) {
|
||||||
|
setData(PreferencesTableCompanion(searchMode: Value(mode)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSkipNonMusic(bool skip) {
|
||||||
|
setData(PreferencesTableCompanion(skipNonMusic: Value(skip)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAudioSource(AudioSource type) {
|
||||||
|
setData(PreferencesTableCompanion(audioSource: Value(type)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSystemTitleBar(bool isSystemTitleBar) {
|
||||||
|
setData(PreferencesTableCompanion(systemTitleBar: Value(isSystemTitleBar)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDiscordPresence(bool discordPresence) {
|
||||||
|
setData(PreferencesTableCompanion(discordPresence: Value(discordPresence)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAmoledDarkTheme(bool isAmoled) {
|
||||||
|
setData(PreferencesTableCompanion(amoledDarkTheme: Value(isAmoled)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setNormalizeAudio(bool normalize) {
|
||||||
|
setData(PreferencesTableCompanion(normalizeAudio: Value(normalize)));
|
||||||
|
audioPlayer.setAudioNormalization(normalize);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEndlessPlayback(bool endless) {
|
||||||
|
setData(PreferencesTableCompanion(endlessPlayback: Value(endless)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEnableConnect(bool enable) {
|
||||||
|
setData(PreferencesTableCompanion(enableConnect: Value(enable)));
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,6 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
|||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
part 'tables/authentication.dart';
|
part 'tables/authentication.dart';
|
||||||
part 'tables/blacklist.dart';
|
|
||||||
part 'tables/preferences.dart';
|
part 'tables/preferences.dart';
|
||||||
part 'tables/scrobbler.dart';
|
part 'tables/scrobbler.dart';
|
||||||
part 'tables/skip_segment.dart';
|
part 'tables/skip_segment.dart';
|
||||||
@ -39,7 +38,6 @@ part 'typeconverters/subtitle.dart';
|
|||||||
@DriftDatabase(
|
@DriftDatabase(
|
||||||
tables: [
|
tables: [
|
||||||
AuthenticationTable,
|
AuthenticationTable,
|
||||||
BlacklistTable,
|
|
||||||
PreferencesTable,
|
PreferencesTable,
|
||||||
ScrobblerTable,
|
ScrobblerTable,
|
||||||
SkipSegmentTable,
|
SkipSegmentTable,
|
||||||
|
@ -281,276 +281,6 @@ class AuthenticationTableCompanion
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class $BlacklistTableTable extends BlacklistTable
|
|
||||||
with TableInfo<$BlacklistTableTable, BlacklistTableData> {
|
|
||||||
@override
|
|
||||||
final GeneratedDatabase attachedDatabase;
|
|
||||||
final String? _alias;
|
|
||||||
$BlacklistTableTable(this.attachedDatabase, [this._alias]);
|
|
||||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
|
||||||
@override
|
|
||||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
|
||||||
'id', aliasedName, false,
|
|
||||||
hasAutoIncrement: true,
|
|
||||||
type: DriftSqlType.int,
|
|
||||||
requiredDuringInsert: false,
|
|
||||||
defaultConstraints:
|
|
||||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
|
||||||
static const VerificationMeta _nameMeta = const VerificationMeta('name');
|
|
||||||
@override
|
|
||||||
late final GeneratedColumn<String> name = GeneratedColumn<String>(
|
|
||||||
'name', aliasedName, false,
|
|
||||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
|
||||||
static const VerificationMeta _elementTypeMeta =
|
|
||||||
const VerificationMeta('elementType');
|
|
||||||
@override
|
|
||||||
late final GeneratedColumnWithTypeConverter<BlacklistedType, String>
|
|
||||||
elementType = GeneratedColumn<String>('element_type', aliasedName, false,
|
|
||||||
type: DriftSqlType.string, requiredDuringInsert: true)
|
|
||||||
.withConverter<BlacklistedType>(
|
|
||||||
$BlacklistTableTable.$converterelementType);
|
|
||||||
static const VerificationMeta _elementIdMeta =
|
|
||||||
const VerificationMeta('elementId');
|
|
||||||
@override
|
|
||||||
late final GeneratedColumn<String> elementId = GeneratedColumn<String>(
|
|
||||||
'element_id', aliasedName, false,
|
|
||||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
|
||||||
@override
|
|
||||||
List<GeneratedColumn> get $columns => [id, name, elementType, elementId];
|
|
||||||
@override
|
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
|
||||||
@override
|
|
||||||
String get actualTableName => $name;
|
|
||||||
static const String $name = 'blacklist_table';
|
|
||||||
@override
|
|
||||||
VerificationContext validateIntegrity(Insertable<BlacklistTableData> instance,
|
|
||||||
{bool isInserting = false}) {
|
|
||||||
final context = VerificationContext();
|
|
||||||
final data = instance.toColumns(true);
|
|
||||||
if (data.containsKey('id')) {
|
|
||||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
|
||||||
}
|
|
||||||
if (data.containsKey('name')) {
|
|
||||||
context.handle(
|
|
||||||
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_nameMeta);
|
|
||||||
}
|
|
||||||
context.handle(_elementTypeMeta, const VerificationResult.success());
|
|
||||||
if (data.containsKey('element_id')) {
|
|
||||||
context.handle(_elementIdMeta,
|
|
||||||
elementId.isAcceptableOrUnknown(data['element_id']!, _elementIdMeta));
|
|
||||||
} else if (isInserting) {
|
|
||||||
context.missing(_elementIdMeta);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Set<GeneratedColumn> get $primaryKey => {id};
|
|
||||||
@override
|
|
||||||
BlacklistTableData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
|
||||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
|
||||||
return BlacklistTableData(
|
|
||||||
id: attachedDatabase.typeMapping
|
|
||||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
|
||||||
name: attachedDatabase.typeMapping
|
|
||||||
.read(DriftSqlType.string, data['${effectivePrefix}name'])!,
|
|
||||||
elementType: $BlacklistTableTable.$converterelementType.fromSql(
|
|
||||||
attachedDatabase.typeMapping.read(
|
|
||||||
DriftSqlType.string, data['${effectivePrefix}element_type'])!),
|
|
||||||
elementId: attachedDatabase.typeMapping
|
|
||||||
.read(DriftSqlType.string, data['${effectivePrefix}element_id'])!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
$BlacklistTableTable createAlias(String alias) {
|
|
||||||
return $BlacklistTableTable(attachedDatabase, alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
static JsonTypeConverter2<BlacklistedType, String, String>
|
|
||||||
$converterelementType =
|
|
||||||
const EnumNameConverter<BlacklistedType>(BlacklistedType.values);
|
|
||||||
}
|
|
||||||
|
|
||||||
class BlacklistTableData extends DataClass
|
|
||||||
implements Insertable<BlacklistTableData> {
|
|
||||||
final int id;
|
|
||||||
final String name;
|
|
||||||
final BlacklistedType elementType;
|
|
||||||
final String elementId;
|
|
||||||
const BlacklistTableData(
|
|
||||||
{required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.elementType,
|
|
||||||
required this.elementId});
|
|
||||||
@override
|
|
||||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
|
||||||
final map = <String, Expression>{};
|
|
||||||
map['id'] = Variable<int>(id);
|
|
||||||
map['name'] = Variable<String>(name);
|
|
||||||
{
|
|
||||||
map['element_type'] = Variable<String>(
|
|
||||||
$BlacklistTableTable.$converterelementType.toSql(elementType));
|
|
||||||
}
|
|
||||||
map['element_id'] = Variable<String>(elementId);
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
BlacklistTableCompanion toCompanion(bool nullToAbsent) {
|
|
||||||
return BlacklistTableCompanion(
|
|
||||||
id: Value(id),
|
|
||||||
name: Value(name),
|
|
||||||
elementType: Value(elementType),
|
|
||||||
elementId: Value(elementId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
factory BlacklistTableData.fromJson(Map<String, dynamic> json,
|
|
||||||
{ValueSerializer? serializer}) {
|
|
||||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
|
||||||
return BlacklistTableData(
|
|
||||||
id: serializer.fromJson<int>(json['id']),
|
|
||||||
name: serializer.fromJson<String>(json['name']),
|
|
||||||
elementType: $BlacklistTableTable.$converterelementType
|
|
||||||
.fromJson(serializer.fromJson<String>(json['elementType'])),
|
|
||||||
elementId: serializer.fromJson<String>(json['elementId']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
|
||||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
|
||||||
return <String, dynamic>{
|
|
||||||
'id': serializer.toJson<int>(id),
|
|
||||||
'name': serializer.toJson<String>(name),
|
|
||||||
'elementType': serializer.toJson<String>(
|
|
||||||
$BlacklistTableTable.$converterelementType.toJson(elementType)),
|
|
||||||
'elementId': serializer.toJson<String>(elementId),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
BlacklistTableData copyWith(
|
|
||||||
{int? id,
|
|
||||||
String? name,
|
|
||||||
BlacklistedType? elementType,
|
|
||||||
String? elementId}) =>
|
|
||||||
BlacklistTableData(
|
|
||||||
id: id ?? this.id,
|
|
||||||
name: name ?? this.name,
|
|
||||||
elementType: elementType ?? this.elementType,
|
|
||||||
elementId: elementId ?? this.elementId,
|
|
||||||
);
|
|
||||||
BlacklistTableData copyWithCompanion(BlacklistTableCompanion data) {
|
|
||||||
return BlacklistTableData(
|
|
||||||
id: data.id.present ? data.id.value : this.id,
|
|
||||||
name: data.name.present ? data.name.value : this.name,
|
|
||||||
elementType:
|
|
||||||
data.elementType.present ? data.elementType.value : this.elementType,
|
|
||||||
elementId: data.elementId.present ? data.elementId.value : this.elementId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return (StringBuffer('BlacklistTableData(')
|
|
||||||
..write('id: $id, ')
|
|
||||||
..write('name: $name, ')
|
|
||||||
..write('elementType: $elementType, ')
|
|
||||||
..write('elementId: $elementId')
|
|
||||||
..write(')'))
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => Object.hash(id, name, elementType, elementId);
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is BlacklistTableData &&
|
|
||||||
other.id == this.id &&
|
|
||||||
other.name == this.name &&
|
|
||||||
other.elementType == this.elementType &&
|
|
||||||
other.elementId == this.elementId);
|
|
||||||
}
|
|
||||||
|
|
||||||
class BlacklistTableCompanion extends UpdateCompanion<BlacklistTableData> {
|
|
||||||
final Value<int> id;
|
|
||||||
final Value<String> name;
|
|
||||||
final Value<BlacklistedType> elementType;
|
|
||||||
final Value<String> elementId;
|
|
||||||
const BlacklistTableCompanion({
|
|
||||||
this.id = const Value.absent(),
|
|
||||||
this.name = const Value.absent(),
|
|
||||||
this.elementType = const Value.absent(),
|
|
||||||
this.elementId = const Value.absent(),
|
|
||||||
});
|
|
||||||
BlacklistTableCompanion.insert({
|
|
||||||
this.id = const Value.absent(),
|
|
||||||
required String name,
|
|
||||||
required BlacklistedType elementType,
|
|
||||||
required String elementId,
|
|
||||||
}) : name = Value(name),
|
|
||||||
elementType = Value(elementType),
|
|
||||||
elementId = Value(elementId);
|
|
||||||
static Insertable<BlacklistTableData> custom({
|
|
||||||
Expression<int>? id,
|
|
||||||
Expression<String>? name,
|
|
||||||
Expression<String>? elementType,
|
|
||||||
Expression<String>? elementId,
|
|
||||||
}) {
|
|
||||||
return RawValuesInsertable({
|
|
||||||
if (id != null) 'id': id,
|
|
||||||
if (name != null) 'name': name,
|
|
||||||
if (elementType != null) 'element_type': elementType,
|
|
||||||
if (elementId != null) 'element_id': elementId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
BlacklistTableCompanion copyWith(
|
|
||||||
{Value<int>? id,
|
|
||||||
Value<String>? name,
|
|
||||||
Value<BlacklistedType>? elementType,
|
|
||||||
Value<String>? elementId}) {
|
|
||||||
return BlacklistTableCompanion(
|
|
||||||
id: id ?? this.id,
|
|
||||||
name: name ?? this.name,
|
|
||||||
elementType: elementType ?? this.elementType,
|
|
||||||
elementId: elementId ?? this.elementId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
|
||||||
final map = <String, Expression>{};
|
|
||||||
if (id.present) {
|
|
||||||
map['id'] = Variable<int>(id.value);
|
|
||||||
}
|
|
||||||
if (name.present) {
|
|
||||||
map['name'] = Variable<String>(name.value);
|
|
||||||
}
|
|
||||||
if (elementType.present) {
|
|
||||||
map['element_type'] = Variable<String>(
|
|
||||||
$BlacklistTableTable.$converterelementType.toSql(elementType.value));
|
|
||||||
}
|
|
||||||
if (elementId.present) {
|
|
||||||
map['element_id'] = Variable<String>(elementId.value);
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return (StringBuffer('BlacklistTableCompanion(')
|
|
||||||
..write('id: $id, ')
|
|
||||||
..write('name: $name, ')
|
|
||||||
..write('elementType: $elementType, ')
|
|
||||||
..write('elementId: $elementId')
|
|
||||||
..write(')'))
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class $PreferencesTableTable extends PreferencesTable
|
class $PreferencesTableTable extends PreferencesTable
|
||||||
with TableInfo<$PreferencesTableTable, PreferencesTableData> {
|
with TableInfo<$PreferencesTableTable, PreferencesTableData> {
|
||||||
@override
|
@override
|
||||||
@ -3226,7 +2956,6 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
$AppDatabaseManager get managers => $AppDatabaseManager(this);
|
||||||
late final $AuthenticationTableTable authenticationTable =
|
late final $AuthenticationTableTable authenticationTable =
|
||||||
$AuthenticationTableTable(this);
|
$AuthenticationTableTable(this);
|
||||||
late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this);
|
|
||||||
late final $PreferencesTableTable preferencesTable =
|
late final $PreferencesTableTable preferencesTable =
|
||||||
$PreferencesTableTable(this);
|
$PreferencesTableTable(this);
|
||||||
late final $ScrobblerTableTable scrobblerTable = $ScrobblerTableTable(this);
|
late final $ScrobblerTableTable scrobblerTable = $ScrobblerTableTable(this);
|
||||||
@ -3236,8 +2965,6 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
$SourceMatchTableTable(this);
|
$SourceMatchTableTable(this);
|
||||||
late final $HistoryTableTable historyTable = $HistoryTableTable(this);
|
late final $HistoryTableTable historyTable = $HistoryTableTable(this);
|
||||||
late final $LyricsTableTable lyricsTable = $LyricsTableTable(this);
|
late final $LyricsTableTable lyricsTable = $LyricsTableTable(this);
|
||||||
late final Index uniqueBlacklist = Index('unique_blacklist',
|
|
||||||
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
|
|
||||||
late final Index uniqTrackMatch = Index('uniq_track_match',
|
late final Index uniqTrackMatch = Index('uniq_track_match',
|
||||||
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)');
|
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)');
|
||||||
@override
|
@override
|
||||||
@ -3246,14 +2973,12 @@ abstract class _$AppDatabase extends GeneratedDatabase {
|
|||||||
@override
|
@override
|
||||||
List<DatabaseSchemaEntity> get allSchemaEntities => [
|
List<DatabaseSchemaEntity> get allSchemaEntities => [
|
||||||
authenticationTable,
|
authenticationTable,
|
||||||
blacklistTable,
|
|
||||||
preferencesTable,
|
preferencesTable,
|
||||||
scrobblerTable,
|
scrobblerTable,
|
||||||
skipSegmentTable,
|
skipSegmentTable,
|
||||||
sourceMatchTable,
|
sourceMatchTable,
|
||||||
historyTable,
|
historyTable,
|
||||||
lyricsTable,
|
lyricsTable,
|
||||||
uniqueBlacklist,
|
|
||||||
uniqTrackMatch
|
uniqTrackMatch
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -3395,139 +3120,6 @@ typedef $$AuthenticationTableTableProcessedTableManager = ProcessedTableManager<
|
|||||||
),
|
),
|
||||||
AuthenticationTableData,
|
AuthenticationTableData,
|
||||||
PrefetchHooks Function()>;
|
PrefetchHooks Function()>;
|
||||||
typedef $$BlacklistTableTableCreateCompanionBuilder = BlacklistTableCompanion
|
|
||||||
Function({
|
|
||||||
Value<int> id,
|
|
||||||
required String name,
|
|
||||||
required BlacklistedType elementType,
|
|
||||||
required String elementId,
|
|
||||||
});
|
|
||||||
typedef $$BlacklistTableTableUpdateCompanionBuilder = BlacklistTableCompanion
|
|
||||||
Function({
|
|
||||||
Value<int> id,
|
|
||||||
Value<String> name,
|
|
||||||
Value<BlacklistedType> elementType,
|
|
||||||
Value<String> elementId,
|
|
||||||
});
|
|
||||||
|
|
||||||
class $$BlacklistTableTableFilterComposer
|
|
||||||
extends FilterComposer<_$AppDatabase, $BlacklistTableTable> {
|
|
||||||
$$BlacklistTableTableFilterComposer(super.$state);
|
|
||||||
ColumnFilters<int> get id => $state.composableBuilder(
|
|
||||||
column: $state.table.id,
|
|
||||||
builder: (column, joinBuilders) =>
|
|
||||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
|
||||||
|
|
||||||
ColumnFilters<String> get name => $state.composableBuilder(
|
|
||||||
column: $state.table.name,
|
|
||||||
builder: (column, joinBuilders) =>
|
|
||||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
|
||||||
|
|
||||||
ColumnWithTypeConverterFilters<BlacklistedType, BlacklistedType, String>
|
|
||||||
get elementType => $state.composableBuilder(
|
|
||||||
column: $state.table.elementType,
|
|
||||||
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
|
|
||||||
column,
|
|
||||||
joinBuilders: joinBuilders));
|
|
||||||
|
|
||||||
ColumnFilters<String> get elementId => $state.composableBuilder(
|
|
||||||
column: $state.table.elementId,
|
|
||||||
builder: (column, joinBuilders) =>
|
|
||||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$BlacklistTableTableOrderingComposer
|
|
||||||
extends OrderingComposer<_$AppDatabase, $BlacklistTableTable> {
|
|
||||||
$$BlacklistTableTableOrderingComposer(super.$state);
|
|
||||||
ColumnOrderings<int> get id => $state.composableBuilder(
|
|
||||||
column: $state.table.id,
|
|
||||||
builder: (column, joinBuilders) =>
|
|
||||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
|
||||||
|
|
||||||
ColumnOrderings<String> get name => $state.composableBuilder(
|
|
||||||
column: $state.table.name,
|
|
||||||
builder: (column, joinBuilders) =>
|
|
||||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
|
||||||
|
|
||||||
ColumnOrderings<String> get elementType => $state.composableBuilder(
|
|
||||||
column: $state.table.elementType,
|
|
||||||
builder: (column, joinBuilders) =>
|
|
||||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
|
||||||
|
|
||||||
ColumnOrderings<String> get elementId => $state.composableBuilder(
|
|
||||||
column: $state.table.elementId,
|
|
||||||
builder: (column, joinBuilders) =>
|
|
||||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
|
||||||
}
|
|
||||||
|
|
||||||
class $$BlacklistTableTableTableManager extends RootTableManager<
|
|
||||||
_$AppDatabase,
|
|
||||||
$BlacklistTableTable,
|
|
||||||
BlacklistTableData,
|
|
||||||
$$BlacklistTableTableFilterComposer,
|
|
||||||
$$BlacklistTableTableOrderingComposer,
|
|
||||||
$$BlacklistTableTableCreateCompanionBuilder,
|
|
||||||
$$BlacklistTableTableUpdateCompanionBuilder,
|
|
||||||
(
|
|
||||||
BlacklistTableData,
|
|
||||||
BaseReferences<_$AppDatabase, $BlacklistTableTable, BlacklistTableData>
|
|
||||||
),
|
|
||||||
BlacklistTableData,
|
|
||||||
PrefetchHooks Function()> {
|
|
||||||
$$BlacklistTableTableTableManager(
|
|
||||||
_$AppDatabase db, $BlacklistTableTable table)
|
|
||||||
: super(TableManagerState(
|
|
||||||
db: db,
|
|
||||||
table: table,
|
|
||||||
filteringComposer:
|
|
||||||
$$BlacklistTableTableFilterComposer(ComposerState(db, table)),
|
|
||||||
orderingComposer:
|
|
||||||
$$BlacklistTableTableOrderingComposer(ComposerState(db, table)),
|
|
||||||
updateCompanionCallback: ({
|
|
||||||
Value<int> id = const Value.absent(),
|
|
||||||
Value<String> name = const Value.absent(),
|
|
||||||
Value<BlacklistedType> elementType = const Value.absent(),
|
|
||||||
Value<String> elementId = const Value.absent(),
|
|
||||||
}) =>
|
|
||||||
BlacklistTableCompanion(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
elementType: elementType,
|
|
||||||
elementId: elementId,
|
|
||||||
),
|
|
||||||
createCompanionCallback: ({
|
|
||||||
Value<int> id = const Value.absent(),
|
|
||||||
required String name,
|
|
||||||
required BlacklistedType elementType,
|
|
||||||
required String elementId,
|
|
||||||
}) =>
|
|
||||||
BlacklistTableCompanion.insert(
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
elementType: elementType,
|
|
||||||
elementId: elementId,
|
|
||||||
),
|
|
||||||
withReferenceMapper: (p0) => p0
|
|
||||||
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
|
|
||||||
.toList(),
|
|
||||||
prefetchHooksCallback: null,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef $$BlacklistTableTableProcessedTableManager = ProcessedTableManager<
|
|
||||||
_$AppDatabase,
|
|
||||||
$BlacklistTableTable,
|
|
||||||
BlacklistTableData,
|
|
||||||
$$BlacklistTableTableFilterComposer,
|
|
||||||
$$BlacklistTableTableOrderingComposer,
|
|
||||||
$$BlacklistTableTableCreateCompanionBuilder,
|
|
||||||
$$BlacklistTableTableUpdateCompanionBuilder,
|
|
||||||
(
|
|
||||||
BlacklistTableData,
|
|
||||||
BaseReferences<_$AppDatabase, $BlacklistTableTable, BlacklistTableData>
|
|
||||||
),
|
|
||||||
BlacklistTableData,
|
|
||||||
PrefetchHooks Function()>;
|
|
||||||
typedef $$PreferencesTableTableCreateCompanionBuilder
|
typedef $$PreferencesTableTableCreateCompanionBuilder
|
||||||
= PreferencesTableCompanion Function({
|
= PreferencesTableCompanion Function({
|
||||||
Value<int> id,
|
Value<int> id,
|
||||||
@ -4727,8 +4319,6 @@ class $AppDatabaseManager {
|
|||||||
$AppDatabaseManager(this._db);
|
$AppDatabaseManager(this._db);
|
||||||
$$AuthenticationTableTableTableManager get authenticationTable =>
|
$$AuthenticationTableTableTableManager get authenticationTable =>
|
||||||
$$AuthenticationTableTableTableManager(_db, _db.authenticationTable);
|
$$AuthenticationTableTableTableManager(_db, _db.authenticationTable);
|
||||||
$$BlacklistTableTableTableManager get blacklistTable =>
|
|
||||||
$$BlacklistTableTableTableManager(_db, _db.blacklistTable);
|
|
||||||
$$PreferencesTableTableTableManager get preferencesTable =>
|
$$PreferencesTableTableTableManager get preferencesTable =>
|
||||||
$$PreferencesTableTableTableManager(_db, _db.preferencesTable);
|
$$PreferencesTableTableTableManager(_db, _db.preferencesTable);
|
||||||
$$ScrobblerTableTableTableManager get scrobblerTable =>
|
$$ScrobblerTableTableTableManager get scrobblerTable =>
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
part of '../database.dart';
|
|
||||||
|
|
||||||
enum BlacklistedType {
|
|
||||||
artist,
|
|
||||||
track;
|
|
||||||
}
|
|
||||||
|
|
||||||
@TableIndex(
|
|
||||||
name: "unique_blacklist",
|
|
||||||
unique: true,
|
|
||||||
columns: {#elementType, #elementId},
|
|
||||||
)
|
|
||||||
class BlacklistTable extends Table {
|
|
||||||
IntColumn get id => integer().autoIncrement()();
|
|
||||||
TextColumn get name => text()();
|
|
||||||
TextColumn get elementType => textEnum<BlacklistedType>()();
|
|
||||||
TextColumn get elementId => text()();
|
|
||||||
}
|
|
@ -13,8 +13,7 @@ enum CloseBehavior {
|
|||||||
|
|
||||||
enum AudioSource {
|
enum AudioSource {
|
||||||
youtube,
|
youtube,
|
||||||
piped,
|
piped;
|
||||||
jiosaavn;
|
|
||||||
|
|
||||||
String get label => name[0].toUpperCase() + name.substring(1);
|
String get label => name[0].toUpperCase() + name.substring(1);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
part of '../database.dart';
|
part of '../database.dart';
|
||||||
|
|
||||||
enum SourceType {
|
enum SourceType {
|
||||||
youtube._("YouTube"),
|
youtube._('YouTube'),
|
||||||
youtubeMusic._("YouTube Music"),
|
youtubeMusic._('YouTube Music');
|
||||||
jiosaavn._("JioSaavn");
|
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
@ -11,7 +10,7 @@ enum SourceType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@TableIndex(
|
@TableIndex(
|
||||||
name: "uniq_track_match",
|
name: 'uniq_track_match',
|
||||||
columns: {#trackId, #sourceId, #sourceType},
|
columns: {#trackId, #sourceId, #sourceType},
|
||||||
unique: true,
|
unique: true,
|
||||||
)
|
)
|
||||||
|
@ -6,14 +6,14 @@ class LocaleConverter extends TypeConverter<Locale, String> {
|
|||||||
@override
|
@override
|
||||||
Locale fromSql(String fromDb) {
|
Locale fromSql(String fromDb) {
|
||||||
final rawMap = jsonDecode(fromDb) as Map<String, dynamic>;
|
final rawMap = jsonDecode(fromDb) as Map<String, dynamic>;
|
||||||
return Locale(rawMap["languageCode"], rawMap["countryCode"]);
|
return Locale(rawMap['languageCode'], rawMap['countryCode']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toSql(Locale value) {
|
String toSql(Locale value) {
|
||||||
return jsonEncode({
|
return jsonEncode({
|
||||||
"languageCode": value.languageCode,
|
'languageCode': value.languageCode,
|
||||||
"countryCode": value.countryCode,
|
'countryCode': value.countryCode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,11 @@ class StringListConverter extends TypeConverter<List<String>, String> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> fromSql(String fromDb) {
|
List<String> fromSql(String fromDb) {
|
||||||
return fromDb.split(",").where((e) => e.isNotEmpty).toList();
|
return fromDb.split(',').where((e) => e.isNotEmpty).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toSql(List<String> value) {
|
String toSql(List<String> value) {
|
||||||
return value.join(",");
|
return value.join(',');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
enum SearchMode {
|
|
||||||
youtube._('YouTube'),
|
|
||||||
youtubeMusic._('YouTube Music');
|
|
||||||
|
|
||||||
final String label;
|
|
||||||
|
|
||||||
const SearchMode._(this.label);
|
|
||||||
|
|
||||||
factory SearchMode.fromString(String key) {
|
|
||||||
return SearchMode.values.firstWhere((e) => e.name == key);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
import 'package:piped_client/piped_client.dart';
|
import 'package:piped_client/piped_client.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/models/search.dart';
|
import 'package:rhythm_box/services/database/database.dart';
|
||||||
|
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
class YoutubeVideoInfo {
|
class YoutubeVideoInfo {
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:rhythm_box/providers/user_preferences.dart';
|
||||||
|
import 'package:rhythm_box/services/database/database.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';
|
||||||
|
|
||||||
@ -40,8 +43,8 @@ abstract class SourcedTrack extends Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static SourcedTrack fromJson(Map<String, dynamic> json) {
|
static SourcedTrack fromJson(Map<String, dynamic> json) {
|
||||||
// TODO Follow user preferences
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
const audioSource = 'youtube';
|
final audioSource = preferences.audioSource;
|
||||||
|
|
||||||
final sourceInfo = SourceInfo.fromJson(json);
|
final sourceInfo = SourceInfo.fromJson(json);
|
||||||
final source = SourceMap.fromJson(json);
|
final source = SourceMap.fromJson(json);
|
||||||
@ -52,7 +55,7 @@ abstract class SourcedTrack extends Track {
|
|||||||
.cast<SourceInfo>();
|
.cast<SourceInfo>();
|
||||||
|
|
||||||
return switch (audioSource) {
|
return switch (audioSource) {
|
||||||
'piped' => PipedSourcedTrack(
|
AudioSource.piped => PipedSourcedTrack(
|
||||||
source: source,
|
source: source,
|
||||||
siblings: siblings,
|
siblings: siblings,
|
||||||
sourceInfo: sourceInfo,
|
sourceInfo: sourceInfo,
|
||||||
@ -86,12 +89,13 @@ abstract class SourcedTrack extends Track {
|
|||||||
static Future<SourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required Track track,
|
||||||
}) async {
|
}) async {
|
||||||
// TODO Follow user preferences
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
const audioSource = 'youtube';
|
final audioSource = preferences.audioSource;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return switch (audioSource) {
|
return switch (audioSource) {
|
||||||
'piped' => await PipedSourcedTrack.fetchFromTrack(track: track),
|
AudioSource.piped =>
|
||||||
|
await PipedSourcedTrack.fetchFromTrack(track: track),
|
||||||
_ => await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
_ => await YoutubeSourcedTrack.fetchFromTrack(track: track),
|
||||||
};
|
};
|
||||||
} on TrackNotFoundError catch (_) {
|
} on TrackNotFoundError catch (_) {
|
||||||
@ -110,11 +114,11 @@ abstract class SourcedTrack extends Track {
|
|||||||
static Future<List<SiblingType>> fetchSiblings({
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
required Track track,
|
required Track track,
|
||||||
}) {
|
}) {
|
||||||
// TODO Follow user preferences
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
const audioSource = 'youtube';
|
final audioSource = preferences.audioSource;
|
||||||
|
|
||||||
return switch (audioSource) {
|
return switch (audioSource) {
|
||||||
'piped' => PipedSourcedTrack.fetchSiblings(track: track),
|
AudioSource.piped => PipedSourcedTrack.fetchSiblings(track: track),
|
||||||
_ => YoutubeSourcedTrack.fetchSiblings(track: track),
|
_ => YoutubeSourcedTrack.fetchSiblings(track: track),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -128,15 +132,15 @@ abstract class SourcedTrack extends Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String get url {
|
String get url {
|
||||||
// TODO Follow user preferences
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
const streamMusicCodec = SourceCodecs.weba;
|
final streamMusicCodec = preferences.streamMusicCodec;
|
||||||
|
|
||||||
return getUrlOfCodec(streamMusicCodec);
|
return getUrlOfCodec(streamMusicCodec);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getUrlOfCodec(SourceCodecs codec) {
|
String getUrlOfCodec(SourceCodecs codec) {
|
||||||
// TODO Follow user preferences
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
const audioQuality = SourceQualities.high;
|
final audioQuality = preferences.audioQuality;
|
||||||
|
|
||||||
return source[codec]?[audioQuality] ??
|
return source[codec]?[audioQuality] ??
|
||||||
// this will ensure playback doesn't break
|
// this will ensure playback doesn't break
|
||||||
@ -145,8 +149,8 @@ abstract class SourcedTrack extends Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SourceCodecs get codec {
|
SourceCodecs get codec {
|
||||||
// TODO Follow user preferences
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
const streamMusicCodec = SourceCodecs.weba;
|
final streamMusicCodec = preferences.streamMusicCodec;
|
||||||
|
|
||||||
return streamMusicCodec;
|
return streamMusicCodec;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:get/get.dart' hide Value;
|
||||||
import 'package:piped_client/piped_client.dart';
|
import 'package:piped_client/piped_client.dart';
|
||||||
import 'package:rhythm_box/services/sourced_track/models/search.dart';
|
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:rhythm_box/services/utils.dart';
|
import 'package:rhythm_box/services/utils.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
|
|
||||||
@ -41,14 +45,35 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
static Future<SourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required Track track,
|
||||||
}) async {
|
}) async {
|
||||||
// TODO Add cache query here
|
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),
|
||||||
|
]))
|
||||||
|
.getSingleOrNull();
|
||||||
|
|
||||||
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
|
|
||||||
|
if (cachedSource == null) {
|
||||||
final siblings = await fetchSiblings(track: track);
|
final siblings = await fetchSiblings(track: track);
|
||||||
if (siblings.isEmpty) {
|
if (siblings.isEmpty) {
|
||||||
throw TrackNotFoundError(track);
|
throw TrackNotFoundError(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Insert to cache here
|
await db.database.into(db.database.sourceMatchTable).insert(
|
||||||
|
SourceMatchTableCompanion.insert(
|
||||||
|
trackId: track.id!,
|
||||||
|
sourceId: siblings.first.info.id,
|
||||||
|
sourceType: Value(
|
||||||
|
preferences.searchMode == SearchMode.youtube
|
||||||
|
? SourceType.youtube
|
||||||
|
: SourceType.youtubeMusic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return PipedSourcedTrack(
|
return PipedSourcedTrack(
|
||||||
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
||||||
@ -56,6 +81,26 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
sourceInfo: siblings.first.info,
|
sourceInfo: siblings.first.info,
|
||||||
track: track,
|
track: track,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
final client = _getClient();
|
||||||
|
final manifest = await client.streams(cachedSource.sourceId);
|
||||||
|
|
||||||
|
return PipedSourcedTrack(
|
||||||
|
siblings: [],
|
||||||
|
source: toSourceMap(manifest),
|
||||||
|
sourceInfo: PipedSourceInfo(
|
||||||
|
id: manifest.id,
|
||||||
|
artist: manifest.uploader,
|
||||||
|
artistUrl: manifest.uploaderUrl,
|
||||||
|
pageUrl: 'https://www.youtube.com/watch?v=${manifest.id}',
|
||||||
|
thumbnail: manifest.thumbnailUrl,
|
||||||
|
title: manifest.title,
|
||||||
|
duration: manifest.duration,
|
||||||
|
album: null,
|
||||||
|
),
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static SourceMap toSourceMap(PipedStreamResponse manifest) {
|
static SourceMap toSourceMap(PipedStreamResponse manifest) {
|
||||||
@ -114,11 +159,10 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
required Track track,
|
required Track track,
|
||||||
}) async {
|
}) async {
|
||||||
final pipedClient = _getClient();
|
final pipedClient = _getClient();
|
||||||
|
final preferences = Get.find<UserPreferencesProvider>().state.value;
|
||||||
|
|
||||||
// TODO Allow user search with normal youtube video (`youtube`)
|
final searchMode = preferences.searchMode;
|
||||||
const searchMode = SearchMode.youtubeMusic;
|
final audioSource = preferences.audioSource;
|
||||||
// TODO Follow user preferences
|
|
||||||
const audioSource = 'youtube';
|
|
||||||
|
|
||||||
final query = SourcedTrack.getSearchTerm(track);
|
final query = SourcedTrack.getSearchTerm(track);
|
||||||
|
|
||||||
@ -130,8 +174,9 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// when falling back to piped API make sure to use the YouTube mode
|
// when falling back to piped API make sure to use the YouTube mode
|
||||||
const isYouTubeMusic =
|
final isYouTubeMusic = audioSource != AudioSource.piped
|
||||||
audioSource != 'piped' ? false : searchMode == SearchMode.youtubeMusic;
|
? false
|
||||||
|
: searchMode == SearchMode.youtubeMusic;
|
||||||
|
|
||||||
if (isYouTubeMusic) {
|
if (isYouTubeMusic) {
|
||||||
final artists = (track.artists ?? [])
|
final artists = (track.artists ?? [])
|
||||||
@ -227,7 +272,18 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
final manifest = await pipedClient.streams(newSourceInfo.id);
|
final manifest = await pipedClient.streams(newSourceInfo.id);
|
||||||
|
|
||||||
// TODO Save to cache here
|
final DatabaseProvider db = Get.find();
|
||||||
|
await db.database.into(db.database.sourceMatchTable).insert(
|
||||||
|
SourceMatchTableCompanion.insert(
|
||||||
|
trackId: id!,
|
||||||
|
sourceId: newSourceInfo.id,
|
||||||
|
sourceType: const Value(SourceType.youtube),
|
||||||
|
// 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 PipedSourcedTrack(
|
return PipedSourcedTrack(
|
||||||
siblings: newSiblings,
|
siblings: newSiblings,
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:get/get.dart' hide Value;
|
||||||
import 'package:http/http.dart';
|
import 'package:http/http.dart';
|
||||||
|
import 'package:rhythm_box/providers/database.dart';
|
||||||
|
import 'package:rhythm_box/services/database/database.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';
|
||||||
import 'package:rhythm_box/services/song_link/song_link.dart';
|
import 'package:rhythm_box/services/song_link/song_link.dart';
|
||||||
@ -43,14 +47,30 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
static Future<YoutubeSourcedTrack> fetchFromTrack({
|
static Future<YoutubeSourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required Track track,
|
||||||
}) async {
|
}) async {
|
||||||
// TODO Add cache query here
|
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.youtube) {
|
||||||
final siblings = await fetchSiblings(track: track);
|
final siblings = await fetchSiblings(track: track);
|
||||||
if (siblings.isEmpty) {
|
if (siblings.isEmpty) {
|
||||||
throw TrackNotFoundError(track);
|
throw TrackNotFoundError(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Save to cache here
|
await db.database.into(db.database.sourceMatchTable).insert(
|
||||||
|
SourceMatchTableCompanion.insert(
|
||||||
|
trackId: track.id!,
|
||||||
|
sourceId: siblings.first.info.id,
|
||||||
|
sourceType: const Value(SourceType.youtube),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return YoutubeSourcedTrack(
|
return YoutubeSourcedTrack(
|
||||||
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
||||||
@ -60,6 +80,32 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final item = await youtubeClient.videos.get(cachedSource.sourceId);
|
||||||
|
final manifest = await youtubeClient.videos.streamsClient
|
||||||
|
.getManifest(
|
||||||
|
cachedSource.sourceId,
|
||||||
|
)
|
||||||
|
.timeout(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
onTimeout: () => throw ClientException('Timeout'),
|
||||||
|
);
|
||||||
|
return YoutubeSourcedTrack(
|
||||||
|
siblings: [],
|
||||||
|
source: toSourceMap(manifest),
|
||||||
|
sourceInfo: YoutubeSourceInfo(
|
||||||
|
id: item.id.value,
|
||||||
|
artist: item.author,
|
||||||
|
artistUrl: 'https://www.youtube.com/channel/${item.channelId}',
|
||||||
|
pageUrl: item.url,
|
||||||
|
thumbnail: item.thumbnails.highResUrl,
|
||||||
|
title: item.title,
|
||||||
|
duration: item.duration ?? Duration.zero,
|
||||||
|
album: null,
|
||||||
|
),
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static SourceMap toSourceMap(StreamManifest manifest) {
|
static SourceMap toSourceMap(StreamManifest manifest) {
|
||||||
var m4a = manifest.audioOnly
|
var m4a = manifest.audioOnly
|
||||||
.where((audio) => audio.codec.mimeType == 'audio/mp4')
|
.where((audio) => audio.codec.mimeType == 'audio/mp4')
|
||||||
@ -243,7 +289,19 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
onTimeout: () => throw ClientException('Timeout'),
|
onTimeout: () => throw ClientException('Timeout'),
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO Save to cache here
|
final DatabaseProvider db = Get.find();
|
||||||
|
|
||||||
|
await db.database.into(db.database.sourceMatchTable).insert(
|
||||||
|
SourceMatchTableCompanion.insert(
|
||||||
|
trackId: id!,
|
||||||
|
sourceId: newSourceInfo.id,
|
||||||
|
sourceType: const Value(SourceType.youtube),
|
||||||
|
// 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 YoutubeSourcedTrack(
|
return YoutubeSourcedTrack(
|
||||||
siblings: newSiblings,
|
siblings: newSiblings,
|
||||||
|
@ -23,4 +23,11 @@ class AutoCacheImage extends StatelessWidget {
|
|||||||
height: height,
|
height: height,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ImageProvider provider(String url) {
|
||||||
|
if (PlatformInfo.canCacheImage) {
|
||||||
|
return CachedNetworkImageProvider(url);
|
||||||
|
}
|
||||||
|
return NetworkImage(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
17
pubspec.lock
17
pubspec.lock
@ -837,6 +837,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
|
palette_generator:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: palette_generator
|
||||||
|
sha256: d50fbcd69abb80c5baec66d700033b1a320108b1aa17a5961866a12c0abb7c0c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.3+4"
|
||||||
path:
|
path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -997,6 +1005,15 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.9"
|
version: "0.1.9"
|
||||||
|
scrobblenaut:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: dart-3-support
|
||||||
|
resolved-ref: d90cb75d71737f3cfa2de4469d48080c0f3eedc2
|
||||||
|
url: "https://github.com/KRTirtho/scrobblenaut.git"
|
||||||
|
source: git
|
||||||
|
version: "3.0.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -76,6 +76,11 @@ dependencies:
|
|||||||
path_provider: ^2.1.4
|
path_provider: ^2.1.4
|
||||||
sqlite3: ^2.4.6
|
sqlite3: ^2.4.6
|
||||||
sqlite3_flutter_libs: ^0.5.24
|
sqlite3_flutter_libs: ^0.5.24
|
||||||
|
palette_generator: ^0.3.3+4
|
||||||
|
scrobblenaut:
|
||||||
|
git:
|
||||||
|
url: https://github.com/KRTirtho/scrobblenaut.git
|
||||||
|
ref: dart-3-support
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user