From 09a141b4729d6898fced6ff106bfd7bcb5fe6996 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 9 Feb 2025 23:02:43 +0600 Subject: [PATCH] feat: implement NewPipe engine --- android/app/build.gradle | 3 + lib/main.dart | 2 + lib/models/database/database.dart | 1 + lib/models/database/tables/preferences.dart | 3 +- .../youtube_engine/youtube_engine.dart | 6 +- .../youtube_engine/newpipe_engine.dart | 109 ++++++++++++++++++ pubspec.lock | 13 ++- pubspec.yaml | 3 + 8 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 lib/services/youtube_engine/newpipe_engine.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 74f6efea..5051f5a3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -38,6 +38,7 @@ android { ndkVersion = "27.0.12077973" compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -120,6 +121,8 @@ flutter { def glanceVersion = "1.1.1" dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' // other deps so just ignore implementation 'com.android.support:multidex:2.0.1' diff --git a/lib/main.dart b/lib/main.dart index 9af53cf2..6f3cbfbf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -51,6 +51,7 @@ import 'package:timezone/data/latest.dart' as tz; import 'package:window_manager/window_manager.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:yt_dlp_dart/yt_dlp_dart.dart'; +import 'package:flutter_new_pipe_extractor/flutter_new_pipe_extractor.dart'; Future main(List rawArgs) async { if (rawArgs.contains("web_view_title_bar")) { @@ -78,6 +79,7 @@ Future main(List rawArgs) async { // force High Refresh Rate on some Android devices (like One Plus) if (kIsAndroid) { await FlutterDisplayMode.setHighRefreshRate(); + await NewPipeExtractor.init(); } if (!kIsWeb) { diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index d6b2786c..199e7147 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -18,6 +18,7 @@ import 'package:spotube/services/sourced_track/enums.dart'; import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; +import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:sqlite3/sqlite3.dart'; diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index 111b2249..492ac1f9 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -34,8 +34,7 @@ enum YoutubeClientEngine { YoutubeClientEngine.youtubeExplode => YouTubeExplodeEngine.isAvailableForPlatform, YoutubeClientEngine.ytDlp => YtDlpEngine.isAvailableForPlatform, - // TODO: Implement new pipe support - YoutubeClientEngine.newPipe => false, + YoutubeClientEngine.newPipe => NewPipeEngine.isAvailableForPlatform, }; } } diff --git a/lib/provider/youtube_engine/youtube_engine.dart b/lib/provider/youtube_engine/youtube_engine.dart index fbac7afe..0aa37db5 100644 --- a/lib/provider/youtube_engine/youtube_engine.dart +++ b/lib/provider/youtube_engine/youtube_engine.dart @@ -1,6 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; @@ -9,8 +10,9 @@ final youtubeEngineProvider = Provider((ref) { userPreferencesProvider.select((value) => value.youtubeClientEngine), ); - if (engineMode == YoutubeClientEngine.newPipe) { - throw UnimplementedError(); + if (engineMode == YoutubeClientEngine.newPipe && + NewPipeEngine.isAvailableForPlatform) { + return NewPipeEngine(); } else if (engineMode == YoutubeClientEngine.ytDlp && YtDlpEngine.isAvailableForPlatform) { return YtDlpEngine(); diff --git a/lib/services/youtube_engine/newpipe_engine.dart b/lib/services/youtube_engine/newpipe_engine.dart new file mode 100644 index 00000000..6e6204f0 --- /dev/null +++ b/lib/services/youtube_engine/newpipe_engine.dart @@ -0,0 +1,109 @@ +import 'package:flutter_new_pipe_extractor/flutter_new_pipe_extractor.dart' + hide Engagement; +import 'package:spotube/services/youtube_engine/youtube_engine.dart'; +import 'package:spotube/utils/platform.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; +import 'package:http_parser/http_parser.dart'; + +class NewPipeEngine implements YouTubeEngine { + static bool get isAvailableForPlatform => kIsAndroid; + + AudioOnlyStreamInfo _parseAudioStream(AudioStream stream, String videoId) { + return AudioOnlyStreamInfo( + VideoId(videoId), + stream.itag, + Uri.parse(stream.content), + StreamContainer.parse(stream.mediaFormat!.mimeType.split("/").last), + FileSize.unknown, + Bitrate(stream.bitrate), + stream.codec, + stream.quality, + [], + MediaType.parse(stream.mediaFormat!.mimeType), + null, + ); + } + + Video _parseVideo(VideoInfo info) { + return Video( + VideoId(info.id), + info.name, + info.uploaderName, + ChannelId(info.uploaderUrl), + info.uploadDate.offsetDateTime, + info.uploadDate.offsetDateTime.toString(), + info.uploadDate.offsetDateTime, + info.description.content ?? "", + Duration(seconds: info.duration), + ThumbnailSet(info.id), + info.tags, + Engagement( + info.viewCount, + info.likeCount, + info.dislikeCount, + ), + !info.streamType.name.toLowerCase().contains("live"), + ); + } + + Video _parseVideoResult(VideoSearchResultItem info) { + final id = Uri.parse(info.url).queryParameters["v"]!; + return Video( + VideoId(id), + info.name, + info.uploaderName, + ChannelId(info.uploaderUrl), + info.uploadDate?.offsetDateTime, + info.uploadDate?.offsetDateTime.toString(), + info.uploadDate?.offsetDateTime, + info.shortDescription ?? "", + Duration(seconds: info.duration), + ThumbnailSet(id), + [], + Engagement(info.viewCount, null, null), + !info.streamType.name.toLowerCase().contains("live"), + ); + } + + @override + Future getStreamManifest(String videoId) async { + final video = await NewPipeExtractor.getVideoInfo(videoId); + + final streams = + video.audioStreams.map((stream) => _parseAudioStream(stream, videoId)); + + return StreamManifest(streams); + } + + @override + Future