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 ## 1.8.0-beta.1
- Removed deprecation of `Video`. - Removed deprecation of `Video`.
- Exported `SearchList`. - Exported `SearchList`.

View File

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

View File

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

View File

@ -5,16 +5,29 @@ import 'package:collection/collection.dart';
import '../../youtube_explode_dart.dart'; import '../../youtube_explode_dart.dart';
/// This list contains search videos. /// This list contains search videos.
class SearchList extends DelegatingList<SearchVideo> { class SearchList extends DelegatingList<Video> {
final Stream<SearchVideo> _stream; 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 { static Future<SearchList> create(Stream<SearchVideo> stream) async {
Stream<SearchVideo> broadcast; Stream<Video> broadcast;
broadcast = stream.asBroadcastStream(onCancel: (subscription) { 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(); subscription.pause();
}, onListen: (subscription) { }, onListen: (subscription) {
subscription.resume(); subscription.resume();
@ -28,4 +41,66 @@ class SearchList extends DelegatingList<SearchVideo> {
final base = await _stream.take(20).toList(); final base = await _stream.take(20).toList();
return SearchList._(base, _stream); 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). /// Metadata related to a search query result (video).
class SearchVideo extends BaseSearchContent { class SearchVideo extends BaseSearchContent {
/// VideoId. /// Video ID.
final VideoId videoId; final VideoId id;
/// Video title. /// Video title.
final String videoTitle; final String title;
/// Video author. /// Video author.
final String videoAuthor; final String author;
/// Video description snippet. (Part of the full description if too long) /// Video description snippet. (Part of the full description if too long)
final String videoDescriptionSnippet; final String description;
/// Video duration as String, HH:MM:SS /// Video duration as String, HH:MM:SS
final String videoDuration; final String duration;
/// Video View Count /// Video View Count
final int videoViewCount; final int viewCount;
/// Video thumbnail /// 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. /// Initialize a [SearchVideo] instance.
const SearchVideo( const SearchVideo(
this.videoId, this.id,
this.videoTitle, this.title,
this.videoAuthor, this.author,
this.videoDescriptionSnippet, this.description,
this.videoDuration, this.duration,
this.videoViewCount, this.viewCount,
this.videoThumbnails, this.thumbnails,
this.uploadDate,
this.isLive // ignore: avoid_positional_boolean_parameters
); );
@override @override
String toString() => '(Video) $videoTitle ($videoId)'; String toString() => '(Video) $title ($id)';
} }

View File

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

View File

@ -1,6 +1,5 @@
import '../channels/channel_id.dart'; import '../channels/channel_id.dart';
import '../common/common.dart'; import '../common/common.dart';
import '../exceptions/exceptions.dart';
import '../reverse_engineering/responses/responses.dart'; import '../reverse_engineering/responses/responses.dart';
import '../reverse_engineering/youtube_http_client.dart'; import '../reverse_engineering/youtube_http_client.dart';
import 'closed_captions/closed_caption_client.dart'; import 'closed_captions/closed_caption_client.dart';
@ -45,43 +44,11 @@ class VideoClient {
playerResponse.videoKeywords, playerResponse.videoKeywords,
Engagement(playerResponse.videoViewCount ?? 0, watchPage.videoLikeCount, Engagement(playerResponse.videoViewCount ?? 0, watchPage.videoLikeCount,
watchPage.videoDislikeCount), watchPage.videoDislikeCount),
playerResponse.isLive,
watchPage); 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] /// Get a [Video] instance from a [videoId]
Future<Video> get(dynamic videoId, {bool forceWatchPage = false}) async { Future<Video> get(dynamic videoId) async =>
videoId = VideoId.fromString(videoId); _getVideoFromWatchPage(VideoId.fromString(videoId));
if (forceWatchPage) {
return _getVideoFromWatchPage(videoId);
}
try {
return await _getVideoFromFixPlaylist(videoId);
} on YoutubeExplodeException {
return _getVideoFromWatchPage(videoId);
}
}
} }

View File

@ -1,6 +1,6 @@
name: youtube_explode_dart 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. 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 homepage: https://github.com/Hexer10/youtube_explode_dart
environment: environment:

View File

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

View File

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