spotube/lib/services/sourced_track/sources/jiosaavn.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: newSourceInfo,
query: query,
source: AudioSource.jiosaavn,
);
}
@override
Future<SourcedTrack> refreshStream() async {
// There's no need to refresh the stream for JioSaavnSourcedTrack
return this;
}
}