mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-10 01:07:29 +00:00
feat: add jiosaavn as audio source
This commit is contained in:
parent
84ed9e1c6e
commit
3de153e478
@ -59,12 +59,12 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
preferences.setYoutubeApiType(value);
|
preferences.setAudioSource(value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: preferences.audioSource == AudioSource.youtube
|
child: preferences.audioSource != AudioSource.piped
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: Consumer(builder: (context, ref, child) {
|
: Consumer(builder: (context, ref, child) {
|
||||||
final instanceList = ref.watch(pipedInstancesFutureProvider);
|
final instanceList = ref.watch(pipedInstancesFutureProvider);
|
||||||
@ -131,7 +131,7 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: preferences.audioSource == AudioSource.youtube
|
child: preferences.audioSource != AudioSource.piped
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: AdaptiveSelectTile<SearchMode>(
|
: AdaptiveSelectTile<SearchMode>(
|
||||||
secondary: const Icon(SpotubeIcons.search),
|
secondary: const Icon(SpotubeIcons.search),
|
||||||
@ -151,17 +151,18 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: preferences.searchMode == SearchMode.youtubeMusic &&
|
child: preferences.searchMode == SearchMode.youtube &&
|
||||||
preferences.audioSource == AudioSource.piped
|
(preferences.audioSource == AudioSource.piped ||
|
||||||
? const SizedBox.shrink()
|
preferences.audioSource == AudioSource.youtube)
|
||||||
: SwitchListTile(
|
? SwitchListTile(
|
||||||
secondary: const Icon(SpotubeIcons.skip),
|
secondary: const Icon(SpotubeIcons.skip),
|
||||||
title: Text(context.l10n.skip_non_music),
|
title: Text(context.l10n.skip_non_music),
|
||||||
value: preferences.skipNonMusic,
|
value: preferences.skipNonMusic,
|
||||||
onChanged: (state) {
|
onChanged: (state) {
|
||||||
preferences.setSkipNonMusic(state);
|
preferences.setSkipNonMusic(state);
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(SpotubeIcons.playlistRemove),
|
leading: const Icon(SpotubeIcons.playlistRemove),
|
||||||
@ -178,44 +179,46 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
value: preferences.normalizeAudio,
|
value: preferences.normalizeAudio,
|
||||||
onChanged: preferences.setNormalizeAudio,
|
onChanged: preferences.setNormalizeAudio,
|
||||||
),
|
),
|
||||||
AdaptiveSelectTile<MusicCodec>(
|
if (preferences.audioSource != AudioSource.jiosaavn)
|
||||||
secondary: const Icon(SpotubeIcons.stream),
|
AdaptiveSelectTile<SourceCodecs>(
|
||||||
title: Text(context.l10n.streaming_music_codec),
|
secondary: const Icon(SpotubeIcons.stream),
|
||||||
value: preferences.streamMusicCodec,
|
title: Text(context.l10n.streaming_music_codec),
|
||||||
showValueWhenUnfolded: false,
|
value: preferences.streamMusicCodec,
|
||||||
options: MusicCodec.values
|
showValueWhenUnfolded: false,
|
||||||
.map((e) => DropdownMenuItem(
|
options: SourceCodecs.values
|
||||||
value: e,
|
.map((e) => DropdownMenuItem(
|
||||||
child: Text(
|
value: e,
|
||||||
e.label,
|
child: Text(
|
||||||
style: theme.textTheme.labelMedium,
|
e.label,
|
||||||
),
|
style: theme.textTheme.labelMedium,
|
||||||
))
|
),
|
||||||
.toList(),
|
))
|
||||||
onChanged: (value) {
|
.toList(),
|
||||||
if (value == null) return;
|
onChanged: (value) {
|
||||||
preferences.setStreamMusicCodec(value);
|
if (value == null) return;
|
||||||
},
|
preferences.setStreamMusicCodec(value);
|
||||||
),
|
},
|
||||||
AdaptiveSelectTile<MusicCodec>(
|
),
|
||||||
secondary: const Icon(SpotubeIcons.file),
|
if (preferences.audioSource != AudioSource.jiosaavn)
|
||||||
title: Text(context.l10n.download_music_codec),
|
AdaptiveSelectTile<SourceCodecs>(
|
||||||
value: preferences.downloadMusicCodec,
|
secondary: const Icon(SpotubeIcons.file),
|
||||||
showValueWhenUnfolded: false,
|
title: Text(context.l10n.download_music_codec),
|
||||||
options: MusicCodec.values
|
value: preferences.downloadMusicCodec,
|
||||||
.map((e) => DropdownMenuItem(
|
showValueWhenUnfolded: false,
|
||||||
value: e,
|
options: SourceCodecs.values
|
||||||
child: Text(
|
.map((e) => DropdownMenuItem(
|
||||||
e.label,
|
value: e,
|
||||||
style: theme.textTheme.labelMedium,
|
child: Text(
|
||||||
),
|
e.label,
|
||||||
))
|
style: theme.textTheme.labelMedium,
|
||||||
.toList(),
|
),
|
||||||
onChanged: (value) {
|
))
|
||||||
if (value == null) return;
|
.toList(),
|
||||||
preferences.setDownloadMusicCodec(value);
|
onChanged: (value) {
|
||||||
},
|
if (value == null) return;
|
||||||
),
|
preferences.setDownloadMusicCodec(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import 'package:path/path.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
import 'package:spotube/provider/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/type_conversion_utils.dart';
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
@ -42,7 +43,7 @@ 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 == MusicCodec.weba) return;
|
downloadCodec == SourceCodecs.weba) return;
|
||||||
|
|
||||||
final file = File(request.path);
|
final file = File(request.path);
|
||||||
|
|
||||||
@ -90,7 +91,7 @@ class DownloadManagerProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
String get downloadDirectory =>
|
String get downloadDirectory =>
|
||||||
ref.read(userPreferencesProvider.select((s) => s.downloadLocation));
|
ref.read(userPreferencesProvider.select((s) => s.downloadLocation));
|
||||||
MusicCodec get downloadCodec =>
|
SourceCodecs get downloadCodec =>
|
||||||
ref.read(userPreferencesProvider.select((s) => s.downloadMusicCodec));
|
ref.read(userPreferencesProvider.select((s) => s.downloadMusicCodec));
|
||||||
|
|
||||||
int get $downloadCount => dl
|
int get $downloadCount => dl
|
||||||
|
|||||||
@ -171,10 +171,11 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final isYTMusicMode = preferences.audioSource == AudioSource.piped &&
|
final isNotYTMode = preferences.audioSource != AudioSource.youtube ||
|
||||||
preferences.searchMode == SearchMode.youtubeMusic;
|
(preferences.audioSource == AudioSource.piped &&
|
||||||
|
preferences.searchMode == SearchMode.youtubeMusic);
|
||||||
|
|
||||||
if (isYTMusicMode || !preferences.skipNonMusic) return;
|
if (isNotYTMode || !preferences.skipNonMusic) return;
|
||||||
|
|
||||||
final isNotSameSegmentId =
|
final isNotSameSegmentId =
|
||||||
currentSegments.value?.source != audioPlayer.currentSource;
|
currentSegments.value?.source != audioPlayer.currentSource;
|
||||||
|
|||||||
@ -37,14 +37,6 @@ enum AudioSource {
|
|||||||
String get label => name[0].toUpperCase() + name.substring(1);
|
String get label => name[0].toUpperCase() + name.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
youtubeMusic;
|
youtubeMusic;
|
||||||
@ -91,8 +83,8 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
String pipedInstance;
|
String pipedInstance;
|
||||||
ThemeMode themeMode;
|
ThemeMode themeMode;
|
||||||
AudioSource audioSource;
|
AudioSource audioSource;
|
||||||
MusicCodec streamMusicCodec;
|
SourceCodecs streamMusicCodec;
|
||||||
MusicCodec downloadMusicCodec;
|
SourceCodecs downloadMusicCodec;
|
||||||
|
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
@ -115,8 +107,8 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
this.systemTitleBar = false,
|
this.systemTitleBar = false,
|
||||||
this.amoledDarkTheme = false,
|
this.amoledDarkTheme = false,
|
||||||
this.normalizeAudio = true,
|
this.normalizeAudio = true,
|
||||||
this.streamMusicCodec = MusicCodec.weba,
|
this.streamMusicCodec = SourceCodecs.weba,
|
||||||
this.downloadMusicCodec = MusicCodec.m4a,
|
this.downloadMusicCodec = SourceCodecs.m4a,
|
||||||
SpotubeColor? accentColorScheme,
|
SpotubeColor? accentColorScheme,
|
||||||
}) : super() {
|
}) : super() {
|
||||||
this.accentColorScheme =
|
this.accentColorScheme =
|
||||||
@ -144,22 +136,22 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
setPipedInstance("https://pipedapi.kavin.rocks");
|
setPipedInstance("https://pipedapi.kavin.rocks");
|
||||||
setSearchMode(SearchMode.youtube);
|
setSearchMode(SearchMode.youtube);
|
||||||
setSkipNonMusic(true);
|
setSkipNonMusic(true);
|
||||||
setYoutubeApiType(AudioSource.youtube);
|
setAudioSource(AudioSource.youtube);
|
||||||
setSystemTitleBar(false);
|
setSystemTitleBar(false);
|
||||||
setAmoledDarkTheme(false);
|
setAmoledDarkTheme(false);
|
||||||
setNormalizeAudio(true);
|
setNormalizeAudio(true);
|
||||||
setAccentColorScheme(SpotubeColor(Colors.blue.value, name: "Blue"));
|
setAccentColorScheme(SpotubeColor(Colors.blue.value, name: "Blue"));
|
||||||
setStreamMusicCodec(MusicCodec.weba);
|
setStreamMusicCodec(SourceCodecs.weba);
|
||||||
setDownloadMusicCodec(MusicCodec.m4a);
|
setDownloadMusicCodec(SourceCodecs.m4a);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setStreamMusicCodec(MusicCodec codec) {
|
void setStreamMusicCodec(SourceCodecs codec) {
|
||||||
streamMusicCodec = codec;
|
streamMusicCodec = codec;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
updatePersistence();
|
updatePersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setDownloadMusicCodec(MusicCodec codec) {
|
void setDownloadMusicCodec(SourceCodecs codec) {
|
||||||
downloadMusicCodec = codec;
|
downloadMusicCodec = codec;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
updatePersistence();
|
updatePersistence();
|
||||||
@ -255,7 +247,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
updatePersistence();
|
updatePersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
void setYoutubeApiType(AudioSource type) {
|
void setAudioSource(AudioSource type) {
|
||||||
audioSource = type;
|
audioSource = type;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
updatePersistence();
|
updatePersistence();
|
||||||
@ -358,14 +350,14 @@ class UserPreferences extends PersistedChangeNotifier {
|
|||||||
normalizeAudio = map["normalizeAudio"] ?? normalizeAudio;
|
normalizeAudio = map["normalizeAudio"] ?? normalizeAudio;
|
||||||
audioPlayer.setAudioNormalization(normalizeAudio);
|
audioPlayer.setAudioNormalization(normalizeAudio);
|
||||||
|
|
||||||
streamMusicCodec = MusicCodec.values.firstWhere(
|
streamMusicCodec = SourceCodecs.values.firstWhere(
|
||||||
(codec) => codec.name == map["streamMusicCodec"],
|
(codec) => codec.name == map["streamMusicCodec"],
|
||||||
orElse: () => MusicCodec.weba,
|
orElse: () => SourceCodecs.weba,
|
||||||
);
|
);
|
||||||
|
|
||||||
downloadMusicCodec = MusicCodec.values.firstWhere(
|
downloadMusicCodec = SourceCodecs.values.firstWhere(
|
||||||
(codec) => codec.name == map["downloadMusicCodec"],
|
(codec) => codec.name == map["downloadMusicCodec"],
|
||||||
orElse: () => MusicCodec.m4a,
|
orElse: () => SourceCodecs.m4a,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,11 @@ import 'package:spotube/services/sourced_track/models/source_info.dart';
|
|||||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
||||||
|
|
||||||
enum SourceCodecs {
|
enum SourceCodecs {
|
||||||
mp4,
|
m4a._("M4a (Best for downloaded music)"),
|
||||||
weba,
|
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
|
||||||
m4a,
|
|
||||||
|
final String label;
|
||||||
|
const SourceCodecs._(this.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SourceQualities {
|
enum SourceQualities {
|
||||||
|
|||||||
@ -34,12 +34,10 @@ class SourceQualityMap {
|
|||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class SourceMap {
|
class SourceMap {
|
||||||
final SourceQualityMap? mp4;
|
|
||||||
final SourceQualityMap? weba;
|
final SourceQualityMap? weba;
|
||||||
final SourceQualityMap? m4a;
|
final SourceQualityMap? m4a;
|
||||||
|
|
||||||
const SourceMap({
|
const SourceMap({
|
||||||
this.mp4,
|
|
||||||
this.weba,
|
this.weba,
|
||||||
this.m4a,
|
this.m4a,
|
||||||
});
|
});
|
||||||
@ -51,8 +49,6 @@ class SourceMap {
|
|||||||
|
|
||||||
operator [](SourceCodecs key) {
|
operator [](SourceCodecs key) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case SourceCodecs.mp4:
|
|
||||||
return mp4;
|
|
||||||
case SourceCodecs.weba:
|
case SourceCodecs.weba:
|
||||||
return weba;
|
return weba;
|
||||||
case SourceCodecs.m4a:
|
case SourceCodecs.m4a:
|
||||||
|
|||||||
@ -21,9 +21,6 @@ Map<String, dynamic> _$SourceQualityMapToJson(SourceQualityMap instance) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
SourceMap _$SourceMapFromJson(Map<String, dynamic> json) => SourceMap(
|
SourceMap _$SourceMapFromJson(Map<String, dynamic> json) => SourceMap(
|
||||||
mp4: json['mp4'] == null
|
|
||||||
? null
|
|
||||||
: SourceQualityMap.fromJson(json['mp4'] as Map<String, dynamic>),
|
|
||||||
weba: json['weba'] == null
|
weba: json['weba'] == null
|
||||||
? null
|
? null
|
||||||
: SourceQualityMap.fromJson(json['weba'] as Map<String, dynamic>),
|
: SourceQualityMap.fromJson(json['weba'] as Map<String, dynamic>),
|
||||||
@ -33,7 +30,6 @@ SourceMap _$SourceMapFromJson(Map<String, dynamic> json) => SourceMap(
|
|||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$SourceMapToJson(SourceMap instance) => <String, dynamic>{
|
Map<String, dynamic> _$SourceMapToJson(SourceMap instance) => <String, dynamic>{
|
||||||
'mp4': instance.mp4,
|
|
||||||
'weba': instance.weba,
|
'weba': instance.weba,
|
||||||
'm4a': instance.m4a,
|
'm4a': instance.m4a,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:spotube/provider/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/models/source_info.dart';
|
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
import 'package:spotube/services/sourced_track/models/source_map.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';
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
@ -60,14 +61,22 @@ abstract class SourcedTrack extends Track {
|
|||||||
source: source,
|
source: source,
|
||||||
siblings: siblings,
|
siblings: siblings,
|
||||||
sourceInfo: sourceInfo,
|
sourceInfo: sourceInfo,
|
||||||
track: track),
|
track: track,
|
||||||
|
),
|
||||||
AudioSource.piped => PipedSourcedTrack(
|
AudioSource.piped => PipedSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
source: source,
|
source: source,
|
||||||
siblings: siblings,
|
siblings: siblings,
|
||||||
sourceInfo: sourceInfo,
|
sourceInfo: sourceInfo,
|
||||||
track: track),
|
track: track,
|
||||||
AudioSource.jiosaavn => throw UnimplementedError(),
|
),
|
||||||
|
AudioSource.jiosaavn => JioSaavnSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
source: source,
|
||||||
|
siblings: siblings,
|
||||||
|
sourceInfo: sourceInfo,
|
||||||
|
track: track,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,16 +99,22 @@ abstract class SourcedTrack extends Track {
|
|||||||
static Future<SourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required Track track,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) {
|
}) async {
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
try {
|
||||||
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
return switch (preferences.audioSource) {
|
return switch (preferences.audioSource) {
|
||||||
AudioSource.piped =>
|
AudioSource.piped =>
|
||||||
PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||||
AudioSource.youtube =>
|
AudioSource.youtube =>
|
||||||
YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||||
AudioSource.jiosaavn => throw UnimplementedError(),
|
AudioSource.jiosaavn =>
|
||||||
};
|
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
print("Got error: $e");
|
||||||
|
return YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<SiblingType>> fetchSiblings({
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
@ -113,7 +128,8 @@ abstract class SourcedTrack extends Track {
|
|||||||
PipedSourcedTrack.fetchSiblings(track: track, ref: ref),
|
PipedSourcedTrack.fetchSiblings(track: track, ref: ref),
|
||||||
AudioSource.youtube =>
|
AudioSource.youtube =>
|
||||||
YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref),
|
YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref),
|
||||||
AudioSource.jiosaavn => throw UnimplementedError(),
|
AudioSource.jiosaavn =>
|
||||||
|
JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,28 +145,26 @@ abstract class SourcedTrack extends Track {
|
|||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
final codec = preferences.audioSource == AudioSource.jiosaavn
|
final codec = preferences.audioSource == AudioSource.jiosaavn
|
||||||
? SourceCodecs.mp4
|
? SourceCodecs.m4a
|
||||||
: switch (preferences.streamMusicCodec) {
|
: preferences.streamMusicCodec;
|
||||||
MusicCodec.m4a => SourceCodecs.m4a,
|
|
||||||
MusicCodec.weba => SourceCodecs.weba,
|
|
||||||
};
|
|
||||||
|
|
||||||
return source[codec]![preferences.audioQuality]!;
|
return getUrlOfCodec(codec);
|
||||||
}
|
}
|
||||||
|
|
||||||
String getUrlOfCodec(MusicCodec codec) {
|
String getUrlOfCodec(SourceCodecs codec) {
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
return source[codec == MusicCodec.m4a
|
return source[codec]?[preferences.audioQuality] ??
|
||||||
? SourceCodecs.m4a
|
// this will ensure playback doesn't break
|
||||||
: SourceCodecs.weba]![preferences.audioQuality]!;
|
source[codec == SourceCodecs.m4a ? SourceCodecs.weba : SourceCodecs.m4a]
|
||||||
|
[preferences.audioQuality];
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicCodec get codec {
|
SourceCodecs get codec {
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
return preferences.audioSource == AudioSource.jiosaavn
|
return preferences.audioSource == AudioSource.jiosaavn
|
||||||
? MusicCodec.m4a
|
? SourceCodecs.m4a
|
||||||
: preferences.streamMusicCodec;
|
: preferences.streamMusicCodec;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
159
lib/services/sourced_track/sources/jiosaavn.dart
Normal file
159
lib/services/sourced_track/sources/jiosaavn.dart
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/models/source_match.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
|
import 'package:jiosaavn/jiosaavn.dart';
|
||||||
|
|
||||||
|
final jiosaavnClient = JioSaavnClient();
|
||||||
|
|
||||||
|
class JioSaavnSourcedTrack extends SourcedTrack {
|
||||||
|
JioSaavnSourcedTrack({
|
||||||
|
required super.ref,
|
||||||
|
required super.source,
|
||||||
|
required super.siblings,
|
||||||
|
required super.sourceInfo,
|
||||||
|
required super.track,
|
||||||
|
});
|
||||||
|
|
||||||
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
|
required Track track,
|
||||||
|
required Ref ref,
|
||||||
|
}) async {
|
||||||
|
final cachedSource = await SourceMatch.box.get(track.id);
|
||||||
|
|
||||||
|
if (cachedSource == null ||
|
||||||
|
cachedSource.sourceType != SourceType.jiosaavn) {
|
||||||
|
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||||
|
|
||||||
|
if (siblings.isEmpty) {
|
||||||
|
throw TrackNotFoundException(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
await SourceMatch.box.put(
|
||||||
|
track.id!,
|
||||||
|
SourceMatch(
|
||||||
|
id: track.id!,
|
||||||
|
sourceType: SourceType.jiosaavn,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
sourceId: siblings.first.info.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return JioSaavnSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
||||||
|
source: siblings.first.source!,
|
||||||
|
sourceInfo: siblings.first.info,
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final [item] =
|
||||||
|
await jiosaavnClient.songs.detailsById([cachedSource.sourceId]);
|
||||||
|
|
||||||
|
final (:info, :source) = toSiblingType(item);
|
||||||
|
|
||||||
|
return JioSaavnSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: [],
|
||||||
|
source: source!,
|
||||||
|
sourceInfo: info,
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SiblingType toSiblingType(SongResponse result) {
|
||||||
|
final SiblingType sibling = (
|
||||||
|
info: SourceInfo(
|
||||||
|
artist: [
|
||||||
|
result.primaryArtists,
|
||||||
|
if (result.featuredArtists.isNotEmpty) ", ",
|
||||||
|
result.featuredArtists
|
||||||
|
].join("").replaceAll("&", "&"),
|
||||||
|
artistUrl:
|
||||||
|
"https://www.jiosaavn.com/artist/${result.primaryArtistsId.split(",").firstOrNull ?? ""}",
|
||||||
|
duration: Duration(seconds: int.parse(result.duration)),
|
||||||
|
id: result.id,
|
||||||
|
pageUrl: result.url,
|
||||||
|
thumbnail: result.image?.last.link ?? "",
|
||||||
|
title: result.name!,
|
||||||
|
album: result.album.name,
|
||||||
|
),
|
||||||
|
source: SourceMap(
|
||||||
|
m4a: SourceQualityMap(
|
||||||
|
high: result.downloadUrl!
|
||||||
|
.firstWhere((element) => element.quality == "320kbps")
|
||||||
|
.link,
|
||||||
|
medium: result.downloadUrl!
|
||||||
|
.firstWhere((element) => element.quality == "160kbps")
|
||||||
|
.link,
|
||||||
|
low: result.downloadUrl!
|
||||||
|
.firstWhere((element) => element.quality == "96kbps")
|
||||||
|
.link,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return sibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
|
required Track track,
|
||||||
|
required Ref ref,
|
||||||
|
}) async {
|
||||||
|
final query = SourcedTrack.getSearchTerm(track);
|
||||||
|
|
||||||
|
final SongSearchResponse(:results) =
|
||||||
|
await jiosaavnClient.search.songs(query, limit: 20);
|
||||||
|
|
||||||
|
return results.map(toSiblingType).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<JioSaavnSourcedTrack> copyWithSibling() async {
|
||||||
|
if (siblings.isNotEmpty) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
final fetchedSiblings = await fetchSiblings(ref: ref, track: this);
|
||||||
|
|
||||||
|
return JioSaavnSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: fetchedSiblings
|
||||||
|
.where((s) => s.info.id != sourceInfo.id)
|
||||||
|
.map((s) => s.info)
|
||||||
|
.toList(),
|
||||||
|
source: source,
|
||||||
|
sourceInfo: sourceInfo,
|
||||||
|
track: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<JioSaavnSourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
||||||
|
if (sibling.id == sourceInfo.id ||
|
||||||
|
siblings.none((s) => s.id == sibling.id)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newSourceInfo = siblings.firstWhere((s) => s.id == sibling.id);
|
||||||
|
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||||
|
..insert(0, sourceInfo);
|
||||||
|
|
||||||
|
final [item] = await jiosaavnClient.songs.detailsById([newSourceInfo.id]);
|
||||||
|
|
||||||
|
final (:info, :source) = toSiblingType(item);
|
||||||
|
|
||||||
|
return JioSaavnSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: newSiblings,
|
||||||
|
source: source!,
|
||||||
|
sourceInfo: info,
|
||||||
|
track: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -55,28 +55,27 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
sourceInfo: siblings.first.info,
|
sourceInfo: siblings.first.info,
|
||||||
track: track,
|
track: track,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
final item = await youtubeClient.videos.get(cachedSource.sourceId);
|
|
||||||
final manifest = await youtubeClient.videos.streamsClient.getManifest(
|
|
||||||
cachedSource.sourceId,
|
|
||||||
);
|
|
||||||
return YoutubeSourcedTrack(
|
|
||||||
ref: ref,
|
|
||||||
siblings: [],
|
|
||||||
source: toSourceMap(manifest),
|
|
||||||
sourceInfo: SourceInfo(
|
|
||||||
id: item.id.value,
|
|
||||||
artist: item.author,
|
|
||||||
artistUrl: "https://www.youtube.com/channel/${item.channelId}",
|
|
||||||
pageUrl: item.url,
|
|
||||||
thumbnail: item.thumbnails.highResUrl,
|
|
||||||
title: item.title,
|
|
||||||
duration: item.duration ?? Duration.zero,
|
|
||||||
album: null,
|
|
||||||
),
|
|
||||||
track: track,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
final item = await youtubeClient.videos.get(cachedSource.sourceId);
|
||||||
|
final manifest = await youtubeClient.videos.streamsClient.getManifest(
|
||||||
|
cachedSource.sourceId,
|
||||||
|
);
|
||||||
|
return YoutubeSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: [],
|
||||||
|
source: toSourceMap(manifest),
|
||||||
|
sourceInfo: SourceInfo(
|
||||||
|
id: item.id.value,
|
||||||
|
artist: item.author,
|
||||||
|
artistUrl: "https://www.youtube.com/channel/${item.channelId}",
|
||||||
|
pageUrl: item.url,
|
||||||
|
thumbnail: item.thumbnails.highResUrl,
|
||||||
|
title: item.title,
|
||||||
|
duration: item.duration ?? Duration.zero,
|
||||||
|
album: null,
|
||||||
|
),
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static SourceMap toSourceMap(StreamManifest manifest) {
|
static SourceMap toSourceMap(StreamManifest manifest) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user