Merge branch 'master' into build

This commit is contained in:
Kingkor Roy Tirtho 2022-06-18 14:05:39 +06:00
commit 7f71440039
7 changed files with 354 additions and 53 deletions

View File

@ -0,0 +1,73 @@
import 'package:hive/hive.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
part 'CacheTrack.g.dart';
@HiveType(typeId: 2)
class CacheTrackEngagement {
@HiveField(0)
late int viewCount;
@HiveField(1)
late int? likeCount;
@HiveField(2)
late int? dislikeCount;
CacheTrackEngagement();
CacheTrackEngagement.fromEngagement(Engagement engagement)
: viewCount = engagement.viewCount,
likeCount = engagement.likeCount,
dislikeCount = engagement.dislikeCount;
}
@HiveType(typeId: 1)
class CacheTrack extends HiveObject {
@HiveField(0)
late String id;
@HiveField(1)
late String title;
@HiveField(2)
late String channelId;
@HiveField(3)
late String? uploadDate;
@HiveField(4)
late String? publishDate;
@HiveField(5)
late String description;
@HiveField(6)
late String? duration;
@HiveField(7)
late List<String>? keywords;
@HiveField(8)
late CacheTrackEngagement engagement;
@HiveField(9)
late String mode;
@HiveField(10)
late String author;
CacheTrack();
CacheTrack.fromVideo(Video video, this.mode)
: id = video.id.value,
title = video.title,
author = video.author,
channelId = video.channelId.value,
uploadDate = video.uploadDate.toString(),
publishDate = video.publishDate.toString(),
description = video.description,
duration = video.duration.toString(),
keywords = video.keywords,
engagement = CacheTrackEngagement.fromEngagement(video.engagement);
}

View File

@ -0,0 +1,109 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'CacheTrack.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class CacheTrackEngagementAdapter extends TypeAdapter<CacheTrackEngagement> {
@override
final int typeId = 2;
@override
CacheTrackEngagement read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return CacheTrackEngagement()
..viewCount = fields[0] as int
..likeCount = fields[1] as int?
..dislikeCount = fields[2] as int?;
}
@override
void write(BinaryWriter writer, CacheTrackEngagement obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.viewCount)
..writeByte(1)
..write(obj.likeCount)
..writeByte(2)
..write(obj.dislikeCount);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CacheTrackEngagementAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class CacheTrackAdapter extends TypeAdapter<CacheTrack> {
@override
final int typeId = 1;
@override
CacheTrack read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return CacheTrack()
..id = fields[0] as String
..title = fields[1] as String
..channelId = fields[2] as String
..uploadDate = fields[3] as String?
..publishDate = fields[4] as String?
..description = fields[5] as String
..duration = fields[6] as String?
..keywords = (fields[7] as List?)?.cast<String>()
..engagement = fields[8] as CacheTrackEngagement
..mode = fields[9] as String
..author = fields[10] as String;
}
@override
void write(BinaryWriter writer, CacheTrack obj) {
writer
..writeByte(11)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.title)
..writeByte(2)
..write(obj.channelId)
..writeByte(3)
..write(obj.uploadDate)
..writeByte(4)
..write(obj.publishDate)
..writeByte(5)
..write(obj.description)
..writeByte(6)
..write(obj.duration)
..writeByte(7)
..write(obj.keywords)
..writeByte(8)
..write(obj.engagement)
..writeByte(9)
..write(obj.mode)
..writeByte(10)
..write(obj.author);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CacheTrackAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -0,0 +1,32 @@
import 'package:spotube/entities/CacheTrack.dart';
import 'package:spotube/utils/duration.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
extension VideoFromCacheTrackExtension on Video {
static Video fromCacheTrack(CacheTrack cacheTrack) {
return Video(
VideoId.fromString(cacheTrack.id),
cacheTrack.title,
cacheTrack.author,
ChannelId.fromString(cacheTrack.channelId),
cacheTrack.uploadDate != null
? DateTime.tryParse(cacheTrack.uploadDate!)
: null,
cacheTrack.publishDate != null
? DateTime.tryParse(cacheTrack.publishDate!)
: null,
cacheTrack.description,
cacheTrack.duration != null
? tryParseDuration(cacheTrack.duration!)
: null,
ThumbnailSet(cacheTrack.id),
cacheTrack.keywords,
Engagement(
cacheTrack.engagement.viewCount,
cacheTrack.engagement.likeCount,
cacheTrack.engagement.dislikeCount,
),
false,
);
}
}

View File

