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
|
## 1.8.0-beta.1
|
||||||
- Removed deprecation of `Video`.
|
- Removed deprecation of `Video`.
|
||||||
- Exported `SearchList`.
|
- Exported `SearchList`.
|
||||||
|
|
|
@ -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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue