mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-06 07:29:42 +00:00
Compare commits
3 Commits
3bc296cf22
...
4b5108e54e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b5108e54e | ||
|
|
6311831902 | ||
|
|
99a84aa6dc |
1
drift_schemas/app_db/drift_schema_v10.json
Normal file
1
drift_schemas/app_db/drift_schema_v10.json
Normal file
File diff suppressed because one or more lines are too long
@ -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 ?? "",
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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
@ -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,
|
||||||
));
|
));
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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)();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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',
|
|
||||||
};
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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,20 +251,19 @@ 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,
|
density: ButtonDensity.dense,
|
||||||
density: ButtonDensity.dense,
|
shape: ButtonShape.rectangle,
|
||||||
shape: ButtonShape.rectangle,
|
).copyWith(
|
||||||
).copyWith(
|
textStyle: (context, states, value) {
|
||||||
textStyle: (context, states, value) {
|
return value.copyWith(fontWeight: FontWeight.w500);
|
||||||
return value.copyWith(fontWeight: FontWeight.w500);
|
},
|
||||||
},
|
),
|
||||||
),
|
leading: const Icon(SpotubeIcons.lightningOutlined),
|
||||||
leading: const Icon(SpotubeIcons.lightningOutlined),
|
child: Text(qualityLabel),
|
||||||
child: Text(activeSourceCodec.qualityLabel),
|
)
|
||||||
)
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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,
|
||||||
@ -244,72 +59,16 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
spacing: 5,
|
spacing: 5,
|
||||||
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: (context, index) => itemBuilder(
|
final sourceInfo = siblings[index];
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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,332 +28,107 @@ 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),
|
secondary: const Icon(SpotubeIcons.engine),
|
||||||
title: Text(context.l10n.audio_quality),
|
title: Text(context.l10n.youtube_engine),
|
||||||
value: preferences.audioQuality,
|
value: preferences.youtubeClientEngine,
|
||||||
options: [
|
options: YoutubeClientEngine.values
|
||||||
if (preferences.audioSource == AudioSource.dabMusic)
|
.where((e) => e.isAvailableForPlatform())
|
||||||
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(
|
.map((e) => SelectItemButton(
|
||||||
value: e,
|
value: e,
|
||||||
child: Text(e.label),
|
child: Text(e.label),
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
preferencesNotifier.setAudioSource(value);
|
if (value == YoutubeClientEngine.ytDlp) {
|
||||||
|
final customPath = KVStoreService.getYoutubeEnginePath(value);
|
||||||
|
if (!await YtDlpEngine.isInstalled() &&
|
||||||
|
(customPath == null || !await File(customPath).exists()) &&
|
||||||
|
context.mounted) {
|
||||||
|
final hasInstalled = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
YouTubeEngineNotInstalledDialog(engine: value),
|
||||||
|
);
|
||||||
|
if (hasInstalled != true) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
preferencesNotifier.setYoutubeClientEngine(value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
AnimatedCrossFade(
|
if (sourcePresets.presets.isNotEmpty) ...[
|
||||||
duration: const Duration(milliseconds: 300),
|
AdaptiveSelectTile(
|
||||||
crossFadeState: preferences.audioSource != AudioSource.piped
|
secondary: const Icon(SpotubeIcons.api),
|
||||||
? CrossFadeState.showFirst
|
title: Text(context.l10n.streaming_music_codec),
|
||||||
: CrossFadeState.showSecond,
|
value: sourcePresets.selectedStreamingContainerIndex,
|
||||||
firstChild: const SizedBox.shrink(),
|
options: [
|
||||||
secondChild: Consumer(
|
for (final MapEntry(:key, value: preset)
|
||||||
builder: (context, ref, child) {
|
in sourcePresets.presets.asMap().entries)
|
||||||
final instanceList = ref.watch(pipedInstancesFutureProvider);
|
SelectItemButton(value: key, child: Text(preset.name)),
|
||||||
|
],
|
||||||
return instanceList.when(
|
onChanged: (value) {
|
||||||
data: (data) {
|
if (value == null) return;
|
||||||
return AdaptiveSelectTile<String>(
|
sourcePresetsNotifier.setSelectedStreamingContainerIndex(value);
|
||||||
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()),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
AdaptiveSelectTile(
|
||||||
AnimatedCrossFade(
|
secondary: const Icon(SpotubeIcons.api),
|
||||||
duration: const Duration(milliseconds: 300),
|
title: const Text("Streaming music quality"),
|
||||||
crossFadeState: preferences.audioSource != AudioSource.invidious
|
value: sourcePresets.selectedStreamingQualityIndex,
|
||||||
? CrossFadeState.showFirst
|
options: [
|
||||||
: CrossFadeState.showSecond,
|
for (final MapEntry(:key, value: quality) in sourcePresets
|
||||||
firstChild: const SizedBox.shrink(),
|
.presets[sourcePresets.selectedStreamingContainerIndex]
|
||||||
secondChild: Consumer(
|
.qualities
|
||||||
builder: (context, ref, child) {
|
.asMap()
|
||||||
final instanceList = ref.watch(invidiousInstancesProvider);
|
.entries)
|
||||||
|
SelectItemButton(value: key, child: Text(quality.toString())),
|
||||||
return instanceList.when(
|
],
|
||||||
data: (data) {
|
onChanged: (value) {
|
||||||
return AdaptiveSelectTile<String>(
|
if (value == null) return;
|
||||||
secondary: const Icon(SpotubeIcons.piped),
|
sourcePresetsNotifier.setSelectedStreamingQualityIndex(value);
|
||||||
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()),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
AdaptiveSelectTile(
|
||||||
switch (preferences.audioSource) {
|
secondary: const Icon(SpotubeIcons.api),
|
||||||
AudioSource.youtube => AdaptiveSelectTile<YoutubeClientEngine>(
|
title: Text(context.l10n.download_music_codec),
|
||||||
secondary: const Icon(SpotubeIcons.engine),
|
value: sourcePresets.selectedDownloadingContainerIndex,
|
||||||
title: Text(context.l10n.youtube_engine),
|
options: [
|
||||||
value: preferences.youtubeClientEngine,
|
for (final MapEntry(:key, value: preset)
|
||||||
options: YoutubeClientEngine.values
|
in sourcePresets.presets.asMap().entries)
|
||||||
.where((e) => e.isAvailableForPlatform())
|
SelectItemButton(value: key, child: Text(preset.name)),
|
||||||
.map((e) => SelectItemButton(
|
],
|
||||||
value: e,
|
onChanged: (value) {
|
||||||
child: Text(e.label),
|
if (value == null) return;
|
||||||
))
|
sourcePresetsNotifier.setSelectedDownloadingContainerIndex(value);
|
||||||
.toList(),
|
},
|
||||||
onChanged: (value) async {
|
|
||||||
if (value == null) return;
|
|
||||||
if (value == YoutubeClientEngine.ytDlp) {
|
|
||||||
final customPath = KVStoreService.getYoutubeEnginePath(value);
|
|
||||||
if (!await YtDlpEngine.isInstalled() &&
|
|
||||||
(customPath == null ||
|
|
||||||
!await File(customPath).exists()) &&
|
|
||||||
context.mounted) {
|
|
||||||
final hasInstalled = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) =>
|
|
||||||
YouTubeEngineNotInstalledDialog(engine: value),
|
|
||||||
);
|
|
||||||
if (hasInstalled != true) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
preferencesNotifier.setYoutubeClientEngine(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
AudioSource.piped ||
|
|
||||||
AudioSource.invidious =>
|
|
||||||
AdaptiveSelectTile<SearchMode>(
|
|
||||||
secondary: const Icon(SpotubeIcons.search),
|
|
||||||
title: Text(context.l10n.search_mode),
|
|
||||||
value: preferences.searchMode,
|
|
||||||
options: SearchMode.values
|
|
||||||
.map((e) => SelectItemButton(
|
|
||||||
value: e,
|
|
||||||
child: Text(e.label),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == null) return;
|
|
||||||
preferencesNotifier.setSearchMode(value);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_ => const SizedBox.shrink(),
|
|
||||||
},
|
|
||||||
AnimatedCrossFade(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
crossFadeState: preferences.searchMode == SearchMode.youtube &&
|
|
||||||
(preferences.audioSource == AudioSource.piped ||
|
|
||||||
preferences.audioSource == AudioSource.youtube ||
|
|
||||||
preferences.audioSource == AudioSource.invidious)
|
|
||||||
? CrossFadeState.showFirst
|
|
||||||
: CrossFadeState.showSecond,
|
|
||||||
firstChild: ListTile(
|
|
||||||
leading: const Icon(SpotubeIcons.skip),
|
|
||||||
title: Text(context.l10n.skip_non_music),
|
|
||||||
trailing: Switch(
|
|
||||||
value: preferences.skipNonMusic,
|
|
||||||
onChanged: (state) {
|
|
||||||
preferencesNotifier.setSkipNonMusic(state);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
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),
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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();
|
|
||||||
});
|
|
||||||
@ -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>[];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
lib/provider/metadata_plugin/audio_source/quality_label.dart
Normal file
12
lib/provider/metadata_plugin/audio_source/quality_label.dart
Normal 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"}";
|
||||||
|
});
|
||||||
120
lib/provider/metadata_plugin/audio_source/quality_presets.dart
Normal file
120
lib/provider/metadata_plugin/audio_source/quality_presets.dart
Normal 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(),
|
||||||
|
);
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
};
|
||||||
@ -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;
|
||||||
|
|||||||
@ -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(
|
||||||
audioPlayerState.activeTrack! as SpotubeFullTrackObject,
|
sourcedTrackProvider(
|
||||||
|
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 (
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
49
lib/provider/server/sourced_track_provider.dart
Normal file
49
lib/provider/server/sourced_track_provider.dart
Normal 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(),
|
||||||
|
);
|
||||||
@ -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(),
|
|
||||||
);
|
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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)));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
});
|
|
||||||
@ -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(", ")}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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 =>
|
final database = ref.read(databaseProvider);
|
||||||
await InvidiousSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
final cachedSource = await (database.select(database.sourceMatchTable)
|
||||||
AudioSource.jiosaavn =>
|
..where((s) =>
|
||||||
await JioSaavnSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
s.trackId.equals(query.id) &
|
||||||
AudioSource.dabMusic =>
|
s.sourceType.equals(audioSourceConfig.slug))
|
||||||
await DABMusicSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
..limit(1)
|
||||||
};
|
..orderBy([
|
||||||
} catch (e) {
|
(s) =>
|
||||||
if (preferences.audioSource == AudioSource.youtube) {
|
OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc),
|
||||||
rethrow;
|
]))
|
||||||
|
.get()
|
||||||
|
.then((s) => s.firstOrNull);
|
||||||
|
|
||||||
|
if (cachedSource == null) {
|
||||||
|
final siblings = await fetchSiblings(ref: ref, query: query);
|
||||||
|
if (siblings.isEmpty) {
|
||||||
|
throw TrackNotFoundError(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await YoutubeSourcedTrack.fetchFromTrack(query: query, ref: ref);
|
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 Future<List<SiblingType>> fetchSiblings({
|
static List<SpotubeAudioSourceMatchObject> rankResults(
|
||||||
required TrackSourceQuery query,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final titleContainsTrackName =
|
||||||
|
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),
|
final videoResults = <SpotubeAudioSourceMatchObject>[];
|
||||||
AudioSource.jiosaavn =>
|
|
||||||
JioSaavnSourcedTrack.fetchSiblings(query: query, ref: ref),
|
final searchResults = await audioSource.audioSource.matches(query);
|
||||||
AudioSource.invidious =>
|
|
||||||
InvidiousSourcedTrack.fetchSiblings(query: query, ref: ref),
|
if (ServiceUtils.onlyContainsEnglish(query.name)) {
|
||||||
AudioSource.dabMusic =>
|
videoResults.addAll(searchResults);
|
||||||
DABMusicSourcedTrack.fetchSiblings(query: query, ref: ref),
|
} else {
|
||||||
};
|
videoResults.addAll(rankResults(searchResults, query));
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoResults.toSet().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SourcedTrack> copyWithSibling();
|
Future<SourcedTrack> copyWithSibling() async {
|
||||||
|
if (siblings.isNotEmpty) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
final fetchedSiblings = await fetchSiblings(ref: ref, query: query);
|
||||||
|
|
||||||
Future<SourcedTrack?> swapWithSibling(TrackSourceInfo sibling);
|
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 closestDiff = (closest?.bitrate ?? 0) - presetBitrate;
|
||||||
|
final sourceDiff = (source.bitrate ?? 0) - presetBitrate;
|
||||||
|
|
||||||
|
if (sourceDiff < closestDiff) {
|
||||||
|
closest = source;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final fallbackSource = sources.sorted((a, b) {
|
return closest;
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
return preferences.audioQuality > SourceQualities.low
|
|
||||||
? fallbackSource.firstOrNull
|
|
||||||
: fallbackSource.lastOrNull;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String? getUrlOfCodec(SourceCodecs codec) {
|
String? getUrlOfQuality(
|
||||||
return getSourceOfCodec(codec)?.url;
|
SpotubeAudioSourceContainerPreset preset,
|
||||||
|
int qualityIndex,
|
||||||
|
) {
|
||||||
|
return getStreamOfQuality(preset, qualityIndex)?.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
SourceCodecs get codec {
|
SpotubeAudioSourceContainerPreset? get qualityPreset {
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final presetState = ref.read(audioSourcePresetsProvider);
|
||||||
|
return presetState.presets
|
||||||
return switch (preferences.audioSource) {
|
.elementAtOrNull(presetState.selectedStreamingContainerIndex);
|
||||||
AudioSource.dabMusic =>
|
|
||||||
preferences.audioQuality == SourceQualities.uncompressed
|
|
||||||
? SourceCodecs.flac
|
|
||||||
: SourceCodecs.mp3,
|
|
||||||
AudioSource.jiosaavn => SourceCodecs.m4a,
|
|
||||||
_ => preferences.streamMusicCodec
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -162,7 +162,6 @@ class YouTubeExplodeEngine implements YouTubeEngine {
|
|||||||
requireWatchPage: false,
|
requireWatchPage: false,
|
||||||
ytClients: [
|
ytClients: [
|
||||||
YoutubeApiClient.ios,
|
YoutubeApiClient.ios,
|
||||||
YoutubeApiClient.android,
|
|
||||||
YoutubeApiClient.androidVr,
|
YoutubeApiClient.androidVr,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
60
pubspec.lock
60
pubspec.lock
@ -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:
|
||||||
|
|||||||
10
pubspec.yaml
10
pubspec.yaml
@ -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
|
||||||
|
|||||||
@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
3407
test/drift/app_db/generated/schema_v10.dart
Normal file
3407
test/drift/app_db/generated/schema_v10.dart
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user