mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
231 lines
6.8 KiB
Dart
231 lines
6.8 KiB
Dart
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,
|
|
);
|
|
}).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: info,
|
|
query: query,
|
|
source: AudioSource.jiosaavn,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Future<SourcedTrack> refreshStream() async {
|
|
// There's no need to refresh the stream for JioSaavnSourcedTrack
|
|
return this;
|
|
}
|
|
}
|