mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00

* feat: add youtube engine abstraction and yt-dlp integration * chore: add yt-dlp as optional dependency * feat: implement custom path support for youtube engines * chore: check for custom path in setting engine select dropdown * chore: update yt_dlp_dart * chore: setting video url instead of video id in fetchSiblings * feat: implement NewPipe engine * chore: update local path to git url for flutter_new_pipe_extractor package * chore: fix android build isn't working * chore: fix routes not working when initially signing in * refactor: drop fallback support to different sources
150 lines
4.1 KiB
Dart
150 lines
4.1 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:collection/collection.dart';
|
|
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:yt_dlp_dart/yt_dlp_dart.dart';
|
|
// ignore: depend_on_referenced_packages
|
|
import 'package:http_parser/http_parser.dart';
|
|
|
|
class YtDlpEngine implements YouTubeEngine {
|
|
StreamManifest _parseFormats(List formats, videoId) {
|
|
final audioOnlyStreams = formats
|
|
.where(
|
|
(f) => f["resolution"] == "audio only" && f["manifest_url"] == null,
|
|
)
|
|
.sorted((a, b) => a["quality"] > b["quality"] ? 1 : -1)
|
|
.map((f) {
|
|
final filesize = f["filesize"] ?? f["filesize_approx"];
|
|
return AudioOnlyStreamInfo(
|
|
VideoId(videoId),
|
|
0,
|
|
Uri.parse(f["url"]),
|
|
StreamContainer.parse(
|
|
f["container"]?.replaceAll("_dash", "").replaceAll("m4a", "mp4"),
|
|
),
|
|
filesize != null ? FileSize(filesize) : FileSize.unknown,
|
|
Bitrate(
|
|
(((f["abr"] ?? f["tbr"] ?? 0) * 1000) as num).toInt(),
|
|
),
|
|
f["acodec"] ?? "webm",
|
|
f["format_note"],
|
|
[],
|
|
MediaType.parse(
|
|
"audio/${f["audio_ext"]}",
|
|
),
|
|
null,
|
|
);
|
|
});
|
|
|
|
return StreamManifest(audioOnlyStreams);
|
|
}
|
|
|
|
Video _parseInfo(Map<String, dynamic> info) {
|
|
final publishDate = info["upload_date"] != null
|
|
? DateTime.fromMillisecondsSinceEpoch(
|
|
int.parse(info["upload_date"]) * 1000,
|
|
)
|
|
: DateTime.now();
|
|
return Video(
|
|
VideoId(info["id"]),
|
|
info["title"],
|
|
info["channel"],
|
|
ChannelId(info["channel_id"]),
|
|
publishDate,
|
|
info["upload_date"] as String? ?? DateTime.now().toString(),
|
|
publishDate,
|
|
info["description"] ?? "",
|
|
Duration(seconds: (info["duration"] as num).toInt()),
|
|
ThumbnailSet(info["id"]),
|
|
info["tags"]?.cast<String>() ?? <String>[],
|
|
Engagement(
|
|
info["view_count"],
|
|
info["like_count"],
|
|
null,
|
|
),
|
|
info["is_live"] ?? false,
|
|
);
|
|
}
|
|
|
|
static bool get isAvailableForPlatform => kIsDesktop;
|
|
|
|
static Future<bool> isInstalled() async {
|
|
return isAvailableForPlatform &&
|
|
await YtDlp.instance.checkAvailableInPath();
|
|
}
|
|
|
|
@override
|
|
Future<StreamManifest> getStreamManifest(String videoId) async {
|
|
final formats = await YtDlp.instance.extractInfo(
|
|
"https://www.youtube.com/watch?v=$videoId",
|
|
formatSpecifiers: "%(formats)j",
|
|
extraArgs: [
|
|
"--no-check-certificate",
|
|
"--geo-bypass",
|
|
"--quiet",
|
|
"--ignore-errors"
|
|
],
|
|
) as List;
|
|
|
|
return _parseFormats(formats, videoId);
|
|
}
|
|
|
|
@override
|
|
Future<Video> getVideo(String videoId) async {
|
|
final info = await YtDlp.instance.extractInfo(
|
|
"https://www.youtube.com/watch?v=$videoId",
|
|
formatSpecifiers: "%()j",
|
|
extraArgs: [
|
|
"--skip-download",
|
|
"--no-check-certificate",
|
|
"--geo-bypass",
|
|
"--quiet",
|
|
"--ignore-errors",
|
|
],
|
|
) as Map<String, dynamic>;
|
|
|
|
return _parseInfo(info);
|
|
}
|
|
|
|
@override
|
|
Future<(Video, StreamManifest)> getVideoWithStreamInfo(String videoId) async {
|
|
final info = await YtDlp.instance.extractInfo(
|
|
"https://www.youtube.com/watch?v=$videoId",
|
|
formatSpecifiers: "%()j",
|
|
extraArgs: [
|
|
"--no-check-certificate",
|
|
"--geo-bypass",
|
|
"--quiet",
|
|
"--ignore-errors",
|
|
],
|
|
) as Map<String, dynamic>;
|
|
|
|
return (_parseInfo(info), _parseFormats(info["formats"], videoId));
|
|
}
|
|
|
|
@override
|
|
Future<List<Video>> searchVideos(String query) async {
|
|
final stdout = await YtDlp.instance.extractInfoString(
|
|
"ytsearch10:$query",
|
|
formatSpecifiers: "%()j",
|
|
extraArgs: [
|
|
"--skip-download",
|
|
"--no-check-certificate",
|
|
"--geo-bypass",
|
|
"--quiet",
|
|
"--ignore-errors",
|
|
"--flat-playlist",
|
|
"--no-playlist",
|
|
],
|
|
);
|
|
|
|
final json = jsonDecode(
|
|
"[${stdout.split("\n").where((s) => s.trim().isNotEmpty).join(",")}]",
|
|
) as List;
|
|
|
|
return json.map((e) => _parseInfo(e)).toList();
|
|
}
|
|
}
|