import 'dart:io'; import 'package:http/http.dart'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.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/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:youtube_explode_dart/youtube_explode_dart.dart'; abstract class SourcedTrack extends Track { final SourceMap source; final List siblings; final SourceInfo sourceInfo; final Ref ref; SourcedTrack({ required this.ref, required this.source, required this.siblings, required this.sourceInfo, required Track track, }) { id = track.id; name = track.name; artists = track.artists; album = track.album; durationMs = track.durationMs; discNumber = track.discNumber; explicit = track.explicit; externalIds = track.externalIds; href = track.href; isPlayable = track.isPlayable; linkedFrom = track.linkedFrom; popularity = track.popularity; previewUrl = track.previewUrl; trackNumber = track.trackNumber; type = track.type; uri = track.uri; } static SourcedTrack fromJson( Map json, { required Ref ref, }) { final preferences = ref.read(userPreferencesProvider); final sourceInfo = SourceInfo.fromJson(json); final source = SourceMap.fromJson(json); final track = Track.fromJson(json); final siblings = (json["siblings"] as List) .map((sibling) => SourceInfo.fromJson(sibling)) .toList() .cast(); return switch (preferences.audioSource) { AudioSource.youtube => YoutubeSourcedTrack( ref: ref, source: source, siblings: siblings, sourceInfo: sourceInfo, track: track, ), AudioSource.piped => PipedSourcedTrack( ref: ref, source: source, siblings: siblings, sourceInfo: sourceInfo, track: track, ), AudioSource.jiosaavn => JioSaavnSourcedTrack( ref: ref, source: source, siblings: siblings, sourceInfo: sourceInfo, track: track, ), }; } static String getSearchTerm(Track track) { final artists = (track.artists ?? []) .map((ar) => ar.name) .toList() .whereNotNull() .toList(); final title = ServiceUtils.getTitle( track.name!, artists: artists, onlyCleanArtist: true, ).trim(); return "$title - ${artists.join(", ")}"; } static Future fetchFromTrack({ required Track track, required Ref ref, }) async { final preferences = ref.read(userPreferencesProvider); try { return switch (preferences.audioSource) { AudioSource.piped => await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref), AudioSource.youtube => await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref), AudioSource.jiosaavn => await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref), }; } on TrackNotFoundError catch (_) { return switch (preferences.audioSource) { AudioSource.piped || AudioSource.youtube => await JioSaavnSourcedTrack.fetchFromTrack( track: track, ref: ref, weakMatch: true, ), AudioSource.jiosaavn => await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref), }; } on HttpClientClosedException catch (_) { return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref); } on VideoUnplayableException catch (_) { return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref); } catch (e) { if (e is DioException || e is ClientException || e is SocketException) { return await JioSaavnSourcedTrack.fetchFromTrack( track: track, ref: ref, weakMatch: true, ); } rethrow; } } static Future> fetchSiblings({ required Track track, required Ref ref, }) { final preferences = ref.read(userPreferencesProvider); return switch (preferences.audioSource) { AudioSource.piped => PipedSourcedTrack.fetchSiblings(track: track, ref: ref), AudioSource.youtube => YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref), AudioSource.jiosaavn => JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref), }; } Future copyWithSibling(); Future swapWithSibling(SourceInfo sibling); Future swapWithSiblingOfIndex(int index) { return swapWithSibling(siblings[index]); } String get url { final preferences = ref.read(userPreferencesProvider); final codec = preferences.audioSource == AudioSource.jiosaavn ? SourceCodecs.m4a : preferences.streamMusicCodec; return getUrlOfCodec(codec); } String getUrlOfCodec(SourceCodecs codec) { final preferences = ref.read(userPreferencesProvider); return source[codec]?[preferences.audioQuality] ?? // this will ensure playback doesn't break source[codec == SourceCodecs.m4a ? SourceCodecs.weba : SourceCodecs.m4a] [preferences.audioQuality]; } SourceCodecs get codec { final preferences = ref.read(userPreferencesProvider); return preferences.audioSource == AudioSource.jiosaavn ? SourceCodecs.m4a : preferences.streamMusicCodec; } }