Fix some streams
This commit is contained in:
parent
407ac50f22
commit
ac5d7b4d5c
|
@ -14,7 +14,7 @@ linter:
|
|||
- prefer_constructors_over_static_methods
|
||||
- prefer_contains
|
||||
- annotate_overrides
|
||||
- await_futures
|
||||
- await_future
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
|
|
|
@ -2,11 +2,10 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
|||
|
||||
Future<void> main() async {
|
||||
var yt = YoutubeExplode();
|
||||
var video = await yt.videos.get(VideoId('https://www.youtube.com/watch?v=bo_efYhYU2A'));
|
||||
var streamManifest = await yt.videos.streamsClient.getManifest(VideoId('https://www.youtube.com/watch?v=bo_efYhYU2A'));
|
||||
var video =
|
||||
await yt.videos.get('https://www.youtube.com/watch?v=bo_efYhYU2A');
|
||||
|
||||
print('Title: ${video.title}');
|
||||
print(streamManifest.streams());
|
||||
|
||||
// Close the YoutubeExplode's http client.
|
||||
yt.close();
|
||||
|
|
|
@ -14,19 +14,12 @@ Future<void> main() async {
|
|||
|
||||
var url = stdin.readLineSync().trim();
|
||||
|
||||
// Get the video url.
|
||||
var id = YoutubeExplode.parseVideoId(url);
|
||||
if (id == null) {
|
||||
console.writeLine('Invalid video id or url.');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Save the video to the download directory.
|
||||
Directory('downloads').createSync();
|
||||
console.hideCursor();
|
||||
|
||||
// Download the video.
|
||||
await download(id);
|
||||
await download(url);
|
||||
|
||||
yt.close();
|
||||
console.showCursor();
|
||||
|
@ -34,24 +27,28 @@ Future<void> main() async {
|
|||
}
|
||||
|
||||
Future<void> download(String id) async {
|
||||
// Get the video media stream.
|
||||
var mediaStream = await yt.getVideoMediaStream(id);
|
||||
// Get video metadata.
|
||||
var video = await yt.videos.get(id);
|
||||
|
||||
// Get the video manifest.
|
||||
var manifest = await yt.videos.streamsClient.getManifest(id);
|
||||
var streams = manifest.getAudioOnly();
|
||||
|
||||
// Get the last audio track (the one with the highest bitrate).
|
||||
var audio = mediaStream.audio.last;
|
||||
var audio = streams.last;
|
||||
var audioStream = yt.videos.streamsClient.get(audio);
|
||||
|
||||
// Compose the file name removing the unallowed characters in windows.
|
||||
var fileName =
|
||||
'${mediaStream.videoDetails.title}.${audio.container.toString()}'
|
||||
.replaceAll('Container.', '')
|
||||
.replaceAll(r'\', '')
|
||||
.replaceAll('/', '')
|
||||
.replaceAll('*', '')
|
||||
.replaceAll('?', '')
|
||||
.replaceAll('"', '')
|
||||
.replaceAll('<', '')
|
||||
.replaceAll('>', '')
|
||||
.replaceAll('|', '');
|
||||
var fileName = '${video.title}.${audio.container.name.toString()}'
|
||||
.replaceAll('Container.', '')
|
||||
.replaceAll(r'\', '')
|
||||
.replaceAll('/', '')
|
||||
.replaceAll('*', '')
|
||||
.replaceAll('?', '')
|
||||
.replaceAll('"', '')
|
||||
.replaceAll('<', '')
|
||||
.replaceAll('>', '')
|
||||
.replaceAll('|', '');
|
||||
var file = File('downloads/$fileName');
|
||||
|
||||
// Create the StreamedRequest to track the download status.
|
||||
|
@ -60,24 +57,26 @@ Future<void> download(String id) async {
|
|||
var output = file.openWrite(mode: FileMode.writeOnlyAppend);
|
||||
|
||||
// Track the file download status.
|
||||
var len = audio.size;
|
||||
var len = audio.size.totalBytes;
|
||||
var count = 0;
|
||||
var oldProgress = -1;
|
||||
|
||||
// Create the message and set the cursor position.
|
||||
var msg = 'Downloading `${mediaStream.videoDetails.title}`: \n';
|
||||
var row = console.cursorPosition.row;
|
||||
var col = msg.length - 2;
|
||||
console.cursorPosition = Coordinate(row, 0);
|
||||
console.write(msg);
|
||||
var msg = 'Downloading `${video.title}`(.${audio.container.name}): \n';
|
||||
print(msg);
|
||||
// var row = console.cursorPosition.row;
|
||||
// var col = msg.length - 2;
|
||||
// console.cursorPosition = Coordinate(row, 0);
|
||||
// console.write(msg);
|
||||
|
||||
|
||||
// Listen for data received.
|
||||
await for (var data in audio.downloadStream()) {
|
||||
await for (var data in audioStream) {
|
||||
count += data.length;
|
||||
var progress = ((count / len) * 100).round();
|
||||
if (progress != oldProgress) {
|
||||
console.cursorPosition = Coordinate(row, col);
|
||||
console.write('$progress%');
|
||||
// console.cursorPosition = Coordinate(row, col);
|
||||
print('$progress%');
|
||||
oldProgress = progress;
|
||||
}
|
||||
output.add(data);
|
||||
|
|
|
@ -16,14 +16,21 @@ class ChannelClient {
|
|||
ChannelClient(this._httpClient);
|
||||
|
||||
/// Gets the metadata associated with the specified channel.
|
||||
Future<Channel> get(ChannelId id) async {
|
||||
/// [id] must be either a [ChannelId] or a string
|
||||
/// which is parsed to a [ChannelId]
|
||||
Future<Channel> get(dynamic id) async {
|
||||
|
||||
var channelPage = await ChannelPage.get(_httpClient, id.value);
|
||||
|
||||
return Channel(id, channelPage.channelTitle, channelPage.channelLogoUrl);
|
||||
}
|
||||
|
||||
/// Gets the metadata associated with the channel of the specified user.
|
||||
Future<Channel> getByUsername(Username username) async {
|
||||
/// [username] must be either a [Username] or a string
|
||||
/// which is parsed to a [Username]
|
||||
Future<Channel> getByUsername(dynamic username) async {
|
||||
username = Username.fromString(username);
|
||||
|
||||
var channelPage =
|
||||
await ChannelPage.getByUsername(_httpClient, username.value);
|
||||
return Channel(ChannelId(channelPage.channelId), channelPage.channelTitle,
|
||||
|
@ -32,7 +39,8 @@ class ChannelClient {
|
|||
|
||||
/// Gets the metadata associated with the channel
|
||||
/// that uploaded the specified video.
|
||||
Future<Channel> getByVideo(VideoId videoId) async {
|
||||
Future<Channel> getByVideo(dynamic videoId) async {
|
||||
videoId = VideoId.fromString(videoId);
|
||||
var videoInfoResponse =
|
||||
await VideoInfoResponse.get(_httpClient, videoId.value);
|
||||
var playerReponse = videoInfoResponse.playerResponse;
|
||||
|
|
|
@ -14,6 +14,7 @@ class ChannelId extends Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns true if the given id is a valid channel id.
|
||||
static bool validateChannelId(String id) {
|
||||
if (id.isNullOrWhiteSpace) {
|
||||
return false;
|
||||
|
@ -50,6 +51,15 @@ class ChannelId extends Equatable {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Converts [obj] to a [ChannelId] by calling .toString on that object.
|
||||
/// If it is already a [ChannelId], [obj] is returned
|
||||
factory ChannelId.fromString(dynamic obj) {
|
||||
if (obj is ChannelId) {
|
||||
return obj;
|
||||
}
|
||||
return ChannelId(obj.toString());
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '$value';
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ class Username {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns true if the given username is a valid username.
|
||||
static bool validateUsername(String name) {
|
||||
if (name.isNullOrWhiteSpace) {
|
||||
return false;
|
||||
|
@ -25,6 +26,7 @@ class Username {
|
|||
return !RegExp('[^0-9a-zA-Z]').hasMatch(name);
|
||||
}
|
||||
|
||||
/// Parses a username from a url.
|
||||
static String parseUsername(String nameOrUrl) {
|
||||
if (nameOrUrl.isNullOrWhiteSpace) {
|
||||
return null;
|
||||
|
@ -42,4 +44,13 @@ class Username {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Converts [obj] to a [Username] by calling .toString on that object.
|
||||
/// If it is already a [Username], [obj] is returned
|
||||
factory Username.fromString(dynamic obj) {
|
||||
if (obj is Username) {
|
||||
return obj;
|
||||
}
|
||||
return Username(obj.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
/// Parent class for domain exceptions thrown by [YoutubeExplode]
|
||||
abstract class YoutubeExplodeException implements Exception {
|
||||
/// Generic message.
|
||||
final String message;
|
||||
|
||||
///
|
||||
YoutubeExplodeException(this.message);
|
||||
|
||||
@override
|
||||
String toString();
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ extension UriUtility on Uri {
|
|||
|
||||
///
|
||||
extension GetOrNull<K, V> on Map<K, V> {
|
||||
/// Get a value from a map
|
||||
V getValue(K key) {
|
||||
var v = this[key];
|
||||
if (v == null) {
|
||||
|
@ -79,6 +80,7 @@ extension GetOrNull<K, V> on Map<K, V> {
|
|||
|
||||
///
|
||||
extension GetOrNullMap on Map {
|
||||
/// Get a map inside a map
|
||||
Map<String, dynamic> get(String key) {
|
||||
var v = this[key];
|
||||
if (v == null) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import '../common/common.dart';
|
||||
import 'playlist_id.dart';
|
||||
|
||||
/// YouTube playlist metadata.
|
||||
class Playlist {
|
||||
/// Playlist ID.
|
||||
final PlaylistId id;
|
||||
|
|
|
@ -14,7 +14,9 @@ class PlaylistClient {
|
|||
PlaylistClient(this._httpClient);
|
||||
|
||||
/// Gets the metadata associated with the specified playlist.
|
||||
Future<Playlist> get(PlaylistId id) async {
|
||||
Future<Playlist> get(dynamic id) async {
|
||||
id = PlaylistId.fromString(id);
|
||||
|
||||
var response = await PlaylistResponse.get(_httpClient, id.value);
|
||||
return Playlist(
|
||||
id,
|
||||
|
@ -26,7 +28,8 @@ class PlaylistClient {
|
|||
}
|
||||
|
||||
/// Enumerates videos included in the specified playlist.
|
||||
Stream<Video> getVideos(PlaylistId id) async* {
|
||||
Stream<Video> getVideos(dynamic id) async* {
|
||||
id = PlaylistId.fromString(id);
|
||||
var encounteredVideoIds = <String>{};
|
||||
var index = 0;
|
||||
while (true) {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import '../extensions/helpers_extension.dart';
|
||||
|
||||
|
||||
/// Encapsulates a valid YouTube playlist ID.
|
||||
class PlaylistId {
|
||||
static final _regMatchExp =
|
||||
RegExp(r'youtube\..+?/playlist.*?list=(.*?)(?:&|/|$)');
|
||||
|
@ -10,6 +12,7 @@ class PlaylistId {
|
|||
static final _embedCompositeMatchExp =
|
||||
RegExp(r'youtube\..+?/embed/.*?/.*?list=(.*?)(?:&|/|$)');
|
||||
|
||||
/// The playlist id as string.
|
||||
final String value;
|
||||
|
||||
/// Initializes an instance of [PlaylistId]
|
||||
|
@ -93,4 +96,13 @@ class PlaylistId {
|
|||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
/// Converts [obj] to a [PlaylistId] by calling .toString on that object.
|
||||
/// If it is already a [PlaylistId], [obj] is returned
|
||||
factory PlaylistId.fromString(dynamic obj) {
|
||||
if (obj is PlaylistId) {
|
||||
return obj;
|
||||
}
|
||||
return PlaylistId(obj.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
library _youtube_explode.retry;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'exceptions/exceptions.dart';
|
||||
|
|
|
@ -174,5 +174,12 @@ class _StreamInfo extends StreamInfoProvider {
|
|||
|
||||
@override
|
||||
String get audioCodec =>
|
||||
isAudioOnly ? codecs : codecs.split(',').last.trim().nullIfWhitespace;
|
||||
isAudioOnly ? codecs : _getAudioCodec(codecs.split(','))?.trim();
|
||||
|
||||
String _getAudioCodec(List<String> codecs) {
|
||||
if (codecs.length == 1) {
|
||||
return null;
|
||||
}
|
||||
return codecs.last;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ class PlayerSource {
|
|||
final RegExp _funcNameExp = RegExp(r'(\w+).\w+\(\w+,\d+\);');
|
||||
|
||||
final RegExp _calledFuncNameExp =
|
||||
RegExp(r'\w+(?:.|\[)(\""?\w+(?:\"")?)\]?\("');
|
||||
RegExp(r'\w+(?:.|\[)(\"?\w+(?:\")?)\]?\(');
|
||||
|
||||
final String _root;
|
||||
|
||||
|
@ -51,9 +51,9 @@ class PlayerSource {
|
|||
var escapedFuncName = RegExp.escape(calledFuncName);
|
||||
// Slice
|
||||
var exp = RegExp('$escapedFuncName'
|
||||
r':\bfunction\b\(\w+\,\w\).\bvar\b.\bc=a\b');
|
||||
r':\bfunction\b\([a],b\).(\breturn\b)?.?\w+\.');
|
||||
|
||||
if (exp.hasMatch(calledFuncName)) {
|
||||
if (exp.hasMatch(definitionBody)) {
|
||||
var index = int.parse(_statIndexExp.firstMatch(statement).group(1));
|
||||
yield SliceCipherOperation(index);
|
||||
}
|
||||
|
@ -61,14 +61,14 @@ class PlayerSource {
|
|||
// Swap
|
||||
exp = RegExp(
|
||||
'$escapedFuncName' r':\bfunction\b\(\w+\,\w\).\bvar\b.\bc=a\b');
|
||||
if (exp.hasMatch(calledFuncName)) {
|
||||
if (exp.hasMatch(definitionBody)) {
|
||||
var index = int.parse(_statIndexExp.firstMatch(statement).group(1));
|
||||
yield SwapCipherOperation(index);
|
||||
}
|
||||
|
||||
// Reverse
|
||||
exp = RegExp('$escapedFuncName' r':\bfunction\b\(\w+\)');
|
||||
if (exp.hasMatch(calledFuncName)) {
|
||||
if (exp.hasMatch(definitionBody)) {
|
||||
yield const ReverseCipherOperation();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:http/http.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:youtube_explode_dart/src/videos/streams/streams.dart';
|
||||
|
||||
import '../exceptions/exceptions.dart';
|
||||
import '../videos/streams/streams.dart';
|
||||
|
||||
class YoutubeHttpClient {
|
||||
final Client _httpClient = Client();
|
||||
|
|
|
@ -20,7 +20,8 @@ class ClosedCaptionClient {
|
|||
|
||||
/// Gets the manifest that contains information
|
||||
/// about available closed caption tracks in the specified video.
|
||||
Future<ClosedCaptionManifest> getManifest(VideoId videoId) async {
|
||||
Future<ClosedCaptionManifest> getManifest(dynamic videoId) async {
|
||||
videoId = VideoId.fromString(videoId);
|
||||
var videoInfoResponse =
|
||||
await VideoInfoResponse.get(_httpClient, videoId.value);
|
||||
var playerResponse = videoInfoResponse.playerResponse;
|
||||
|
|
|
@ -20,7 +20,7 @@ abstract class StreamInfo {
|
|||
/// Stream bitrate.
|
||||
final Bitrate bitrate;
|
||||
|
||||
///
|
||||
/// Initialize an instance of [StreamInfo].
|
||||
StreamInfo(this.tag, this.url, this.container, this.size, this.bitrate);
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ abstract class StreamInfo {
|
|||
extension StreamInfoExt on StreamInfo {
|
||||
static final _exp = RegExp('ratebypass[=/]yes');
|
||||
|
||||
/// Returns true if this video is rate limited.
|
||||
bool isRateLimited() => _exp.hasMatch(url.toString());
|
||||
|
||||
/// Gets the stream with highest bitrate.
|
||||
|
|
|
@ -128,11 +128,11 @@ class StreamsClient {
|
|||
|
||||
// Signature
|
||||
var signature = streamInfo.signature;
|
||||
var signatureParameters = streamInfo.signatureParameter;
|
||||
var signatureParameter = streamInfo.signatureParameter ?? "signature";
|
||||
|
||||
if (!signature.isNullOrWhiteSpace) {
|
||||
signature = streamContext.cipherOperations.decipher(signature);
|
||||
url = url.setQueryParam(signatureParameters, signature);
|
||||
url = url.setQueryParam(signatureParameter, signature);
|
||||
}
|
||||
|
||||
// Content length
|
||||
|
@ -199,7 +199,7 @@ class StreamsClient {
|
|||
continue;
|
||||
}
|
||||
// Audio-only
|
||||
if (audioCodec.isNullOrWhiteSpace) {
|
||||
if (!audioCodec.isNullOrWhiteSpace) {
|
||||
streams[tag] = AudioOnlyStreamInfo(
|
||||
tag, url, container, fileSize, bitrate, audioCodec);
|
||||
}
|
||||
|
@ -213,7 +213,8 @@ class StreamsClient {
|
|||
|
||||
/// Gets the manifest that contains information
|
||||
/// about available streams in the specified video.
|
||||
Future<StreamManifest> getManifest(VideoId videoId) async {
|
||||
Future<StreamManifest> getManifest(dynamic videoId) async {
|
||||
videoId = VideoId.fromString(videoId);
|
||||
// We can try to extract the manifest from two sources:
|
||||
// get_video_info and the video watch page.
|
||||
// In some cases one works, in some cases another does.
|
||||
|
|
|
@ -20,19 +20,21 @@ class VideoClient {
|
|||
closedCaptions = ClosedCaptionClient(_httpClient);
|
||||
|
||||
/// Gets the metadata associated with the specified video.
|
||||
Future<Video> get(VideoId id) async {
|
||||
var videoInfoResponse = await VideoInfoResponse.get(_httpClient, id.value);
|
||||
Future<Video> get(dynamic videoId) async {
|
||||
videoId = VideoId.fromString(videoId);
|
||||
var videoInfoResponse =
|
||||
await VideoInfoResponse.get(_httpClient, videoId.value);
|
||||
var playerResponse = videoInfoResponse.playerResponse;
|
||||
|
||||
var watchPage = await WatchPage.get(_httpClient, id.value);
|
||||
var watchPage = await WatchPage.get(_httpClient, videoId.value);
|
||||
return Video(
|
||||
id,
|
||||
videoId,
|
||||
playerResponse.videoTitle,
|
||||
playerResponse.videoAuthor,
|
||||
playerResponse.videoUploadDate,
|
||||
playerResponse.videoDescription,
|
||||
playerResponse.videoDuration,
|
||||
ThumbnailSet(id.value),
|
||||
ThumbnailSet(videoId.value),
|
||||
playerResponse.videoKeywords,
|
||||
Engagement(playerResponse.videoViewCount ?? 0, watchPage.videoLikeCount,
|
||||
watchPage.videoDislikeCount));
|
||||
|
|
|
@ -68,4 +68,13 @@ class VideoId extends Equatable {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Converts [obj] to a [VideoId] by calling .toString on that object.
|
||||
/// If it is already a [VideoId], [obj] is returned
|
||||
factory VideoId.fromString(dynamic obj) {
|
||||
if (obj is VideoId) {
|
||||
return obj;
|
||||
}
|
||||
return VideoId(obj.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
library youtube_explode.base;
|
||||
|
||||
import 'channels/channels.dart';
|
||||
import 'playlists/playlist_client.dart';
|
||||
import 'reverse_engineering/youtube_http_client.dart';
|
||||
|
|
|
@ -58,6 +58,7 @@ void main() {
|
|||
'rsAAeyAr-9Y',
|
||||
};
|
||||
for (var videoId in data) {
|
||||
print('Matchin $videoId');
|
||||
var manifest =
|
||||
await yt.videos.streamsClient.getManifest(VideoId(videoId));
|
||||
for (var streamInfo in manifest.streams) {
|
||||
|
@ -65,6 +66,8 @@ void main() {
|
|||
expect(stream, isNotEmpty);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
timeout: const Timeout(Duration(minutes: 10)),
|
||||
skip: 'Currently now working.');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -20,8 +20,9 @@ var sep = 'ytplayer.config = ';
|
|||
|
||||
Future<void> main() async {
|
||||
var yt = YoutubeExplode();
|
||||
var m = await yt.videos.streamsClient.getManifest(VideoId('382BTxLNrow'));
|
||||
await yt.videos.streamsClient.get(m.streams.first);
|
||||
var m = await yt.videos.streamsClient.getManifest(VideoId('9bZkp7q19f0'));
|
||||
var s = await yt.videos.streamsClient.get(m.streams.first).toList();
|
||||
print(s);
|
||||
yt.close();
|
||||
print('Done!');
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue