175 lines
4.7 KiB
Dart
175 lines
4.7 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:http_parser/http_parser.dart';
|
|
import 'package:youtube_explode_dart/src/reverse_engineering/responses/stream_info_provider.dart';
|
|
|
|
class PlayerResponse {
|
|
// Json parsed map
|
|
final Map<String, dynamic> _root;
|
|
|
|
PlayerResponse(this._root);
|
|
|
|
String get playabilityStatus => _root['playabilityStatus']['status'];
|
|
|
|
// Can be null
|
|
String get getVideoPlayabilityError =>
|
|
_root.get('playabilityStatus')?.get('reason');
|
|
|
|
bool get isVideoAvailable => playabilityStatus != 'error';
|
|
|
|
bool get isVideoPlayable => playabilityStatus == 'ok';
|
|
|
|
String get videoTitle => _root['videoDetails']['title'];
|
|
|
|
String get videoAuthor => _root['videoDetails']['author'];
|
|
|
|
//TODO: Check how this is formatted.
|
|
|
|
String /* DateTime */ get videoUploadDate =>
|
|
_root['microformat']['playerMicroformatRenderer']['uploadDate'];
|
|
|
|
String get videoChannelId => _root['videoDetails']['channelId'];
|
|
|
|
Duration get videoDuration =>
|
|
Duration(seconds: _root['videoDetails']['lengthSeconds']);
|
|
|
|
Iterable<String> get videoKeywords =>
|
|
_root['videoDetails']['keywords'] ?? const [];
|
|
|
|
String get videoDescription => _root['videoDetails']['shortDescription'];
|
|
|
|
int get videoViewCount => int.parse(_root['videoDetails']['viewCount']);
|
|
|
|
// Can be null
|
|
String get previewVideoId =>
|
|
_root
|
|
.get('playabilityStatus')
|
|
?.get('errorScreen')
|
|
?.get('playerLegacyDesktopYpcTrailerRenderer')
|
|
?.get('trailerVideoId') ??
|
|
Uri.splitQueryString(_root
|
|
.get('playabilityStatus')
|
|
?.get('errorScreen')
|
|
?.get('')
|
|
?.get('ypcTrailerRenderer')
|
|
?.get('playerVars') ??
|
|
'')['video_id'];
|
|
|
|
bool get isLive => _root['videoDetails'].get('isLive') ?? false;
|
|
|
|
// Can be null
|
|
String get dashManifestUrl =>
|
|
_root.get('streamingData')?.get('dashManifestUrl');
|
|
|
|
Iterable<StreamInfoProvider> get muxedStreams =>
|
|
_root?.get('streamingData')?.get('formats')?.map((e) => _StreamInfo(e)) ??
|
|
const [];
|
|
|
|
Iterable<StreamInfoProvider> get adaptiveStreams =>
|
|
_root
|
|
?.get('streamingData')
|
|
?.get('adaptiveFormats')
|
|
?.map((e) => _StreamInfo(e)) ??
|
|
const [];
|
|
|
|
Iterable<StreamInfoProvider> get streams =>
|
|
[...muxedStreams, ...adaptiveStreams];
|
|
|
|
Iterable<ClosedCaptionTrack> get closedCaptionTrack =>
|
|
_root
|
|
.get('captions')
|
|
?.get('playerCaptionsTracklistRenderer')
|
|
?.get('captionTracks')
|
|
?.map((e) => ClosedCaptionTrack(e)) ??
|
|
const [];
|
|
|
|
PlayerResponse.parse(String raw) : _root = json.decode(raw);
|
|
}
|
|
|
|
class ClosedCaptionTrack {
|
|
// Json parsed map
|
|
final Map<String, dynamic> _root;
|
|
|
|
ClosedCaptionTrack(this._root);
|
|
|
|
String get url => _root['baseUrl'];
|
|
|
|
String get language => _root['name']['simpleText'];
|
|
|
|
bool get autoGenerated => _root['vssId'].startsWith("a.");
|
|
}
|
|
|
|
class _StreamInfo extends StreamInfoProvider {
|
|
// Json parsed map
|
|
final Map<String, dynamic> _root;
|
|
|
|
_StreamInfo(this._root);
|
|
|
|
@override
|
|
int get bitrate => _root['bitrate'];
|
|
|
|
@override
|
|
String get container => mimeType.subtype;
|
|
|
|
@override
|
|
int get contentLength =>
|
|
_root['contentLength'] ??
|
|
StreamInfoProvider.contentLenExp.firstMatch(url).group(1);
|
|
|
|
@override
|
|
int get framerate => int.tryParse(_root['fps'] ?? '');
|
|
|
|
@override
|
|
String get signature => Uri.splitQueryString(_root.get('cipher') ?? '')['s'];
|
|
|
|
@override
|
|
String get signatureParameter =>
|
|
Uri.splitQueryString(_root['cipher'] ?? '')['sp'];
|
|
|
|
@override
|
|
int get tag => int.parse(_root['itag']);
|
|
|
|
@override
|
|
String get url =>
|
|
_root?.get('url') ??
|
|
Uri.splitQueryString(_root?.get('cipher') ?? '')['s'];
|
|
|
|
@override
|
|
// TODO: implement videoCodec, gotta debug how the mimeType is formatted
|
|
String get videoCodec => throw UnimplementedError();
|
|
|
|
@override
|
|
// TODO: implement videoHeight, gotta debug how the mimeType is formatted
|
|
int get videoHeight => _root['height'];
|
|
|
|
@override
|
|
// TODO: implement videoQualityLabel
|
|
String get videoQualityLabel => _root['qualityLabel'];
|
|
|
|
@override
|
|
// TODO: implement videoWidth
|
|
int get videoWidth => _root['width'];
|
|
|
|
// TODO: implement audioOnly, gotta debug how the mimeType is formatted
|
|
bool get audioOnly => throw UnimplementedError();
|
|
|
|
MediaType get mimeType => MediaType.parse(_root['mimeType']);
|
|
|
|
String get codecs => mimeType?.parameters['codecs']?.toLowerCase();
|
|
|
|
@override
|
|
// TODO: Finish implementing this, gotta debug how the mimeType is formatted
|
|
String get audioCodec => audioOnly ? codecs : throw UnimplementedError();
|
|
}
|
|
|
|
///
|
|
extension GetOrNull<K, V> on Map<K, V> {
|
|
V get(K key) {
|
|
var v = this[key];
|
|
if (v == null) {
|
|
return null;
|
|
}
|
|
return v;
|
|
}
|
|
}
|