Fix some streams

This commit is contained in:
Hexah 2020-06-05 20:08:04 +02:00
parent 407ac50f22
commit ac5d7b4d5c
23 changed files with 140 additions and 61 deletions

View File

@ -14,7 +14,7 @@ linter:
- prefer_constructors_over_static_methods
- prefer_contains
- annotate_overrides
- await_futures
- await_future
analyzer:
exclude:

View File

@ -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();

View File

@ -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);

View File

@ -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;

View File

@ -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';

View File

@ -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());
}
}

View File

@ -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();
}

View File

@ -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) {

View File

@ -1,6 +1,7 @@
import '../common/common.dart';
import 'playlist_id.dart';
/// YouTube playlist metadata.
class Playlist {
/// Playlist ID.
final PlaylistId id;

View File

@ -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) {

View File

@ -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());
}
}

View File

@ -1,3 +1,5 @@
library _youtube_explode.retry;
import 'dart:async';
import 'exceptions/exceptions.dart';

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();

View File

@ -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;

View File

@ -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.

View File

@ -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.

View File

@ -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));

View File

@ -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());
}
}

View File

@ -1,3 +1,5 @@
library youtube_explode.base;
import 'channels/channels.dart';
import 'playlists/playlist_client.dart';
import 'reverse_engineering/youtube_http_client.dart';

View File

@ -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.');
});
}

View File

@ -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!');
}