@ -1,5 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:hive/hive.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/entities/CacheTrack.dart';
import 'package:spotube/helpers/contains-text-in-bracket.dart'; import 'package:spotube/helpers/contains-text-in-bracket.dart';
import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/helpers/getLyrics.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
@ -7,6 +9,7 @@ import 'package:spotube/models/SpotubeTrack.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:spotube/extensions/list-sort-multiple.dart'; import 'package:spotube/extensions/list-sort-multiple.dart';
import 'package:spotube/extensions/yt-video-from-cache-track.dart';
enum AudioQuality { enum AudioQuality {
high, high,
@ -20,6 +23,7 @@ Future<SpotubeTrack> toSpotubeTrack({
required String format, required String format,
required SpotubeTrackMatchAlgorithm matchAlgorithm, required SpotubeTrackMatchAlgorithm matchAlgorithm,
required AudioQuality audioQuality, required AudioQuality audioQuality,
LazyBox<CacheTrack>? box,
}) async { }) async {
final artistsName = final artistsName =
track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ?? track.artists?.map((ar) => ar.name).toList().whereNotNull().toList() ??
@ -40,9 +44,15 @@ Future<SpotubeTrack> toSpotubeTrack({
.replaceAll("\$FEATURED_ARTISTS", featuredArtists); .replaceAll("\$FEATURED_ARTISTS", featuredArtists);
logger.v("[Youtube Search Term] $queryString"); logger.v("[Youtube Search Term] $queryString");
VideoSearchList videos = await youtube.search.search(queryString);
Video ytVideo; Video ytVideo;
final cachedTrack = await box?.get(track.id);
if (cachedTrack != null && cachedTrack.mode == matchAlgorithm.name) {
logger.v(
"[Playing track from cache] youtubeId: ${cachedTrack.id} mode: ${cachedTrack.mode}",
);
ytVideo = VideoFromCacheTrackExtension.fromCacheTrack(cachedTrack);
} else {
VideoSearchList videos = await youtube.search.search(queryString);
if (matchAlgorithm != SpotubeTrackMatchAlgorithm.youtube) { if (matchAlgorithm != SpotubeTrackMatchAlgorithm.youtube) {
List<Map> ratedRankedVideos = videos List<Map> ratedRankedVideos = videos
.map((video) { .map((video) {
@ -57,7 +67,8 @@ Future<SpotubeTrack> toSpotubeTrack({
track.artists?.first.name?.toLowerCase() == track.artists?.first.name?.toLowerCase() ==
video.author.toLowerCase(); video.author.toLowerCase();
final bool hasNoLiveInTitle = !containsTextInBracket(ytTitle, "live"); final bool hasNoLiveInTitle =
!containsTextInBracket(ytTitle, "live");
int rate = 0; int rate = 0;
for (final el in [ for (final el in [
@ -88,6 +99,7 @@ Future<SpotubeTrack> toSpotubeTrack({
} else { } else {
ytVideo = videos.where((video) => !video.isLive).first; ytVideo = videos.where((video) => !video.isLive).first;
} }
}
final trackManifest = await youtube.videos.streams.getManifest(ytVideo.id); final trackManifest = await youtube.videos.streams.getManifest(ytVideo.id);
@ -100,16 +112,25 @@ Future<SpotubeTrack> toSpotubeTrack({
.where((info) => info.codec.mimeType == "audio/mp4") .where((info) => info.codec.mimeType == "audio/mp4")
: trackManifest.audioOnly; : trackManifest.audioOnly;
final ytUri = (audioQuality == AudioQuality.high
? audioManifest.withHighestBitrate()
: audioManifest.sortByBitrate().last)
.url
.toString();
// only save when the track isn't available in the cache with same
// matchAlgorithm
if (cachedTrack == null || cachedTrack.mode != matchAlgorithm.name) {
await box?.put(
track.id!, CacheTrack.fromVideo(ytVideo, matchAlgorithm.name));
}
return SpotubeTrack.fromTrack( return SpotubeTrack.fromTrack(
track: track, track: track,
ytTrack: ytVideo, ytTrack: ytVideo,
// Since Mac OS's & IOS's CodeAudio doesn't support WebMedia // Since Mac OS's & IOS's CodeAudio doesn't support WebMedia
// ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg' // ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg'
// codec/mimetype for those Platforms // codec/mimetype for those Platforms
ytUri: (audioQuality == AudioQuality.high ytUri: ytUri,
? audioManifest.withHighestBitrate()
: audioManifest.sortByBitrate().last)
.url
.toString(),
); );
} }

View File

@ -5,8 +5,10 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:spotube/entities/CacheTrack.dart';
import 'package:spotube/models/GoRouteDeclarations.dart'; import 'package:spotube/models/GoRouteDeclarations.dart';
import 'package:spotube/models/Logger.dart'; import 'package:spotube/models/Logger.dart';
import 'package:spotube/provider/AudioPlayer.dart'; import 'package:spotube/provider/AudioPlayer.dart';
@ -19,6 +21,9 @@ import 'package:spotube/utils/AudioPlayerHandler.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
void main() async { void main() async {
await Hive.initFlutter();
Hive.registerAdapter(CacheTrackAdapter());
Hive.registerAdapter(CacheTrackEngagementAdapter());
AudioPlayerHandler audioPlayerHandler = await AudioService.init( AudioPlayerHandler audioPlayerHandler = await AudioService.init(
builder: () => AudioPlayerHandler(), builder: () => AudioPlayerHandler(),
config: const AudioServiceConfig( config: const AudioServiceConfig(

View File

@ -3,9 +3,10 @@ import 'dart:convert';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/entities/CacheTrack.dart';
import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/search-youtube.dart'; import 'package:spotube/helpers/search-youtube.dart';
@ -33,6 +34,9 @@ class Playback extends PersistedChangeNotifier {
AudioPlayerHandler player; AudioPlayerHandler player;
YoutubeExplode youtube; YoutubeExplode youtube;
Ref ref; Ref ref;
LazyBox<CacheTrack>? cacheTrackBox;
Playback({ Playback({
required this.player, required this.player,
required this.youtube, required this.youtube,
@ -57,6 +61,8 @@ class Playback extends PersistedChangeNotifier {
StreamSubscription<bool>? _playingStream; StreamSubscription<bool>? _playingStream;
void _init() async { void _init() async {
cacheTrackBox = await Hive.openLazyBox<CacheTrack>("track-cache");
_playingStream = player.core.playingStream.listen( _playingStream = player.core.playingStream.listen(
(playing) { (playing) {
_isPlaying = playing; _isPlaying = playing;
@ -111,6 +117,7 @@ class Playback extends PersistedChangeNotifier {
_positionStream?.cancel(); _positionStream?.cancel();
_playingStream?.cancel(); _playingStream?.cancel();
_durationStream?.cancel(); _durationStream?.cancel();
cacheTrackBox?.close();
super.dispose(); super.dispose();
} }
@ -213,7 +220,6 @@ class Playback extends PersistedChangeNotifier {
notifyListeners(); notifyListeners();
updatePersistence(); updatePersistence();
}); });
// await player.play();
return; return;
} }
final preferences = ref.read(userPreferencesProvider); final preferences = ref.read(userPreferencesProvider);
@ -223,6 +229,7 @@ class Playback extends PersistedChangeNotifier {
format: preferences.ytSearchFormat, format: preferences.ytSearchFormat,
matchAlgorithm: preferences.trackMatchAlgorithm, matchAlgorithm: preferences.trackMatchAlgorithm,
audioQuality: preferences.audioQuality, audioQuality: preferences.audioQuality,
box: cacheTrackBox,
); );
if (setTrackUriById(track.id!, spotubeTrack.ytUri)) { if (setTrackUriById(track.id!, spotubeTrack.ytUri)) {
logger.v("[Track Direct Source] - ${spotubeTrack.ytUri}"); logger.v("[Track Direct Source] - ${spotubeTrack.ytUri}");
@ -237,7 +244,6 @@ class Playback extends PersistedChangeNotifier {
notifyListeners(); notifyListeners();
updatePersistence(); updatePersistence();
}); });
// await player.play();
} }
} }
} catch (e, stack) { } catch (e, stack) {

55
lib/utils/duration.dart Normal file
View File

@ -0,0 +1,55 @@
/// Parses duration string formatted by Duration.toString() to [Duration].
/// The string should be of form hours:minutes:seconds.microseconds
///
/// Example:
/// parseTime('245:09:08.007006');
Duration parseDuration(String input) {
final parts = input.split(':');
if (parts.length != 3) throw FormatException('Invalid time format');
int days;
int hours;
int minutes;
int seconds;
int milliseconds;
int microseconds;
{
final p = parts[2].split('.');
if (p.length != 2) throw FormatException('Invalid time format');
final p2 = int.parse(p[1]);
microseconds = p2 % 1000;
milliseconds = p2 ~/ 1000;
seconds = int.parse(p[0]);
}
minutes = int.parse(parts[1]);
{
int p = int.parse(parts[0]);
hours = p % 24;
days = p ~/ 24;
}
// TODO verify that there are no negative parts
return Duration(
days: days,
hours: hours,
minutes: minutes,
seconds: seconds,
milliseconds: milliseconds,
microseconds: microseconds);
}
Duration? tryParseDuration(String input) {
try {
return parseDuration(input);
} catch (_) {
return null;
}
}