diff --git a/lib/src/extensions/caption_extension.dart b/lib/src/extensions/caption_extension.dart new file mode 100644 index 0000000..6ff3668 --- /dev/null +++ b/lib/src/extensions/caption_extension.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; + +import 'package:xml/xml.dart' as xml; + +import '../models/models.dart'; +import '../youtube_explode_base.dart'; +import 'helpers_extension.dart'; + +/// Caption extension for [YoutubeExplode] +extension CaptionExtension on YoutubeExplode { + /// Gets all available closed caption track infos for given video. + Future> getVideoClosedCaptionTrackInfos( + String videoId) async { + if (!YoutubeExplode.validateVideoId(videoId)) { + throw ArgumentError.value(videoId, 'videoId', 'Invalid video id'); + } + + var videoInfoDic = await getVideoInfoDictionary(videoId); + + var playerResponseJson = json.decode(videoInfoDic['player_response']); + + var playAbility = playerResponseJson['playabilityStatus']; + + if (playAbility['status'].toLowerCase() == 'error') { + throw Exception('Video [$videoId] is unavailable'); + } + + var trackInfos = []; + for (var trackJson in playerResponseJson['captions'] + ['playerCaptionsTracklistRenderer']['captionTracks']) { + var url = Uri.parse(trackJson['baseUrl']); + + var query = Map.from(url.queryParameters); + query['format'] = '3'; + + url = url.replace(queryParameters: query); + + var languageCode = trackJson['languageCode']; + var languageName = trackJson['name']['simpleText']; + var language = Language(languageCode, languageName); + + var isAutoGenerated = trackJson['vssId'].toLowerCase().startsWith('a.'); + + trackInfos.add(ClosedCaptionTrackInfo(url, language, isAutoGenerated)); + } + return trackInfos; + } + + Future _getClosedCaptionTrackXml(Uri url) async { + var raw = (await client.get(url)).body; + + return xml.parse(raw); + } + + /// Gets the closed caption track associated with given metadata. + Future getClosedCaptionTrack( + ClosedCaptionTrackInfo info) async { + var trackXml = await _getClosedCaptionTrackXml(info.url); + + var captions = []; + for (var captionXml in trackXml.findAllElements('p')) { + var text = captionXml.text; + if (text.isNullOrWhiteSpace) { + continue; + } + + var offset = + Duration(milliseconds: int.parse(captionXml.getAttribute('t'))); + var duration = Duration( + milliseconds: int.parse(captionXml.getAttribute('d') ?? '-1')); + + captions.add(ClosedCaption(text, offset, duration)); + } + + return ClosedCaptionTrack(info, captions); + } +} diff --git a/lib/src/extensions/channel_extension.dart b/lib/src/extensions/channel_extension.dart index d719b7e..300e19b 100644 --- a/lib/src/extensions/channel_extension.dart +++ b/lib/src/extensions/channel_extension.dart @@ -6,7 +6,7 @@ import '../youtube_explode_base.dart'; import 'helpers_extension.dart'; import 'playlist_extension.dart'; -/// Channel extension for YoutubeExplode +/// Channel extension for [YoutubeExplode] extension ChannelExtension on YoutubeExplode { static final _usernameRegMatchExp = RegExp(r'youtube\..+?/user/(.*?)(?:\?|&|/|$)'); diff --git a/lib/src/extensions/extensions.dart b/lib/src/extensions/extensions.dart index 85c291e..2094623 100644 --- a/lib/src/extensions/extensions.dart +++ b/lib/src/extensions/extensions.dart @@ -1,3 +1,4 @@ +export 'caption_extension.dart'; export 'channel_extension.dart'; export 'helpers_extension.dart'; export 'playlist_extension.dart'; diff --git a/lib/src/extensions/playlist_extension.dart b/lib/src/extensions/playlist_extension.dart index 45eca25..8c2d1fe 100644 --- a/lib/src/extensions/playlist_extension.dart +++ b/lib/src/extensions/playlist_extension.dart @@ -5,7 +5,7 @@ import '../parser.dart' as parser; import '../youtube_explode_base.dart'; import 'helpers_extension.dart'; -/// Playlist extension for YoutubeExplode +/// Playlist extension for [YoutubeExplode] extension PlaylistExtension on YoutubeExplode { static final _regMatchExp = RegExp(r'youtube\..+?/playlist.*?list=(.*?)(?:&|/|$)'); diff --git a/lib/src/extensions/search_extension.dart b/lib/src/extensions/search_extension.dart index fee118a..cdb81dd 100644 --- a/lib/src/extensions/search_extension.dart +++ b/lib/src/extensions/search_extension.dart @@ -4,7 +4,7 @@ import '../models/models.dart'; import '../youtube_explode_base.dart'; import 'helpers_extension.dart'; -/// Search extension for YoutubeExplode +/// Search extension for [YoutubeExplode] extension SearchExtension on YoutubeExplode { Future> _getSearchResults(String query, int page) async { var url = diff --git a/lib/src/models/closed_caption.dart b/lib/src/models/closed_caption.dart new file mode 100644 index 0000000..72822e6 --- /dev/null +++ b/lib/src/models/closed_caption.dart @@ -0,0 +1,24 @@ +/// Text that gets displayed at specific time during video playback, as part of a . +class ClosedCaption { + /// Text displayed by this caption. + final String text; + + /// Time at which this caption starts being displayed. + final Duration offset; + + /// Duration this caption is displayed. + /// Negative if not found. + final Duration duration; + + /// Initializes an instance of [ClosedCaption] + const ClosedCaption(this.text, this.offset, this.duration); + + /// Time at which this caption starts being displayed. + Duration get start => offset; + + /// Time at which this caption ends being displayed. + Duration get end => duration + offset; + + @override + String toString() => 'Caption: $text ($offset - $end)'; +} diff --git a/lib/src/models/closed_caption_track.dart b/lib/src/models/closed_caption_track.dart new file mode 100644 index 0000000..7bbe633 --- /dev/null +++ b/lib/src/models/closed_caption_track.dart @@ -0,0 +1,13 @@ +import 'models.dart'; + +/// Set of captions that get displayed during video playback. +class ClosedCaptionTrack { + /// Metadata associated with this track. + final ClosedCaptionTrackInfo info; + + /// Collection of closed captions that belong to this track. + final List captions; + + /// Initializes an instance of [ClosedCaptionTrack] + const ClosedCaptionTrack(this.info, this.captions); +} diff --git a/lib/src/models/closed_caption_track_info.dart b/lib/src/models/closed_caption_track_info.dart new file mode 100644 index 0000000..2284265 --- /dev/null +++ b/lib/src/models/closed_caption_track_info.dart @@ -0,0 +1,17 @@ +import 'models.dart'; + +/// Metadata associated with a certain [ClosedCaptionTrack] +class ClosedCaptionTrackInfo { + + /// Manifest URL of the associated track. + final Uri url; + +/// Language of the associated track. + final Language language; + +/// Whether the associated track was automatically generated. + final bool isAutoGenerated; + +/// Initializes an instance of [ClosedCaptionTrackInfo] + const ClosedCaptionTrackInfo(this.url, this.language, this.isAutoGenerated); +} \ No newline at end of file diff --git a/lib/src/models/language.dart b/lib/src/models/language.dart new file mode 100644 index 0000000..bce863f --- /dev/null +++ b/lib/src/models/language.dart @@ -0,0 +1,11 @@ +/// Language information. +class Language { + /// ISO 639-1 code of this language. + final String code; + + /// Full English name of this language. + final String name; + + /// Initializes an instance of [Language] + const Language(this.code, this.name); +} diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index 5c035bf..dc27598 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -3,7 +3,11 @@ library youtube_explode.models; export 'audio_encoding.dart'; export 'audio_stream_info.dart'; export 'channel.dart'; +export 'closed_caption.dart'; +export 'closed_caption_track.dart'; +export 'closed_caption_track_info.dart'; export 'container.dart'; +export 'language.dart'; export 'media_stream_info.dart'; export 'media_stream_info_set.dart'; export 'muxed_stream_info.dart'; diff --git a/lib/src/youtube_explode_base.dart b/lib/src/youtube_explode_base.dart index ad8558a..3f2342b 100644 --- a/lib/src/youtube_explode_base.dart +++ b/lib/src/youtube_explode_base.dart @@ -267,7 +267,7 @@ class YoutubeExplode { /// Returns the video info dictionary for a given vide. Future> getVideoInfoDictionary(String videoId) async { - var eurl = Uri.encodeFull('https://youtube.googleapis.com/v/$videoId'); + var eurl = Uri.encodeComponent('https://youtube.googleapis.com/v/$videoId'); var url = 'https://youtube.com/get_video_info?video_id=$videoId' '&el=embedded&eurl=$eurl&hl=en'; var raw = (await client.get(url)).body; diff --git a/pubspec.yaml b/pubspec.yaml index 3357deb..d17875a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,12 +4,13 @@ version: 0.0.5 homepage: https://github.com/Hexer10/youtube_explode_dart environment: - sdk: '>=2.7.0 <3.0.0' + sdk: '>=2.6.0 <3.0.0' dependencies: html: ^0.14.0+3 http: ^0.12.0+4 http_parser: ^3.1.3 + xml: ^3.7.0 dev_dependencies: effective_dart: ^1.2.1