✨ Impl more features (clean up 20+ todo)
⚡ Add query cache
This commit is contained in:
@ -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: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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user