mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-06 07:29:42 +00:00
Compare commits
2 Commits
ca6924f5a9
...
cecb687592
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cecb687592 | ||
|
|
e8a54d3209 |
@ -39,6 +39,11 @@ class InstallDependenciesCommand extends Command {
|
|||||||
|
|
||||||
switch (argResults!.option("platform")) {
|
switch (argResults!.option("platform")) {
|
||||||
case "windows":
|
case "windows":
|
||||||
|
await shell.run(
|
||||||
|
"""
|
||||||
|
choco install innosetup -y
|
||||||
|
""",
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "linux":
|
case "linux":
|
||||||
await shell.run(
|
await shell.run(
|
||||||
|
|||||||
@ -462,5 +462,6 @@
|
|||||||
"configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider",
|
"configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider",
|
||||||
"audio_scrobblers": "Audio Scrobblers",
|
"audio_scrobblers": "Audio Scrobblers",
|
||||||
"scrobbling": "Scrobbling",
|
"scrobbling": "Scrobbling",
|
||||||
"source": "Source: "
|
"source": "Source: ",
|
||||||
|
"uncompressed": "Uncompressed"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2936,6 +2936,12 @@ abstract class AppLocalizations {
|
|||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Source: '**
|
/// **'Source: '**
|
||||||
String get source;
|
String get source;
|
||||||
|
|
||||||
|
/// No description provided for @uncompressed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Uncompressed'**
|
||||||
|
String get uncompressed;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppLocalizationsDelegate
|
class _AppLocalizationsDelegate
|
||||||
|
|||||||
@ -1540,4 +1540,7 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1541,4 +1541,7 @@ class AppLocalizationsBn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1551,4 +1551,7 @@ class AppLocalizationsCa extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1541,4 +1541,7 @@ class AppLocalizationsCs extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1553,4 +1553,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1539,4 +1539,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1554,4 +1554,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1551,4 +1551,7 @@ class AppLocalizationsEu extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1539,4 +1539,7 @@ class AppLocalizationsFa extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1539,4 +1539,7 @@ class AppLocalizationsFi extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1559,4 +1559,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1545,4 +1545,7 @@ class AppLocalizationsHi extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1547,4 +1547,7 @@ class AppLocalizationsId extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1546,4 +1546,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1510,4 +1510,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1548,4 +1548,7 @@ class AppLocalizationsKa extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1514,4 +1514,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1551,4 +1551,7 @@ class AppLocalizationsNe extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1545,4 +1545,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1547,4 +1547,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1544,4 +1544,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1547,4 +1547,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1553,4 +1553,7 @@ class AppLocalizationsTa extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1536,4 +1536,7 @@ class AppLocalizationsTh extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1554,4 +1554,7 @@ class AppLocalizationsTl extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1547,4 +1547,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1543,4 +1543,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1549,4 +1549,7 @@ class AppLocalizationsVi extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1503,6 +1503,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String get source => 'Source: ';
|
String get source => 'Source: ';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get uncompressed => 'Uncompressed';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
/// The translations for Chinese, as used in Taiwan (`zh_TW`).
|
||||||
|
|||||||
@ -12,12 +12,14 @@ enum CloseBehavior {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum AudioSource {
|
enum AudioSource {
|
||||||
youtube,
|
youtube("YouTube"),
|
||||||
piped,
|
piped("Piped"),
|
||||||
jiosaavn,
|
jiosaavn("JioSaavn"),
|
||||||
invidious;
|
invidious("Invidious"),
|
||||||
|
dabMusic("DAB Music");
|
||||||
|
|
||||||
String get label => name[0].toUpperCase() + name.substring(1);
|
final String label;
|
||||||
|
const AudioSource(this.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum YoutubeClientEngine {
|
enum YoutubeClientEngine {
|
||||||
@ -39,14 +41,6 @@ enum YoutubeClientEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MusicCodec {
|
|
||||||
m4a._("M4a (Best for downloaded music)"),
|
|
||||||
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
|
|
||||||
|
|
||||||
final String label;
|
|
||||||
const MusicCodec._(this.label);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SearchMode {
|
enum SearchMode {
|
||||||
youtube._("YouTube"),
|
youtube._("YouTube"),
|
||||||
youtubeMusic._("YouTube Music");
|
youtubeMusic._("YouTube Music");
|
||||||
|
|||||||
@ -3,7 +3,8 @@ part of '../database.dart';
|
|||||||
enum SourceType {
|
enum SourceType {
|
||||||
youtube._("YouTube"),
|
youtube._("YouTube"),
|
||||||
youtubeMusic._("YouTube Music"),
|
youtubeMusic._("YouTube Music"),
|
||||||
jiosaavn._("JioSaavn");
|
jiosaavn._("JioSaavn"),
|
||||||
|
dabMusic._("DAB Music");
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import 'package:flutter/material.dart' show ListTile;
|
|||||||
import 'package:google_fonts/google_fonts.dart';
|
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:piped_client/piped_client.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer;
|
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/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
@ -44,6 +44,11 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
title: Text(context.l10n.audio_quality),
|
title: Text(context.l10n.audio_quality),
|
||||||
value: preferences.audioQuality,
|
value: preferences.audioQuality,
|
||||||
options: [
|
options: [
|
||||||
|
if (preferences.audioSource == AudioSource.dabMusic)
|
||||||
|
SelectItemButton(
|
||||||
|
value: SourceQualities.uncompressed,
|
||||||
|
child: Text(context.l10n.uncompressed),
|
||||||
|
),
|
||||||
SelectItemButton(
|
SelectItemButton(
|
||||||
value: SourceQualities.high,
|
value: SourceQualities.high,
|
||||||
child: Text(context.l10n.high),
|
child: Text(context.l10n.high),
|
||||||
@ -396,7 +401,9 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
onChanged: preferencesNotifier.setNormalizeAudio,
|
onChanged: preferencesNotifier.setNormalizeAudio,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (preferences.audioSource != AudioSource.jiosaavn) ...[
|
if (const [AudioSource.jiosaavn, AudioSource.dabMusic]
|
||||||
|
.contains(preferences.audioSource) ==
|
||||||
|
false) ...[
|
||||||
AdaptiveSelectTile<SourceCodecs>(
|
AdaptiveSelectTile<SourceCodecs>(
|
||||||
popupConstraints: const BoxConstraints(maxWidth: 300),
|
popupConstraints: const BoxConstraints(maxWidth: 300),
|
||||||
secondary: const Icon(SpotubeIcons.stream),
|
secondary: const Icon(SpotubeIcons.stream),
|
||||||
|
|||||||
@ -101,10 +101,7 @@ class ServerPlaybackRoutes {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final contentLengthRes = await Future<dio_lib.Response?>.value(
|
final contentLengthRes = await Future<dio_lib.Response?>.value(
|
||||||
dio.head(
|
dio.head(url, options: options),
|
||||||
url,
|
|
||||||
options: options,
|
|
||||||
),
|
|
||||||
).catchError((e, stack) async {
|
).catchError((e, stack) async {
|
||||||
AppLogger.reportError(e, stack);
|
AppLogger.reportError(e, stack);
|
||||||
|
|
||||||
@ -135,19 +132,17 @@ class ServerPlaybackRoutes {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final contentLength = contentLengthRes?.headers.value("content-length");
|
if (headers["range"] == "bytes=0-") {
|
||||||
|
final bufferSize =
|
||||||
/// Forcing partial content range as mpv sometimes greedily wants
|
userPreferences.audioQuality == SourceQualities.uncompressed
|
||||||
/// everything at one go. Slows down overall streaming.
|
? 6 * 1024 * 1024
|
||||||
final range = RangeHeader.parse(headers["range"] ?? "");
|
: 4 * 1024 * 1024;
|
||||||
final contentPartialLength = int.tryParse(contentLength ?? "");
|
final endRange = min(bufferSize,
|
||||||
if ((range.end == null) &&
|
int.parse(contentLengthRes?.headers.value("content-length") ?? "0"));
|
||||||
contentPartialLength != null &&
|
|
||||||
range.start == 0) {
|
|
||||||
options = options.copyWith(
|
options = options.copyWith(
|
||||||
headers: {
|
headers: {
|
||||||
...?options.headers,
|
...options.headers ?? {},
|
||||||
"range": "$range${(contentPartialLength * 0.3).ceil()}",
|
"range": "bytes=0-$endRange",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,6 +54,7 @@ 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);
|
||||||
}
|
}
|
||||||
@ -79,6 +80,24 @@ 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);
|
||||||
|
|
||||||
@ -155,6 +174,7 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
|
|||||||
|
|
||||||
void setAudioQuality(SourceQualities quality) {
|
void setAudioQuality(SourceQualities quality) {
|
||||||
setData(PreferencesTableCompanion(audioQuality: Value(quality)));
|
setData(PreferencesTableCompanion(audioQuality: Value(quality)));
|
||||||
|
_updatePlayerBufferSize(quality, state.audioQuality);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setDownloadLocation(String downloadDir) {
|
void setDownloadLocation(String downloadDir) {
|
||||||
@ -204,6 +224,11 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setAudioSource(AudioSource type) {
|
void setAudioSource(AudioSource type) {
|
||||||
|
// Only allow uncompressed quality for DAB Music
|
||||||
|
if (type != AudioSource.dabMusic &&
|
||||||
|
state.audioQuality == SourceQualities.uncompressed) {
|
||||||
|
setAudioQuality(SourceQualities.high);
|
||||||
|
}
|
||||||
setData(PreferencesTableCompanion(audioSource: Value(type)));
|
setData(PreferencesTableCompanion(audioSource: Value(type)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,7 @@ abstract class AudioPlayerInterface {
|
|||||||
configuration: const mk.PlayerConfiguration(
|
configuration: const mk.PlayerConfiguration(
|
||||||
title: "Spotube",
|
title: "Spotube",
|
||||||
logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error,
|
logLevel: kDebugMode ? mk.MPVLogLevel.info : mk.MPVLogLevel.error,
|
||||||
|
bufferSize: 4 * 1024 * 1024, // 4MB buffer
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
_mkPlayer.stream.error.listen((event) {
|
_mkPlayer.stream.error.listen((event) {
|
||||||
|
|||||||
@ -131,4 +131,8 @@ class SpotubeAudioPlayer extends AudioPlayerInterface
|
|||||||
Future<void> setAudioNormalization(bool normalize) async {
|
Future<void> setAudioNormalization(bool normalize) async {
|
||||||
await _mkPlayer.setAudioNormalization(normalize);
|
await _mkPlayer.setAudioNormalization(normalize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setDemuxerBufferSize(int sizeInBytes) async {
|
||||||
|
await _mkPlayer.setDemuxerBufferSize(sizeInBytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,4 +133,12 @@ class CustomPlayer extends Player {
|
|||||||
await nativePlayer.setProperty('af', '');
|
await nativePlayer.setProperty('af', '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setDemuxerBufferSize(int sizeInBytes) async {
|
||||||
|
await nativePlayer.setProperty('demuxer-max-bytes', sizeInBytes.toString());
|
||||||
|
await nativePlayer.setProperty(
|
||||||
|
'demuxer-max-back-bytes',
|
||||||
|
sizeInBytes.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,13 +2,16 @@ import 'package:spotube/models/playback/track_sources.dart';
|
|||||||
|
|
||||||
enum SourceCodecs {
|
enum SourceCodecs {
|
||||||
m4a._("M4a (Best for downloaded music)"),
|
m4a._("M4a (Best for downloaded music)"),
|
||||||
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
|
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;
|
final String label;
|
||||||
const SourceCodecs._(this.label);
|
const SourceCodecs._(this.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SourceQualities {
|
enum SourceQualities {
|
||||||
|
uncompressed(3),
|
||||||
high(2),
|
high(2),
|
||||||
medium(1),
|
medium(1),
|
||||||
low(0);
|
low(0);
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:spotube/models/playback/track_sources.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/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.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/invidious.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/jiosaavn.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/piped.dart';
|
||||||
@ -74,6 +75,14 @@ abstract class SourcedTrack extends BasicSourcedTrack {
|
|||||||
query: query,
|
query: query,
|
||||||
sources: sources,
|
sources: sources,
|
||||||
),
|
),
|
||||||
|
AudioSource.dabMusic => DABMusicSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
source: source,
|
||||||
|
siblings: siblings,
|
||||||
|
info: info,
|
||||||
|
query: query,
|
||||||
|
sources: sources,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +113,8 @@ abstract class SourcedTrack extends BasicSourcedTrack {
|
|||||||
await InvidiousSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
await InvidiousSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
||||||
AudioSource.jiosaavn =>
|
AudioSource.jiosaavn =>
|
||||||
await JioSaavnSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
await JioSaavnSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
||||||
|
AudioSource.dabMusic =>
|
||||||
|
await DABMusicSourcedTrack.fetchFromTrack(query: query, ref: ref),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (preferences.audioSource == AudioSource.youtube) {
|
if (preferences.audioSource == AudioSource.youtube) {
|
||||||
@ -129,6 +140,8 @@ abstract class SourcedTrack extends BasicSourcedTrack {
|
|||||||
JioSaavnSourcedTrack.fetchSiblings(query: query, ref: ref),
|
JioSaavnSourcedTrack.fetchSiblings(query: query, ref: ref),
|
||||||
AudioSource.invidious =>
|
AudioSource.invidious =>
|
||||||
InvidiousSourcedTrack.fetchSiblings(query: query, ref: ref),
|
InvidiousSourcedTrack.fetchSiblings(query: query, ref: ref),
|
||||||
|
AudioSource.dabMusic =>
|
||||||
|
DABMusicSourcedTrack.fetchSiblings(query: query, ref: ref),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,9 +211,14 @@ abstract class SourcedTrack extends BasicSourcedTrack {
|
|||||||
SourceCodecs get codec {
|
SourceCodecs get codec {
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
return preferences.audioSource == AudioSource.jiosaavn
|
return switch (preferences.audioSource) {
|
||||||
? SourceCodecs.m4a
|
AudioSource.dabMusic =>
|
||||||
: preferences.streamMusicCodec;
|
preferences.audioQuality == SourceQualities.uncompressed
|
||||||
|
? SourceCodecs.flac
|
||||||
|
: SourceCodecs.mp3,
|
||||||
|
AudioSource.jiosaavn => SourceCodecs.m4a,
|
||||||
|
_ => preferences.streamMusicCodec
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackSource get activeTrackSource {
|
TrackSource get activeTrackSource {
|
||||||
|
|||||||
206
lib/services/sourced_track/sources/dab_music.dart
Normal file
206
lib/services/sourced_track/sources/dab_music.dart
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:dio/dio.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/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 siblings = await fetchSiblings(ref: ref, query: query);
|
||||||
|
|
||||||
|
if (siblings.isEmpty) {
|
||||||
|
throw TrackNotFoundError(query);
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
) 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");
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
TrackSource(
|
||||||
|
url: streamResponse.url!,
|
||||||
|
quality: isUncompressed
|
||||||
|
? SourceQualities.uncompressed
|
||||||
|
: SourceQualities.high,
|
||||||
|
bitrate: isUncompressed ? "2998kbps" : "320kbps",
|
||||||
|
codec: isUncompressed ? SourceCodecs.flac : SourceCodecs.mp3,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
pubspec.lock
17
pubspec.lock
@ -458,6 +458,15 @@ 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:
|
dart_des:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2046,6 +2055,14 @@ 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:
|
||||||
|
|||||||
@ -24,6 +24,10 @@ 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
|
||||||
|
|||||||
@ -1,118 +1,147 @@
|
|||||||
{
|
{
|
||||||
"ar": [
|
"ar": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"bn": [
|
"bn": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ca": [
|
"ca": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"cs": [
|
"cs": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"de": [
|
"de": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"eu": [
|
"eu": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fa": [
|
"fa": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fi": [
|
"fi": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"hi": [
|
"hi": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"id": [
|
"id": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"it": [
|
"it": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ja": [
|
"ja": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ka": [
|
"ka": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ko": [
|
"ko": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ne": [
|
"ne": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"nl": [
|
"nl": [
|
||||||
"audio_source",
|
"audio_source",
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pl": [
|
"pl": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ta": [
|
"ta": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"th": [
|
"th": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"tl": [
|
"tl": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"tr": [
|
"tr": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"uk": [
|
"uk": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"vi": [
|
"vi": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh_TW": [
|
"zh_TW": [
|
||||||
"source"
|
"source",
|
||||||
|
"uncompressed"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user