Version 1.8.0-beta.2

#100
This commit is contained in:
Mattia 2021-02-27 18:58:42 +01:00
parent b123a39189
commit 386069c8a4
10 changed files with 136 additions and 69 deletions

View File

@ -1,3 +1,6 @@
## 1.8.0-beta.2
- `search.getVideos` now returns a `Video` instance.
## 1.8.0-beta.1
- Removed deprecation of `Video`.
- Exported `SearchList`.

View File

@ -56,7 +56,8 @@ class PlaylistClient {
video.duration,
ThumbnailSet(videoId),
video.keywords,
Engagement(video.viewCount, video.likes, video.dislikes));
Engagement(video.viewCount, video.likes, video.dislikes),
null);
countDelta++;
}

View File

@ -251,10 +251,16 @@ class _InitialData {
int.parse(renderer.viewCountText?.simpleText
?.stripNonDigits()
?.nullIfWhitespace ??
renderer.viewCountText?.runs?.first?.text
?.stripNonDigits()
?.nullIfWhitespace ??
'0'),
(renderer.thumbnail.thumbnails ?? <ThumbnailElement>[])
.map((e) => Thumbnail(Uri.parse(e.url), e.height, e.width))
.toList());
.toList(),
renderer.publishedTimeText?.simpleText,
renderer?.viewCountText?.runs?.elementAt(1)?.text?.trim() ==
'watching');
}
if (content.radioRenderer != null) {
var renderer = content.radioRenderer;

View File

@ -5,16 +5,29 @@ import 'package:collection/collection.dart';
import '../../youtube_explode_dart.dart';
/// This list contains search videos.
class SearchList extends DelegatingList<SearchVideo> {
final Stream<SearchVideo> _stream;
class SearchList extends DelegatingList<Video> {
final Stream<Video> _stream;
///
SearchList._(List<SearchVideo> base, this._stream) : super(base);
SearchList._(List<Video> base, this._stream) : super(base);
///
static Future<SearchList> create(Stream<SearchVideo> stream) async {
Stream<SearchVideo> broadcast;
broadcast = stream.asBroadcastStream(onCancel: (subscription) {
Stream<Video> broadcast;
broadcast = stream
.map((e) => Video(
e.id,
e.title,
e.author,
null,
_stringToDateTime(e.uploadDate),
e.description,
_stringToDuration(e.duration),
ThumbnailSet(e.id.value),
null,
Engagement(e.viewCount, null, null),
e.isLive))
.asBroadcastStream(onCancel: (subscription) {
subscription.pause();
}, onListen: (subscription) {
subscription.resume();
@ -28,4 +41,66 @@ class SearchList extends DelegatingList<SearchVideo> {
final base = await _stream.take(20).toList();
return SearchList._(base, _stream);
}
/// Format: <quantity> <unit> ago (5 years ago)
static DateTime _stringToDateTime(String string) {
if (string == null) {
return null;
}
var parts = string.split(' ');
assert(parts.length == 3);
var qty = int.parse(parts.first);
// Try to get the unit
var unit = parts[1];
Duration time;
if (unit.startsWith('second')) {
time = Duration(seconds: qty);
} else if (unit.startsWith('minute')) {
time = Duration(minutes: qty);
} else if (unit.startsWith('hour')) {
time = Duration(hours: qty);
} else if (unit.startsWith('day')) {
time = Duration(days: qty);
} else if (unit.startsWith('week')) {
time = Duration(days: qty * 7);
} else if (unit.startsWith('month')) {
time = Duration(days: qty * 30);
} else if (unit.startsWith('year')) {
time = Duration(days: qty * 365);
} else {
throw StateError('Couldn\'t parse $unit unit of time. '
'Please report this to the project page!');
}
return DateTime.now().subtract(time);
}
/// Format: HH:MM:SS (5 years ago)
static Duration _stringToDuration(String string) {
if (string == null || string.trim().isEmpty) {
return null;
}
var parts = string.split(':');
assert(parts.length <= 3);
if (parts.length == 1) {
return Duration(seconds: int.parse(parts.first));
}
if (parts.length == 2) {
return Duration(
minutes: int.parse(parts[1]), seconds: int.parse(parts.first));
}
if (parts.length == 3) {
return Duration(
hours: int.parse(parts[2]),
minutes: int.parse(parts[1]),
seconds: int.parse(parts[0]));
}
// Should reach here.
throw Error();
}
}

View File

@ -4,38 +4,46 @@ import 'base_search_content.dart';
/// Metadata related to a search query result (video).
class SearchVideo extends BaseSearchContent {
/// VideoId.
final VideoId videoId;
/// Video ID.
final VideoId id;
/// Video title.
final String videoTitle;
final String title;
/// Video author.
final String videoAuthor;
final String author;
/// Video description snippet. (Part of the full description if too long)
final String videoDescriptionSnippet;
final String description;
/// Video duration as String, HH:MM:SS
final String videoDuration;
final String duration;
/// Video View Count
final int videoViewCount;
final int viewCount;
/// Video thumbnail
final List<Thumbnail> videoThumbnails;
final List<Thumbnail> thumbnails;
/// Video upload date - As string: 5 years ago.
final String uploadDate;
/// True if this video is a live stream.
final bool isLive;
/// Initialize a [SearchVideo] instance.
const SearchVideo(
this.videoId,
this.videoTitle,
this.videoAuthor,
this.videoDescriptionSnippet,
this.videoDuration,
this.videoViewCount,
this.videoThumbnails,
this.id,
this.title,
this.author,
this.description,
this.duration,
this.viewCount,
this.thumbnails,
this.uploadDate,
this.isLive // ignore: avoid_positional_boolean_parameters
);
@override
String toString() => '(Video) $videoTitle ($videoId)';
String toString() => '(Video) $title ($id)';
}

View File

@ -22,9 +22,12 @@ class Video with EquatableMixin {
final String author;
/// Video author Id.
/// Note: null if the video is from a search query.
final ChannelId channelId;
/// Video upload date.
/// Note: For search queries it is calculated with:
/// DateTime.now() - how much time is was published.
final DateTime uploadDate;
/// Video description.
@ -42,6 +45,9 @@ class Video with EquatableMixin {
/// Engagement statistics for this video.
final Engagement engagement;
/// Returns true if this is a live stream.
final bool isLive;
/// Used internally.
/// Shouldn't be used in the code.
final WatchPage watchPage;
@ -61,6 +67,7 @@ class Video with EquatableMixin {
this.thumbnails,
Iterable<String> keywords,
this.engagement,
this.isLive, // ignore: avoid_positional_boolean_parameters
[this.watchPage])
: keywords = UnmodifiableListView(keywords);

View File

@ -1,6 +1,5 @@
import '../channels/channel_id.dart';
import '../common/common.dart';
import '../exceptions/exceptions.dart';
import '../reverse_engineering/responses/responses.dart';
import '../reverse_engineering/youtube_http_client.dart';
import 'closed_captions/closed_caption_client.dart';
@ -45,43 +44,11 @@ class VideoClient {
playerResponse.videoKeywords,
Engagement(playerResponse.videoViewCount ?? 0, watchPage.videoLikeCount,
watchPage.videoDislikeCount),
playerResponse.isLive,
watchPage);
}
Future<Video> _getVideoFromFixPlaylist(VideoId id) async {
var playlistInfo = await PlaylistResponse.get(_httpClient, 'RD${id.value}');
var video = playlistInfo.videos
.firstWhere((e) => e.id == id.value, orElse: () => null);
if (video == null) {
throw TransientFailureException('Video not found in mix playlist');
}
return Video(
id,
video.title,
video.author,
video.channelId,
video.uploadDate,
video.description,
video.duration,
ThumbnailSet(id.value),
video.keywords,
Engagement(video.viewCount, video.likes, video.dislikes));
}
/// Get a [Video] instance from a [videoId]
Future<Video> get(dynamic videoId, {bool forceWatchPage = false}) async {
videoId = VideoId.fromString(videoId);
if (forceWatchPage) {
return _getVideoFromWatchPage(videoId);
}
try {
return await _getVideoFromFixPlaylist(videoId);
} on YoutubeExplodeException {
return _getVideoFromWatchPage(videoId);
}
}
Future<Video> get(dynamic videoId) async =>
_getVideoFromWatchPage(VideoId.fromString(videoId));
}

View File

@ -1,6 +1,6 @@
name: youtube_explode_dart
description: A port in dart of the youtube explode library. Supports several API functions without the need of Youtube API Key.
version: 1.8.0-beta.1
version: 1.8.0-beta.2
homepage: https://github.com/Hexer10/youtube_explode_dart
environment:

View File

@ -13,7 +13,7 @@ void main() {
test('Get comments of a video', () async {
var videoUrl = 'https://www.youtube.com/watch?v=AI7ULzgf8RU';
var video = await yt.videos.get(VideoId(videoUrl), forceWatchPage: true);
var video = await yt.videos.get(VideoId(videoUrl));
var comments = await yt.videos.commentsClient.getComments(video).toList();
expect(comments.length, greaterThanOrEqualTo(1));
}, skip: 'This may fail on some environments');

View File

@ -26,14 +26,14 @@ void main() {
.toList();
expect(videos, hasLength(10));
var video = videos.first;
expect(video.videoId, isNotNull);
expect(video.id, isNotNull);
expect(video.videoTitle, isNotEmpty);
expect(video.videoAuthor, isNotEmpty);
expect(video.videoDescriptionSnippet, isNotEmpty);
expect(video.videoDuration, isNotEmpty);
expect(video.videoViewCount, greaterThan(0));
expect(video.videoThumbnails, isNotEmpty);
expect(video.title, isNotEmpty);
expect(video.author, isNotEmpty);
expect(video.description, isNotEmpty);
expect(video.duration, isNotEmpty);
expect(video.viewCount, greaterThan(0));
expect(video.thumbnails, isNotEmpty);
});
test('Search a youtube videos from the search page - old', () async {
@ -61,7 +61,7 @@ void main() {
expect(searchQuery.content.first, isA<SearchVideo>());
var video = searchQuery.content.first as SearchVideo;
expect(video.videoThumbnails, isNotEmpty);
expect(video.thumbnails, isNotEmpty);
});
test('Search youtube videos from search page (stream) - old', () async {