parent
b123a39189
commit
386069c8a4
|
@ -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`.
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)';
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue