Compare commits

...

3 Commits

Author SHA1 Message Date
Kingkor Roy Tirtho
4b5108e54e fix: streaming not working 2025-11-03 21:27:06 +06:00
Kingkor Roy Tirtho
6311831902 feat: move away from track source query and preferences audio quality and codec 2025-11-03 19:33:47 +06:00
Kingkor Roy Tirtho
99a84aa6dc chore: create sourced track from active audio source plugin 2025-11-03 13:32:48 +06:00
55 changed files with 5110 additions and 4618 deletions

File diff suppressed because one or more lines are too long

View File

@ -7,8 +7,7 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart';
import 'package:spotube/provider/server/track_sources.dart';
class TrackDetailsDialog extends HookConsumerWidget { class TrackDetailsDialog extends HookConsumerWidget {
final SpotubeFullTrackObject track; final SpotubeFullTrackObject track;
@ -21,8 +20,7 @@ class TrackDetailsDialog extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final sourcedTrack = final sourcedTrack = ref.read(sourcedTrackProvider(track));
ref.read(trackSourcesProvider(TrackSourceQuery.fromTrack(track)));
final detailsMap = { final detailsMap = {
context.l10n.title: track.name, context.l10n.title: track.name,
@ -39,8 +37,7 @@ class TrackDetailsDialog extends HookConsumerWidget {
// style: const TextStyle(color: Colors.blue), // style: const TextStyle(color: Colors.blue),
// ), // ),
context.l10n.duration: sourcedTrack.asData != null context.l10n.duration: sourcedTrack.asData != null
? Duration(milliseconds: sourcedTrack.asData!.value.info.durationMs) ? sourcedTrack.asData!.value.info.duration.toHumanReadableString()
.toHumanReadableString()
: Duration(milliseconds: track.durationMs).toHumanReadableString(), : Duration(milliseconds: track.durationMs).toHumanReadableString(),
if (track.album.releaseDate != null) if (track.album.releaseDate != null)
context.l10n.released: track.album.releaseDate, context.l10n.released: track.album.releaseDate,
@ -57,7 +54,7 @@ class TrackDetailsDialog extends HookConsumerWidget {
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
context.l10n.channel: Text(sourceInfo.artists), context.l10n.channel: Text(sourceInfo.artists.join(", ")),
if (sourcedTrack.asData?.value.url != null) if (sourcedTrack.asData?.value.url != null)
context.l10n.streamUrl: Hyperlink( context.l10n.streamUrl: Hyperlink(
sourcedTrack.asData!.value.url ?? "", sourcedTrack.asData!.value.url ?? "",

View File

@ -8,12 +8,10 @@ import 'package:spotube/components/dialogs/playlist_add_track_dialog.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_props.dart';
import 'package:spotube/components/track_presentation/presentation_state.dart'; import 'package:spotube/components/track_presentation/presentation_state.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
ToastOverlay showToastForAction( ToastOverlay showToastForAction(
BuildContext context, BuildContext context,
@ -70,8 +68,6 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
final downloader = ref.watch(downloadManagerProvider.notifier); final downloader = ref.watch(downloadManagerProvider.notifier);
final playlistNotifier = ref.watch(audioPlayerProvider.notifier); final playlistNotifier = ref.watch(audioPlayerProvider.notifier);
final historyNotifier = ref.watch(playbackHistoryActionsProvider); final historyNotifier = ref.watch(playbackHistoryActionsProvider);
final audioSource =
ref.watch(userPreferencesProvider.select((s) => s.audioSource));
final state = ref.watch(presentationStateProvider(options.collection)); final state = ref.watch(presentationStateProvider(options.collection));
final notifier = final notifier =
@ -85,14 +81,13 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
}) async { }) async {
final fullTrackObjects = final fullTrackObjects =
tracks.whereType<SpotubeFullTrackObject>().toList(); tracks.whereType<SpotubeFullTrackObject>().toList();
final confirmed = audioSource == AudioSource.piped || final confirmed = await showDialog<bool>(
(await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) {
return const ConfirmDownloadDialog(); return const ConfirmDownloadDialog();
}, },
) ?? ) ??
false); false;
if (confirmed != true) return; if (confirmed != true) return;
downloader.batchAddToQueue(fullTrackObjects); downloader.batchAddToQueue(fullTrackObjects);
notifier.deselectAllTracks(); notifier.deselectAllTracks();

View File

@ -16,10 +16,10 @@ import 'package:spotube/models/metadata/market.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/services/kv_store/encrypted_kv_store.dart'; import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:flutter/widgets.dart' hide Table, Key, View;
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/newpipe_engine.dart';
import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart';
import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart';
@ -65,7 +65,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());
@override @override
int get schemaVersion => 9; int get schemaVersion => 10;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@ -211,6 +211,28 @@ class AppDatabase extends _$AppDatabase {
pluginsTable.selectedForAudioSource, pluginsTable.selectedForAudioSource,
); );
}, },
from9To10: (m, schema) async {
try {
await m
.dropColumn(schema.preferencesTable, "piped_instance")
.catchError((e) {});
await m
.dropColumn(schema.preferencesTable, "invidious_instance")
.catchError((e) {});
await m
.addColumn(
schema.sourceMatchTable,
sourceMatchTable.sourceInfo,
)
.catchError((e) {});
await m
.dropColumn(schema.sourceMatchTable, "source_id")
.catchError((e) {});
} catch (e) {
AppLogger.log.e(e);
return;
}
},
), ),
); );
} }

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,6 @@ import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/market.dart'; import 'package:spotube/models/metadata/market.dart';
import 'package:spotube/services/sourced_track/enums.dart';
// GENERATED BY drift_dev, DO NOT MODIFY. // GENERATED BY drift_dev, DO NOT MODIFY.
final class Schema2 extends i0.VersionedSchema { final class Schema2 extends i0.VersionedSchema {
@ -330,8 +329,7 @@ class Shape2 extends i0.VersionedTable {
i1.GeneratedColumn<String> _column_7(String aliasedName) => i1.GeneratedColumn<String> _column_7(String aliasedName) =>
i1.GeneratedColumn<String>('audio_quality', aliasedName, false, i1.GeneratedColumn<String>('audio_quality', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string, defaultValue: Constant("high"));
defaultValue: Constant(SourceQualities.high.name));
i1.GeneratedColumn<bool> _column_8(String aliasedName) => i1.GeneratedColumn<bool> _column_8(String aliasedName) =>
i1.GeneratedColumn<bool>('album_color_sync', aliasedName, false, i1.GeneratedColumn<bool>('album_color_sync', aliasedName, false,
type: i1.DriftSqlType.bool, type: i1.DriftSqlType.bool,
@ -418,16 +416,13 @@ i1.GeneratedColumn<String> _column_25(String aliasedName) =>
defaultValue: Constant(ThemeMode.system.name)); defaultValue: Constant(ThemeMode.system.name));
i1.GeneratedColumn<String> _column_26(String aliasedName) => i1.GeneratedColumn<String> _column_26(String aliasedName) =>
i1.GeneratedColumn<String>('audio_source', aliasedName, false, i1.GeneratedColumn<String>('audio_source', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string, defaultValue: Constant("youtube"));
defaultValue: Constant(AudioSource.youtube.name));
i1.GeneratedColumn<String> _column_27(String aliasedName) => i1.GeneratedColumn<String> _column_27(String aliasedName) =>
i1.GeneratedColumn<String>('stream_music_codec', aliasedName, false, i1.GeneratedColumn<String>('stream_music_codec', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string, defaultValue: Constant("weba"));
defaultValue: Constant(SourceCodecs.weba.name));
i1.GeneratedColumn<String> _column_28(String aliasedName) => i1.GeneratedColumn<String> _column_28(String aliasedName) =>
i1.GeneratedColumn<String>('download_music_codec', aliasedName, false, i1.GeneratedColumn<String>('download_music_codec', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string, defaultValue: Constant("m4a"));
defaultValue: Constant(SourceCodecs.m4a.name));
i1.GeneratedColumn<bool> _column_29(String aliasedName) => i1.GeneratedColumn<bool> _column_29(String aliasedName) =>
i1.GeneratedColumn<bool>('discord_presence', aliasedName, false, i1.GeneratedColumn<bool>('discord_presence', aliasedName, false,
type: i1.DriftSqlType.bool, type: i1.DriftSqlType.bool,
@ -512,8 +507,7 @@ i1.GeneratedColumn<String> _column_38(String aliasedName) =>
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
i1.GeneratedColumn<String> _column_39(String aliasedName) => i1.GeneratedColumn<String> _column_39(String aliasedName) =>
i1.GeneratedColumn<String>('source_type', aliasedName, false, i1.GeneratedColumn<String>('source_type', aliasedName, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string, defaultValue: Constant("youtube"));
defaultValue: Constant(SourceType.youtube.name));
class Shape6 extends i0.VersionedTable { class Shape6 extends i0.VersionedTable {
Shape6({required super.source, required super.alias}) : super.aliased(); Shape6({required super.source, required super.alias}) : super.aliased();
@ -2462,6 +2456,289 @@ i1.GeneratedColumn<bool> _column_72(String aliasedName) =>
i1.GeneratedColumn<String> _column_73(String aliasedName) => i1.GeneratedColumn<String> _column_73(String aliasedName) =>
i1.GeneratedColumn<String>('plugin_api_version', aliasedName, false, i1.GeneratedColumn<String>('plugin_api_version', aliasedName, false,
type: i1.DriftSqlType.string, defaultValue: const Constant('2.0.0')); type: i1.DriftSqlType.string, defaultValue: const Constant('2.0.0'));
final class Schema10 extends i0.VersionedSchema {
Schema10({required super.database}) : super(version: 10);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
authenticationTable,
blacklistTable,
preferencesTable,
scrobblerTable,
skipSegmentTable,
sourceMatchTable,
audioPlayerStateTable,
historyTable,
lyricsTable,
pluginsTable,
uniqueBlacklist,
uniqTrackMatch,
];
late final Shape0 authenticationTable = Shape0(
source: i0.VersionedTable(
entityName: 'authentication_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 blacklistTable = Shape1(
source: i0.VersionedTable(
entityName: 'blacklist_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_4,
_column_5,
_column_6,
],
attachedDatabase: database,
),
alias: null);
late final Shape17 preferencesTable = Shape17(
source: i0.VersionedTable(
entityName: 'preferences_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_8,
_column_9,
_column_10,
_column_11,
_column_12,
_column_13,
_column_14,
_column_15,
_column_69,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_22,
_column_25,
_column_74,
_column_54,
_column_29,
_column_30,
_column_31,
_column_56,
_column_53,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 scrobblerTable = Shape3(
source: i0.VersionedTable(
entityName: 'scrobbler_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_33,
_column_34,
],
attachedDatabase: database,
),
alias: null);
late final Shape4 skipSegmentTable = Shape4(
source: i0.VersionedTable(
entityName: 'skip_segment_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_35,
_column_36,
_column_37,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape18 sourceMatchTable = Shape18(
source: i0.VersionedTable(
entityName: 'source_match_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_75,
_column_76,
_column_32,
],
attachedDatabase: database,
),
alias: null);
late final Shape14 audioPlayerStateTable = Shape14(
source: i0.VersionedTable(
entityName: 'audio_player_state_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_40,
_column_41,
_column_42,
_column_43,
_column_57,
_column_58,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 historyTable = Shape9(
source: i0.VersionedTable(
entityName: 'history_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_32,
_column_50,
_column_51,
_column_52,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 lyricsTable = Shape10(
source: i0.VersionedTable(
entityName: 'lyrics_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_37,
_column_52,
],
attachedDatabase: database,
),
alias: null);
late final Shape16 pluginsTable = Shape16(
source: i0.VersionedTable(
entityName: 'plugins_table',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_59,
_column_60,
_column_61,
_column_62,
_column_63,
_column_64,
_column_65,
_column_71,
_column_72,
_column_67,
_column_73,
],
attachedDatabase: database,
),
alias: null);
final i1.Index uniqueBlacklist = i1.Index('unique_blacklist',
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
final i1.Index uniqTrackMatch = i1.Index('uniq_track_match',
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_info, source_type)');
}
class Shape17 extends i0.VersionedTable {
Shape17({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<bool> get albumColorSync =>
columnsByName['album_color_sync']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get amoledDarkTheme =>
columnsByName['amoled_dark_theme']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get checkUpdate =>
columnsByName['check_update']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get normalizeAudio =>
columnsByName['normalize_audio']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get showSystemTrayIcon =>
columnsByName['show_system_tray_icon']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get systemTitleBar =>
columnsByName['system_title_bar']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get skipNonMusic =>
columnsByName['skip_non_music']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<String> get closeBehavior =>
columnsByName['close_behavior']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get accentColorScheme =>
columnsByName['accent_color_scheme']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get layoutMode =>
columnsByName['layout_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get locale =>
columnsByName['locale']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get market =>
columnsByName['market']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get searchMode =>
columnsByName['search_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get downloadLocation =>
columnsByName['download_location']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get localLibraryLocation =>
columnsByName['local_library_location']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get themeMode =>
columnsByName['theme_mode']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get audioSourceId =>
columnsByName['audio_source_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get youtubeClientEngine =>
columnsByName['youtube_client_engine']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get discordPresence =>
columnsByName['discord_presence']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get endlessPlayback =>
columnsByName['endless_playback']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<bool> get enableConnect =>
columnsByName['enable_connect']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<int> get connectPort =>
columnsByName['connect_port']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<bool> get cacheMusic =>
columnsByName['cache_music']! as i1.GeneratedColumn<bool>;
}
i1.GeneratedColumn<String> _column_74(String aliasedName) =>
i1.GeneratedColumn<String>('audio_source_id', aliasedName, true,
type: i1.DriftSqlType.string);
class Shape18 extends i0.VersionedTable {
Shape18({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<int> get id =>
columnsByName['id']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get trackId =>
columnsByName['track_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get sourceInfo =>
columnsByName['source_info']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get sourceType =>
columnsByName['source_type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
}
i1.GeneratedColumn<String> _column_75(String aliasedName) =>
i1.GeneratedColumn<String>('source_info', aliasedName, false,
type: i1.DriftSqlType.string, defaultValue: const Constant("{}"));
i1.GeneratedColumn<String> _column_76(String aliasedName) =>
i1.GeneratedColumn<String>('source_type', aliasedName, false,
type: i1.DriftSqlType.string);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -2471,6 +2748,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -2514,6 +2792,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from8To9(migrator, schema); await from8To9(migrator, schema);
return 9; return 9;
case 9:
final schema = Schema10(database: database);
final migrator = i1.Migrator(database, schema);
await from9To10(migrator, schema);
return 10;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -2529,6 +2812,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) => }) =>
i0.VersionedSchema.stepByStepHelper( i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
@ -2540,4 +2824,5 @@ i1.OnUpgrade stepByStep({
from6To7: from6To7, from6To7: from6To7,
from7To8: from7To8, from7To8: from7To8,
from8To9: from8To9, from8To9: from8To9,
from9To10: from9To10,
)); ));

View File

@ -11,17 +11,6 @@ enum CloseBehavior {
close, close,
} }
enum AudioSource {
youtube("YouTube"),
piped("Piped"),
jiosaavn("JioSaavn"),
invidious("Invidious"),
dabMusic("DAB Music");
final String label;
const AudioSource(this.label);
}
enum YoutubeClientEngine { enum YoutubeClientEngine {
ytDlp("yt-dlp"), ytDlp("yt-dlp"),
youtubeExplode("YouTubeExplode"), youtubeExplode("YouTubeExplode"),
@ -56,8 +45,6 @@ enum SearchMode {
class PreferencesTable extends Table { class PreferencesTable extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
TextColumn get audioQuality => textEnum<SourceQualities>()
.withDefault(Constant(SourceQualities.high.name))();
BoolColumn get albumColorSync => BoolColumn get albumColorSync =>
boolean().withDefault(const Constant(true))(); boolean().withDefault(const Constant(true))();
BoolColumn get amoledDarkTheme => BoolColumn get amoledDarkTheme =>
@ -89,20 +76,11 @@ class PreferencesTable extends Table {
TextColumn get downloadLocation => text().withDefault(const Constant(""))(); TextColumn get downloadLocation => text().withDefault(const Constant(""))();
TextColumn get localLibraryLocation => TextColumn get localLibraryLocation =>
text().withDefault(const Constant("")).map(const StringListConverter())(); text().withDefault(const Constant("")).map(const StringListConverter())();
TextColumn get pipedInstance =>
text().withDefault(const Constant("https://pipedapi.kavin.rocks"))();
TextColumn get invidiousInstance =>
text().withDefault(const Constant("https://inv.nadeko.net"))();
TextColumn get themeMode => TextColumn get themeMode =>
textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))(); textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))();
TextColumn get audioSource => TextColumn get audioSourceId => text().nullable()();
textEnum<AudioSource>().withDefault(Constant(AudioSource.youtube.name))();
TextColumn get youtubeClientEngine => textEnum<YoutubeClientEngine>() TextColumn get youtubeClientEngine => textEnum<YoutubeClientEngine>()
.withDefault(Constant(YoutubeClientEngine.youtubeExplode.name))(); .withDefault(Constant(YoutubeClientEngine.youtubeExplode.name))();
TextColumn get streamMusicCodec =>
textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.weba.name))();
TextColumn get downloadMusicCodec =>
textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.m4a.name))();
BoolColumn get discordPresence => BoolColumn get discordPresence =>
boolean().withDefault(const Constant(true))(); boolean().withDefault(const Constant(true))();
BoolColumn get endlessPlayback => BoolColumn get endlessPlayback =>
@ -116,7 +94,6 @@ class PreferencesTable extends Table {
static PreferencesTableData defaults() { static PreferencesTableData defaults() {
return PreferencesTableData( return PreferencesTableData(
id: 0, id: 0,
audioQuality: SourceQualities.high,
albumColorSync: true, albumColorSync: true,
amoledDarkTheme: false, amoledDarkTheme: false,
checkUpdate: true, checkUpdate: true,
@ -132,13 +109,9 @@ class PreferencesTable extends Table {
searchMode: SearchMode.youtube, searchMode: SearchMode.youtube,
downloadLocation: "", downloadLocation: "",
localLibraryLocation: [], localLibraryLocation: [],
pipedInstance: "https://pipedapi.kavin.rocks",
invidiousInstance: "https://inv.nadeko.net",
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
audioSource: AudioSource.youtube, audioSourceId: null,
youtubeClientEngine: YoutubeClientEngine.youtubeExplode, youtubeClientEngine: YoutubeClientEngine.youtubeExplode,
streamMusicCodec: SourceCodecs.m4a,
downloadMusicCodec: SourceCodecs.m4a,
discordPresence: true, discordPresence: true,
endlessPlayback: true, endlessPlayback: true,
enableConnect: false, enableConnect: false,

View File

@ -1,26 +1,14 @@
part of '../database.dart'; part of '../database.dart';
enum SourceType {
youtube._("YouTube"),
youtubeMusic._("YouTube Music"),
jiosaavn._("JioSaavn"),
dabMusic._("DAB Music");
final String label;
const SourceType._(this.label);
}
@TableIndex( @TableIndex(
name: "uniq_track_match", name: "uniq_track_match",
columns: {#trackId, #sourceId, #sourceType}, columns: {#trackId, #sourceInfo, #sourceType},
unique: true, unique: true,
) )
class SourceMatchTable extends Table { class SourceMatchTable extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
TextColumn get trackId => text()(); TextColumn get trackId => text()();
TextColumn get sourceId => text()(); TextColumn get sourceInfo => text().withDefault(const Constant("{}"))();
TextColumn get sourceType => TextColumn get sourceType => text()();
textEnum<SourceType>().withDefault(Constant(SourceType.youtube.name))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
} }

View File

@ -1,5 +1,7 @@
part of 'metadata.dart'; part of 'metadata.dart';
final oneOptionalDecimalFormatter = NumberFormat('0.#', 'en_US');
enum SpotubeMediaCompressionType { enum SpotubeMediaCompressionType {
lossy, lossy,
lossless, lossless,
@ -30,26 +32,40 @@ class SpotubeAudioSourceContainerPreset
@freezed @freezed
class SpotubeAudioLossyContainerQuality class SpotubeAudioLossyContainerQuality
with _$SpotubeAudioLossyContainerQuality { with _$SpotubeAudioLossyContainerQuality {
const SpotubeAudioLossyContainerQuality._();
factory SpotubeAudioLossyContainerQuality({ factory SpotubeAudioLossyContainerQuality({
required double bitrate, required int bitrate, // bits per second
}) = _SpotubeAudioLossyContainerQuality; }) = _SpotubeAudioLossyContainerQuality;
factory SpotubeAudioLossyContainerQuality.fromJson( factory SpotubeAudioLossyContainerQuality.fromJson(
Map<String, dynamic> json) => Map<String, dynamic> json) =>
_$SpotubeAudioLossyContainerQualityFromJson(json); _$SpotubeAudioLossyContainerQualityFromJson(json);
@override
toString() {
return "${oneOptionalDecimalFormatter.format(bitrate)}kbps";
}
} }
@freezed @freezed
class SpotubeAudioLosslessContainerQuality class SpotubeAudioLosslessContainerQuality
with _$SpotubeAudioLosslessContainerQuality { with _$SpotubeAudioLosslessContainerQuality {
const SpotubeAudioLosslessContainerQuality._();
factory SpotubeAudioLosslessContainerQuality({ factory SpotubeAudioLosslessContainerQuality({
required int bitDepth, required int bitDepth, // bit
required double sampleRate, required int sampleRate, // hz
}) = _SpotubeAudioLosslessContainerQuality; }) = _SpotubeAudioLosslessContainerQuality;
factory SpotubeAudioLosslessContainerQuality.fromJson( factory SpotubeAudioLosslessContainerQuality.fromJson(
Map<String, dynamic> json) => Map<String, dynamic> json) =>
_$SpotubeAudioLosslessContainerQualityFromJson(json); _$SpotubeAudioLosslessContainerQualityFromJson(json);
@override
toString() {
return "${bitDepth}bit • ${oneOptionalDecimalFormatter.format(sampleRate / 1000)}kHz";
}
} }
@freezed @freezed

View File

@ -5,6 +5,7 @@ import 'dart:typed_data';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:intl/intl.dart';
import 'package:metadata_god/metadata_god.dart'; import 'package:metadata_god/metadata_god.dart';
import 'package:mime/mime.dart'; import 'package:mime/mime.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';

View File

@ -595,7 +595,7 @@ SpotubeAudioLossyContainerQuality _$SpotubeAudioLossyContainerQualityFromJson(
/// @nodoc /// @nodoc
mixin _$SpotubeAudioLossyContainerQuality { mixin _$SpotubeAudioLossyContainerQuality {
double get bitrate => throw _privateConstructorUsedError; int get bitrate => throw _privateConstructorUsedError;
/// Serializes this SpotubeAudioLossyContainerQuality to a JSON map. /// Serializes this SpotubeAudioLossyContainerQuality to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -615,7 +615,7 @@ abstract class $SpotubeAudioLossyContainerQualityCopyWith<$Res> {
_$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res, _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res,
SpotubeAudioLossyContainerQuality>; SpotubeAudioLossyContainerQuality>;
@useResult @useResult
$Res call({double bitrate}); $Res call({int bitrate});
} }
/// @nodoc /// @nodoc
@ -640,7 +640,7 @@ class _$SpotubeAudioLossyContainerQualityCopyWithImpl<$Res,
bitrate: null == bitrate bitrate: null == bitrate
? _value.bitrate ? _value.bitrate
: bitrate // ignore: cast_nullable_to_non_nullable : bitrate // ignore: cast_nullable_to_non_nullable
as double, as int,
) as $Val); ) as $Val);
} }
} }
@ -654,7 +654,7 @@ abstract class _$$SpotubeAudioLossyContainerQualityImplCopyWith<$Res>
__$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res>; __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({double bitrate}); $Res call({int bitrate});
} }
/// @nodoc /// @nodoc
@ -678,7 +678,7 @@ class __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res>
bitrate: null == bitrate bitrate: null == bitrate
? _value.bitrate ? _value.bitrate
: bitrate // ignore: cast_nullable_to_non_nullable : bitrate // ignore: cast_nullable_to_non_nullable
as double, as int,
)); ));
} }
} }
@ -686,20 +686,15 @@ class __$$SpotubeAudioLossyContainerQualityImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$SpotubeAudioLossyContainerQualityImpl class _$SpotubeAudioLossyContainerQualityImpl
implements _SpotubeAudioLossyContainerQuality { extends _SpotubeAudioLossyContainerQuality {
_$SpotubeAudioLossyContainerQualityImpl({required this.bitrate}); _$SpotubeAudioLossyContainerQualityImpl({required this.bitrate}) : super._();
factory _$SpotubeAudioLossyContainerQualityImpl.fromJson( factory _$SpotubeAudioLossyContainerQualityImpl.fromJson(
Map<String, dynamic> json) => Map<String, dynamic> json) =>
_$$SpotubeAudioLossyContainerQualityImplFromJson(json); _$$SpotubeAudioLossyContainerQualityImplFromJson(json);
@override @override
final double bitrate; final int bitrate;
@override
String toString() {
return 'SpotubeAudioLossyContainerQuality(bitrate: $bitrate)';
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@ -732,16 +727,17 @@ class _$SpotubeAudioLossyContainerQualityImpl
} }
abstract class _SpotubeAudioLossyContainerQuality abstract class _SpotubeAudioLossyContainerQuality
implements SpotubeAudioLossyContainerQuality { extends SpotubeAudioLossyContainerQuality {
factory _SpotubeAudioLossyContainerQuality({required final double bitrate}) = factory _SpotubeAudioLossyContainerQuality({required final int bitrate}) =
_$SpotubeAudioLossyContainerQualityImpl; _$SpotubeAudioLossyContainerQualityImpl;
_SpotubeAudioLossyContainerQuality._() : super._();
factory _SpotubeAudioLossyContainerQuality.fromJson( factory _SpotubeAudioLossyContainerQuality.fromJson(
Map<String, dynamic> json) = Map<String, dynamic> json) =
_$SpotubeAudioLossyContainerQualityImpl.fromJson; _$SpotubeAudioLossyContainerQualityImpl.fromJson;
@override @override
double get bitrate; int get bitrate;
/// Create a copy of SpotubeAudioLossyContainerQuality /// Create a copy of SpotubeAudioLossyContainerQuality
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -759,8 +755,8 @@ SpotubeAudioLosslessContainerQuality
/// @nodoc /// @nodoc
mixin _$SpotubeAudioLosslessContainerQuality { mixin _$SpotubeAudioLosslessContainerQuality {
int get bitDepth => throw _privateConstructorUsedError; int get bitDepth => throw _privateConstructorUsedError; // bit
double get sampleRate => throw _privateConstructorUsedError; int get sampleRate => throw _privateConstructorUsedError;
/// Serializes this SpotubeAudioLosslessContainerQuality to a JSON map. /// Serializes this SpotubeAudioLosslessContainerQuality to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -781,7 +777,7 @@ abstract class $SpotubeAudioLosslessContainerQualityCopyWith<$Res> {
_$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res, _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res,
SpotubeAudioLosslessContainerQuality>; SpotubeAudioLosslessContainerQuality>;
@useResult @useResult
$Res call({int bitDepth, double sampleRate}); $Res call({int bitDepth, int sampleRate});
} }
/// @nodoc /// @nodoc
@ -811,7 +807,7 @@ class _$SpotubeAudioLosslessContainerQualityCopyWithImpl<$Res,
sampleRate: null == sampleRate sampleRate: null == sampleRate
? _value.sampleRate ? _value.sampleRate
: sampleRate // ignore: cast_nullable_to_non_nullable : sampleRate // ignore: cast_nullable_to_non_nullable
as double, as int,
) as $Val); ) as $Val);
} }
} }
@ -825,7 +821,7 @@ abstract class _$$SpotubeAudioLosslessContainerQualityImplCopyWith<$Res>
__$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res>; __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res>;
@override @override
@useResult @useResult
$Res call({int bitDepth, double sampleRate}); $Res call({int bitDepth, int sampleRate});
} }
/// @nodoc /// @nodoc
@ -854,7 +850,7 @@ class __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res>
sampleRate: null == sampleRate sampleRate: null == sampleRate
? _value.sampleRate ? _value.sampleRate
: sampleRate // ignore: cast_nullable_to_non_nullable : sampleRate // ignore: cast_nullable_to_non_nullable
as double, as int,
)); ));
} }
} }
@ -862,9 +858,10 @@ class __$$SpotubeAudioLosslessContainerQualityImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$SpotubeAudioLosslessContainerQualityImpl class _$SpotubeAudioLosslessContainerQualityImpl
implements _SpotubeAudioLosslessContainerQuality { extends _SpotubeAudioLosslessContainerQuality {
_$SpotubeAudioLosslessContainerQualityImpl( _$SpotubeAudioLosslessContainerQualityImpl(
{required this.bitDepth, required this.sampleRate}); {required this.bitDepth, required this.sampleRate})
: super._();
factory _$SpotubeAudioLosslessContainerQualityImpl.fromJson( factory _$SpotubeAudioLosslessContainerQualityImpl.fromJson(
Map<String, dynamic> json) => Map<String, dynamic> json) =>
@ -872,13 +869,9 @@ class _$SpotubeAudioLosslessContainerQualityImpl
@override @override
final int bitDepth; final int bitDepth;
// bit
@override @override
final double sampleRate; final int sampleRate;
@override
String toString() {
return 'SpotubeAudioLosslessContainerQuality(bitDepth: $bitDepth, sampleRate: $sampleRate)';
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@ -914,19 +907,20 @@ class _$SpotubeAudioLosslessContainerQualityImpl
} }
abstract class _SpotubeAudioLosslessContainerQuality abstract class _SpotubeAudioLosslessContainerQuality
implements SpotubeAudioLosslessContainerQuality { extends SpotubeAudioLosslessContainerQuality {
factory _SpotubeAudioLosslessContainerQuality( factory _SpotubeAudioLosslessContainerQuality(
{required final int bitDepth, required final double sampleRate}) = {required final int bitDepth, required final int sampleRate}) =
_$SpotubeAudioLosslessContainerQualityImpl; _$SpotubeAudioLosslessContainerQualityImpl;
_SpotubeAudioLosslessContainerQuality._() : super._();
factory _SpotubeAudioLosslessContainerQuality.fromJson( factory _SpotubeAudioLosslessContainerQuality.fromJson(
Map<String, dynamic> json) = Map<String, dynamic> json) =
_$SpotubeAudioLosslessContainerQualityImpl.fromJson; _$SpotubeAudioLosslessContainerQualityImpl.fromJson;
@override @override
int get bitDepth; int get bitDepth; // bit
@override @override
double get sampleRate; int get sampleRate;
/// Create a copy of SpotubeAudioLosslessContainerQuality /// Create a copy of SpotubeAudioLosslessContainerQuality
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View File

@ -52,7 +52,7 @@ Map<String, dynamic> _$$SpotubeAudioSourceContainerPresetLosslessImplToJson(
_$SpotubeAudioLossyContainerQualityImpl _$SpotubeAudioLossyContainerQualityImpl
_$$SpotubeAudioLossyContainerQualityImplFromJson(Map json) => _$$SpotubeAudioLossyContainerQualityImplFromJson(Map json) =>
_$SpotubeAudioLossyContainerQualityImpl( _$SpotubeAudioLossyContainerQualityImpl(
bitrate: (json['bitrate'] as num).toDouble(), bitrate: (json['bitrate'] as num).toInt(),
); );
Map<String, dynamic> _$$SpotubeAudioLossyContainerQualityImplToJson( Map<String, dynamic> _$$SpotubeAudioLossyContainerQualityImplToJson(
@ -65,7 +65,7 @@ _$SpotubeAudioLosslessContainerQualityImpl
_$$SpotubeAudioLosslessContainerQualityImplFromJson(Map json) => _$$SpotubeAudioLosslessContainerQualityImplFromJson(Map json) =>
_$SpotubeAudioLosslessContainerQualityImpl( _$SpotubeAudioLosslessContainerQualityImpl(
bitDepth: (json['bitDepth'] as num).toInt(), bitDepth: (json['bitDepth'] as num).toInt(),
sampleRate: (json['sampleRate'] as num).toDouble(), sampleRate: (json['sampleRate'] as num).toInt(),
); );
Map<String, dynamic> _$$SpotubeAudioLosslessContainerQualityImplToJson( Map<String, dynamic> _$$SpotubeAudioLosslessContainerQualityImplToJson(

View File

@ -1,122 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
part 'track_sources.freezed.dart';
part 'track_sources.g.dart'; part 'track_sources.g.dart';
@freezed
class TrackSourceQuery with _$TrackSourceQuery {
TrackSourceQuery._();
factory TrackSourceQuery({
required String id,
required String title,
required List<String> artists,
required String album,
required int durationMs,
required String isrc,
required bool explicit,
}) = _TrackSourceQuery;
factory TrackSourceQuery.fromJson(Map<String, dynamic> json) =>
_$TrackSourceQueryFromJson(json);
factory TrackSourceQuery.fromTrack(SpotubeFullTrackObject track) {
return TrackSourceQuery(
id: track.id,
title: track.name,
artists: track.artists.map((e) => e.name).toList(),
album: track.album.name,
durationMs: track.durationMs,
isrc: track.isrc,
explicit: track.explicit,
);
}
/// Parses [SpotubeMedia]'s [uri] property to create a [TrackSourceQuery].
factory TrackSourceQuery.parseUri(String url) {
final isLocal = !url.startsWith("http");
if (isLocal) {
try {
return TrackSourceQuery(
id: url,
title: '',
artists: [],
album: '',
durationMs: 0,
isrc: '',
explicit: false,
);
} catch (e, stackTrace) {
AppLogger.log.e(
"Failed to parse local track URI: $url\n$e",
stackTrace: stackTrace,
);
}
}
final uri = Uri.parse(url);
return TrackSourceQuery(
id: uri.pathSegments.last,
title: uri.queryParameters['title'] ?? '',
artists: uri.queryParameters['artists']?.split(',') ?? [],
album: uri.queryParameters['album'] ?? '',
durationMs: int.tryParse(uri.queryParameters['durationMs'] ?? '0') ?? 0,
isrc: uri.queryParameters['isrc'] ?? '',
explicit: uri.queryParameters['explicit']?.toLowerCase() == 'true',
);
}
String queryString() {
return toJson()
.entries
.map((e) =>
"${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value is List<String> ? e.value.join(",") : e.value.toString())}")
.join("&");
}
}
@freezed
class TrackSourceInfo with _$TrackSourceInfo {
factory TrackSourceInfo({
required String id,
required String title,
required String artists,
required String thumbnail,
required String pageUrl,
required int durationMs,
}) = _TrackSourceInfo;
factory TrackSourceInfo.fromJson(Map<String, dynamic> json) =>
_$TrackSourceInfoFromJson(json);
}
@freezed
class TrackSource with _$TrackSource {
factory TrackSource({
required String url,
required SourceQualities quality,
required SourceCodecs codec,
required String bitrate,
required String qualityLabel,
}) = _TrackSource;
factory TrackSource.fromJson(Map<String, dynamic> json) =>
_$TrackSourceFromJson(json);
}
@JsonSerializable() @JsonSerializable()
class BasicSourcedTrack { class BasicSourcedTrack {
final TrackSourceQuery query; final SpotubeFullTrackObject query;
final AudioSource source; final SpotubeAudioSourceMatchObject info;
final TrackSourceInfo info; final String source;
final List<TrackSource> sources; final List<SpotubeAudioSourceStreamObject> sources;
final List<TrackSourceInfo> siblings; final List<SpotubeAudioSourceMatchObject> siblings;
BasicSourcedTrack({ BasicSourcedTrack({
required this.query, required this.query,
required this.source, required this.source,

View File

@ -1,800 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'track_sources.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
TrackSourceQuery _$TrackSourceQueryFromJson(Map<String, dynamic> json) {
return _TrackSourceQuery.fromJson(json);
}
/// @nodoc
mixin _$TrackSourceQuery {
String get id => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError;
List<String> get artists => throw _privateConstructorUsedError;
String get album => throw _privateConstructorUsedError;
int get durationMs => throw _privateConstructorUsedError;
String get isrc => throw _privateConstructorUsedError;
bool get explicit => throw _privateConstructorUsedError;
/// Serializes this TrackSourceQuery to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of TrackSourceQuery
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$TrackSourceQueryCopyWith<TrackSourceQuery> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TrackSourceQueryCopyWith<$Res> {
factory $TrackSourceQueryCopyWith(
TrackSourceQuery value, $Res Function(TrackSourceQuery) then) =
_$TrackSourceQueryCopyWithImpl<$Res, TrackSourceQuery>;
@useResult
$Res call(
{String id,
String title,
List<String> artists,
String album,
int durationMs,
String isrc,
bool explicit});
}
/// @nodoc
class _$TrackSourceQueryCopyWithImpl<$Res, $Val extends TrackSourceQuery>
implements $TrackSourceQueryCopyWith<$Res> {
_$TrackSourceQueryCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of TrackSourceQuery
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? title = null,
Object? artists = null,
Object? album = null,
Object? durationMs = null,
Object? isrc = null,
Object? explicit = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
artists: null == artists
? _value.artists
: artists // ignore: cast_nullable_to_non_nullable
as List<String>,
album: null == album
? _value.album
: album // ignore: cast_nullable_to_non_nullable
as String,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
isrc: null == isrc
? _value.isrc
: isrc // ignore: cast_nullable_to_non_nullable
as String,
explicit: null == explicit
? _value.explicit
: explicit // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$TrackSourceQueryImplCopyWith<$Res>
implements $TrackSourceQueryCopyWith<$Res> {
factory _$$TrackSourceQueryImplCopyWith(_$TrackSourceQueryImpl value,
$Res Function(_$TrackSourceQueryImpl) then) =
__$$TrackSourceQueryImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String id,
String title,
List<String> artists,
String album,
int durationMs,
String isrc,
bool explicit});
}
/// @nodoc
class __$$TrackSourceQueryImplCopyWithImpl<$Res>
extends _$TrackSourceQueryCopyWithImpl<$Res, _$TrackSourceQueryImpl>
implements _$$TrackSourceQueryImplCopyWith<$Res> {
__$$TrackSourceQueryImplCopyWithImpl(_$TrackSourceQueryImpl _value,
$Res Function(_$TrackSourceQueryImpl) _then)
: super(_value, _then);
/// Create a copy of TrackSourceQuery
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? title = null,
Object? artists = null,
Object? album = null,
Object? durationMs = null,
Object? isrc = null,
Object? explicit = null,
}) {
return _then(_$TrackSourceQueryImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
artists: null == artists
? _value._artists
: artists // ignore: cast_nullable_to_non_nullable
as List<String>,
album: null == album
? _value.album
: album // ignore: cast_nullable_to_non_nullable
as String,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
isrc: null == isrc
? _value.isrc
: isrc // ignore: cast_nullable_to_non_nullable
as String,
explicit: null == explicit
? _value.explicit
: explicit // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _$TrackSourceQueryImpl extends _TrackSourceQuery {
_$TrackSourceQueryImpl(
{required this.id,
required this.title,
required final List<String> artists,
required this.album,
required this.durationMs,
required this.isrc,
required this.explicit})
: _artists = artists,
super._();
factory _$TrackSourceQueryImpl.fromJson(Map<String, dynamic> json) =>
_$$TrackSourceQueryImplFromJson(json);
@override
final String id;
@override
final String title;
final List<String> _artists;
@override
List<String> get artists {
if (_artists is EqualUnmodifiableListView) return _artists;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_artists);
}
@override
final String album;
@override
final int durationMs;
@override
final String isrc;
@override
final bool explicit;
@override
String toString() {
return 'TrackSourceQuery(id: $id, title: $title, artists: $artists, album: $album, durationMs: $durationMs, isrc: $isrc, explicit: $explicit)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TrackSourceQueryImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.title, title) || other.title == title) &&
const DeepCollectionEquality().equals(other._artists, _artists) &&
(identical(other.album, album) || other.album == album) &&
(identical(other.durationMs, durationMs) ||
other.durationMs == durationMs) &&
(identical(other.isrc, isrc) || other.isrc == isrc) &&
(identical(other.explicit, explicit) ||
other.explicit == explicit));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
title,
const DeepCollectionEquality().hash(_artists),
album,
durationMs,
isrc,
explicit);
/// Create a copy of TrackSourceQuery
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith =>
__$$TrackSourceQueryImplCopyWithImpl<_$TrackSourceQueryImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$TrackSourceQueryImplToJson(
this,
);
}
}
abstract class _TrackSourceQuery extends TrackSourceQuery {
factory _TrackSourceQuery(
{required final String id,
required final String title,
required final List<String> artists,
required final String album,
required final int durationMs,
required final String isrc,
required final bool explicit}) = _$TrackSourceQueryImpl;
_TrackSourceQuery._() : super._();
factory _TrackSourceQuery.fromJson(Map<String, dynamic> json) =
_$TrackSourceQueryImpl.fromJson;
@override
String get id;
@override
String get title;
@override
List<String> get artists;
@override
String get album;
@override
int get durationMs;
@override
String get isrc;
@override
bool get explicit;
/// Create a copy of TrackSourceQuery
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$TrackSourceQueryImplCopyWith<_$TrackSourceQueryImpl> get copyWith =>
throw _privateConstructorUsedError;
}
TrackSourceInfo _$TrackSourceInfoFromJson(Map<String, dynamic> json) {
return _TrackSourceInfo.fromJson(json);
}
/// @nodoc
mixin _$TrackSourceInfo {
String get id => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError;
String get artists => throw _privateConstructorUsedError;
String get thumbnail => throw _privateConstructorUsedError;
String get pageUrl => throw _privateConstructorUsedError;
int get durationMs => throw _privateConstructorUsedError;
/// Serializes this TrackSourceInfo to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of TrackSourceInfo
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$TrackSourceInfoCopyWith<TrackSourceInfo> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TrackSourceInfoCopyWith<$Res> {
factory $TrackSourceInfoCopyWith(
TrackSourceInfo value, $Res Function(TrackSourceInfo) then) =
_$TrackSourceInfoCopyWithImpl<$Res, TrackSourceInfo>;
@useResult
$Res call(
{String id,
String title,
String artists,
String thumbnail,
String pageUrl,
int durationMs});
}
/// @nodoc
class _$TrackSourceInfoCopyWithImpl<$Res, $Val extends TrackSourceInfo>
implements $TrackSourceInfoCopyWith<$Res> {
_$TrackSourceInfoCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of TrackSourceInfo
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? title = null,
Object? artists = null,
Object? thumbnail = null,
Object? pageUrl = null,
Object? durationMs = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
artists: null == artists
? _value.artists
: artists // ignore: cast_nullable_to_non_nullable
as String,
thumbnail: null == thumbnail
? _value.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable
as String,
pageUrl: null == pageUrl
? _value.pageUrl
: pageUrl // ignore: cast_nullable_to_non_nullable
as String,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$TrackSourceInfoImplCopyWith<$Res>
implements $TrackSourceInfoCopyWith<$Res> {
factory _$$TrackSourceInfoImplCopyWith(_$TrackSourceInfoImpl value,
$Res Function(_$TrackSourceInfoImpl) then) =
__$$TrackSourceInfoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String id,
String title,
String artists,
String thumbnail,
String pageUrl,
int durationMs});
}
/// @nodoc
class __$$TrackSourceInfoImplCopyWithImpl<$Res>
extends _$TrackSourceInfoCopyWithImpl<$Res, _$TrackSourceInfoImpl>
implements _$$TrackSourceInfoImplCopyWith<$Res> {
__$$TrackSourceInfoImplCopyWithImpl(
_$TrackSourceInfoImpl _value, $Res Function(_$TrackSourceInfoImpl) _then)
: super(_value, _then);
/// Create a copy of TrackSourceInfo
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? title = null,
Object? artists = null,
Object? thumbnail = null,
Object? pageUrl = null,
Object? durationMs = null,
}) {
return _then(_$TrackSourceInfoImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
artists: null == artists
? _value.artists
: artists // ignore: cast_nullable_to_non_nullable
as String,
thumbnail: null == thumbnail
? _value.thumbnail
: thumbnail // ignore: cast_nullable_to_non_nullable
as String,
pageUrl: null == pageUrl
? _value.pageUrl
: pageUrl // ignore: cast_nullable_to_non_nullable
as String,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$TrackSourceInfoImpl implements _TrackSourceInfo {
_$TrackSourceInfoImpl(
{required this.id,
required this.title,
required this.artists,
required this.thumbnail,
required this.pageUrl,
required this.durationMs});
factory _$TrackSourceInfoImpl.fromJson(Map<String, dynamic> json) =>
_$$TrackSourceInfoImplFromJson(json);
@override
final String id;
@override
final String title;
@override
final String artists;
@override
final String thumbnail;
@override
final String pageUrl;
@override
final int durationMs;
@override
String toString() {
return 'TrackSourceInfo(id: $id, title: $title, artists: $artists, thumbnail: $thumbnail, pageUrl: $pageUrl, durationMs: $durationMs)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TrackSourceInfoImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.title, title) || other.title == title) &&
(identical(other.artists, artists) || other.artists == artists) &&
(identical(other.thumbnail, thumbnail) ||
other.thumbnail == thumbnail) &&
(identical(other.pageUrl, pageUrl) || other.pageUrl == pageUrl) &&
(identical(other.durationMs, durationMs) ||
other.durationMs == durationMs));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType, id, title, artists, thumbnail, pageUrl, durationMs);
/// Create a copy of TrackSourceInfo
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith =>
__$$TrackSourceInfoImplCopyWithImpl<_$TrackSourceInfoImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$TrackSourceInfoImplToJson(
this,
);
}
}
abstract class _TrackSourceInfo implements TrackSourceInfo {
factory _TrackSourceInfo(
{required final String id,
required final String title,
required final String artists,
required final String thumbnail,
required final String pageUrl,
required final int durationMs}) = _$TrackSourceInfoImpl;
factory _TrackSourceInfo.fromJson(Map<String, dynamic> json) =
_$TrackSourceInfoImpl.fromJson;
@override
String get id;
@override
String get title;
@override
String get artists;
@override
String get thumbnail;
@override
String get pageUrl;
@override
int get durationMs;
/// Create a copy of TrackSourceInfo
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$TrackSourceInfoImplCopyWith<_$TrackSourceInfoImpl> get copyWith =>
throw _privateConstructorUsedError;
}
TrackSource _$TrackSourceFromJson(Map<String, dynamic> json) {
return _TrackSource.fromJson(json);
}
/// @nodoc
mixin _$TrackSource {
String get url => throw _privateConstructorUsedError;
SourceQualities get quality => throw _privateConstructorUsedError;
SourceCodecs get codec => throw _privateConstructorUsedError;
String get bitrate => throw _privateConstructorUsedError;
String get qualityLabel => throw _privateConstructorUsedError;
/// Serializes this TrackSource to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of TrackSource
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$TrackSourceCopyWith<TrackSource> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $TrackSourceCopyWith<$Res> {
factory $TrackSourceCopyWith(
TrackSource value, $Res Function(TrackSource) then) =
_$TrackSourceCopyWithImpl<$Res, TrackSource>;
@useResult
$Res call(
{String url,
SourceQualities quality,
SourceCodecs codec,
String bitrate,
String qualityLabel});
}
/// @nodoc
class _$TrackSourceCopyWithImpl<$Res, $Val extends TrackSource>
implements $TrackSourceCopyWith<$Res> {
_$TrackSourceCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of TrackSource
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? url = null,
Object? quality = null,
Object? codec = null,
Object? bitrate = null,
Object? qualityLabel = null,
}) {
return _then(_value.copyWith(
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
quality: null == quality
? _value.quality
: quality // ignore: cast_nullable_to_non_nullable
as SourceQualities,
codec: null == codec
? _value.codec
: codec // ignore: cast_nullable_to_non_nullable
as SourceCodecs,
bitrate: null == bitrate
? _value.bitrate
: bitrate // ignore: cast_nullable_to_non_nullable
as String,
qualityLabel: null == qualityLabel
? _value.qualityLabel
: qualityLabel // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$TrackSourceImplCopyWith<$Res>
implements $TrackSourceCopyWith<$Res> {
factory _$$TrackSourceImplCopyWith(
_$TrackSourceImpl value, $Res Function(_$TrackSourceImpl) then) =
__$$TrackSourceImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String url,
SourceQualities quality,
SourceCodecs codec,
String bitrate,
String qualityLabel});
}
/// @nodoc
class __$$TrackSourceImplCopyWithImpl<$Res>
extends _$TrackSourceCopyWithImpl<$Res, _$TrackSourceImpl>
implements _$$TrackSourceImplCopyWith<$Res> {
__$$TrackSourceImplCopyWithImpl(
_$TrackSourceImpl _value, $Res Function(_$TrackSourceImpl) _then)
: super(_value, _then);
/// Create a copy of TrackSource
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? url = null,
Object? quality = null,
Object? codec = null,
Object? bitrate = null,
Object? qualityLabel = null,
}) {
return _then(_$TrackSourceImpl(
url: null == url
? _value.url
: url // ignore: cast_nullable_to_non_nullable
as String,
quality: null == quality
? _value.quality
: quality // ignore: cast_nullable_to_non_nullable
as SourceQualities,
codec: null == codec
? _value.codec
: codec // ignore: cast_nullable_to_non_nullable
as SourceCodecs,
bitrate: null == bitrate
? _value.bitrate
: bitrate // ignore: cast_nullable_to_non_nullable
as String,
qualityLabel: null == qualityLabel
? _value.qualityLabel
: qualityLabel // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$TrackSourceImpl implements _TrackSource {
_$TrackSourceImpl(
{required this.url,
required this.quality,
required this.codec,
required this.bitrate,
required this.qualityLabel});
factory _$TrackSourceImpl.fromJson(Map<String, dynamic> json) =>
_$$TrackSourceImplFromJson(json);
@override
final String url;
@override
final SourceQualities quality;
@override
final SourceCodecs codec;
@override
final String bitrate;
@override
final String qualityLabel;
@override
String toString() {
return 'TrackSource(url: $url, quality: $quality, codec: $codec, bitrate: $bitrate, qualityLabel: $qualityLabel)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TrackSourceImpl &&
(identical(other.url, url) || other.url == url) &&
(identical(other.quality, quality) || other.quality == quality) &&
(identical(other.codec, codec) || other.codec == codec) &&
(identical(other.bitrate, bitrate) || other.bitrate == bitrate) &&
(identical(other.qualityLabel, qualityLabel) ||
other.qualityLabel == qualityLabel));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, url, quality, codec, bitrate, qualityLabel);
/// Create a copy of TrackSource
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith =>
__$$TrackSourceImplCopyWithImpl<_$TrackSourceImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$TrackSourceImplToJson(
this,
);
}
}
abstract class _TrackSource implements TrackSource {
factory _TrackSource(
{required final String url,
required final SourceQualities quality,
required final SourceCodecs codec,
required final String bitrate,
required final String qualityLabel}) = _$TrackSourceImpl;
factory _TrackSource.fromJson(Map<String, dynamic> json) =
_$TrackSourceImpl.fromJson;
@override
String get url;
@override
SourceQualities get quality;
@override
SourceCodecs get codec;
@override
String get bitrate;
@override
String get qualityLabel;
/// Create a copy of TrackSource
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$TrackSourceImplCopyWith<_$TrackSourceImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -7,17 +7,18 @@ part of 'track_sources.dart';
// ************************************************************************** // **************************************************************************
BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack( BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
query: TrackSourceQuery.fromJson( query: SpotubeFullTrackObject.fromJson(
Map<String, dynamic>.from(json['query'] as Map)), Map<String, dynamic>.from(json['query'] as Map)),
source: $enumDecode(_$AudioSourceEnumMap, json['source']), source: json['source'] as String,
info: TrackSourceInfo.fromJson( info: SpotubeAudioSourceMatchObject.fromJson(
Map<String, dynamic>.from(json['info'] as Map)), Map<String, dynamic>.from(json['info'] as Map)),
sources: (json['sources'] as List<dynamic>) sources: (json['sources'] as List<dynamic>)
.map((e) => TrackSource.fromJson(Map<String, dynamic>.from(e as Map))) .map((e) => SpotubeAudioSourceStreamObject.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList(), .toList(),
siblings: (json['siblings'] as List<dynamic>?) siblings: (json['siblings'] as List<dynamic>?)
?.map((e) => ?.map((e) => SpotubeAudioSourceMatchObject.fromJson(
TrackSourceInfo.fromJson(Map<String, dynamic>.from(e as Map))) Map<String, dynamic>.from(e as Map)))
.toList() ?? .toList() ??
const [], const [],
); );
@ -25,92 +26,8 @@ BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
Map<String, dynamic> _$BasicSourcedTrackToJson(BasicSourcedTrack instance) => Map<String, dynamic> _$BasicSourcedTrackToJson(BasicSourcedTrack instance) =>
<String, dynamic>{ <String, dynamic>{
'query': instance.query.toJson(), 'query': instance.query.toJson(),
'source': _$AudioSourceEnumMap[instance.source]!,
'info': instance.info.toJson(), 'info': instance.info.toJson(),
'source': instance.source,
'sources': instance.sources.map((e) => e.toJson()).toList(), 'sources': instance.sources.map((e) => e.toJson()).toList(),
'siblings': instance.siblings.map((e) => e.toJson()).toList(), 'siblings': instance.siblings.map((e) => e.toJson()).toList(),
}; };
const _$AudioSourceEnumMap = {
AudioSource.youtube: 'youtube',
AudioSource.piped: 'piped',
AudioSource.jiosaavn: 'jiosaavn',
AudioSource.invidious: 'invidious',
AudioSource.dabMusic: 'dabMusic',
};
_$TrackSourceQueryImpl _$$TrackSourceQueryImplFromJson(Map json) =>
_$TrackSourceQueryImpl(
id: json['id'] as String,
title: json['title'] as String,
artists:
(json['artists'] as List<dynamic>).map((e) => e as String).toList(),
album: json['album'] as String,
durationMs: (json['durationMs'] as num).toInt(),
isrc: json['isrc'] as String,
explicit: json['explicit'] as bool,
);
Map<String, dynamic> _$$TrackSourceQueryImplToJson(
_$TrackSourceQueryImpl instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'artists': instance.artists,
'album': instance.album,
'durationMs': instance.durationMs,
'isrc': instance.isrc,
'explicit': instance.explicit,
};
_$TrackSourceInfoImpl _$$TrackSourceInfoImplFromJson(Map json) =>
_$TrackSourceInfoImpl(
id: json['id'] as String,
title: json['title'] as String,
artists: json['artists'] as String,
thumbnail: json['thumbnail'] as String,
pageUrl: json['pageUrl'] as String,
durationMs: (json['durationMs'] as num).toInt(),
);
Map<String, dynamic> _$$TrackSourceInfoImplToJson(
_$TrackSourceInfoImpl instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'artists': instance.artists,
'thumbnail': instance.thumbnail,
'pageUrl': instance.pageUrl,
'durationMs': instance.durationMs,
};
_$TrackSourceImpl _$$TrackSourceImplFromJson(Map json) => _$TrackSourceImpl(
url: json['url'] as String,
quality: $enumDecode(_$SourceQualitiesEnumMap, json['quality']),
codec: $enumDecode(_$SourceCodecsEnumMap, json['codec']),
bitrate: json['bitrate'] as String,
qualityLabel: json['qualityLabel'] as String,
);
Map<String, dynamic> _$$TrackSourceImplToJson(_$TrackSourceImpl instance) =>
<String, dynamic>{
'url': instance.url,
'quality': _$SourceQualitiesEnumMap[instance.quality]!,
'codec': _$SourceCodecsEnumMap[instance.codec]!,
'bitrate': instance.bitrate,
'qualityLabel': instance.qualityLabel,
};
const _$SourceQualitiesEnumMap = {
SourceQualities.uncompressed: 'uncompressed',
SourceQualities.high: 'high',
SourceQualities.medium: 'medium',
SourceQualities.low: 'low',
};
const _$SourceCodecsEnumMap = {
SourceCodecs.m4a: 'm4a',
SourceCodecs.weba: 'weba',
SourceCodecs.mp3: 'mp3',
SourceCodecs.flac: 'flac',
};

View File

@ -6,9 +6,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
final codecs = SourceCodecs.values.map((s) => s.name); const containers = ["m4a", "mp3", "mp4", "ogg", "wav", "flac"];
class LocalFolderCacheExportDialog extends HookConsumerWidget { class LocalFolderCacheExportDialog extends HookConsumerWidget {
final Directory exportDir; final Directory exportDir;
@ -30,7 +29,8 @@ class LocalFolderCacheExportDialog extends HookConsumerWidget {
final stream = cacheDir.list().where( final stream = cacheDir.list().where(
(event) => (event) =>
event is File && event is File &&
codecs.contains(path.extension(event.path).replaceAll(".", "")), containers
.contains(path.extension(event.path).replaceAll(".", "")),
); );
stream.listen( stream.listen(

View File

@ -21,11 +21,9 @@ import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/metadata_plugin/audio_source/quality_label.dart';
import 'package:spotube/provider/server/active_track_sources.dart'; import 'package:spotube/provider/server/active_track_sources.dart';
import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:url_launcher/url_launcher_string.dart';
class PlayerView extends HookConsumerWidget { class PlayerView extends HookConsumerWidget {
final PanelController panelController; final PanelController panelController;
@ -45,14 +43,7 @@ class PlayerView extends HookConsumerWidget {
final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source; final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source;
final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject; final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject;
final mediaQuery = MediaQuery.sizeOf(context); final mediaQuery = MediaQuery.sizeOf(context);
final qualityLabel = ref.watch(audioSourceQualityLabelProvider);
final activeSourceCodec = useMemoized(
() {
return currentActiveTrackSource
?.getSourceOfCodec(currentActiveTrackSource.codec);
},
[currentActiveTrackSource?.sources, currentActiveTrackSource?.codec],
);
final shouldHide = useState(true); final shouldHide = useState(true);
@ -117,22 +108,6 @@ class PlayerView extends HookConsumerWidget {
) )
], ],
trailing: [ trailing: [
if (currentActiveTrackSource is YoutubeSourcedTrack)
TextButton(
size: const ButtonSize(1.2),
leading: Assets.images.logos.songlinkTransparent.image(
width: 20,
height: 20,
color: theme.colorScheme.foreground,
),
onPressed: () {
final url =
"https://song.link/s/${currentActiveTrack?.id}";
launchUrlString(url);
},
child: Text(context.l10n.song_link),
),
if (!isLocalTrack) if (!isLocalTrack)
Tooltip( Tooltip(
tooltip: TooltipContainer( tooltip: TooltipContainer(
@ -276,7 +251,6 @@ class PlayerView extends HookConsumerWidget {
}), }),
), ),
const Gap(25), const Gap(25),
if (activeSourceCodec != null)
OutlineBadge( OutlineBadge(
style: const ButtonStyle.outline( style: const ButtonStyle.outline(
size: ButtonSize.normal, size: ButtonSize.normal,
@ -288,7 +262,7 @@ class PlayerView extends HookConsumerWidget {
}, },
), ),
leading: const Icon(SpotubeIcons.lightningOutlined), leading: const Icon(SpotubeIcons.lightningOutlined),
child: Text(activeSourceCodec.qualityLabel), child: Text(qualityLabel),
) )
], ],
), ),

View File

@ -1,60 +1,16 @@
import 'package:collection/collection.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/button/back_button.dart';
import 'package:spotube/components/image/universal_image.dart'; import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart'; import 'package:spotube/components/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart'; import 'package:spotube/extensions/duration.dart';
import 'package:spotube/hooks/controllers/use_shadcn_text_editing_controller.dart';
import 'package:spotube/hooks/utils/use_debounce.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart';
import 'package:spotube/provider/server/active_track_sources.dart'; import 'package:spotube/provider/server/active_track_sources.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/service_utils.dart';
final sourceInfoToIconMap = {
AudioSource.youtube:
const Icon(SpotubeIcons.youtube, color: Color(0xFFFF0000)),
AudioSource.jiosaavn: Container(
height: 30,
width: 30,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(90),
image: DecorationImage(
image: Assets.images.logos.jiosaavn.provider(),
fit: BoxFit.cover,
),
),
),
AudioSource.piped: const Icon(SpotubeIcons.piped),
AudioSource.invidious: Container(
height: 18,
width: 18,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(90),
image: DecorationImage(
image: Assets.images.logos.invidious.provider(),
fit: BoxFit.cover,
),
),
),
};
class SiblingTracksSheet extends HookConsumerWidget { class SiblingTracksSheet extends HookConsumerWidget {
final bool floating; final bool floating;
@ -65,94 +21,21 @@ class SiblingTracksSheet extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final theme = Theme.of(context); final controller = useScrollController();
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final preferences = ref.watch(userPreferencesProvider);
final youtubeEngine = ref.watch(youtubeEngineProvider);
final isLoading = useState(false); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
final isSearching = useState(false);
final searchMode = useState(preferences.searchMode);
final activeTrackSources = ref.watch(activeTrackSourcesProvider); final activeTrackSources = ref.watch(activeTrackSourcesProvider);
final activeTrackNotifier = activeTrackSources.asData?.value?.notifier; final activeTrackNotifier = activeTrackSources.asData?.value?.notifier;
final activeTrack = activeTrackSources.asData?.value?.track; final activeTrack = activeTrackSources.asData?.value?.track;
final activeTrackSource = activeTrackSources.asData?.value?.source; final activeTrackSource = activeTrackSources.asData?.value?.source;
final title = ServiceUtils.getTitle( final siblings = useMemoized<List<SpotubeAudioSourceMatchObject>>(
activeTrack?.name ?? "",
artists: activeTrack?.artists.map((e) => e.name).toList() ?? [],
onlyCleanArtist: true,
).trim();
final defaultSearchTerm =
"$title - ${activeTrack?.artists.asString() ?? ""}";
final searchController = useShadcnTextEditingController(
text: defaultSearchTerm,
);
final searchTerm = useDebounce<String>(
useValueListenable(searchController).text,
);
final controller = useScrollController();
final searchRequest = useMemoized(() async {
if (searchTerm.trim().isEmpty || activeTrackSource == null) {
return <TrackSourceInfo>[];
}
if (preferences.audioSource == AudioSource.jiosaavn) {
final resultsJioSaavn =
await jiosaavnClient.search.songs(searchTerm.trim());
final results = await Future.wait(
resultsJioSaavn.results.mapIndexed((i, song) async {
final siblingType = JioSaavnSourcedTrack.toSiblingType(song);
return siblingType.info;
}));
final activeSourceInfo = activeTrackSource.info;
return results
..removeWhere((element) => element.id == activeSourceInfo.id)
..insert(
0,
activeSourceInfo,
);
} else {
final resultsYt = await youtubeEngine.searchVideos(searchTerm.trim());
final searchResults = await Future.wait(
resultsYt
.map(YoutubeVideoInfo.fromVideo)
.mapIndexed((i, video) async {
if (!context.mounted) return null;
final siblingType =
await YoutubeSourcedTrack.toSiblingType(i, video, ref);
return siblingType.info;
})
.whereType<Future<TrackSourceInfo>>()
.toList(),
);
final activeSourceInfo = activeTrackSource.info;
return searchResults
..removeWhere((element) => element.id == activeSourceInfo.id)
..insert(0, activeSourceInfo);
}
}, [
searchTerm,
searchMode.value,
activeTrack,
activeTrackSource,
preferences.audioSource,
youtubeEngine,
]);
final siblings = useMemoized(
() => !isFetchingActiveTrack () => !isFetchingActiveTrack
? [ ? [
if (activeTrackSource != null) activeTrackSource.info, if (activeTrackSource != null) activeTrackSource.info,
...?activeTrackSource?.siblings, ...?activeTrackSource?.siblings,
] ]
: <TrackSourceInfo>[], : <SpotubeAudioSourceMatchObject>[],
[activeTrackSource, isFetchingActiveTrack], [activeTrackSource, isFetchingActiveTrack],
); );
@ -166,74 +49,6 @@ class SiblingTracksSheet extends HookConsumerWidget {
return null; return null;
}, [activeTrack, previousActiveTrack]); }, [activeTrack, previousActiveTrack]);
final itemBuilder = useCallback(
(TrackSourceInfo sourceInfo, AudioSource source) {
final icon = sourceInfoToIconMap[source];
return ButtonTile(
style: ButtonVariance.ghost,
padding: const EdgeInsets.symmetric(horizontal: 8),
title: Text(
sourceInfo.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
leading: UniversalImage(
path: sourceInfo.thumbnail,
height: 60,
width: 60,
),
trailing: Text(Duration(milliseconds: sourceInfo.durationMs)
.toHumanReadableString()),
subtitle: Row(
children: [
if (icon != null) icon,
Flexible(
child: Text(
"${sourceInfo.artists}",
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
enabled: !isFetchingActiveTrack && !isLoading.value,
selected: !isFetchingActiveTrack &&
sourceInfo.id == activeTrackSource?.info.id,
onPressed: () async {
if (!isFetchingActiveTrack &&
sourceInfo.id != activeTrackSource?.info.id) {
try {
isLoading.value = true;
await activeTrackNotifier?.swapWithSibling(sourceInfo);
await ref.read(audioPlayerProvider.notifier).swapActiveSource();
if (context.mounted) {
if (MediaQuery.sizeOf(context).mdAndUp) {
closeOverlay(context);
} else {
closeDrawer(context);
}
}
} finally {
if (context.mounted) {
isLoading.value = false;
}
}
}
},
);
},
[
activeTrackSource,
activeTrackNotifier,
siblings,
isFetchingActiveTrack,
isLoading.value,
],
);
final scale = context.theme.scaling;
return SafeArea( return SafeArea(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -245,71 +60,15 @@ class SiblingTracksSheet extends HookConsumerWidget {
children: [ children: [
AnimatedSwitcher( AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: !isSearching.value child: Text(
? Text(
context.l10n.alternative_track_sources, context.l10n.alternative_track_sources,
).bold() ).bold()),
: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 320 * scale,
maxHeight: 38 * scale,
),
child: TextField(
autofocus: true,
controller: searchController,
placeholder: Text(context.l10n.search),
style: theme.typography.bold,
),
),
),
const Spacer(),
if (!isSearching.value) ...[
IconButton.outline(
icon: const Icon(SpotubeIcons.search, size: 18),
onPressed: () {
isSearching.value = true;
},
),
if (!floating) const BackButton(icon: SpotubeIcons.angleDown)
] else ...[
if (preferences.audioSource == AudioSource.piped)
IconButton.outline(
icon: const Icon(SpotubeIcons.filter, size: 18),
onPressed: () {
showPopover(
context: context,
alignment: Alignment.bottomRight,
builder: (context) {
return DropdownMenu(
children: SearchMode.values
.map(
(e) => MenuButton(
onPressed: (context) {
searchMode.value = e;
},
enabled: searchMode.value != e,
child: Text(e.label),
),
)
.toList(),
);
},
);
},
),
IconButton.outline(
icon: const Icon(SpotubeIcons.close, size: 18),
onPressed: () {
isSearching.value = false;
},
),
]
], ],
), ),
), ),
AnimatedSwitcher( AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: isLoading.value child: activeTrackSources.isLoading
? const SizedBox( ? const SizedBox(
width: double.infinity, width: double.infinity,
child: LinearProgressIndicator(), child: LinearProgressIndicator(),
@ -323,42 +82,62 @@ class SiblingTracksSheet extends HookConsumerWidget {
FadeTransition(opacity: animation, child: child), FadeTransition(opacity: animation, child: child),
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,
child: switch (isSearching.value) { child: ListView.separated(
false => ListView.separated(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
controller: controller, controller: controller,
itemCount: siblings.length, itemCount: siblings.length,
separatorBuilder: (context, index) => const Gap(8), separatorBuilder: (context, index) => const Gap(8),
itemBuilder: (context, index) => itemBuilder( itemBuilder: (context, index) {
siblings[index], final sourceInfo = siblings[index];
activeTrackSource!.source,
),
),
true => FutureBuilder(
future: searchRequest,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
} else if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator());
}
return ListView.separated( return ButtonTile(
padding: const EdgeInsets.all(8.0), style: ButtonVariance.ghost,
controller: controller, padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: snapshot.data!.length, title: Text(
separatorBuilder: (context, index) => const Gap(8), sourceInfo.title,
itemBuilder: (context, index) => itemBuilder( maxLines: 2,
snapshot.data![index], overflow: TextOverflow.ellipsis,
preferences.audioSource,
), ),
leading: sourceInfo.thumbnail != null
? UniversalImage(
path: sourceInfo.thumbnail!,
height: 60,
width: 60,
)
: null,
trailing:
Text(sourceInfo.duration.toHumanReadableString()),
subtitle: Flexible(
child: Text(
sourceInfo.artists.join(", "),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
enabled: !isFetchingActiveTrack,
selected: !isFetchingActiveTrack &&
sourceInfo.id == activeTrackSource?.info.id,
onPressed: () async {
if (!isFetchingActiveTrack &&
sourceInfo.id != activeTrackSource?.info.id) {
await activeTrackNotifier
?.swapWithSibling(sourceInfo);
await ref
.read(audioPlayerProvider.notifier)
.swapActiveSource();
if (context.mounted) {
if (MediaQuery.sizeOf(context).mdAndUp) {
closeOverlay(context);
} else {
closeDrawer(context);
}
}
}
},
); );
}, },
), ),
},
), ),
), ),
), ),

View File

@ -22,7 +22,7 @@ class Sidebar extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final ThemeData(:colorScheme) = Theme.of(context); final ThemeData(:colorScheme) = Theme.of(context);
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.sizeOf(context);
final layoutMode = final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode)); ref.watch(userPreferencesProvider.select((s) => s.layoutMode));

View File

@ -1,32 +1,11 @@
import 'package:flutter/material.dart' show Badge;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/ui/button_tile.dart'; import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/modules/getting_started/blur_card.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
final audioSourceToIconMap = {
AudioSource.youtube: const Icon(
SpotubeIcons.youtube,
color: Colors.red,
size: 20,
),
AudioSource.piped: const Icon(SpotubeIcons.piped, size: 20),
AudioSource.invidious: ClipRRect(
borderRadius: BorderRadius.circular(26),
child: Assets.images.logos.invidious.image(width: 26, height: 26),
),
AudioSource.jiosaavn:
Assets.images.logos.jiosaavn.image(width: 20, height: 20),
AudioSource.dabMusic:
Assets.images.logos.dabMusic.image(width: 20, height: 20),
};
class GettingStartedPagePlaybackSection extends HookConsumerWidget { class GettingStartedPagePlaybackSection extends HookConsumerWidget {
final VoidCallback onNext; final VoidCallback onNext;
final VoidCallback onPrevious; final VoidCallback onPrevious;
@ -42,19 +21,19 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.read(userPreferencesProvider.notifier); final preferencesNotifier = ref.read(userPreferencesProvider.notifier);
final audioSourceToDescription = useMemoized( // final audioSourceToDescription = useMemoized(
() => { // () => {
AudioSource.youtube: "${context.l10n.youtube_source_description}\n" // AudioSource.youtube: "${context.l10n.youtube_source_description}\n"
"${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}", // "${context.l10n.highest_quality("148kbps mp4, 128kbps opus")}",
AudioSource.piped: context.l10n.piped_source_description, // AudioSource.piped: context.l10n.piped_source_description,
AudioSource.jiosaavn: // AudioSource.jiosaavn:
"${context.l10n.jiosaavn_source_description}\n" // "${context.l10n.jiosaavn_source_description}\n"
"${context.l10n.highest_quality("320kbps mp4")}", // "${context.l10n.highest_quality("320kbps mp4")}",
AudioSource.invidious: context.l10n.invidious_source_description, // AudioSource.invidious: context.l10n.invidious_source_description,
AudioSource.dabMusic: "${context.l10n.dab_music_source_description}\n" // AudioSource.dabMusic: "${context.l10n.dab_music_source_description}\n"
"${context.l10n.highest_quality("320kbps mp3, HI-RES 24bit 44.1kHz-96kHz flac")}", // "${context.l10n.highest_quality("320kbps mp3, HI-RES 24bit 44.1kHz-96kHz flac")}",
}, // },
[]); // []);
return Center( return Center(
child: BlurCard( child: BlurCard(
@ -69,44 +48,44 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
], ],
), ),
const Gap(16), const Gap(16),
Align( // Align(
alignment: Alignment.centerLeft, // alignment: Alignment.centerLeft,
child: Text(context.l10n.select_audio_source).semiBold().large(), // child: Text(context.l10n.select_audio_source).semiBold().large(),
), // ),
const Gap(16), // const Gap(16),
RadioGroup<AudioSource>( // RadioGroup<AudioSource>(
value: preferences.audioSource, // value: preferences.audioSource,
onChanged: (value) { // onChanged: (value) {
preferencesNotifier.setAudioSource(value); // preferencesNotifier.setAudioSource(value);
}, // },
child: Wrap( // child: Wrap(
spacing: 6, // spacing: 6,
runSpacing: 6, // runSpacing: 6,
children: [ // children: [
for (final source in AudioSource.values) // for (final source in AudioSource.values)
Badge( // Badge(
isLabelVisible: source == AudioSource.dabMusic, // isLabelVisible: source == AudioSource.dabMusic,
label: const Text("NEW"), // label: const Text("NEW"),
backgroundColor: Colors.lime[300], // backgroundColor: Colors.lime[300],
textColor: Colors.black, // textColor: Colors.black,
child: RadioCard( // child: RadioCard(
value: source, // value: source,
child: Column( // child: Column(
mainAxisSize: MainAxisSize.min, // mainAxisSize: MainAxisSize.min,
children: [ // children: [
audioSourceToIconMap[source]!, // audioSourceToIconMap[source]!,
Text(source.label), // Text(source.label),
], // ],
), // ),
), // ),
), // ),
], // ],
), // ),
), // ),
const Gap(16), // const Gap(16),
Text( // Text(
audioSourceToDescription[preferences.audioSource]!, // audioSourceToDescription[preferences.audioSource]!,
).small().muted(), // ).small().muted(),
const Gap(16), const Gap(16),
ButtonTile( ButtonTile(
title: Text(context.l10n.endless_playback), title: Text(context.l10n.endless_playback),

View File

@ -1,30 +1,24 @@
import 'dart:io'; import 'dart:io';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' show ListTile; import 'package:flutter/material.dart' show ListTile;
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:piped_client/piped_client.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/routes.gr.dart'; import 'package:spotube/collections/routes.gr.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/playback/edit_connect_port_dialog.dart'; import 'package:spotube/modules/settings/playback/edit_connect_port_dialog.dart';
import 'package:spotube/modules/settings/playback/edit_instance_url_dialog.dart';
import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart';
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart'; import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart';
import 'package:spotube/provider/audio_player/sources/invidious_instances_provider.dart'; import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart';
import 'package:spotube/provider/audio_player/sources/piped_instances_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/kv_store/kv_store.dart'; import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
class SettingsPlaybackSection extends HookConsumerWidget { class SettingsPlaybackSection extends HookConsumerWidget {
@ -34,264 +28,15 @@ class SettingsPlaybackSection extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.watch(userPreferencesProvider.notifier); final preferencesNotifier = ref.watch(userPreferencesProvider.notifier);
final sourcePresets = ref.watch(audioSourcePresetsProvider);
final sourcePresetsNotifier =
ref.watch(audioSourcePresetsProvider.notifier);
final theme = Theme.of(context); final theme = Theme.of(context);
return SectionCardWithHeading( return SectionCardWithHeading(
heading: context.l10n.playback, heading: context.l10n.playback,
children: [ children: [
AdaptiveSelectTile<SourceQualities>( AdaptiveSelectTile<YoutubeClientEngine>(
secondary: const Icon(SpotubeIcons.audioQuality),
title: Text(context.l10n.audio_quality),
value: preferences.audioQuality,
options: [
if (preferences.audioSource == AudioSource.dabMusic)
SelectItemButton(
value: SourceQualities.uncompressed,
child: Text(context.l10n.uncompressed),
),
SelectItemButton(
value: SourceQualities.high,
child: Text(context.l10n.high),
),
if (preferences.audioSource != AudioSource.dabMusic) ...[
SelectItemButton(
value: SourceQualities.medium,
child: Text(context.l10n.medium),
),
SelectItemButton(
value: SourceQualities.low,
child: Text(context.l10n.low),
),
]
],
onChanged: (value) {
if (value != null) {
preferencesNotifier.setAudioQuality(value);
}
},
),
AdaptiveSelectTile<AudioSource>(
secondary: const Icon(SpotubeIcons.api),
title: Text(context.l10n.audio_source),
value: preferences.audioSource,
options: AudioSource.values
.map((e) => SelectItemButton(
value: e,
child: Text(e.label),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferencesNotifier.setAudioSource(value);
},
),
AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
crossFadeState: preferences.audioSource != AudioSource.piped
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: const SizedBox.shrink(),
secondChild: Consumer(
builder: (context, ref, child) {
final instanceList = ref.watch(pipedInstancesFutureProvider);
return instanceList.when(
data: (data) {
return AdaptiveSelectTile<String>(
secondary: const Icon(SpotubeIcons.piped),
title: Text(context.l10n.piped_instance),
subtitle: Text(
"${context.l10n.piped_description}\n"
"${context.l10n.piped_warning}",
),
value: preferences.pipedInstance,
showValueWhenUnfolded: false,
trailing: [
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_custom_url),
).call,
child: IconButton.outline(
icon: const Icon(SpotubeIcons.edit),
size: ButtonSize.small,
onPressed: () {
showDialog(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.5),
builder: (context) =>
SettingsPlaybackEditInstanceUrlDialog(
title: context.l10n.piped_instance,
initialValue: preferences.pipedInstance,
onSave: (value) {
preferencesNotifier.setPipedInstance(value);
},
),
);
},
),
)
],
options: [
if (data
.none((e) => e.apiUrl == preferences.pipedInstance))
SelectItemButton(
value: preferences.pipedInstance,
child: Text.rich(
TextSpan(
style: theme.typography.xSmall.copyWith(
color: theme.colorScheme.foreground,
),
children: [
TextSpan(text: context.l10n.custom),
const TextSpan(text: "\n"),
TextSpan(text: preferences.pipedInstance),
],
),
),
),
for (final e in data.sortedBy((e) => e.name))
SelectItemButton(
value: e.apiUrl,
child: RichText(
text: TextSpan(
style: theme.typography.normal.copyWith(
color: theme.colorScheme.foreground,
),
children: [
TextSpan(
text: "${e.name.trim()}\n",
),
TextSpan(
text: e.locations
.map(countryCodeToEmoji)
.join(""),
style: GoogleFonts.notoColorEmoji(),
),
],
),
),
),
],
onChanged: (value) {
if (value != null) {
preferencesNotifier.setPipedInstance(value);
}
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Text(error.toString()),
);
},
),
),
AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
crossFadeState: preferences.audioSource != AudioSource.invidious
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: const SizedBox.shrink(),
secondChild: Consumer(
builder: (context, ref, child) {
final instanceList = ref.watch(invidiousInstancesProvider);
return instanceList.when(
data: (data) {
return AdaptiveSelectTile<String>(
secondary: const Icon(SpotubeIcons.piped),
title: Text(context.l10n.invidious_instance),
subtitle: Text(
"${context.l10n.invidious_description}\n"
"${context.l10n.invidious_warning}",
),
trailing: [
Tooltip(
tooltip: TooltipContainer(
child: Text(context.l10n.add_custom_url),
).call,
child: IconButton.outline(
icon: const Icon(SpotubeIcons.edit),
size: ButtonSize.small,
onPressed: () {
showDialog(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.5),
builder: (context) =>
SettingsPlaybackEditInstanceUrlDialog(
title: context.l10n.invidious_instance,
initialValue: preferences.invidiousInstance,
onSave: (value) {
preferencesNotifier
.setInvidiousInstance(value);
},
),
);
},
),
)
],
value: preferences.invidiousInstance,
showValueWhenUnfolded: false,
options: [
if (data.none((e) =>
e.details.uri == preferences.invidiousInstance))
SelectItemButton(
value: preferences.invidiousInstance,
child: Text.rich(
TextSpan(
style: theme.typography.xSmall.copyWith(
color: theme.colorScheme.foreground,
),
children: [
TextSpan(text: context.l10n.custom),
const TextSpan(text: "\n"),
TextSpan(text: preferences.invidiousInstance),
],
),
),
),
for (final e in data.sortedBy((e) => e.name))
SelectItemButton(
value: e.details.uri,
child: RichText(
text: TextSpan(
style: theme.typography.normal.copyWith(
color: theme.colorScheme.foreground,
),
children: [
TextSpan(
text: "${e.name.trim()}\n",
),
TextSpan(
text: countryCodeToEmoji(
e.details.region,
),
style: GoogleFonts.notoColorEmoji(),
),
],
),
),
),
],
onChanged: (value) {
if (value != null) {
preferencesNotifier.setInvidiousInstance(value);
}
},
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Text(error.toString()),
);
},
),
),
switch (preferences.audioSource) {
AudioSource.youtube => AdaptiveSelectTile<YoutubeClientEngine>(
secondary: const Icon(SpotubeIcons.engine), secondary: const Icon(SpotubeIcons.engine),
title: Text(context.l10n.youtube_engine), title: Text(context.l10n.youtube_engine),
value: preferences.youtubeClientEngine, value: preferences.youtubeClientEngine,
@ -307,8 +52,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
if (value == YoutubeClientEngine.ytDlp) { if (value == YoutubeClientEngine.ytDlp) {
final customPath = KVStoreService.getYoutubeEnginePath(value); final customPath = KVStoreService.getYoutubeEnginePath(value);
if (!await YtDlpEngine.isInstalled() && if (!await YtDlpEngine.isInstalled() &&
(customPath == null || (customPath == null || !await File(customPath).exists()) &&
!await File(customPath).exists()) &&
context.mounted) { context.mounted) {
final hasInstalled = await showDialog<bool>( final hasInstalled = await showDialog<bool>(
context: context, context: context,
@ -321,45 +65,70 @@ class SettingsPlaybackSection extends HookConsumerWidget {
preferencesNotifier.setYoutubeClientEngine(value); preferencesNotifier.setYoutubeClientEngine(value);
}, },
), ),
AudioSource.piped || if (sourcePresets.presets.isNotEmpty) ...[
AudioSource.invidious => AdaptiveSelectTile(
AdaptiveSelectTile<SearchMode>( secondary: const Icon(SpotubeIcons.api),
secondary: const Icon(SpotubeIcons.search), title: Text(context.l10n.streaming_music_codec),
title: Text(context.l10n.search_mode), value: sourcePresets.selectedStreamingContainerIndex,
value: preferences.searchMode, options: [
options: SearchMode.values for (final MapEntry(:key, value: preset)
.map((e) => SelectItemButton( in sourcePresets.presets.asMap().entries)
value: e, SelectItemButton(value: key, child: Text(preset.name)),
child: Text(e.label), ],
))
.toList(),
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
preferencesNotifier.setSearchMode(value); sourcePresetsNotifier.setSelectedStreamingContainerIndex(value);
}, },
), ),
_ => const SizedBox.shrink(), AdaptiveSelectTile(
}, secondary: const Icon(SpotubeIcons.api),
AnimatedCrossFade( title: const Text("Streaming music quality"),
duration: const Duration(milliseconds: 300), value: sourcePresets.selectedStreamingQualityIndex,
crossFadeState: preferences.searchMode == SearchMode.youtube && options: [
(preferences.audioSource == AudioSource.piped || for (final MapEntry(:key, value: quality) in sourcePresets
preferences.audioSource == AudioSource.youtube || .presets[sourcePresets.selectedStreamingContainerIndex]
preferences.audioSource == AudioSource.invidious) .qualities
? CrossFadeState.showFirst .asMap()
: CrossFadeState.showSecond, .entries)
firstChild: ListTile( SelectItemButton(value: key, child: Text(quality.toString())),
leading: const Icon(SpotubeIcons.skip), ],
title: Text(context.l10n.skip_non_music), onChanged: (value) {
trailing: Switch( if (value == null) return;
value: preferences.skipNonMusic, sourcePresetsNotifier.setSelectedStreamingQualityIndex(value);
onChanged: (state) {
preferencesNotifier.setSkipNonMusic(state);
}, },
), ),
AdaptiveSelectTile(
secondary: const Icon(SpotubeIcons.api),
title: Text(context.l10n.download_music_codec),
value: sourcePresets.selectedDownloadingContainerIndex,
options: [
for (final MapEntry(:key, value: preset)
in sourcePresets.presets.asMap().entries)
SelectItemButton(value: key, child: Text(preset.name)),
],
onChanged: (value) {
if (value == null) return;
sourcePresetsNotifier.setSelectedDownloadingContainerIndex(value);
},
), ),
secondChild: const SizedBox.shrink(), AdaptiveSelectTile(
secondary: const Icon(SpotubeIcons.api),
title: const Text("Downloading music quality"),
value: sourcePresets.selectedStreamingQualityIndex,
options: [
for (final MapEntry(:key, value: quality) in sourcePresets
.presets[sourcePresets.selectedDownloadingContainerIndex]
.qualities
.asMap()
.entries)
SelectItemButton(value: key, child: Text(quality.toString())),
],
onChanged: (value) {
if (value == null) return;
sourcePresetsNotifier.setSelectedStreamingQualityIndex(value);
},
), ),
],
ListTile( ListTile(
title: Text(context.l10n.cache_music), title: Text(context.l10n.cache_music),
subtitle: kIsMobile subtitle: kIsMobile
@ -403,50 +172,6 @@ class SettingsPlaybackSection extends HookConsumerWidget {
onChanged: preferencesNotifier.setNormalizeAudio, onChanged: preferencesNotifier.setNormalizeAudio,
), ),
), ),
if (const [AudioSource.jiosaavn, AudioSource.dabMusic]
.contains(preferences.audioSource) ==
false) ...[
AdaptiveSelectTile<SourceCodecs>(
popupConstraints: const BoxConstraints(maxWidth: 300),
secondary: const Icon(SpotubeIcons.stream),
title: Text(context.l10n.streaming_music_codec),
value: preferences.streamMusicCodec,
showValueWhenUnfolded: false,
options: SourceCodecs.values
.map((e) => SelectItemButton(
value: e,
child: Text(
e.label,
style: theme.typography.small,
),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferencesNotifier.setStreamMusicCodec(value);
},
),
AdaptiveSelectTile<SourceCodecs>(
popupConstraints: const BoxConstraints(maxWidth: 300),
secondary: const Icon(SpotubeIcons.file),
title: Text(context.l10n.download_music_codec),
value: preferences.downloadMusicCodec,
showValueWhenUnfolded: false,
options: SourceCodecs.values
.map((e) => SelectItemButton(
value: e,
child: Text(
e.label,
style: theme.typography.small,
),
))
.toList(),
onChanged: (value) {
if (value == null) return;
preferencesNotifier.setDownloadMusicCodec(value);
},
),
],
ListTile( ListTile(
leading: const Icon(SpotubeIcons.repeat), leading: const Icon(SpotubeIcons.repeat),
title: Text(context.l10n.endless_playback), title: Text(context.l10n.endless_playback),

View File

@ -7,12 +7,11 @@ import 'package:media_kit/media_kit.dart';
import 'package:spotube/extensions/list.dart'; import 'package:spotube/extensions/list.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/discord_provider.dart'; import 'package:spotube/provider/discord_provider.dart';
import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
@ -164,8 +163,8 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
final tracks = <SpotubeTrackObject>[]; final tracks = <SpotubeTrackObject>[];
for (final media in playlist.medias) { for (final media in playlist.medias) {
final trackQuery = TrackSourceQuery.parseUri(media.uri); final track = trackGroupedById[SpotubeMedia.media(media).track.id]
final track = trackGroupedById[trackQuery.id]?.firstOrNull; ?.firstOrNull;
if (track != null) { if (track != null) {
tracks.add(track); tracks.add(track);
} }
@ -400,10 +399,9 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
// because of timeout // because of timeout
final intendedActiveTrack = medias.elementAt(initialIndex); final intendedActiveTrack = medias.elementAt(initialIndex);
if (intendedActiveTrack.track is! SpotubeLocalTrackObject) { if (intendedActiveTrack.track is! SpotubeLocalTrackObject) {
await ref.read( ref.read(
trackSourcesProvider( sourcedTrackProvider(
TrackSourceQuery.fromTrack( intendedActiveTrack.track as SpotubeFullTrackObject,
intendedActiveTrack.track as SpotubeFullTrackObject),
).future, ).future,
); );
} }

View File

@ -3,14 +3,13 @@ import 'dart:math';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/discord_provider.dart'; import 'package:spotube/provider/discord_provider.dart';
import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/history/history.dart';
import 'package:spotube/provider/metadata_plugin/core/scrobble.dart'; import 'package:spotube/provider/metadata_plugin/core/scrobble.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart';
import 'package:spotube/provider/skip_segments/skip_segments.dart'; import 'package:spotube/provider/skip_segments/skip_segments.dart';
import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/scrobbler/scrobbler.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -156,9 +155,7 @@ class AudioPlayerStreamListeners {
try { try {
await ref.read( await ref.read(
trackSourcesProvider( sourcedTrackProvider(nextTrack as SpotubeFullTrackObject).future,
TrackSourceQuery.fromTrack(nextTrack as SpotubeFullTrackObject),
).future,
); );
} finally { } finally {
lastTrack = nextTrack.id; lastTrack = nextTrack.id;

View File

@ -1,8 +1,7 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart';
final queryingTrackInfoProvider = Provider<bool>((ref) { final queryingTrackInfoProvider = Provider<bool>((ref) {
final audioPlayer = ref.watch(audioPlayerProvider); final audioPlayer = ref.watch(audioPlayerProvider);
@ -16,10 +15,9 @@ final queryingTrackInfoProvider = Provider<bool>((ref) {
} }
return ref return ref
.watch(trackSourcesProvider( .watch(
TrackSourceQuery.fromTrack( sourcedTrackProvider(
audioPlayer.activeTrack! as SpotubeFullTrackObject, audioPlayer.activeTrack! as SpotubeFullTrackObject),
), )
))
.isLoading; .isLoading;
}); });

View File

@ -1,12 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/services/sourced_track/sources/invidious.dart';
final invidiousInstancesProvider = FutureProvider((ref) async {
final invidious = ref.watch(invidiousProvider);
final instances = await invidious.instances();
return instances
.where((instance) => instance.details.type == "https")
.toList();
});

View File

@ -1,17 +0,0 @@
import 'package:spotube/services/logger/logger.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:piped_client/piped_client.dart';
import 'package:spotube/services/sourced_track/sources/piped.dart';
final pipedInstancesFutureProvider = FutureProvider<List<PipedInstance>>(
(ref) async {
try {
final pipedClient = ref.watch(pipedProvider);
return await pipedClient.instanceList();
} catch (e, stack) {
AppLogger.reportError(e, stack);
return <PipedInstance>[];
}
},
);

View File

@ -2,8 +2,8 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart';
import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -12,7 +12,6 @@ import 'package:metadata_god/metadata_god.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/download_manager/download_manager.dart'; import 'package:spotube/services/download_manager/download_manager.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
@ -27,7 +26,10 @@ class DownloadManagerProvider extends ChangeNotifier {
final (:request, :status) = event; final (:request, :status) = event;
final sourcedTrack = $history.firstWhereOrNull( final sourcedTrack = $history.firstWhereOrNull(
(element) => element.getUrlOfCodec(downloadCodec) == request.url, (element) =>
element.getUrlOfQuality(
downloadContainer, downloadQualityIndex) ==
request.url,
); );
if (sourcedTrack == null) return; if (sourcedTrack == null) return;
final track = $backHistory.firstWhereOrNull( final track = $backHistory.firstWhereOrNull(
@ -49,7 +51,8 @@ class DownloadManagerProvider extends ChangeNotifier {
//? WebA audiotagging is not supported yet //? WebA audiotagging is not supported yet
//? Although in future by converting weba to opus & then tagging it //? Although in future by converting weba to opus & then tagging it
//? is possible using vorbis comments //? is possible using vorbis comments
downloadCodec == SourceCodecs.weba) { downloadContainer.name == "weba" ||
downloadContainer.name == "webm") {
return; return;
} }
@ -88,8 +91,13 @@ class DownloadManagerProvider extends ChangeNotifier {
String get downloadDirectory => String get downloadDirectory =>
ref.read(userPreferencesProvider.select((s) => s.downloadLocation)); ref.read(userPreferencesProvider.select((s) => s.downloadLocation));
SourceCodecs get downloadCodec => SpotubeAudioSourceContainerPreset get downloadContainer => ref.read(
ref.read(userPreferencesProvider.select((s) => s.downloadMusicCodec)); audioSourcePresetsProvider
.select((s) => s.presets[s.selectedDownloadingContainerIndex]),
);
int get downloadQualityIndex => ref.read(audioSourcePresetsProvider
.select((s) => s.selectedDownloadingQualityIndex));
int get $downloadCount => dl int get $downloadCount => dl
.getAllDownloads() .getAllDownloads()
@ -108,7 +116,7 @@ class DownloadManagerProvider extends ChangeNotifier {
String getTrackFileUrl(SourcedTrack track) { String getTrackFileUrl(SourcedTrack track) {
final name = final name =
"${track.query.title} - ${track.query.artists.join(", ")}.${downloadCodec.name}"; "${track.query.name} - ${track.query.artists.join(", ")}.${downloadContainer.name}";
return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name)); return join(downloadDirectory, PrimitiveUtils.toSafeFileName(name));
} }
@ -130,13 +138,16 @@ class DownloadManagerProvider extends ChangeNotifier {
download.status.value == DownloadStatus.queued, download.status.value == DownloadStatus.queued,
) )
.map((e) => e.request.url) .map((e) => e.request.url)
.contains(sourcedTrack.getUrlOfCodec(downloadCodec)!); .contains(sourcedTrack.getUrlOfQuality(
downloadContainer,
downloadQualityIndex,
)!);
} }
/// For singular downloads /// For singular downloads
Future<void> addToQueue(SpotubeFullTrackObject track) async { Future<void> addToQueue(SpotubeFullTrackObject track) async {
final sourcedTrack = await ref.read( final sourcedTrack = await ref.read(
trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future, sourcedTrackProvider(track).future,
); );
final savePath = getTrackFileUrl(sourcedTrack); final savePath = getTrackFileUrl(sourcedTrack);
@ -150,9 +161,9 @@ class DownloadManagerProvider extends ChangeNotifier {
await oldFile.rename("$savePath.old"); await oldFile.rename("$savePath.old");
} }
if (sourcedTrack.codec == downloadCodec) { if (sourcedTrack.qualityPreset == downloadContainer) {
final downloadTask = await dl.addDownload( final downloadTask = await dl.addDownload(
sourcedTrack.getUrlOfCodec(downloadCodec)!, sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!,
savePath, savePath,
); );
if (downloadTask != null) { if (downloadTask != null) {
@ -160,18 +171,13 @@ class DownloadManagerProvider extends ChangeNotifier {
} }
} else { } else {
$backHistory.add(track); $backHistory.add(track);
final sourcedTrack = await ref final sourcedTrack =
.read( await ref.read(sourcedTrackProvider(track).future).then((d) {
trackSourcesProvider(
TrackSourceQuery.fromTrack(track),
).future,
)
.then((d) {
$backHistory.remove(track); $backHistory.remove(track);
return d; return d;
}); });
final downloadTask = await dl.addDownload( final downloadTask = await dl.addDownload(
sourcedTrack.getUrlOfCodec(downloadCodec)!, sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!,
savePath, savePath,
); );
if (downloadTask != null) { if (downloadTask != null) {
@ -204,18 +210,21 @@ class DownloadManagerProvider extends ChangeNotifier {
Future<void> removeFromQueue(SpotubeFullTrackObject track) async { Future<void> removeFromQueue(SpotubeFullTrackObject track) async {
final sourcedTrack = await mapToSourcedTrack(track); final sourcedTrack = await mapToSourcedTrack(track);
await dl.removeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); await dl.removeDownload(
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!);
$history.remove(sourcedTrack); $history.remove(sourcedTrack);
} }
Future<void> pause(SpotubeFullTrackObject track) async { Future<void> pause(SpotubeFullTrackObject track) async {
final sourcedTrack = await mapToSourcedTrack(track); final sourcedTrack = await mapToSourcedTrack(track);
return dl.pauseDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); return dl.pauseDownload(
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!);
} }
Future<void> resume(SpotubeFullTrackObject track) async { Future<void> resume(SpotubeFullTrackObject track) async {
final sourcedTrack = await mapToSourcedTrack(track); final sourcedTrack = await mapToSourcedTrack(track);
return dl.resumeDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); return dl.resumeDownload(
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!);
} }
Future<void> retry(SpotubeFullTrackObject track) { Future<void> retry(SpotubeFullTrackObject track) {
@ -224,7 +233,8 @@ class DownloadManagerProvider extends ChangeNotifier {
void cancel(SpotubeFullTrackObject track) async { void cancel(SpotubeFullTrackObject track) async {
final sourcedTrack = await mapToSourcedTrack(track); final sourcedTrack = await mapToSourcedTrack(track);
return dl.cancelDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!); return dl.cancelDownload(
sourcedTrack.getUrlOfQuality(downloadContainer, downloadQualityIndex)!);
} }
void cancelAll() { void cancelAll() {
@ -242,9 +252,7 @@ class DownloadManagerProvider extends ChangeNotifier {
return historicTrack; return historicTrack;
} }
final sourcedTrack = await ref.read( final sourcedTrack = await ref.read(sourcedTrackProvider(track).future);
trackSourcesProvider(TrackSourceQuery.fromTrack(track)).future,
);
return sourcedTrack; return sourcedTrack;
} }
@ -258,7 +266,10 @@ class DownloadManagerProvider extends ChangeNotifier {
if (sourcedTrack == null) { if (sourcedTrack == null) {
return null; return null;
} }
return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!)?.status; return dl
.getDownload(sourcedTrack.getUrlOfQuality(
downloadContainer, downloadQualityIndex)!)
?.status;
} }
ValueNotifier<double>? getProgressNotifier(SpotubeFullTrackObject track) { ValueNotifier<double>? getProgressNotifier(SpotubeFullTrackObject track) {
@ -268,7 +279,10 @@ class DownloadManagerProvider extends ChangeNotifier {
if (sourcedTrack == null) { if (sourcedTrack == null) {
return null; return null;
} }
return dl.getDownload(sourcedTrack.getUrlOfCodec(downloadCodec)!)?.progress; return dl
.getDownload(sourcedTrack.getUrlOfQuality(
downloadContainer, downloadQualityIndex)!)
?.progress;
} }
} }

View File

@ -0,0 +1,12 @@
import 'package:riverpod/riverpod.dart';
import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart';
final audioSourceQualityLabelProvider = Provider<String>((ref) {
final sourceQuality = ref.watch(audioSourcePresetsProvider);
final sourceContainer = sourceQuality.presets
.elementAtOrNull(sourceQuality.selectedStreamingContainerIndex);
final quality = sourceContainer?.qualities
.elementAtOrNull(sourceQuality.selectedStreamingQualityIndex);
return "${sourceContainer?.name ?? "Unknown"}${quality?.toString() ?? "Unknown"}";
});

View File

@ -0,0 +1,120 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
part 'quality_presets.g.dart';
part 'quality_presets.freezed.dart';
@freezed
class AudioSourcePresetsState with _$AudioSourcePresetsState {
factory AudioSourcePresetsState({
@Default([]) final List<SpotubeAudioSourceContainerPreset> presets,
@Default(0) final int selectedStreamingQualityIndex,
@Default(0) final int selectedStreamingContainerIndex,
@Default(0) final int selectedDownloadingQualityIndex,
@Default(0) final int selectedDownloadingContainerIndex,
}) = _AudioSourcePresetsState;
factory AudioSourcePresetsState.fromJson(Map<String, dynamic> json) =>
_$AudioSourcePresetsStateFromJson(json);
}
class AudioSourceAvailableQualityPresetsNotifier
extends Notifier<AudioSourcePresetsState> {
@override
build() {
ref.watch(audioSourcePluginProvider);
_initialize();
listenSelf((previous, next) {
final isNewLossless =
next.presets.elementAtOrNull(next.selectedStreamingContainerIndex)
is SpotubeAudioSourceContainerPresetLossless;
final isOldLossless = previous?.presets
.elementAtOrNull(previous.selectedStreamingContainerIndex)
is SpotubeAudioSourceContainerPresetLossless;
if (!isOldLossless && isNewLossless) {
audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB
} else if (isOldLossless && !isNewLossless) {
audioPlayer.setDemuxerBufferSize(4 * 1024 * 1024); // 4MB
}
});
return AudioSourcePresetsState();
}
void _initialize() async {
final audioSource = await ref.read(audioSourcePluginProvider.future);
final audioSourceConfig = await ref.read(
metadataPluginsProvider
.selectAsync((data) => data.defaultAudioSourcePluginConfig),
);
if (audioSource == null || audioSourceConfig == null) {
throw Exception("Dude wat?");
}
final preferences = await SharedPreferences.getInstance();
final persistedStateStr =
preferences.getString("audioSourceState-${audioSourceConfig.slug}");
if (persistedStateStr != null) {
state = AudioSourcePresetsState.fromJson(jsonDecode(persistedStateStr));
} else {
state = AudioSourcePresetsState(
presets: audioSource.audioSource.supportedPresets,
);
}
}
void setSelectedStreamingContainerIndex(int index) {
state = state.copyWith(
selectedStreamingContainerIndex: index,
selectedStreamingQualityIndex:
0, // Resetting both because it's a different quality
);
_updatePreferences();
}
void setSelectedStreamingQualityIndex(int index) {
state = state.copyWith(selectedStreamingQualityIndex: index);
_updatePreferences();
}
void setSelectedDownloadingContainerIndex(int index) {
state = state.copyWith(
selectedDownloadingContainerIndex: index,
selectedDownloadingQualityIndex:
0, // Resetting both because it's a different quality
);
_updatePreferences();
}
void setSelectedDownloadingQualityIndex(int index) {
state = state.copyWith(selectedDownloadingQualityIndex: index);
_updatePreferences();
}
void _updatePreferences() async {
final audioSourceConfig = await ref.read(metadataPluginsProvider
.selectAsync((data) => data.defaultAudioSourcePluginConfig));
if (audioSourceConfig == null) {
throw Exception("Dude wat?");
}
final preferences = await SharedPreferences.getInstance();
await preferences.setString(
"audioSourceState-${audioSourceConfig.slug}",
jsonEncode(state),
);
}
}
final audioSourcePresetsProvider = NotifierProvider<
AudioSourceAvailableQualityPresetsNotifier, AudioSourcePresetsState>(
() => AudioSourceAvailableQualityPresetsNotifier(),
);

View File

@ -0,0 +1,289 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'quality_presets.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
AudioSourcePresetsState _$AudioSourcePresetsStateFromJson(
Map<String, dynamic> json) {
return _AudioSourcePresetsState.fromJson(json);
}
/// @nodoc
mixin _$AudioSourcePresetsState {
List<SpotubeAudioSourceContainerPreset> get presets =>
throw _privateConstructorUsedError;
int get selectedStreamingQualityIndex => throw _privateConstructorUsedError;
int get selectedStreamingContainerIndex => throw _privateConstructorUsedError;
int get selectedDownloadingQualityIndex => throw _privateConstructorUsedError;
int get selectedDownloadingContainerIndex =>
throw _privateConstructorUsedError;
/// Serializes this AudioSourcePresetsState to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of AudioSourcePresetsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$AudioSourcePresetsStateCopyWith<AudioSourcePresetsState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AudioSourcePresetsStateCopyWith<$Res> {
factory $AudioSourcePresetsStateCopyWith(AudioSourcePresetsState value,
$Res Function(AudioSourcePresetsState) then) =
_$AudioSourcePresetsStateCopyWithImpl<$Res, AudioSourcePresetsState>;
@useResult
$Res call(
{List<SpotubeAudioSourceContainerPreset> presets,
int selectedStreamingQualityIndex,
int selectedStreamingContainerIndex,
int selectedDownloadingQualityIndex,
int selectedDownloadingContainerIndex});
}
/// @nodoc
class _$AudioSourcePresetsStateCopyWithImpl<$Res,
$Val extends AudioSourcePresetsState>
implements $AudioSourcePresetsStateCopyWith<$Res> {
_$AudioSourcePresetsStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of AudioSourcePresetsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? presets = null,
Object? selectedStreamingQualityIndex = null,
Object? selectedStreamingContainerIndex = null,
Object? selectedDownloadingQualityIndex = null,
Object? selectedDownloadingContainerIndex = null,
}) {
return _then(_value.copyWith(
presets: null == presets
? _value.presets
: presets // ignore: cast_nullable_to_non_nullable
as List<SpotubeAudioSourceContainerPreset>,
selectedStreamingQualityIndex: null == selectedStreamingQualityIndex
? _value.selectedStreamingQualityIndex
: selectedStreamingQualityIndex // ignore: cast_nullable_to_non_nullable
as int,
selectedStreamingContainerIndex: null == selectedStreamingContainerIndex
? _value.selectedStreamingContainerIndex
: selectedStreamingContainerIndex // ignore: cast_nullable_to_non_nullable
as int,
selectedDownloadingQualityIndex: null == selectedDownloadingQualityIndex
? _value.selectedDownloadingQualityIndex
: selectedDownloadingQualityIndex // ignore: cast_nullable_to_non_nullable
as int,
selectedDownloadingContainerIndex: null ==
selectedDownloadingContainerIndex
? _value.selectedDownloadingContainerIndex
: selectedDownloadingContainerIndex // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$AudioSourcePresetsStateImplCopyWith<$Res>
implements $AudioSourcePresetsStateCopyWith<$Res> {
factory _$$AudioSourcePresetsStateImplCopyWith(
_$AudioSourcePresetsStateImpl value,
$Res Function(_$AudioSourcePresetsStateImpl) then) =
__$$AudioSourcePresetsStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<SpotubeAudioSourceContainerPreset> presets,
int selectedStreamingQualityIndex,
int selectedStreamingContainerIndex,
int selectedDownloadingQualityIndex,
int selectedDownloadingContainerIndex});
}
/// @nodoc
class __$$AudioSourcePresetsStateImplCopyWithImpl<$Res>
extends _$AudioSourcePresetsStateCopyWithImpl<$Res,
_$AudioSourcePresetsStateImpl>
implements _$$AudioSourcePresetsStateImplCopyWith<$Res> {
__$$AudioSourcePresetsStateImplCopyWithImpl(
_$AudioSourcePresetsStateImpl _value,
$Res Function(_$AudioSourcePresetsStateImpl) _then)
: super(_value, _then);
/// Create a copy of AudioSourcePresetsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? presets = null,
Object? selectedStreamingQualityIndex = null,
Object? selectedStreamingContainerIndex = null,
Object? selectedDownloadingQualityIndex = null,
Object? selectedDownloadingContainerIndex = null,
}) {
return _then(_$AudioSourcePresetsStateImpl(
presets: null == presets
? _value._presets
: presets // ignore: cast_nullable_to_non_nullable
as List<SpotubeAudioSourceContainerPreset>,
selectedStreamingQualityIndex: null == selectedStreamingQualityIndex
? _value.selectedStreamingQualityIndex
: selectedStreamingQualityIndex // ignore: cast_nullable_to_non_nullable
as int,
selectedStreamingContainerIndex: null == selectedStreamingContainerIndex
? _value.selectedStreamingContainerIndex
: selectedStreamingContainerIndex // ignore: cast_nullable_to_non_nullable
as int,
selectedDownloadingQualityIndex: null == selectedDownloadingQualityIndex
? _value.selectedDownloadingQualityIndex
: selectedDownloadingQualityIndex // ignore: cast_nullable_to_non_nullable
as int,
selectedDownloadingContainerIndex: null ==
selectedDownloadingContainerIndex
? _value.selectedDownloadingContainerIndex
: selectedDownloadingContainerIndex // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _$AudioSourcePresetsStateImpl implements _AudioSourcePresetsState {
_$AudioSourcePresetsStateImpl(
{final List<SpotubeAudioSourceContainerPreset> presets = const [],
this.selectedStreamingQualityIndex = 0,
this.selectedStreamingContainerIndex = 0,
this.selectedDownloadingQualityIndex = 0,
this.selectedDownloadingContainerIndex = 0})
: _presets = presets;
factory _$AudioSourcePresetsStateImpl.fromJson(Map<String, dynamic> json) =>
_$$AudioSourcePresetsStateImplFromJson(json);
final List<SpotubeAudioSourceContainerPreset> _presets;
@override
@JsonKey()
List<SpotubeAudioSourceContainerPreset> get presets {
if (_presets is EqualUnmodifiableListView) return _presets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_presets);
}
@override
@JsonKey()
final int selectedStreamingQualityIndex;
@override
@JsonKey()
final int selectedStreamingContainerIndex;
@override
@JsonKey()
final int selectedDownloadingQualityIndex;
@override
@JsonKey()
final int selectedDownloadingContainerIndex;
@override
String toString() {
return 'AudioSourcePresetsState(presets: $presets, selectedStreamingQualityIndex: $selectedStreamingQualityIndex, selectedStreamingContainerIndex: $selectedStreamingContainerIndex, selectedDownloadingQualityIndex: $selectedDownloadingQualityIndex, selectedDownloadingContainerIndex: $selectedDownloadingContainerIndex)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AudioSourcePresetsStateImpl &&
const DeepCollectionEquality().equals(other._presets, _presets) &&
(identical(other.selectedStreamingQualityIndex,
selectedStreamingQualityIndex) ||
other.selectedStreamingQualityIndex ==
selectedStreamingQualityIndex) &&
(identical(other.selectedStreamingContainerIndex,
selectedStreamingContainerIndex) ||
other.selectedStreamingContainerIndex ==
selectedStreamingContainerIndex) &&
(identical(other.selectedDownloadingQualityIndex,
selectedDownloadingQualityIndex) ||
other.selectedDownloadingQualityIndex ==
selectedDownloadingQualityIndex) &&
(identical(other.selectedDownloadingContainerIndex,
selectedDownloadingContainerIndex) ||
other.selectedDownloadingContainerIndex ==
selectedDownloadingContainerIndex));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_presets),
selectedStreamingQualityIndex,
selectedStreamingContainerIndex,
selectedDownloadingQualityIndex,
selectedDownloadingContainerIndex);
/// Create a copy of AudioSourcePresetsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$AudioSourcePresetsStateImplCopyWith<_$AudioSourcePresetsStateImpl>
get copyWith => __$$AudioSourcePresetsStateImplCopyWithImpl<
_$AudioSourcePresetsStateImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$AudioSourcePresetsStateImplToJson(
this,
);
}
}
abstract class _AudioSourcePresetsState implements AudioSourcePresetsState {
factory _AudioSourcePresetsState(
{final List<SpotubeAudioSourceContainerPreset> presets,
final int selectedStreamingQualityIndex,
final int selectedStreamingContainerIndex,
final int selectedDownloadingQualityIndex,
final int selectedDownloadingContainerIndex}) =
_$AudioSourcePresetsStateImpl;
factory _AudioSourcePresetsState.fromJson(Map<String, dynamic> json) =
_$AudioSourcePresetsStateImpl.fromJson;
@override
List<SpotubeAudioSourceContainerPreset> get presets;
@override
int get selectedStreamingQualityIndex;
@override
int get selectedStreamingContainerIndex;
@override
int get selectedDownloadingQualityIndex;
@override
int get selectedDownloadingContainerIndex;
/// Create a copy of AudioSourcePresetsState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$AudioSourcePresetsStateImplCopyWith<_$AudioSourcePresetsStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,38 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'quality_presets.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$AudioSourcePresetsStateImpl _$$AudioSourcePresetsStateImplFromJson(
Map json) =>
_$AudioSourcePresetsStateImpl(
presets: (json['presets'] as List<dynamic>?)
?.map((e) => SpotubeAudioSourceContainerPreset.fromJson(
Map<String, dynamic>.from(e as Map)))
.toList() ??
const [],
selectedStreamingQualityIndex:
(json['selectedStreamingQualityIndex'] as num?)?.toInt() ?? 0,
selectedStreamingContainerIndex:
(json['selectedStreamingContainerIndex'] as num?)?.toInt() ?? 0,
selectedDownloadingQualityIndex:
(json['selectedDownloadingQualityIndex'] as num?)?.toInt() ?? 0,
selectedDownloadingContainerIndex:
(json['selectedDownloadingContainerIndex'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$$AudioSourcePresetsStateImplToJson(
_$AudioSourcePresetsStateImpl instance) =>
<String, dynamic>{
'presets': instance.presets.map((e) => e.toJson()).toList(),
'selectedStreamingQualityIndex': instance.selectedStreamingQualityIndex,
'selectedStreamingContainerIndex':
instance.selectedStreamingContainerIndex,
'selectedDownloadingQualityIndex':
instance.selectedDownloadingQualityIndex,
'selectedDownloadingContainerIndex':
instance.selectedDownloadingContainerIndex,
};

View File

@ -543,7 +543,7 @@ final audioSourcePluginProvider = FutureProvider<MetadataPlugin?>(
metadataPluginsProvider metadataPluginsProvider
.selectAsync((data) => data.defaultAudioSourcePluginConfig), .selectAsync((data) => data.defaultAudioSourcePluginConfig),
); );
final youtubeEngine = ref.read(youtubeEngineProvider); final youtubeEngine = ref.watch(youtubeEngineProvider);
if (defaultPlugin == null) { if (defaultPlugin == null) {
return null; return null;

View File

@ -1,14 +1,13 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
final activeTrackSourcesProvider = FutureProvider< final activeTrackSourcesProvider = FutureProvider<
({ ({
SourcedTrack? source, SourcedTrack? source,
TrackSourcesNotifier? notifier, SourcedTrackNotifier? notifier,
SpotubeTrackObject track, SpotubeTrackObject track,
})?>((ref) async { })?>((ref) async {
final audioPlayerState = ref.watch(audioPlayerProvider); final audioPlayerState = ref.watch(audioPlayerProvider);
@ -25,13 +24,15 @@ final activeTrackSourcesProvider = FutureProvider<
); );
} }
final trackQuery = TrackSourceQuery.fromTrack( final sourcedTrack = await ref.watch(
sourcedTrackProvider(
audioPlayerState.activeTrack! as SpotubeFullTrackObject, audioPlayerState.activeTrack! as SpotubeFullTrackObject,
).future,
); );
final sourcedTrack = await ref.watch(trackSourcesProvider(trackQuery).future);
final sourcedTrackNotifier = ref.watch( final sourcedTrackNotifier = ref.watch(
trackSourcesProvider(trackQuery).notifier, sourcedTrackProvider(
audioPlayerState.activeTrack! as SpotubeFullTrackObject,
).notifier,
); );
return ( return (

View File

@ -11,16 +11,14 @@ import 'package:path/path.dart';
import 'package:shelf/shelf.dart'; import 'package:shelf/shelf.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/parser/range_headers.dart'; import 'package:spotube/models/parser/range_headers.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/audio_player/state.dart'; import 'package:spotube/provider/audio_player/state.dart';
import 'package:spotube/provider/server/active_track_sources.dart'; import 'package:spotube/provider/server/active_track_sources.dart';
import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
@ -50,26 +48,30 @@ class ServerPlaybackRoutes {
return join( return join(
await UserPreferencesNotifier.getMusicCacheDir(), await UserPreferencesNotifier.getMusicCacheDir(),
ServiceUtils.sanitizeFilename( ServiceUtils.sanitizeFilename(
'${track.query.title} - ${track.query.artists.join(",")} (${track.info.id}).${track.codec.name}', '${track.query.name} - ${track.query.artists.join(",")} (${track.info.id}).${track.qualityPreset!.name}',
), ),
); );
} }
Future<SourcedTrack?> _getSourcedTrack( Future<SourcedTrack?> _getSourcedTrack(
Request request, String trackId) async { Request request,
String trackId,
) async {
final track = final track =
playlist.tracks.firstWhere((element) => element.id == trackId); playlist.tracks.firstWhere((element) => element.id == trackId);
final activeSourcedTrack = final activeSourcedTrack =
await ref.read(activeTrackSourcesProvider.future); await ref.read(activeTrackSourcesProvider.future);
final media = audioPlayer.playlist.medias
.firstWhere((e) => e.uri == request.requestedUri.toString());
final spotubeMedia =
media is SpotubeMedia ? media : SpotubeMedia.media(media);
final sourcedTrack = activeSourcedTrack?.track.id == track.id final sourcedTrack = activeSourcedTrack?.track.id == track.id
? activeSourcedTrack?.source ? activeSourcedTrack?.source
: await ref.read( : await ref.read(
trackSourcesProvider( sourcedTrackProvider(spotubeMedia.track as SpotubeFullTrackObject)
//! Use [Request.requestedUri] as it contains full https url. .future,
//! [Request.url] will exclude and starts relatively. (streams/<trackId>... basically)
TrackSourceQuery.parseUri(request.requestedUri.toString()),
).future,
); );
return sourcedTrack; return sourcedTrack;
@ -80,7 +82,7 @@ class ServerPlaybackRoutes {
SourcedTrack track, SourcedTrack track,
) async { ) async {
AppLogger.log.i( AppLogger.log.i(
"HEAD request for track: ${track.query.title}\n" "HEAD request for track: ${track.query.name}\n"
"Headers: ${request.headers}", "Headers: ${request.headers}",
); );
@ -92,7 +94,7 @@ class ServerPlaybackRoutes {
return dio_lib.Response( return dio_lib.Response(
statusCode: 200, statusCode: 200,
headers: Headers.fromMap({ headers: Headers.fromMap({
"content-type": ["audio/${track.codec.name}"], "content-type": ["audio/${track.qualityPreset!.name}"],
"content-length": ["$fileLength"], "content-length": ["$fileLength"],
"accept-ranges": ["bytes"], "accept-ranges": ["bytes"],
"content-range": ["bytes 0-$fileLength/$fileLength"], "content-range": ["bytes 0-$fileLength/$fileLength"],
@ -103,7 +105,7 @@ class ServerPlaybackRoutes {
String url = track.url ?? String url = track.url ??
await ref await ref
.read(trackSourcesProvider(track.query).notifier) .read(sourcedTrackProvider(track.query).notifier)
.swapWithNextSibling() .swapWithNextSibling()
.then((track) => track.url!); .then((track) => track.url!);
@ -129,7 +131,7 @@ class ServerPlaybackRoutes {
Map<String, dynamic> headers, Map<String, dynamic> headers,
) async { ) async {
AppLogger.log.i( AppLogger.log.i(
"GET request for track: ${track.query.title}\n" "GET request for track: ${track.query.name}\n"
"Headers: ${request.headers}", "Headers: ${request.headers}",
); );
@ -143,7 +145,7 @@ class ServerPlaybackRoutes {
response: dio_lib.Response<Uint8List>( response: dio_lib.Response<Uint8List>(
statusCode: 200, statusCode: 200,
headers: Headers.fromMap({ headers: Headers.fromMap({
"content-type": ["audio/${track.codec.name}"], "content-type": ["audio/${track.qualityPreset!.name}"],
"content-length": ["$cachedFileLength"], "content-length": ["$cachedFileLength"],
"accept-ranges": ["bytes"], "accept-ranges": ["bytes"],
"content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"], "content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"],
@ -158,7 +160,7 @@ class ServerPlaybackRoutes {
String url = track.url ?? String url = track.url ??
await ref await ref
.read(trackSourcesProvider(track.query).notifier) .read(sourcedTrackProvider(track.query).notifier)
.swapWithNextSibling() .swapWithNextSibling()
.then((track) => track.url!); .then((track) => track.url!);
@ -180,7 +182,7 @@ class ServerPlaybackRoutes {
AppLogger.reportError(e, stack); AppLogger.reportError(e, stack);
final sourcedTrack = await ref final sourcedTrack = await ref
.read(trackSourcesProvider(track.query).notifier) .read(sourcedTrackProvider(track.query).notifier)
.refreshStreamingUrl(); .refreshStreamingUrl();
url = sourcedTrack.url!; url = sourcedTrack.url!;
@ -206,11 +208,9 @@ class ServerPlaybackRoutes {
); );
} }
if (headers["range"] == "bytes=0-" && track.codec == SourceCodecs.flac) { if (headers["range"] == "bytes=0-" &&
final bufferSize = track.qualityPreset is SpotubeAudioSourceContainerPresetLossless) {
userPreferences.audioQuality == SourceQualities.uncompressed const bufferSize = 6 * 1024 * 1024; // 6MB for lossless
? 6 * 1024 * 1024 // 6MB for lossless
: 4 * 1024 * 1024; // 4MB for lossy
final endRange = min( final endRange = min(
bufferSize, bufferSize,
@ -228,7 +228,7 @@ class ServerPlaybackRoutes {
final res = await dio.get<Uint8List>(url, options: options); final res = await dio.get<Uint8List>(url, options: options);
AppLogger.log.i( AppLogger.log.i(
"Response for track: ${track.query.title}\n" "Response for track: ${track.query.name}\n"
"Status Code: ${res.statusCode}\n" "Status Code: ${res.statusCode}\n"
"Headers: ${res.headers.map}", "Headers: ${res.headers.map}",
); );
@ -262,7 +262,9 @@ class ServerPlaybackRoutes {
await trackPartialCacheFile.rename(trackCacheFile.path); await trackPartialCacheFile.rename(trackCacheFile.path);
} }
if (contentRange.total == fileLength && track.codec != SourceCodecs.weba) { if (contentRange.total == fileLength &&
track.qualityPreset!.name != "webm" ||
track.qualityPreset!.name != "weba") {
final playlistTrack = playlist.tracks.firstWhereOrNull( final playlistTrack = playlist.tracks.firstWhereOrNull(
(element) => element.id == track.query.id, (element) => element.id == track.query.id,
); );

View File

@ -0,0 +1,49 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
class SourcedTrackNotifier
extends FamilyAsyncNotifier<SourcedTrack, SpotubeFullTrackObject> {
@override
FutureOr<SourcedTrack> build(query) {
ref.watch(audioSourcePluginProvider);
ref.watch(audioSourcePresetsProvider);
return SourcedTrack.fetchFromTrack(query: query, ref: ref);
}
Future<SourcedTrack> refreshStreamingUrl() async {
return await update((prev) async {
return await prev.refreshStream();
});
}
Future<SourcedTrack> copyWithSibling() async {
return await update((prev) async {
return prev.copyWithSibling();
});
}
Future<SourcedTrack> swapWithSibling(
SpotubeAudioSourceMatchObject sibling,
) async {
return await update((prev) async {
return await prev.swapWithSibling(sibling) ?? prev;
});
}
Future<SourcedTrack> swapWithNextSibling() async {
return await update((prev) async {
return await prev.swapWithSibling(prev.siblings.first) as SourcedTrack;
});
}
}
final sourcedTrackProvider = AsyncNotifierProviderFamily<SourcedTrackNotifier,
SourcedTrack, SpotubeFullTrackObject>(
() => SourcedTrackNotifier(),
);

View File

@ -1,48 +0,0 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
class TrackSourcesNotifier
extends FamilyAsyncNotifier<SourcedTrack, TrackSourceQuery> {
@override
FutureOr<SourcedTrack> build(query) {
ref.watch(userPreferencesProvider.select((p) => p.audioQuality));
ref.watch(userPreferencesProvider.select((p) => p.audioSource));
ref.watch(userPreferencesProvider.select((p) => p.streamMusicCodec));
ref.watch(userPreferencesProvider.select((p) => p.downloadMusicCodec));
return SourcedTrack.fetchFromQuery(query: query, ref: ref);
}
Future<SourcedTrack> refreshStreamingUrl() async {
return await update((prev) async {
return await prev.refreshStream();
});
}
Future<SourcedTrack> copyWithSibling() async {
return await update((prev) async {
return prev.copyWithSibling();
});
}
Future<SourcedTrack> swapWithSibling(TrackSourceInfo sibling) async {
return await update((prev) async {
return await prev.swapWithSibling(sibling) ?? prev;
});
}
Future<SourcedTrack> swapWithNextSibling() async {
return await update((prev) async {
return await prev.swapWithSibling(prev.siblings.first) as SourcedTrack;
});
}
}
final trackSourcesProvider = AsyncNotifierProviderFamily<TrackSourcesNotifier,
SourcedTrack, TrackSourceQuery>(
() => TrackSourcesNotifier(),
);

View File

@ -86,18 +86,10 @@ final segmentProvider = FutureProvider<SourcedSegments?>(
if (snapshot == null) return null; if (snapshot == null) return null;
final (:track, :source, :notifier) = snapshot; final (:track, :source, :notifier) = snapshot;
if (track is SpotubeLocalTrackObject) return null; if (track is SpotubeLocalTrackObject) return null;
if (source!.source case AudioSource.jiosaavn) return null; if (!source!.source.toLowerCase().contains("youtube")) return null;
final skipNonMusic = ref.watch( final skipNonMusic =
userPreferencesProvider.select( ref.watch(userPreferencesProvider.select((s) => s.skipNonMusic));
(s) {
final isPipedYTMusicMode = s.audioSource == AudioSource.piped &&
s.searchMode == SearchMode.youtubeMusic;
return s.skipNonMusic && !isPipedYTMusicMode;
},
),
);
if (!skipNonMusic) { if (!skipNonMusic) {
return SourcedSegments(segments: [], source: source.info.id); return SourcedSegments(segments: [], source: source.info.id);

View File

@ -10,7 +10,6 @@ import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/database/database.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:open_file/open_file.dart'; import 'package:open_file/open_file.dart';
@ -54,7 +53,6 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
} }
await audioPlayer.setAudioNormalization(state.normalizeAudio); await audioPlayer.setAudioNormalization(state.normalizeAudio);
await _updatePlayerBufferSize(event.audioQuality, state.audioQuality);
} catch (e, stack) { } catch (e, stack) {
AppLogger.reportError(e, stack); AppLogger.reportError(e, stack);
} }
@ -80,24 +78,6 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
}); });
} }
/// Sets audio player's buffer size based on the selected audio quality
/// Uncompressed quality gets a larger buffer size for smoother playback
/// while other qualities use a standard buffer size.
Future<void> _updatePlayerBufferSize(
SourceQualities newQuality,
SourceQualities oldQuality,
) async {
if (newQuality == SourceQualities.uncompressed) {
audioPlayer.setDemuxerBufferSize(6 * 1024 * 1024); // 6MB
return;
}
if (oldQuality == SourceQualities.uncompressed &&
newQuality != SourceQualities.uncompressed) {
audioPlayer.setDemuxerBufferSize(4 * 1024 * 1024); // 4MB
}
}
Future<void> setData(PreferencesTableCompanion data) async { Future<void> setData(PreferencesTableCompanion data) async {
final db = ref.read(databaseProvider); final db = ref.read(databaseProvider);
@ -138,14 +118,6 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
} }
} }
void setStreamMusicCodec(SourceCodecs codec) {
setData(PreferencesTableCompanion(streamMusicCodec: Value(codec)));
}
void setDownloadMusicCodec(SourceCodecs codec) {
setData(PreferencesTableCompanion(downloadMusicCodec: Value(codec)));
}
void setThemeMode(ThemeMode mode) { void setThemeMode(ThemeMode mode) {
setData(PreferencesTableCompanion(themeMode: Value(mode))); setData(PreferencesTableCompanion(themeMode: Value(mode)));
} }
@ -172,11 +144,6 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
setData(PreferencesTableCompanion(checkUpdate: Value(check))); setData(PreferencesTableCompanion(checkUpdate: Value(check)));
} }
void setAudioQuality(SourceQualities quality) {
setData(PreferencesTableCompanion(audioQuality: Value(quality)));
_updatePlayerBufferSize(quality, state.audioQuality);
}
void setDownloadLocation(String downloadDir) { void setDownloadLocation(String downloadDir) {
if (downloadDir.isEmpty) return; if (downloadDir.isEmpty) return;
setData(PreferencesTableCompanion(downloadLocation: Value(downloadDir))); setData(PreferencesTableCompanion(downloadLocation: Value(downloadDir)));
@ -207,14 +174,6 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
setData(PreferencesTableCompanion(locale: Value(locale))); setData(PreferencesTableCompanion(locale: Value(locale)));
} }
void setPipedInstance(String instance) {
setData(PreferencesTableCompanion(pipedInstance: Value(instance)));
}
void setInvidiousInstance(String instance) {
setData(PreferencesTableCompanion(invidiousInstance: Value(instance)));
}
void setSearchMode(SearchMode mode) { void setSearchMode(SearchMode mode) {
setData(PreferencesTableCompanion(searchMode: Value(mode))); setData(PreferencesTableCompanion(searchMode: Value(mode)));
} }
@ -223,27 +182,6 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
setData(PreferencesTableCompanion(skipNonMusic: Value(skip))); setData(PreferencesTableCompanion(skipNonMusic: Value(skip)));
} }
void setAudioSource(AudioSource type) {
switch ((type, state.audioQuality)) {
// DAB music only supports high quality/uncompressed streams
case (
AudioSource.dabMusic,
SourceQualities.low || SourceQualities.medium
):
setAudioQuality(SourceQualities.high);
break;
// If the user switches from DAB music to other sources and has
// uncompressed quality selected, downgrade to high quality
case (!= AudioSource.dabMusic, SourceQualities.uncompressed):
setAudioQuality(SourceQualities.high);
break;
default:
break;
}
setData(PreferencesTableCompanion(audioSource: Value(type)));
}
void setYoutubeClientEngine(YoutubeClientEngine engine) { void setYoutubeClientEngine(YoutubeClientEngine engine) {
setData(PreferencesTableCompanion(youtubeClientEngine: Value(engine))); setData(PreferencesTableCompanion(youtubeClientEngine: Value(engine)));
} }

View File

@ -2,7 +2,6 @@ import 'dart:io';
import 'package:media_kit/media_kit.dart' hide Track; import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:spotube/services/audio_player/custom_player.dart'; import 'package:spotube/services/audio_player/custom_player.dart';
@ -22,21 +21,9 @@ class SpotubeMedia extends mk.Media {
static String get _host => static String get _host =>
kIsWindows ? "localhost" : InternetAddress.anyIPv4.address; kIsWindows ? "localhost" : InternetAddress.anyIPv4.address;
static String _queries(SpotubeFullTrackObject track) {
final params = TrackSourceQuery.fromTrack(track).toJson();
return params.entries
.map((e) =>
"${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value is List<String> ? e.value.join(",") : e.value.toString())}")
.join("&");
}
final SpotubeTrackObject track; final SpotubeTrackObject track;
SpotubeMedia( SpotubeMedia(this.track)
this.track, { : assert(
Map<String, dynamic>? extras,
super.httpHeaders,
}) : assert(
track is SpotubeLocalTrackObject || track is SpotubeFullTrackObject, track is SpotubeLocalTrackObject || track is SpotubeFullTrackObject,
"Track must be a either a local track or a full track object with ISRC", "Track must be a either a local track or a full track object with ISRC",
), ),
@ -44,8 +31,14 @@ class SpotubeMedia extends mk.Media {
super( super(
track is SpotubeLocalTrackObject track is SpotubeLocalTrackObject
? track.path ? track.path
: "http://$_host:$serverPort/stream/${track.id}?${_queries(track as SpotubeFullTrackObject)}", : "http://$_host:$serverPort/stream/${track.id}",
extras: track.toJson(),
); );
factory SpotubeMedia.media(Media media) {
assert(media.extras != null, "[Media] must have extra metadata set");
return SpotubeMedia(SpotubeFullTrackObject.fromJson(media.extras!));
}
} }
abstract class AudioPlayerInterface { abstract class AudioPlayerInterface {

View File

@ -22,7 +22,7 @@ class MetadataPluginAudioSourceEndpoint {
SpotubeFullTrackObject track, SpotubeFullTrackObject track,
) async { ) async {
final raw = await hetuMetadataAudioSource final raw = await hetuMetadataAudioSource
.invoke("matches", positionalArgs: [track]) as List; .invoke("matches", positionalArgs: [track.toJson()]) as List;
return raw.map((e) => SpotubeAudioSourceMatchObject.fromJson(e)).toList(); return raw.map((e) => SpotubeAudioSourceMatchObject.fromJson(e)).toList();
} }
@ -31,7 +31,7 @@ class MetadataPluginAudioSourceEndpoint {
SpotubeAudioSourceMatchObject match, SpotubeAudioSourceMatchObject match,
) async { ) async {
final raw = await hetuMetadataAudioSource final raw = await hetuMetadataAudioSource
.invoke("streams", positionalArgs: [match]) as List; .invoke("streams", positionalArgs: [match.toJson()]) as List;
return raw.map((e) => SpotubeAudioSourceStreamObject.fromJson(e)).toList(); return raw.map((e) => SpotubeAudioSourceStreamObject.fromJson(e)).toList();
} }

View File

@ -1,34 +0,0 @@
import 'package:spotube/models/playback/track_sources.dart';
enum SourceCodecs {
m4a._("M4a (Best for downloaded music)"),
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"),
mp3._("MP3 (Widely supported audio format)"),
flac._("FLAC (Lossless, best quality)\nLarge file size");
final String label;
const SourceCodecs._(this.label);
}
enum SourceQualities {
uncompressed(3),
high(2),
medium(1),
low(0);
final int priority;
const SourceQualities(this.priority);
bool operator <(SourceQualities other) {
return priority < other.priority;
}
operator >(SourceQualities other) {
return priority > other.priority;
}
}
typedef SiblingType<T extends TrackSourceInfo> = ({
T info,
List<TrackSource>? source
});

View File

@ -1,12 +1,12 @@
import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/models/metadata/metadata.dart';
class TrackNotFoundError extends Error { class TrackNotFoundError extends Error {
final TrackSourceQuery track; final SpotubeTrackObject track;
TrackNotFoundError(this.track); TrackNotFoundError(this.track);
@override @override
String toString() { String toString() {
return '[TrackNotFoundError] ${track.title} - ${track.artists.join(", ")}'; return '[TrackNotFoundError] ${track.name} - ${track.artists.join(", ")}';
} }
} }

View File

@ -1,136 +0,0 @@
import 'package:invidious/invidious.dart';
import 'package:piped_client/piped_client.dart';
import 'package:spotube/models/database/database.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
class YoutubeVideoInfo {
final SearchMode searchMode;
final String title;
final Duration duration;
final String thumbnailUrl;
final String id;
final int likes;
final int dislikes;
final int views;
final String channelName;
final String channelId;
final DateTime publishedAt;
YoutubeVideoInfo({
required this.searchMode,
required this.title,
required this.duration,
required this.thumbnailUrl,
required this.id,
required this.likes,
required this.dislikes,
required this.views,
required this.channelName,
required this.publishedAt,
required this.channelId,
});
YoutubeVideoInfo.fromJson(Map<String, dynamic> json)
: title = json['title'],
searchMode = SearchMode.fromString(json['searchMode']),
duration = Duration(seconds: json['duration']),
thumbnailUrl = json['thumbnailUrl'],
id = json['id'],
likes = json['likes'],
dislikes = json['dislikes'],
views = json['views'],
channelName = json['channelName'],
channelId = json['channelId'],
publishedAt = DateTime.tryParse(json['publishedAt']) ?? DateTime.now();
Map<String, dynamic> toJson() => {
'title': title,
'duration': duration.inSeconds,
'thumbnailUrl': thumbnailUrl,
'id': id,
'likes': likes,
'dislikes': dislikes,
'views': views,
'channelName': channelName,
'channelId': channelId,
'publishedAt': publishedAt.toIso8601String(),
'searchMode': searchMode.name,
};
factory YoutubeVideoInfo.fromVideo(Video video) {
return YoutubeVideoInfo(
searchMode: SearchMode.youtube,
title: video.title,
duration: video.duration ?? Duration.zero,
thumbnailUrl: video.thumbnails.mediumResUrl,
id: video.id.value,
likes: video.engagement.likeCount ?? 0,
dislikes: video.engagement.dislikeCount ?? 0,
views: video.engagement.viewCount,
channelName: video.author,
channelId: '/c/${video.channelId.value}',
publishedAt: video.uploadDate ?? DateTime(2003, 9, 9),
);
}
factory YoutubeVideoInfo.fromSearchItemStream(
PipedSearchItemStream searchItem,
SearchMode searchMode,
) {
return YoutubeVideoInfo(
searchMode: searchMode,
title: searchItem.title,
duration: searchItem.duration,
thumbnailUrl: searchItem.thumbnail,
id: searchItem.id,
likes: 0,
dislikes: 0,
views: searchItem.views,
channelName: searchItem.uploaderName,
channelId: searchItem.uploaderUrl ?? "",
publishedAt: searchItem.uploadedDate != null
? DateTime.tryParse(searchItem.uploadedDate!) ?? DateTime(2003, 9, 9)
: DateTime(2003, 9, 9),
);
}
factory YoutubeVideoInfo.fromStreamResponse(
PipedStreamResponse stream, SearchMode searchMode) {
return YoutubeVideoInfo(
searchMode: searchMode,
title: stream.title,
duration: stream.duration,
thumbnailUrl: stream.thumbnailUrl,
id: stream.id,
likes: stream.likes,
dislikes: stream.dislikes,
views: stream.views,
channelName: stream.uploader,
publishedAt: stream.uploadedDate != null
? DateTime.tryParse(stream.uploadedDate!) ?? DateTime(2003, 9, 9)
: DateTime(2003, 9, 9),
channelId: stream.uploaderUrl,
);
}
factory YoutubeVideoInfo.fromSearchResponse(
InvidiousSearchResponseVideo searchResponse,
SearchMode searchMode,
) {
return YoutubeVideoInfo(
searchMode: searchMode,
title: searchResponse.title,
duration: Duration(seconds: searchResponse.lengthSeconds),
thumbnailUrl: searchResponse.videoThumbnails.first.url,
id: searchResponse.videoId,
likes: 0,
dislikes: 0,
views: searchResponse.viewCount,
channelName: searchResponse.author,
channelId: searchResponse.authorId,
publishedAt:
DateTime.fromMillisecondsSinceEpoch(searchResponse.published * 1000),
);
}
}

View File

@ -1,18 +1,27 @@
import 'dart:convert';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:spotube/services/dio/dio.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart'; import 'package:spotube/services/sourced_track/exceptions.dart';
import 'package:spotube/services/sourced_track/sources/dab_music.dart';
import 'package:spotube/services/sourced_track/sources/invidious.dart';
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
import 'package:spotube/services/sourced_track/sources/piped.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
abstract class SourcedTrack extends BasicSourcedTrack { final officialMusicRegex = RegExp(
r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)",
caseSensitive: false,
);
class SourcedTrack extends BasicSourcedTrack {
final Ref ref; final Ref ref;
SourcedTrack({ SourcedTrack({
@ -24,144 +33,267 @@ abstract class SourcedTrack extends BasicSourcedTrack {
required super.sources, required super.sources,
}); });
static SourcedTrack fromJson( static Future<SourcedTrack> fetchFromTrack({
Map<String, dynamic> json, { required SpotubeFullTrackObject query,
required Ref ref,
}) {
final preferences = ref.read(userPreferencesProvider);
final info = TrackSourceInfo.fromJson(json["info"]);
final query = TrackSourceQuery.fromJson(json["query"]);
final source = AudioSource.values.firstWhereOrNull(
(source) => source.name == json["source"],
) ??
preferences.audioSource;
final siblings = (json["siblings"] as List)
.map((s) => TrackSourceInfo.fromJson(s))
.toList();
final sources =
(json["sources"] as List).map((s) => TrackSource.fromJson(s)).toList();
return switch (preferences.audioSource) {
AudioSource.youtube => YoutubeSourcedTrack(
ref: ref,
source: source,
siblings: siblings,
info: info,
query: query,
sources: sources,
),
AudioSource.piped => PipedSourcedTrack(
ref: ref,
source: source,
siblings: siblings,
info: info,
query: query,
sources: sources,
),
AudioSource.jiosaavn => JioSaavnSourcedTrack(
ref: ref,
source: source,
siblings: siblings,
info: info,
query: query,
sources: sources,
),
AudioSource.invidious => InvidiousSourcedTrack(
ref: ref,
source: source,
siblings: siblings,
info: info,
query: query,
sources: sources,
),
AudioSource.dabMusic => DABMusicSourcedTrack(
ref: ref,
source: source,
siblings: siblings,
info: info,
query: query,
sources: sources,
),
};
}
static String getSearchTerm(TrackSourceQuery track) {
final title = ServiceUtils.getTitle(
track.title,
artists: track.artists,
onlyCleanArtist: true,
).trim();
assert(title.trim().isNotEmpty, "Title should not be empty");
return "$title - ${track.artists.join(", ")}";
}
static Future<SourcedTrack> fetchFromQuery({
required TrackSourceQuery query,
required Ref ref, required Ref ref,
}) async { }) async {
final preferences = ref.read(userPreferencesProvider); final audioSource = await ref.read(audioSourcePluginProvider.future);
try { final audioSourceConfig = await ref.read(metadataPluginsProvider
return switch (preferences.audioSource) { .selectAsync((data) => data.defaultAudioSourcePluginConfig));
AudioSource.youtube => if (audioSource == null || audioSourceConfig == null) {
await YoutubeSourcedTrack.fetchFromTrack(query: query, ref: ref), throw Exception("Dude wat?");
AudioSource.piped =>
await PipedSourcedTrack.fetchFromTrack(query: query, ref: ref),
AudioSource.invidious =>
await InvidiousSourcedTrack.fetchFromTrack(query: query, ref: ref),
AudioSource.jiosaavn =>
await JioSaavnSourcedTrack.fetchFromTrack(query: query, ref: ref),
AudioSource.dabMusic =>
await DABMusicSourcedTrack.fetchFromTrack(query: query, ref: ref),
};
} catch (e) {
if (preferences.audioSource == AudioSource.youtube) {
rethrow;
} }
return await YoutubeSourcedTrack.fetchFromTrack(query: query, ref: ref); final database = ref.read(databaseProvider);
final cachedSource = await (database.select(database.sourceMatchTable)
..where((s) =>
s.trackId.equals(query.id) &
s.sourceType.equals(audioSourceConfig.slug))
..limit(1)
..orderBy([
(s) =>
OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc),
]))
.get()
.then((s) => s.firstOrNull);
if (cachedSource == null) {
final siblings = await fetchSiblings(ref: ref, query: query);
if (siblings.isEmpty) {
throw TrackNotFoundError(query);
}
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.id,
sourceInfo: Value(jsonEncode(siblings.first)),
sourceType: audioSourceConfig.slug,
),
);
final manifest = await audioSource.audioSource.streams(siblings.first);
return SourcedTrack(
ref: ref,
siblings: siblings.skip(1).toList(),
info: siblings.first,
source: audioSourceConfig.slug,
sources: manifest,
query: query,
);
}
final item = SpotubeAudioSourceMatchObject.fromJson(
jsonDecode(cachedSource.sourceInfo),
);
final manifest = await audioSource.audioSource.streams(item);
final sourcedTrack = SourcedTrack(
ref: ref,
siblings: [],
sources: manifest,
info: item,
query: query,
source: audioSourceConfig.slug,
);
AppLogger.log.i("${query.name}: ${sourcedTrack.url}");
return sourcedTrack;
}
static List<SpotubeAudioSourceMatchObject> rankResults(
List<SpotubeAudioSourceMatchObject> results,
SpotubeFullTrackObject track,
) {
return results
.map((sibling) {
int score = 0;
for (final artist in track.artists) {
final isSameChannelArtist =
sibling.artists.any((a) => a.toLowerCase() == artist.name);
if (isSameChannelArtist) {
score += 1;
}
final titleContainsArtist =
sibling.title.toLowerCase().contains(artist.name.toLowerCase());
if (titleContainsArtist) {
score += 1;
} }
} }
static Future<List<SiblingType>> fetchSiblings({ final titleContainsTrackName =
required TrackSourceQuery query, sibling.title.toLowerCase().contains(track.name.toLowerCase());
final hasOfficialFlag =
officialMusicRegex.hasMatch(sibling.title.toLowerCase());
if (titleContainsTrackName) {
score += 3;
}
if (hasOfficialFlag) {
score += 1;
}
if (hasOfficialFlag && titleContainsTrackName) {
score += 2;
}
return (sibling: sibling, score: score);
})
.sorted((a, b) => b.score.compareTo(a.score))
.map((e) => e.sibling)
.toList();
}
static Future<List<SpotubeAudioSourceMatchObject>> fetchSiblings({
required SpotubeFullTrackObject query,
required Ref ref, required Ref ref,
}) { }) async {
final preferences = ref.read(userPreferencesProvider); final audioSource = await ref.read(audioSourcePluginProvider.future);
return switch (preferences.audioSource) { if (audioSource == null) {
AudioSource.piped => throw Exception("Dude wat?");
PipedSourcedTrack.fetchSiblings(query: query, ref: ref),
AudioSource.youtube =>
YoutubeSourcedTrack.fetchSiblings(query: query, ref: ref),
AudioSource.jiosaavn =>
JioSaavnSourcedTrack.fetchSiblings(query: query, ref: ref),
AudioSource.invidious =>
InvidiousSourcedTrack.fetchSiblings(query: query, ref: ref),
AudioSource.dabMusic =>
DABMusicSourcedTrack.fetchSiblings(query: query, ref: ref),
};
} }
Future<SourcedTrack> copyWithSibling(); final videoResults = <SpotubeAudioSourceMatchObject>[];
Future<SourcedTrack?> swapWithSibling(TrackSourceInfo sibling); final searchResults = await audioSource.audioSource.matches(query);
if (ServiceUtils.onlyContainsEnglish(query.name)) {
videoResults.addAll(searchResults);
} else {
videoResults.addAll(rankResults(searchResults, query));
}
return videoResults.toSet().toList();
}
Future<SourcedTrack> copyWithSibling() async {
if (siblings.isNotEmpty) {
return this;
}
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
return SourcedTrack(
ref: ref,
siblings: fetchedSiblings.where((s) => s.id != info.id).toList(),
source: source,
sources: sources,
info: info,
query: query,
);
}
Future<SourcedTrack?> swapWithSibling(
SpotubeAudioSourceMatchObject sibling) async {
if (sibling.id == info.id) {
return null;
}
final audioSource = await ref.read(audioSourcePluginProvider.future);
final audioSourceConfig = await ref.read(metadataPluginsProvider
.selectAsync((data) => data.defaultAudioSourcePluginConfig));
if (audioSource == null || audioSourceConfig == null) {
throw Exception("Dude wat?");
}
// a sibling source that was fetched from the search results
final isStepSibling = siblings.none((s) => s.id == sibling.id);
final newSourceInfo = isStepSibling
? sibling
: siblings.firstWhere((s) => s.id == sibling.id);
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
..insert(0, info);
final manifest = await audioSource.audioSource.streams(newSourceInfo);
final database = ref.read(databaseProvider);
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.id,
sourceInfo: Value(jsonEncode(siblings.first)),
sourceType: audioSourceConfig.slug,
createdAt: Value(DateTime.now()),
),
mode: InsertMode.replace,
);
return SourcedTrack(
ref: ref,
source: source,
siblings: newSiblings,
sources: manifest,
info: newSourceInfo,
query: query,
);
}
Future<SourcedTrack?> swapWithSiblingOfIndex(int index) { Future<SourcedTrack?> swapWithSiblingOfIndex(int index) {
return swapWithSibling(siblings[index]); return swapWithSibling(siblings[index]);
} }
Future<SourcedTrack> refreshStream(); Future<SourcedTrack> refreshStream() async {
final audioSource = await ref.read(audioSourcePluginProvider.future);
final audioSourceConfig = await ref.read(metadataPluginsProvider
.selectAsync((data) => data.defaultAudioSourcePluginConfig));
if (audioSource == null || audioSourceConfig == null) {
throw Exception("Dude wat?");
}
List<SpotubeAudioSourceStreamObject> validStreams = [];
final stringBuffer = StringBuffer();
for (final source in sources) {
final res = await globalDio.head(
source.url,
options:
Options(validateStatus: (status) => status != null && status < 500),
);
stringBuffer.writeln(
"[${query.id}] ${res.statusCode} ${source.container} ${source.codec} ${source.bitrate}",
);
if (res.statusCode! < 400) {
validStreams.add(source);
}
}
AppLogger.log.d(stringBuffer.toString());
if (validStreams.isEmpty) {
validStreams = await audioSource.audioSource.streams(info);
}
final sourcedTrack = SourcedTrack(
ref: ref,
siblings: siblings,
source: source,
sources: validStreams,
info: info,
query: query,
);
AppLogger.log.i("Refreshing ${query.name}: ${sourcedTrack.url}");
return sourcedTrack;
}
String? get url { String? get url {
final preferences = ref.read(userPreferencesProvider); final preferences = ref.read(audioSourcePresetsProvider);
final codec = preferences.audioSource == AudioSource.jiosaavn return getUrlOfQuality(
? SourceCodecs.m4a preferences.presets[preferences.selectedStreamingContainerIndex],
: preferences.streamMusicCodec; preferences.selectedStreamingQualityIndex,
);
return getUrlOfCodec(codec);
} }
/// Returns the URL of the track based on the codec and quality preferences. /// Returns the URL of the track based on the codec and quality preferences.
@ -170,58 +302,81 @@ abstract class SourcedTrack extends BasicSourcedTrack {
/// ///
/// If no sources match the codec, it will return the first or last source /// If no sources match the codec, it will return the first or last source
/// based on the user's audio quality preference. /// based on the user's audio quality preference.
TrackSource? getSourceOfCodec(SourceCodecs codec) { SpotubeAudioSourceStreamObject? getStreamOfQuality(
final preferences = ref.read(userPreferencesProvider); SpotubeAudioSourceContainerPreset preset,
int qualityIndex,
) {
final quality = preset.qualities[qualityIndex];
final exactMatch = sources.firstWhereOrNull( final exactMatch = sources.firstWhereOrNull(
(source) => (source) {
source.codec == codec && source.quality == preferences.audioQuality, if (source.container != preset.name) return false;
if (quality case SpotubeAudioLosslessContainerQuality()) {
return source.sampleRate == quality.sampleRate &&
source.bitDepth == quality.bitDepth;
} else {
return source.bitrate ==
(preset as SpotubeAudioLossyContainerQuality).bitrate;
}
},
); );
if (exactMatch != null) { if (exactMatch != null) {
return exactMatch; return exactMatch;
} }
final sameCodecSources = sources // Find the closest to preset
.where((source) => source.codec == codec) SpotubeAudioSourceStreamObject? closest;
.toList() for (final source in sources) {
.sorted((a, b) { if (source.container != preset.name) continue;
final aDiff = (a.quality.index - preferences.audioQuality.index).abs();
final bDiff = (b.quality.index - preferences.audioQuality.index).abs();
return aDiff != bDiff ? aDiff - bDiff : a.quality.index - b.quality.index;
}).toList();
if (sameCodecSources.isNotEmpty) { if (quality case SpotubeAudioLosslessContainerQuality()) {
return preferences.audioQuality > SourceQualities.low final sourceBps = (source.bitDepth ?? 0) * (source.sampleRate ?? 0);
? sameCodecSources.first final qualityBps = quality.bitDepth * quality.sampleRate;
: sameCodecSources.last; final closestBps =
(closest?.bitDepth ?? 0) * (closest?.sampleRate ?? 0);
if (sourceBps == qualityBps) {
closest = source;
break;
}
final closestDiff = (closestBps - qualityBps).abs();
final sourceDiff = (sourceBps - qualityBps).abs();
if (sourceDiff < closestDiff) {
closest = source;
}
} else {
final presetBitrate =
(preset as SpotubeAudioLossyContainerQuality).bitrate;
if (presetBitrate == source.bitrate) {
closest = source;
break;
} }
final fallbackSource = sources.sorted((a, b) { final closestDiff = (closest?.bitrate ?? 0) - presetBitrate;
final aDiff = (a.quality.index - preferences.audioQuality.index).abs(); final sourceDiff = (source.bitrate ?? 0) - presetBitrate;
final bDiff = (b.quality.index - preferences.audioQuality.index).abs();
return aDiff != bDiff ? aDiff - bDiff : a.quality.index - b.quality.index;
});
return preferences.audioQuality > SourceQualities.low if (sourceDiff < closestDiff) {
? fallbackSource.firstOrNull closest = source;
: fallbackSource.lastOrNull; }
} }
}
String? getUrlOfCodec(SourceCodecs codec) {
return getSourceOfCodec(codec)?.url; return closest;
} }
SourceCodecs get codec { String? getUrlOfQuality(
final preferences = ref.read(userPreferencesProvider); SpotubeAudioSourceContainerPreset preset,
int qualityIndex,
return switch (preferences.audioSource) { ) {
AudioSource.dabMusic => return getStreamOfQuality(preset, qualityIndex)?.url;
preferences.audioQuality == SourceQualities.uncompressed }
? SourceCodecs.flac
: SourceCodecs.mp3, SpotubeAudioSourceContainerPreset? get qualityPreset {
AudioSource.jiosaavn => SourceCodecs.m4a, final presetState = ref.read(audioSourcePresetsProvider);
_ => preferences.streamMusicCodec return presetState.presets
}; .elementAtOrNull(presetState.selectedStreamingContainerIndex);
} }
} }

View File

@ -1,303 +0,0 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/exceptions.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:dab_music_api/dab_music_api.dart';
final dabMusicApiClient = DabMusicApiClient(
Dio(),
baseUrl: "https://dab.yeet.su/api",
);
/// Only Music source that can't support database caching due to having no endpoint.
/// But ISRC search is 100% reliable so caching is actually not necessary.
class DABMusicSourcedTrack extends SourcedTrack {
DABMusicSourcedTrack({
required super.ref,
required super.source,
required super.siblings,
required super.info,
required super.query,
required super.sources,
});
static Future<SourcedTrack> fetchFromTrack({
required TrackSourceQuery query,
required Ref ref,
}) async {
try {
final database = ref.read(databaseProvider);
final cachedSource = await (database.select(database.sourceMatchTable)
..where((s) => s.trackId.equals(query.id))
..limit(1)
..orderBy([
(s) => OrderingTerm(
expression: s.createdAt,
mode: OrderingMode.desc,
),
]))
.get()
.then((s) => s.firstOrNull);
if (cachedSource != null &&
cachedSource.sourceType == SourceType.dabMusic) {
final json = jsonDecode(cachedSource.sourceId);
final info = TrackSourceInfo.fromJson(json["info"]);
final source = (json["sources"] as List?)
?.map((s) => TrackSource.fromJson(s))
.toList();
final [updatedSource] = await fetchSources(
info.id,
ref.read(userPreferencesProvider).audioQuality,
const AudioQuality(
isHiRes: true,
maximumBitDepth: 16,
maximumSamplingRate: 44.1,
),
);
return DABMusicSourcedTrack(
ref: ref,
source: AudioSource.dabMusic,
siblings: [],
info: info,
query: query,
sources: [
source!.first.copyWith(url: updatedSource.url),
],
);
}
final siblings = await fetchSiblings(ref: ref, query: query);
if (siblings.isEmpty) {
throw TrackNotFoundError(query);
}
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.id,
sourceId: jsonEncode({
"info": siblings.first.info.toJson(),
"sources": (siblings.first.source ?? [])
.map((s) => s.toJson())
.toList(),
}),
sourceType: const Value(SourceType.dabMusic),
),
);
return DABMusicSourcedTrack(
ref: ref,
siblings: siblings.map((s) => s.info).skip(1).toList(),
sources: siblings.first.source!,
info: siblings.first.info,
query: query,
source: AudioSource.dabMusic,
);
} catch (e, stackTrace) {
AppLogger.reportError(e, stackTrace);
rethrow;
}
}
static Future<List<TrackSource>> fetchSources(
String id,
SourceQualities quality,
AudioQuality trackMaximumQuality,
) async {
try {
final isUncompressed = quality == SourceQualities.uncompressed;
final streamResponse = await dabMusicApiClient.music.getStream(
trackId: id,
quality: isUncompressed ? "27" : "5",
);
if (streamResponse.url == null) {
throw Exception("No stream URL found for track ID: $id");
}
// kbps = (bitDepth * sampleRate * channels) / 1000
final uncompressedBitrate = !isUncompressed
? 0
: ((trackMaximumQuality.maximumBitDepth ?? 0) *
((trackMaximumQuality.maximumSamplingRate ?? 0) * 1000) *
2) /
1000;
return [
TrackSource(
url: streamResponse.url!,
quality: isUncompressed
? SourceQualities.uncompressed
: SourceQualities.high,
bitrate:
isUncompressed ? "${uncompressedBitrate.floor()}kbps" : "320kbps",
codec: isUncompressed ? SourceCodecs.flac : SourceCodecs.mp3,
qualityLabel: isUncompressed
? "${trackMaximumQuality.maximumBitDepth}bit • ${trackMaximumQuality.maximumSamplingRate}kHz • FLAC • Stereo"
: "MP3 • 320kbps • mp3 • Stereo",
),
];
} catch (e, stackTrace) {
AppLogger.reportError(e, stackTrace);
rethrow;
}
}
static Future<SiblingType> toSiblingType(
Ref ref,
int index,
Track result,
) async {
try {
List<TrackSource>? source;
if (index == 0) {
source = await fetchSources(
result.id.toString(),
ref.read(userPreferencesProvider).audioQuality,
result.audioQuality!,
);
}
final SiblingType sibling = (
info: TrackSourceInfo(
artists: result.artist!,
durationMs: Duration(seconds: result.duration!).inMilliseconds,
id: result.id.toString(),
pageUrl: "https://dab.yeet.su/music/${result.id}",
thumbnail: result.albumCover!,
title: result.title!,
),
source: source,
);
return sibling;
} catch (e, stackTrace) {
AppLogger.reportError(e, stackTrace);
rethrow;
}
}
static Future<List<SiblingType>> fetchSiblings({
required TrackSourceQuery query,
required Ref ref,
}) async {
try {
List<Track> results = [];
if (query.isrc.isNotEmpty) {
final res =
await dabMusicApiClient.music.getSearch(q: query.isrc, limit: 1);
results = res.tracks ?? <Track>[];
}
if (results.isEmpty) {
final res = await dabMusicApiClient.music.getSearch(
q: SourcedTrack.getSearchTerm(query),
limit: 5,
);
results = res.tracks ?? <Track>[];
}
if (results.isEmpty) {
return [];
}
final matchedResults =
results.mapIndexed((index, d) => toSiblingType(ref, index, d));
return Future.wait(matchedResults);
} catch (e, stackTrace) {
AppLogger.reportError(e, stackTrace);
rethrow;
}
}
@override
Future<DABMusicSourcedTrack> copyWithSibling() async {
if (siblings.isNotEmpty) {
return this;
}
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
return DABMusicSourcedTrack(
ref: ref,
siblings: fetchedSiblings
.where((s) => s.info.id != info.id)
.map((s) => s.info)
.toList(),
source: source,
info: info,
query: query,
sources: sources,
);
}
@override
Future<DABMusicSourcedTrack?> swapWithSibling(TrackSourceInfo sibling) async {
if (sibling.id == this.info.id) {
return null;
}
// a sibling source that was fetched from the search results
final isStepSibling = siblings.none((s) => s.id == sibling.id);
final newSourceInfo = isStepSibling
? sibling
: siblings.firstWhere((s) => s.id == sibling.id);
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
..insert(0, this.info);
final source = await fetchSources(
sibling.id,
ref.read(userPreferencesProvider).audioQuality,
const AudioQuality(
isHiRes: true,
maximumBitDepth: 16,
maximumSamplingRate: 44.1,
),
);
final database = ref.read(databaseProvider);
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.id,
sourceId: jsonEncode({
"info": newSourceInfo.toJson(),
"sources": source.map((s) => s.toJson()).toList(),
}),
sourceType: const Value(SourceType.dabMusic),
// 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 DABMusicSourcedTrack(
ref: ref,
siblings: newSiblings,
sources: source,
info: newSourceInfo,
query: query,
source: AudioSource.dabMusic,
);
}
@override
Future<SourcedTrack> refreshStream() async {
// There's no need to refresh the stream for DABMusicSourcedTrack
return this;
}
}

View File

@ -1,263 +0,0 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/exceptions.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:invidious/invidious.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/service_utils.dart';
final invidiousProvider = Provider<InvidiousClient>(
(ref) {
final invidiousInstance = ref.watch(
userPreferencesProvider.select((s) => s.invidiousInstance),
);
return InvidiousClient(server: invidiousInstance);
},
);
class InvidiousSourcedTrack extends SourcedTrack {
InvidiousSourcedTrack({
required super.ref,
required super.source,
required super.siblings,
required super.info,
required super.query,
required super.sources,
});
static Future<SourcedTrack> fetchFromTrack({
required TrackSourceQuery query,
required Ref ref,
}) async {
final audioSource = ref.read(userPreferencesProvider).audioSource;
final database = ref.read(databaseProvider);
final cachedSource = await (database.select(database.sourceMatchTable)
..where((s) => s.trackId.equals(query.id))
..limit(1)
..orderBy([
(s) =>
OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc),
]))
.getSingleOrNull();
final invidiousClient = ref.read(invidiousProvider);
if (cachedSource == null) {
final siblings = await fetchSiblings(ref: ref, query: query);
if (siblings.isEmpty) {
throw TrackNotFoundError(query);
}
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.id,
sourceId: siblings.first.info.id,
sourceType: const Value(SourceType.youtube),
),
);
return InvidiousSourcedTrack(
ref: ref,
siblings: siblings.map((s) => s.info).skip(1).toList(),
sources: siblings.first.source as List<TrackSource>,
info: siblings.first.info,
query: query,
source: audioSource,
);
} else {
final manifest =
await invidiousClient.videos.get(cachedSource.sourceId, local: true);
return InvidiousSourcedTrack(
ref: ref,
siblings: [],
sources: toSources(manifest),
info: TrackSourceInfo(
id: manifest.videoId,
artists: manifest.author,
pageUrl: "https://www.youtube.com/watch?v=${manifest.videoId}",
thumbnail: manifest.videoThumbnails.first.url,
title: manifest.title,
durationMs: Duration(seconds: manifest.lengthSeconds).inMilliseconds,
),
query: query,
source: audioSource,
);
}
}
static List<TrackSource> toSources(InvidiousVideoResponse manifest) {
return manifest.adaptiveFormats.map((stream) {
var isWebm = stream.type.contains("audio/webm");
return TrackSource(
url: stream.url.toString(),
quality: switch (stream.qualityLabel) {
"high" => SourceQualities.high,
"medium" => SourceQualities.medium,
_ => SourceQualities.low,
},
codec: isWebm ? SourceCodecs.weba : SourceCodecs.m4a,
bitrate: stream.bitrate,
qualityLabel:
"${isWebm ? "Opus" : "AAC"}${stream.bitrate.replaceAll("kbps", "")}kbps "
"${isWebm ? "weba" : "m4a"} • Stereo",
);
}).toList();
}
static Future<SiblingType> toSiblingType(
int index,
YoutubeVideoInfo item,
InvidiousClient invidiousClient,
) async {
List<TrackSource>? sourceMap;
if (index == 0) {
final manifest = await invidiousClient.videos.get(item.id, local: true);
sourceMap = toSources(manifest);
}
final SiblingType sibling = (
info: TrackSourceInfo(
id: item.id,
artists: item.channelName,
pageUrl: "https://www.youtube.com/watch?v=${item.id}",
thumbnail: item.thumbnailUrl,
title: item.title,
durationMs: item.duration.inMilliseconds,
),
source: sourceMap,
);
return sibling;
}
static Future<List<SiblingType>> fetchSiblings({
required TrackSourceQuery query,
required Ref ref,
}) async {
final invidiousClient = ref.read(invidiousProvider);
final preference = ref.read(userPreferencesProvider);
final searchQuery = SourcedTrack.getSearchTerm(query);
final searchResults = await invidiousClient.search.list(
searchQuery,
type: InvidiousSearchType.video,
);
if (ServiceUtils.onlyContainsEnglish(searchQuery)) {
return await Future.wait(
searchResults
.whereType<InvidiousSearchResponseVideo>()
.map(
(result) => YoutubeVideoInfo.fromSearchResponse(
result,
preference.searchMode,
),
)
.mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)),
);
}
final rankedSiblings = YoutubeSourcedTrack.rankResults(
searchResults
.whereType<InvidiousSearchResponseVideo>()
.map(
(result) => YoutubeVideoInfo.fromSearchResponse(
result,
preference.searchMode,
),
)
.toList(),
query,
);
return await Future.wait(
rankedSiblings.mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)),
);
}
@override
Future<SourcedTrack> copyWithSibling() async {
if (siblings.isNotEmpty) {
return this;
}
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
return InvidiousSourcedTrack(
ref: ref,
siblings: fetchedSiblings
.where((s) => s.info.id != info.id)
.map((s) => s.info)
.toList(),
source: source,
info: info,
query: query,
sources: sources,
);
}
@override
Future<SourcedTrack?> swapWithSibling(TrackSourceInfo sibling) async {
if (sibling.id == info.id) {
return null;
}
// a sibling source that was fetched from the search results
final isStepSibling = siblings.none((s) => s.id == sibling.id);
final newSourceInfo = isStepSibling
? sibling
: siblings.firstWhere((s) => s.id == sibling.id);
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
..insert(0, info);
final pipedClient = ref.read(invidiousProvider);
final manifest =
await pipedClient.videos.get(newSourceInfo.id, local: true);
final database = ref.read(databaseProvider);
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.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 InvidiousSourcedTrack(
ref: ref,
siblings: newSiblings,
sources: toSources(manifest),
info: newSourceInfo,
query: query,
source: source,
);
}
@override
Future<SourcedTrack> refreshStream() async {
final manifest =
await ref.read(invidiousProvider).videos.get(info.id, local: true);
return InvidiousSourcedTrack(
ref: ref,
siblings: siblings,
sources: toSources(manifest),
info: info,
query: query,
source: source,
);
}
}

View File

@ -1,231 +0,0 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/exceptions.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:jiosaavn/jiosaavn.dart';
import 'package:spotube/extensions/string.dart';
final jiosaavnClient = JioSaavnClient();
class JioSaavnSourcedTrack extends SourcedTrack {
JioSaavnSourcedTrack({
required super.ref,
required super.source,
required super.siblings,
required super.info,
required super.query,
required super.sources,
});
static Future<SourcedTrack> fetchFromTrack({
required TrackSourceQuery query,
required Ref ref,
bool weakMatch = false,
}) async {
final database = ref.read(databaseProvider);
final cachedSource = await (database.select(database.sourceMatchTable)
..where((s) => s.trackId.equals(query.id))
..limit(1)
..orderBy([
(s) =>
OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc),
]))
.getSingleOrNull();
if (cachedSource == null ||
cachedSource.sourceType != SourceType.jiosaavn) {
final siblings =
await fetchSiblings(ref: ref, query: query, weakMatch: weakMatch);
if (siblings.isEmpty) {
throw TrackNotFoundError(query);
}
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.id,
sourceId: siblings.first.info.id,
sourceType: const Value(SourceType.jiosaavn),
),
);
return JioSaavnSourcedTrack(
ref: ref,
siblings: siblings.map((s) => s.info).skip(1).toList(),
sources: siblings.first.source!,
info: siblings.first.info,
query: query,
source: AudioSource.jiosaavn,
);
}
final [item] =
await jiosaavnClient.songs.detailsById([cachedSource.sourceId]);
final (:info, :source) = toSiblingType(item);
return JioSaavnSourcedTrack(
ref: ref,
siblings: [],
sources: source!,
query: query,
info: info,
source: AudioSource.jiosaavn,
);
}
static SiblingType toSiblingType(SongResponse result) {
final SiblingType sibling = (
info: TrackSourceInfo(
artists: [
result.primaryArtists,
if (result.featuredArtists.isNotEmpty) ", ",
result.featuredArtists
].join("").unescapeHtml(),
durationMs:
Duration(seconds: int.parse(result.duration)).inMilliseconds,
id: result.id,
pageUrl: result.url,
thumbnail: result.image?.last.link ?? "",
title: result.name!.unescapeHtml(),
),
source: result.downloadUrl!.map((link) {
return TrackSource(
url: link.link,
quality: link.quality == "320kbps"
? SourceQualities.high
: link.quality == "160kbps"
? SourceQualities.medium
: SourceQualities.low,
codec: SourceCodecs.m4a,
bitrate: link.quality,
qualityLabel: "AAC • ${link.quality} • MP4 • Stereo",
);
}).toList()
);
return sibling;
}
static Future<List<SiblingType>> fetchSiblings({
required TrackSourceQuery query,
required Ref ref,
bool weakMatch = false,
}) async {
final searchQuery = SourcedTrack.getSearchTerm(query);
final SongSearchResponse(:results) =
await jiosaavnClient.search.songs(searchQuery, limit: 20);
final trackArtistNames = query.artists;
final matchedResults = results
.where(
(s) {
s.name?.unescapeHtml().contains(query.title) ?? false;
final sameName = s.name?.unescapeHtml() == query.title;
final artistNames = [
s.primaryArtists,
if (s.featuredArtists.isNotEmpty) ", ",
s.featuredArtists
].join("").unescapeHtml();
final sameArtists = artistNames.split(", ").any(
(artist) => trackArtistNames.any((ar) => artist == ar),
);
if (weakMatch) {
final containsName =
s.name?.unescapeHtml().contains(query.title) ?? false;
final containsPrimaryArtist = s.primaryArtists
.unescapeHtml()
.contains(trackArtistNames.first);
return containsName && containsPrimaryArtist;
}
return sameName && sameArtists;
},
)
.map(toSiblingType)
.toList();
if (weakMatch && matchedResults.isEmpty) {
return results.map(toSiblingType).toList();
}
return matchedResults;
}
@override
Future<JioSaavnSourcedTrack> copyWithSibling() async {
if (siblings.isNotEmpty) {
return this;
}
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
return JioSaavnSourcedTrack(
ref: ref,
siblings: fetchedSiblings
.where((s) => s.info.id != info.id)
.map((s) => s.info)
.toList(),
source: source,
info: info,
query: query,
sources: sources,
);
}
@override
Future<JioSaavnSourcedTrack?> swapWithSibling(TrackSourceInfo sibling) async {
if (sibling.id == this.info.id) {
return null;
}
// a sibling source that was fetched from the search results
final isStepSibling = siblings.none((s) => s.id == sibling.id);
final newSourceInfo = isStepSibling
? sibling
: siblings.firstWhere((s) => s.id == sibling.id);
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
..insert(0, this.info);
final [item] = await jiosaavnClient.songs.detailsById([newSourceInfo.id]);
final (:info, :source) = toSiblingType(item);
final database = ref.read(databaseProvider);
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.id,
sourceId: info.id,
sourceType: const Value(SourceType.jiosaavn),
// 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 JioSaavnSourcedTrack(
ref: ref,
siblings: newSiblings,
sources: source!,
info: newSourceInfo,
query: query,
source: AudioSource.jiosaavn,
);
}
@override
Future<SourcedTrack> refreshStream() async {
// There's no need to refresh the stream for JioSaavnSourcedTrack
return this;
}
}

View File

@ -1,292 +0,0 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:piped_client/piped_client.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/exceptions.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/service_utils.dart';
final pipedProvider = Provider<PipedClient>(
(ref) {
final instance =
ref.watch(userPreferencesProvider.select((s) => s.pipedInstance));
return PipedClient(instance: instance);
},
);
class PipedSourcedTrack extends SourcedTrack {
PipedSourcedTrack({
required super.ref,
required super.source,
required super.siblings,
required super.info,
required super.query,
required super.sources,
});
static Future<SourcedTrack> fetchFromTrack({
required TrackSourceQuery query,
required Ref ref,
}) async {
final audioSource = ref.read(userPreferencesProvider).audioSource;
final database = ref.read(databaseProvider);
final cachedSource = await (database.select(database.sourceMatchTable)
..where((s) => s.trackId.equals(query.id))
..limit(1)
..orderBy([
(s) =>
OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc),
]))
.getSingleOrNull();
final preferences = ref.read(userPreferencesProvider);
final pipedClient = ref.read(pipedProvider);
if (cachedSource == null) {
final siblings = await fetchSiblings(ref: ref, query: query);
if (siblings.isEmpty) {
throw TrackNotFoundError(query);
}
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.id,
sourceId: siblings.first.info.id,
sourceType: Value(
preferences.searchMode == SearchMode.youtube
? SourceType.youtube
: SourceType.youtubeMusic,
),
),
);
return PipedSourcedTrack(
ref: ref,
siblings: siblings.map((s) => s.info).skip(1).toList(),
source: audioSource,
info: siblings.first.info,
query: query,
sources: siblings.first.source!,
);
} else {
final manifest = await pipedClient.streams(cachedSource.sourceId);
return PipedSourcedTrack(
ref: ref,
siblings: [],
sources: toSources(manifest),
info: TrackSourceInfo(
id: manifest.id,
artists: manifest.uploader,
pageUrl: "https://www.youtube.com/watch?v=${manifest.id}",
thumbnail: manifest.thumbnailUrl,
title: manifest.title,
durationMs: manifest.duration.inMilliseconds,
),
query: query,
source: audioSource,
);
}
}
static List<TrackSource> toSources(PipedStreamResponse manifest) {
return manifest.audioStreams.map((audio) {
final isMp4 = audio.format == PipedAudioStreamFormat.m4a;
return TrackSource(
url: audio.url.toString(),
quality: switch (audio.quality) {
"high" => SourceQualities.high,
"medium" => SourceQualities.medium,
_ => SourceQualities.low,
},
codec: isMp4 ? SourceCodecs.m4a : SourceCodecs.weba,
bitrate: audio.bitrate.toString(),
qualityLabel:
"${isMp4 ? "AAC" : "Opus"}${(audio.bitrate / 1000).floor()}kbps "
"${isMp4 ? "m4a" : "weba"} • Stereo",
);
}).toList();
}
static Future<SiblingType> toSiblingType(
int index,
YoutubeVideoInfo item,
PipedClient pipedClient,
) async {
List<TrackSource>? sources;
if (index == 0) {
final manifest = await pipedClient.streams(item.id);
sources = toSources(manifest);
}
final SiblingType sibling = (
info: TrackSourceInfo(
id: item.id,
artists: item.channelName,
pageUrl: "https://www.youtube.com/watch?v=${item.id}",
thumbnail: item.thumbnailUrl,
title: item.title,
durationMs: item.duration.inMilliseconds,
),
source: sources,
);
return sibling;
}
static Future<List<SiblingType>> fetchSiblings({
required TrackSourceQuery query,
required Ref ref,
}) async {
final pipedClient = ref.read(pipedProvider);
final preference = ref.read(userPreferencesProvider);
final searchQuery = SourcedTrack.getSearchTerm(query);
final PipedSearchResult(items: searchResults) = await pipedClient.search(
searchQuery,
preference.searchMode == SearchMode.youtube
? PipedFilter.videos
: PipedFilter.musicSongs,
);
// when falling back to piped API make sure to use the YouTube mode
final isYouTubeMusic = preference.audioSource != AudioSource.piped
? false
: preference.searchMode == SearchMode.youtubeMusic;
if (isYouTubeMusic) {
final artists = query.artists;
return await Future.wait(
searchResults
.map(
(result) => YoutubeVideoInfo.fromSearchItemStream(
result as PipedSearchItemStream,
preference.searchMode,
),
)
.sorted((a, b) => b.views.compareTo(a.views))
.where(
(item) => artists.any(
(artist) =>
artist.toLowerCase() == item.channelName.toLowerCase(),
),
)
.mapIndexed((i, r) => toSiblingType(i, r, pipedClient)),
);
}
if (ServiceUtils.onlyContainsEnglish(searchQuery)) {
return await Future.wait(
searchResults
.whereType<PipedSearchItemStream>()
.map(
(result) => YoutubeVideoInfo.fromSearchItemStream(
result,
preference.searchMode,
),
)
.mapIndexed((i, r) => toSiblingType(i, r, pipedClient)),
);
}
final rankedSiblings = YoutubeSourcedTrack.rankResults(
searchResults
.map(
(result) => YoutubeVideoInfo.fromSearchItemStream(
result as PipedSearchItemStream,
preference.searchMode,
),
)
.toList(),
query,
);
return await Future.wait(
rankedSiblings.mapIndexed((i, r) => toSiblingType(i, r, pipedClient)),
);
}
@override
Future<SourcedTrack> copyWithSibling() async {
if (siblings.isNotEmpty) {
return this;
}
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
return PipedSourcedTrack(
ref: ref,
siblings: fetchedSiblings
.where((s) => s.info.id != info.id)
.map((s) => s.info)
.toList(),
source: source,
info: info,
query: query,
sources: sources,
);
}
@override
Future<SourcedTrack?> swapWithSibling(TrackSourceInfo sibling) async {
if (sibling.id == info.id) {
return null;
}
// a sibling source that was fetched from the search results
final isStepSibling = siblings.none((s) => s.id == sibling.id);
final newSourceInfo = isStepSibling
? sibling
: siblings.firstWhere((s) => s.id == sibling.id);
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
..insert(0, info);
final pipedClient = ref.read(pipedProvider);
final manifest = await pipedClient.streams(newSourceInfo.id);
final database = ref.read(databaseProvider);
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.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(
ref: ref,
siblings: newSiblings,
sources: toSources(manifest),
info: newSourceInfo,
query: query,
source: source,
);
}
@override
Future<SourcedTrack> refreshStream() async {
final manifest = await ref.read(pipedProvider).streams(info.id);
return PipedSourcedTrack(
ref: ref,
siblings: siblings,
info: info,
source: source,
query: query,
sources: toSources(manifest),
);
}
}

View File

@ -1,439 +0,0 @@
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/database/database.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
import 'package:spotube/services/dio/dio.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/song_link/song_link.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:spotube/services/sourced_track/exceptions.dart';
import 'package:spotube/services/sourced_track/models/video_info.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
final officialMusicRegex = RegExp(
r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)",
caseSensitive: false,
);
class YoutubeSourcedTrack extends SourcedTrack {
YoutubeSourcedTrack({
required super.source,
required super.siblings,
required super.info,
required super.query,
required super.sources,
required super.ref,
});
static Future<YoutubeSourcedTrack> fetchFromTrack({
required TrackSourceQuery query,
required Ref ref,
}) async {
final audioSource = ref.read(userPreferencesProvider).audioSource;
final database = ref.read(databaseProvider);
final cachedSource = await (database.select(database.sourceMatchTable)
..where((s) => s.trackId.equals(query.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(ref: ref, query: query);
if (siblings.isEmpty) {
throw TrackNotFoundError(query);
}
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.id,
sourceId: siblings.first.info.id,
sourceType: const Value(SourceType.youtube),
),
);
return YoutubeSourcedTrack(
ref: ref,
siblings: siblings.map((s) => s.info).skip(1).toList(),
info: siblings.first.info,
source: audioSource,
sources: siblings.first.source ?? [],
query: query,
);
}
final (item, manifest) = await ref
.read(youtubeEngineProvider)
.getVideoWithStreamInfo(cachedSource.sourceId);
final sourcedTrack = YoutubeSourcedTrack(
ref: ref,
siblings: [],
sources: toTrackSources(manifest),
info: TrackSourceInfo(
id: item.id.value,
artists: item.author,
pageUrl: item.url,
thumbnail: item.thumbnails.highResUrl,
title: item.title,
durationMs: item.duration?.inMilliseconds ?? 0,
),
query: query,
source: audioSource,
);
AppLogger.log.i("${query.title}: ${sourcedTrack.url}");
return sourcedTrack;
}
static List<TrackSource> toTrackSources(StreamManifest manifest) {
return manifest.audioOnly.map((streamInfo) {
var isWebm = streamInfo.codec.mimeType == "audio/webm";
return TrackSource(
url: streamInfo.url.toString(),
quality: switch (streamInfo.qualityLabel) {
"medium" => SourceQualities.medium,
"high" => SourceQualities.high,
"low" => SourceQualities.low,
_ => SourceQualities.high,
},
codec: isWebm ? SourceCodecs.weba : SourceCodecs.m4a,
bitrate: streamInfo.bitrate.bitsPerSecond.toString(),
qualityLabel:
"${isWebm ? "Opus" : "AAC"}${(streamInfo.bitrate.kiloBitsPerSecond).floor()}kbps "
"${isWebm ? "weba" : "m4a"} • Stereo",
);
}).toList();
}
static Future<SiblingType> toSiblingType(
int index,
YoutubeVideoInfo item,
dynamic ref,
) async {
assert(ref is WidgetRef || ref is Ref, "Invalid ref type");
List<TrackSource>? sourceMap;
if (index == 0) {
final manifest =
await ref.read(youtubeEngineProvider).getStreamManifest(item.id);
sourceMap = toTrackSources(manifest);
}
final SiblingType sibling = (
info: TrackSourceInfo(
id: item.id,
artists: item.channelName,
pageUrl: "https://www.youtube.com/watch?v=${item.id}",
thumbnail: item.thumbnailUrl,
title: item.title,
durationMs: item.duration.inMilliseconds,
),
source: sourceMap,
);
return sibling;
}
static List<YoutubeVideoInfo> rankResults(
List<YoutubeVideoInfo> results, TrackSourceQuery track) {
return results
.sorted((a, b) => b.views.compareTo(a.views))
.map((sibling) {
int score = 0;
for (final artist in track.artists) {
final isSameChannelArtist =
sibling.channelName.toLowerCase() == artist.toLowerCase();
final channelContainsArtist = sibling.channelName
.toLowerCase()
.contains(artist.toLowerCase());
if (isSameChannelArtist || channelContainsArtist) {
score += 1;
}
final titleContainsArtist =
sibling.title.toLowerCase().contains(artist.toLowerCase());
if (titleContainsArtist) {
score += 1;
}
}
final titleContainsTrackName =
sibling.title.toLowerCase().contains(track.title.toLowerCase());
final hasOfficialFlag =
officialMusicRegex.hasMatch(sibling.title.toLowerCase());
if (titleContainsTrackName) {
score += 3;
}
if (hasOfficialFlag) {
score += 1;
}
if (hasOfficialFlag && titleContainsTrackName) {
score += 2;
}
return (sibling: sibling, score: score);
})
.sorted((a, b) => b.score.compareTo(a.score))
.map((e) => e.sibling)
.toList();
}
static Future<List<YoutubeVideoInfo>> fetchFromIsrc({
required TrackSourceQuery track,
required Ref ref,
}) async {
final isrcResults = <YoutubeVideoInfo>[];
final isrc = track.isrc;
if (isrc.isNotEmpty) {
final searchedVideos =
await ref.read(youtubeEngineProvider).searchVideos(isrc.toString());
if (searchedVideos.isNotEmpty) {
AppLogger.log
.d("${track.title} ISRC $isrc Total ${searchedVideos.length}");
final stringBuffer = StringBuffer();
final filteredMatches = searchedVideos
.map<YoutubeVideoInfo>(YoutubeVideoInfo.fromVideo)
.map((YoutubeVideoInfo videoInfo) {
final ytWords = videoInfo.title
.toLowerCase()
.replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '')
.split(RegExp(r'\p{Z}+', unicode: true))
.where((item) => item.isNotEmpty);
final spWords = track.title
.toLowerCase()
.replaceAll(RegExp(r'[^\p{L}\p{N}\p{Z}]+', unicode: true), '')
.split(RegExp(r'\p{Z}+', unicode: true))
.where((item) => item.isNotEmpty);
// Single word and duration match with 3 second tolerance
if (ytWords.any((word) => spWords.contains(word)) &&
(videoInfo.duration -
Duration(milliseconds: track.durationMs))
.abs()
.inMilliseconds <=
3000) {
stringBuffer.writeln(
"ISRC MATCH: ${videoInfo.id} ${videoInfo.title} by ${videoInfo.channelName} ${videoInfo.duration}",
);
return videoInfo;
}
return null;
})
.nonNulls
.toList();
AppLogger.log.d(stringBuffer.toString());
isrcResults.addAll(filteredMatches);
}
}
return isrcResults;
}
static Future<List<SiblingType>> fetchSiblings({
required TrackSourceQuery query,
required Ref ref,
}) async {
final videoResults = <YoutubeVideoInfo>[];
if (query is! SourcedTrack) {
final isrcResults = await fetchFromIsrc(
track: query,
ref: ref,
);
videoResults.addAll(isrcResults);
if (isrcResults.isEmpty) {
AppLogger.log.w("No ISRC results found, falling back to SongLink");
final links = await SongLinkService.links(query.id);
final stringBuffer = links.fold(
StringBuffer(),
(previousValue, element) {
previousValue.writeln(
"SongLink ${query.id} ${element.platform} ${element.url}");
return previousValue;
},
);
AppLogger.log.d(stringBuffer.toString());
final ytLink = links.firstWhereOrNull(
(link) => link.platform == "youtube",
);
if (ytLink?.url != null) {
try {
videoResults.add(
YoutubeVideoInfo.fromVideo(await ref
.read(youtubeEngineProvider)
.getVideo(Uri.parse(ytLink!.url!).queryParameters["v"]!)),
);
} on VideoUnplayableException catch (e, stack) {
// Ignore this error and continue with the search
AppLogger.reportError(e, stack);
}
} else {
AppLogger.log.w("No YouTube link found in SongLink results");
}
}
}
final searchQuery = SourcedTrack.getSearchTerm(query);
final searchResults =
await ref.read(youtubeEngineProvider).searchVideos(searchQuery);
if (ServiceUtils.onlyContainsEnglish(searchQuery)) {
videoResults
.addAll(searchResults.map(YoutubeVideoInfo.fromVideo).toList());
} else {
videoResults.addAll(rankResults(
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
query,
));
}
final seenIds = <String>{};
int index = 0;
return await Future.wait(
videoResults.map((videoResult) async {
// Deduplicate results
if (!seenIds.contains(videoResult.id)) {
seenIds.add(videoResult.id);
return await toSiblingType(index++, videoResult, ref);
}
return null;
}),
).then((s) => s.whereType<SiblingType>().toList());
}
@override
Future<YoutubeSourcedTrack?> swapWithSibling(TrackSourceInfo sibling) async {
if (sibling.id == info.id) {
return null;
}
// a sibling source that was fetched from the search results
final isStepSibling = siblings.none((s) => s.id == sibling.id);
final newSourceInfo = isStepSibling
? sibling
: siblings.firstWhere((s) => s.id == sibling.id);
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
..insert(0, info);
final manifest = await ref
.read(youtubeEngineProvider)
.getStreamManifest(newSourceInfo.id);
final database = ref.read(databaseProvider);
await database.into(database.sourceMatchTable).insert(
SourceMatchTableCompanion.insert(
trackId: query.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(
ref: ref,
source: source,
siblings: newSiblings,
sources: toTrackSources(manifest),
info: newSourceInfo,
query: query,
);
}
@override
Future<YoutubeSourcedTrack> copyWithSibling() async {
if (siblings.isNotEmpty) {
return this;
}
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
return YoutubeSourcedTrack(
ref: ref,
siblings: fetchedSiblings
.where((s) => s.info.id != info.id)
.map((s) => s.info)
.toList(),
source: source,
sources: sources,
info: info,
query: query,
);
}
@override
Future<SourcedTrack> refreshStream() async {
List<TrackSource> validStreams = [];
final stringBuffer = StringBuffer();
for (final source in sources) {
final res = await globalDio.head(
source.url,
options:
Options(validateStatus: (status) => status != null && status < 500),
);
stringBuffer.writeln(
"[${query.id}] ${res.statusCode} ${source.quality} ${source.codec} ${source.bitrate}",
);
if (res.statusCode! < 400) {
validStreams.add(source);
}
}
AppLogger.log.d(stringBuffer.toString());
if (validStreams.isEmpty) {
final manifest =
await ref.read(youtubeEngineProvider).getStreamManifest(info.id);
validStreams = toTrackSources(manifest);
}
final sourcedTrack = YoutubeSourcedTrack(
ref: ref,
siblings: siblings,
source: source,
sources: validStreams,
info: info,
query: query,
);
AppLogger.log.i("Refreshing ${query.title}: ${sourcedTrack.url}");
return sourcedTrack;
}
}

View File

@ -162,7 +162,6 @@ class YouTubeExplodeEngine implements YouTubeEngine {
requireWatchPage: false, requireWatchPage: false,
ytClients: [ ytClients: [
YoutubeApiClient.ios, YoutubeApiClient.ios,
YoutubeApiClient.android,
YoutubeApiClient.androidVr, YoutubeApiClient.androidVr,
], ],
); );

View File

@ -10,11 +10,9 @@ import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart'; import 'package:spotube/pages/library/user_local_tracks/user_local_tracks.dart';
import 'package:spotube/modules/root/update_dialog.dart'; import 'package:spotube/modules/root/update_dialog.dart';
import 'package:spotube/models/lyrics.dart';
import 'package:spotube/provider/database/database.dart'; import 'package:spotube/provider/database/database.dart';
import 'package:spotube/services/dio/dio.dart'; import 'package:spotube/services/dio/dio.dart';
import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -189,95 +187,6 @@ abstract class ServiceUtils {
return lyrics; return lyrics;
} }
@Deprecated("In favor spotify lyrics api, this isn't needed anymore")
static const baseUri = "https://www.rentanadviser.com/subtitles";
@Deprecated("In favor spotify lyrics api, this isn't needed anymore")
static Future<SubtitleSimple?> getTimedLyrics(SourcedTrack track) async {
final artistNames = track.query.artists;
final query = getTitle(
track.query.title,
artists: artistNames,
);
final searchUri = Uri.parse("$baseUri/subtitles4songs.aspx").replace(
queryParameters: {"q": query},
);
final res = await globalDio.getUri(
searchUri,
options: Options(responseType: ResponseType.plain),
);
final document = parser.parse(res.data);
final results =
document.querySelectorAll("#tablecontainer table tbody tr td a");
final rateSortedResults = results.map((result) {
final title = result.text.trim().toLowerCase();
int points = 0;
final hasAllArtists = track.query.artists
.every((artist) => title.contains(artist.toLowerCase()));
final hasTrackName = title.contains(track.query.title.toLowerCase());
final isNotLive = !PrimitiveUtils.containsTextInBracket(title, "live");
final exactYtMatch = title == track.info.title.toLowerCase();
if (exactYtMatch) points = 7;
for (final criteria in [hasTrackName, hasAllArtists, isNotLive]) {
if (criteria) points++;
}
return {"result": result, "points": points};
}).sorted((a, b) => (b["points"] as int).compareTo(a["points"] as int));
// not result was found at all
if (rateSortedResults.first["points"] == 0) {
return Future.error("Subtitle lookup failed", StackTrace.current);
}
final topResult = rateSortedResults.first["result"] as Element;
final subtitleUri =
Uri.parse("$baseUri/${topResult.attributes["href"]}&type=lrc");
final lrcDocument = parser.parse((await globalDio.getUri(
subtitleUri,
options: Options(responseType: ResponseType.plain),
))
.data);
final lrcList = lrcDocument
.querySelector("#ctl00_ContentPlaceHolder1_lbllyrics")
?.innerHtml
.replaceAll(RegExp(r'<h3>.*</h3>'), "")
.split("<br>")
.map((e) {
e = e.trim();
final regexp = RegExp(r'\[.*\]');
final timeStr = regexp
.firstMatch(e)
?.group(0)
?.replaceAll(RegExp(r'\[|\]'), "")
.trim()
.split(":");
final minuteSeconds = timeStr?.last.split(".");
return LyricSlice(
time: Duration(
minutes: int.parse(timeStr?.first ?? "0"),
seconds: int.parse(minuteSeconds?.first ?? "0"),
milliseconds: int.parse(minuteSeconds?.last ?? "0"),
),
text: e.split(regexp).last);
}).toList() ??
[];
final subtitle = SubtitleSimple(
name: topResult.text.trim(),
uri: subtitleUri,
lyrics: lrcList,
rating: rateSortedResults.first["points"] as int,
provider: "Rent An Adviser",
);
return subtitle;
}
static DateTime parseSpotifyAlbumDate(SpotubeFullAlbumObject? album) { static DateTime parseSpotifyAlbumDate(SpotubeFullAlbumObject? album) {
if (album == null) { if (album == null) {
return DateTime.parse("1975-01-01"); return DateTime.parse("1975-01-01");

View File

@ -458,23 +458,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" version: "1.0.2"
dab_music_api:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: "55f96368b7465eec2e5e81774f9f2a7b18acc4ab"
url: "https://github.com/KRTirtho/dab_music_api.git"
source: git
version: "0.1.0"
dart_des:
dependency: transitive
description:
name: dart_des
sha256: "0a66afb8883368c824497fd2a1fd67bdb1a785965a3956728382c03d40747c33"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
dart_mappable: dart_mappable:
dependency: transitive dependency: transitive
description: description:
@ -1408,14 +1391,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.20.2" version: "0.20.2"
invidious:
dependency: "direct main"
description:
name: invidious
sha256: "0da8ebc4c4110057f03302bbd54514b10642154d7be569e7994172f2202dcfe8"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
io: io:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -1440,14 +1415,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.0" version: "0.7.0"
jiosaavn:
dependency: "direct main"
description:
name: jiosaavn
sha256: b6bde15c56398ebfd439825a64fb540a265773d1a518ba103e79988e13d16e1d
url: "https://pub.dev"
source: hosted
version: "0.1.1"
jovial_misc: jovial_misc:
dependency: transitive dependency: transitive
description: description:
@ -1935,14 +1902,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.0.4" version: "0.0.4"
piped_client:
dependency: "direct main"
description:
name: piped_client
sha256: "947613e2a8d368b72cb36473de2c5c2784e4e72b2d3f17e5a5181b98b1a5436e"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
pixel_snap: pixel_snap:
dependency: transitive dependency: transitive
description: description:
@ -2055,14 +2014,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
retrofit:
dependency: transitive
description:
name: retrofit
sha256: "699cf44ec6c7fc7d248740932eca75d334e36bdafe0a8b3e9ff93100591c8a25"
url: "https://pub.dev"
source: hosted
version: "4.7.2"
riverpod: riverpod:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2888,12 +2839,11 @@ packages:
youtube_explode_dart: youtube_explode_dart:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." name: youtube_explode_dart
ref: HEAD sha256: "947ba05e0c4f050743e480e7bca3575ff6427d86cc898c1a69f5e1d188cdc9e0"
resolved-ref: caa3023386dbc10e69c99f49f491148094874671 url: "https://pub.dev"
url: "https://github.com/Coronon/youtube_explode_dart" source: hosted
source: git version: "2.5.3"
version: "2.5.2"
yt_dlp_dart: yt_dlp_dart:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -24,10 +24,6 @@ dependencies:
bonsoir: ^5.1.10 bonsoir: ^5.1.10
cached_network_image: ^3.3.1 cached_network_image: ^3.3.1
connectivity_plus: ^6.1.2 connectivity_plus: ^6.1.2
dab_music_api:
git:
url: https://github.com/KRTirtho/dab_music_api.git
ref: main
desktop_webview_window: desktop_webview_window:
git: git:
path: packages/desktop_webview_window path: packages/desktop_webview_window
@ -81,8 +77,6 @@ dependencies:
http: ^1.2.1 http: ^1.2.1
image_picker: ^1.1.0 image_picker: ^1.1.0
intl: any intl: any
invidious: ^0.1.2
jiosaavn: ^0.1.1
json_annotation: ^4.8.1 json_annotation: ^4.8.1
local_notifier: ^0.1.6 local_notifier: ^0.1.6
logger: ^2.0.2 logger: ^2.0.2
@ -104,7 +98,6 @@ dependencies:
path: ^1.9.0 path: ^1.9.0
path_provider: ^2.1.3 path_provider: ^2.1.3
permission_handler: ^11.3.1 permission_handler: ^11.3.1
piped_client: ^0.1.2
riverpod: ^2.5.1 riverpod: ^2.5.1
scrobblenaut: scrobblenaut:
git: git:
@ -138,8 +131,7 @@ dependencies:
wikipedia_api: ^0.1.0 wikipedia_api: ^0.1.0
win32_registry: ^1.1.5 win32_registry: ^1.1.5
window_manager: ^0.4.3 window_manager: ^0.4.3
youtube_explode_dart: youtube_explode_dart: ^2.5.3
git: https://github.com/Coronon/youtube_explode_dart
yt_dlp_dart: yt_dlp_dart:
git: git:
url: https://github.com/KRTirtho/yt_dlp_dart.git url: https://github.com/KRTirtho/yt_dlp_dart.git

View File

@ -12,6 +12,7 @@ import 'schema_v6.dart' as v6;
import 'schema_v7.dart' as v7; import 'schema_v7.dart' as v7;
import 'schema_v8.dart' as v8; import 'schema_v8.dart' as v8;
import 'schema_v9.dart' as v9; import 'schema_v9.dart' as v9;
import 'schema_v10.dart' as v10;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -35,10 +36,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v8.DatabaseAtV8(db); return v8.DatabaseAtV8(db);
case 9: case 9:
return v9.DatabaseAtV9(db); return v9.DatabaseAtV9(db);
case 10:
return v10.DatabaseAtV10(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9]; static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
} }

File diff suppressed because it is too large Load Diff