Implement Video from watch page.

Closes #6
This commit is contained in:
Hexah 2020-02-24 14:50:12 +01:00
parent 12cbc41b6d
commit e5bdf928dd
4 changed files with 115 additions and 15 deletions

View File

@ -86,7 +86,7 @@ Future<List<CipherOperation>> getCipherOperations(
// Reverse
exp = RegExp('$funcNameEsc' r':\bfunction\b\(\w+\)');
if (exp.hasMatch(deciphererDefBody)) {
operations.add(ReverseCipherOperation());
operations.add(const ReverseCipherOperation());
}
}

View File

@ -1,12 +1,16 @@
import 'exceptions.dart';
/// Thrown when a video is not playable because it requires purchase.
class VideoRequiresPurchaseException implements Exception {
class VideoRequiresPurchaseException implements VideoUnplayableException {
/// ID of the video.
final String videoId;
/// ID of the preview video.
final String previewVideoId;
/// Initializes an instance of [VideoRequiresPurchaseException]
const VideoRequiresPurchaseException(this.previewVideoId);
const VideoRequiresPurchaseException(this.videoId, this.previewVideoId);
@override
String toString() => 'VideoRequiresPurchaseException: The video '
'$previewVideoId requires a purchase';
String get reason => 'Requires purchase';
}

View File

@ -5,9 +5,13 @@ class VideoUnplayableException {
/// ID of the video.
final String videoId;
/// Reason why the video can't be played.
final String reason;
/// Initializes an instance of [VideoUnplayableException]
const VideoUnplayableException(this.videoId);
const VideoUnplayableException(this.videoId, [this.reason]);
String toString() =>
'VideoUnplayableException: Video $videoId couldn\'t be played';
'VideoUnplayableException: Video $videoId couldn\'t be played.'
'${reason == null ? '' : 'Reason: $reason'}';
}

View File

@ -16,11 +16,11 @@ class YoutubeExplode {
static final _regMatchExp = RegExp(r'youtube\..+?/watch.*?v=(.*?)(?:&|/|$)');
static final _shortMatchExp = RegExp(r'youtu\.be/(.*?)(?:\?|&|/|$)');
static final _embedMatchExp = RegExp(r'youtube\..+?/embed/(.*?)(?:\?|&|/|$)');
static final _playerConfigRegexp = RegExp(
static final _playerConfigExp = RegExp(
r"yt\.setConfig\({'PLAYER_CONFIG':(.*)}\);",
multiLine: true,
caseSensitive: false);
static final _contentLenRegexp = RegExp(r'clen=(\d+)');
static final _contentLenExp = RegExp(r'clen=(\d+)');
/// HTTP Client.
// Visible only for extensions.
@ -191,6 +191,16 @@ class YoutubeExplode {
/// Returns the player configuration for a given video.
Future<PlayerConfiguration> getPlayerConfiguration(String videoId) async {
var playerConfiguration = await _getPlayerConfigEmbed(videoId);
// If still null try from the watch page.
playerConfiguration ??= await _getPlayerConfigWatchPage(videoId);
assert(playerConfiguration != null);
return playerConfiguration;
}
Future<PlayerConfiguration> _getPlayerConfigEmbed(String videoId) async {
var body = (await client.get(
'https://www.youtube.com/embed/$videoId?disable_polymer=true&hl=en'))
.body;
@ -198,7 +208,7 @@ class YoutubeExplode {
var playerConfigRaw = document
.getElementsByTagName('script')
.map((e) => e.innerHtml)
.map((e) => _playerConfigRegexp?.firstMatch(e)?.group(1))
.map((e) => _playerConfigExp?.firstMatch(e)?.group(1))
.firstWhere((s) => s?.trim()?.isNotEmpty ?? false);
var playerConfigJson = json.decode(playerConfigRaw);
@ -221,7 +231,6 @@ class YoutubeExplode {
// Valid configuration
if (errorReason.isNullOrWhiteSpace) {
// Extract if it is a live stream.
var isLiveStream = playerResponseJson['videoDetails']['isLive'] == true;
var videoInfo = playerResponseJson['videoDetails'];
var video = Video(
@ -235,6 +244,8 @@ class YoutubeExplode {
videoInfo['keywords']?.cast<String>() ?? const <String>[],
Statistics(int.parse(videoInfo['viewCount']), 0, 0));
var isLiveStream = playerResponseJson['videoDetails']['isLive'] == true;
var streamingData = playerResponseJson['streamingData'];
var validUntil = DateTime.now()
.add(Duration(seconds: int.parse(streamingData['expiresInSeconds'])));
@ -262,11 +273,92 @@ class YoutubeExplode {
validUntil);
}
throw UnimplementedError(
'Get from video watch page or purchase video not implemented yet');
var previewVideoId = playAbility['errorScreen']
['playerLegacyDesktopYpcTrailerRenderer']['trailerVideoId'] as String;
if (!previewVideoId.isNullOrWhiteSpace) {
throw VideoRequiresPurchaseException(videoId, previewVideoId);
}
// If the video requires purchase - throw (approach two)
var previewVideoInfoRaw = playAbility['errorScreen']['ypcTrailerRenderer']
['playerVars'] as String;
if (!previewVideoInfoRaw.isNullOrWhiteSpace) {
var previewVideoInfoDic = Uri.splitQueryString(previewVideoInfoRaw);
var previewVideoId = previewVideoInfoDic['video_id'];
throw VideoRequiresPurchaseException(videoId, previewVideoId);
}
return null;
}
/// Returns the video info dictionary for a given vide.
Future<PlayerConfiguration> _getPlayerConfigWatchPage(String videoId) async {
var videoWatchPageHtml = await _getVideoWatchPageHtml(videoId);
var playerConfigScript = videoWatchPageHtml
.querySelectorAll('script')
.map((e) => e.text)
.firstWhere((e) => e.contains('ytplayer.config ='));
if (playerConfigScript == null) {
var errorReason =
videoWatchPageHtml.querySelector('#unavailable-message').text.trim();
throw VideoUnplayableException(videoId, errorReason);
}
// Workaround: Couldn't get RegExp to work.
var startIndex = playerConfigScript.indexOf('ytplayer.config =');
var endIndex = playerConfigScript.indexOf(';ytplayer.load =');
var playerConfigRaw =
playerConfigScript.substring(startIndex + 17, endIndex);
var playerConfigJson = json.decode(playerConfigRaw);
var playerResponseJson =
json.decode(playerConfigJson['args']['player_response']);
var playerSourceUrl =
'https://youtube.com${playerConfigJson['assets']['js']}';
var videoInfo = playerResponseJson['videoDetails'];
var video = Video(
videoId,
videoInfo['author'],
null,
videoInfo['title'],
videoInfo['shortDescription'],
ThumbnailSet(videoId),
Duration(seconds: int.parse(videoInfo['lengthSeconds'])),
videoInfo['keywords']?.cast<String>() ?? const <String>[],
Statistics(int.parse(videoInfo['viewCount']), 0, 0));
var isLiveStream = playerResponseJson['videoDetails']['isLive'] == true;
var streamingData = playerResponseJson['streamingData'];
var validUntil = DateTime.now()
.add(Duration(seconds: int.parse(streamingData['expiresInSeconds'])));
var hlsManifestUrl = isLiveStream ? streamingData['hlsManifestUrl'] : null;
var dashManifestUrl =
isLiveStream ? null : streamingData['dashManifestUrl'];
var muxedStreamInfosUrlEncoded = isLiveStream
? null
: playerConfigJson['args']['url_encoded_fmt_stream_map'];
var adaptiveStreamInfosUrlEncoded =
isLiveStream ? null : playerConfigJson['args']['adaptive_fmts'];
var muxedStreamInfosJson = isLiveStream ? null : streamingData['formats'];
var adaptiveStreamInfosJson =
isLiveStream ? null : streamingData['adaptiveFormats'];
return PlayerConfiguration(
playerSourceUrl,
dashManifestUrl,
hlsManifestUrl,
muxedStreamInfosUrlEncoded,
adaptiveStreamInfosUrlEncoded,
muxedStreamInfosJson,
adaptiveStreamInfosJson,
video,
validUntil);
}
/// Returns the video info dictionary for a given video.
Future<Map<String, String>> getVideoInfoDictionary(String videoId) async {
var eurl = Uri.encodeComponent('https://youtube.googleapis.com/v/$videoId');
var url = 'https://youtube.com/get_video_info?video_id=$videoId'
@ -333,7 +425,7 @@ class YoutubeExplode {
var contentLength = int.tryParse(contentLengthString ?? '') ?? -1;
if (contentLength <= 0) {
contentLength = _contentLenRegexp?.firstMatch(url)?.group(1) ?? -1;
contentLength = _contentLenExp?.firstMatch(url)?.group(1) ?? -1;
}
if (contentLength <= 0) {