Impl more features (clean up 20+ todo)

 Add query cache
This commit is contained in:
LittleSheep 2024-08-27 16:37:31 +08:00
parent a162619a88
commit 2e17078fea
24 changed files with 907 additions and 503 deletions

View File

@ -2,7 +2,14 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:media_kit/media_kit.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/user_preferences.dart';
import 'package:rhythm_box/router.dart';
import 'package:rhythm_box/services/server/active_sourced_track.dart';
import 'package:rhythm_box/services/server/routes/playback.dart';
@ -51,7 +58,16 @@ class MyApp extends StatelessWidget {
void _initializeProviders(BuildContext context) async {
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(QueryingTrackInfoProvider());

View File

@ -9,7 +9,7 @@ abstract class PlatformInfo {
static bool get isLinux => !kIsWeb && Platform.isLinux;
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;

View 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
});
}
}

View 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();
}

View 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();
}
}

View 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;
}
}

View 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();
}
}

View 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
}
}

View 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)));
}
}

View File

@ -21,7 +21,6 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
part 'database.g.dart';
part 'tables/authentication.dart';
part 'tables/blacklist.dart';
part 'tables/preferences.dart';
part 'tables/scrobbler.dart';
part 'tables/skip_segment.dart';
@ -39,7 +38,6 @@ part 'typeconverters/subtitle.dart';
@DriftDatabase(
tables: [
AuthenticationTable,
BlacklistTable,
PreferencesTable,
ScrobblerTable,
SkipSegmentTable,

View File

@ -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
with TableInfo<$PreferencesTableTable, PreferencesTableData> {
@override
@ -3226,7 +2956,6 @@ abstract class _$AppDatabase extends GeneratedDatabase {
$AppDatabaseManager get managers => $AppDatabaseManager(this);
late final $AuthenticationTableTable authenticationTable =
$AuthenticationTableTable(this);
late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this);
late final $PreferencesTableTable preferencesTable =
$PreferencesTableTable(this);
late final $ScrobblerTableTable scrobblerTable = $ScrobblerTableTable(this);
@ -3236,8 +2965,6 @@ abstract class _$AppDatabase extends GeneratedDatabase {
$SourceMatchTableTable(this);
late final $HistoryTableTable historyTable = $HistoryTableTable(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',
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)');
@override
@ -3246,14 +2973,12 @@ abstract class _$AppDatabase extends GeneratedDatabase {
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [
authenticationTable,
blacklistTable,
preferencesTable,
scrobblerTable,
skipSegmentTable,
sourceMatchTable,
historyTable,
lyricsTable,
uniqueBlacklist,
uniqTrackMatch
];
}
@ -3395,139 +3120,6 @@ typedef $$AuthenticationTableTableProcessedTableManager = ProcessedTableManager<
),
AuthenticationTableData,
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
= PreferencesTableCompanion Function({
Value<int> id,
@ -4727,8 +4319,6 @@ class $AppDatabaseManager {
$AppDatabaseManager(this._db);
$$AuthenticationTableTableTableManager get authenticationTable =>
$$AuthenticationTableTableTableManager(_db, _db.authenticationTable);
$$BlacklistTableTableTableManager get blacklistTable =>
$$BlacklistTableTableTableManager(_db, _db.blacklistTable);
$$PreferencesTableTableTableManager get preferencesTable =>
$$PreferencesTableTableTableManager(_db, _db.preferencesTable);
$$ScrobblerTableTableTableManager get scrobblerTable =>

View File

@ -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()();
}

View File

@ -13,8 +13,7 @@ enum CloseBehavior {
enum AudioSource {
youtube,
piped,
jiosaavn;
piped;
String get label => name[0].toUpperCase() + name.substring(1);
}

View File

@ -1,9 +1,8 @@
part of '../database.dart';
enum SourceType {
youtube._("YouTube"),
youtubeMusic._("YouTube Music"),
jiosaavn._("JioSaavn");
youtube._('YouTube'),
youtubeMusic._('YouTube Music');
final String label;
@ -11,7 +10,7 @@ enum SourceType {
}
@TableIndex(
name: "uniq_track_match",
name: 'uniq_track_match',
columns: {#trackId, #sourceId, #sourceType},
unique: true,
)

View File

@ -6,14 +6,14 @@ class LocaleConverter extends TypeConverter<Locale, String> {
@override
Locale fromSql(String fromDb) {
final rawMap = jsonDecode(fromDb) as Map<String, dynamic>;
return Locale(rawMap["languageCode"], rawMap["countryCode"]);
return Locale(rawMap['languageCode'], rawMap['countryCode']);
}
@override
String toSql(Locale value) {
return jsonEncode({
"languageCode": value.languageCode,
"countryCode": value.countryCode,
'languageCode': value.languageCode,
'countryCode': value.countryCode,
});
}
}

View File

@ -5,11 +5,11 @@ class StringListConverter extends TypeConverter<List<String>, String> {
@override
List<String> fromSql(String fromDb) {
return fromDb.split(",").where((e) => e.isNotEmpty).toList();
return fromDb.split(',').where((e) => e.isNotEmpty).toList();
}
@override
String toSql(List<String> value) {
return value.join(",");
return value.join(',');
}
}

View File

@ -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);
}
}

View File

@ -1,6 +1,5 @@
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';
class YoutubeVideoInfo {

View File

@ -1,4 +1,7 @@
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:spotify/spotify.dart';
@ -40,8 +43,8 @@ abstract class SourcedTrack extends Track {
}
static SourcedTrack fromJson(Map<String, dynamic> json) {
// TODO Follow user preferences
const audioSource = 'youtube';
final preferences = Get.find<UserPreferencesProvider>().state.value;
final audioSource = preferences.audioSource;
final sourceInfo = SourceInfo.fromJson(json);
final source = SourceMap.fromJson(json);
@ -52,7 +55,7 @@ abstract class SourcedTrack extends Track {
.cast<SourceInfo>();
return switch (audioSource) {
'piped' => PipedSourcedTrack(
AudioSource.piped => PipedSourcedTrack(
source: source,
siblings: siblings,
sourceInfo: sourceInfo,
@ -86,12 +89,13 @@ abstract class SourcedTrack extends Track {
static Future<SourcedTrack> fetchFromTrack({
required Track track,
}) async {
// TODO Follow user preferences
const audioSource = 'youtube';
final preferences = Get.find<UserPreferencesProvider>().state.value;
final audioSource = preferences.audioSource;
try {
return switch (audioSource) {
'piped' => await PipedSourcedTrack.fetchFromTrack(track: track),
AudioSource.piped =>
await PipedSourcedTrack.fetchFromTrack(track: track),
_ => await YoutubeSourcedTrack.fetchFromTrack(track: track),
};
} on TrackNotFoundError catch (_) {
@ -110,11 +114,11 @@ abstract class SourcedTrack extends Track {
static Future<List<SiblingType>> fetchSiblings({
required Track track,
}) {
// TODO Follow user preferences
const audioSource = 'youtube';
final preferences = Get.find<UserPreferencesProvider>().state.value;
final audioSource = preferences.audioSource;
return switch (audioSource) {
'piped' => PipedSourcedTrack.fetchSiblings(track: track),
AudioSource.piped => PipedSourcedTrack.fetchSiblings(track: track),
_ => YoutubeSourcedTrack.fetchSiblings(track: track),
};
}
@ -128,15 +132,15 @@ abstract class SourcedTrack extends Track {
}
String get url {
// TODO Follow user preferences
const streamMusicCodec = SourceCodecs.weba;
final preferences = Get.find<UserPreferencesProvider>().state.value;
final streamMusicCodec = preferences.streamMusicCodec;
return getUrlOfCodec(streamMusicCodec);
}
String getUrlOfCodec(SourceCodecs codec) {
// TODO Follow user preferences
const audioQuality = SourceQualities.high;
final preferences = Get.find<UserPreferencesProvider>().state.value;
final audioQuality = preferences.audioQuality;
return source[codec]?[audioQuality] ??
// this will ensure playback doesn't break
@ -145,8 +149,8 @@ abstract class SourcedTrack extends Track {
}
SourceCodecs get codec {
// TODO Follow user preferences
const streamMusicCodec = SourceCodecs.weba;
final preferences = Get.find<UserPreferencesProvider>().state.value;
final streamMusicCodec = preferences.streamMusicCodec;
return streamMusicCodec;
}

View File

@ -1,6 +1,10 @@
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: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:spotify/spotify.dart';
@ -41,21 +45,62 @@ class PipedSourcedTrack extends SourcedTrack {
static Future<SourcedTrack> fetchFromTrack({
required Track track,
}) 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 siblings = await fetchSiblings(track: track);
if (siblings.isEmpty) {
throw TrackNotFoundError(track);
final preferences = Get.find<UserPreferencesProvider>().state.value;
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: Value(
preferences.searchMode == SearchMode.youtube
? SourceType.youtube
: SourceType.youtubeMusic,
),
),
);
return PipedSourcedTrack(
siblings: siblings.map((s) => s.info).skip(1).toList(),
source: siblings.first.source as SourceMap,
sourceInfo: siblings.first.info,
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,
);
}
// TODO Insert to cache here
return PipedSourcedTrack(
siblings: siblings.map((s) => s.info).skip(1).toList(),
source: siblings.first.source as SourceMap,
sourceInfo: siblings.first.info,
track: track,
);
}
static SourceMap toSourceMap(PipedStreamResponse manifest) {
@ -114,11 +159,10 @@ class PipedSourcedTrack extends SourcedTrack {
required Track track,
}) async {
final pipedClient = _getClient();
final preferences = Get.find<UserPreferencesProvider>().state.value;
// TODO Allow user search with normal youtube video (`youtube`)
const searchMode = SearchMode.youtubeMusic;
// TODO Follow user preferences
const audioSource = 'youtube';
final searchMode = preferences.searchMode;
final audioSource = preferences.audioSource;
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
const isYouTubeMusic =
audioSource != 'piped' ? false : searchMode == SearchMode.youtubeMusic;
final isYouTubeMusic = audioSource != AudioSource.piped
? false
: searchMode == SearchMode.youtubeMusic;
if (isYouTubeMusic) {
final artists = (track.artists ?? [])
@ -227,7 +272,18 @@ class PipedSourcedTrack extends SourcedTrack {
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(
siblings: newSiblings,

View File

@ -1,7 +1,11 @@
import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:get/get.dart' hide Value;
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:spotify/spotify.dart';
import 'package:rhythm_box/services/song_link/song_link.dart';
@ -43,19 +47,61 @@ class YoutubeSourcedTrack extends SourcedTrack {
static Future<YoutubeSourcedTrack> fetchFromTrack({
required Track track,
}) 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);
final siblings = await fetchSiblings(track: track);
if (siblings.isEmpty) {
throw TrackNotFoundError(track);
if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) {
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.youtube),
),
);
return YoutubeSourcedTrack(
siblings: siblings.map((s) => s.info).skip(1).toList(),
source: siblings.first.source as SourceMap,
sourceInfo: siblings.first.info,
track: track,
);
}
// TODO Save to cache here
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: siblings.map((s) => s.info).skip(1).toList(),
source: siblings.first.source as SourceMap,
sourceInfo: siblings.first.info,
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,
);
}
@ -243,7 +289,19 @@ class YoutubeSourcedTrack extends SourcedTrack {
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(
siblings: newSiblings,

View File

@ -23,4 +23,11 @@ class AutoCacheImage extends StatelessWidget {
height: height,
);
}
static ImageProvider provider(String url) {
if (PlatformInfo.canCacheImage) {
return CachedNetworkImageProvider(url);
}
return NetworkImage(url);
}
}

View File

@ -837,6 +837,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@ -997,6 +1005,15 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:

View File

@ -76,6 +76,11 @@ dependencies:
path_provider: ^2.1.4
sqlite3: ^2.4.6
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:
flutter_test: