From d458e23f3ba9cf794247deaad8858c7a0f2fea35 Mon Sep 17 00:00:00 2001 From: Hexah Date: Thu, 20 Feb 2020 22:13:51 +0100 Subject: [PATCH] Add channel api --- .gitignore | 1 + lib/src/cipher/cipher.dart | 2 +- lib/src/extensions/channel_extension.dart | 140 ++++++++++++++++++ lib/src/extensions/extensions.dart | 3 + .../helpers_extension.dart} | 2 +- .../{ => extensions}/playlist_extension.dart | 11 +- lib/src/models/channel.dart | 14 ++ lib/src/models/models.dart | 1 + lib/src/youtube_explode_base.dart | 34 ++++- lib/youtube_explode_dart.dart | 3 +- pubspec.yaml | 2 +- 11 files changed, 196 insertions(+), 17 deletions(-) create mode 100644 lib/src/extensions/channel_extension.dart create mode 100644 lib/src/extensions/extensions.dart rename lib/src/{extensions.dart => extensions/helpers_extension.dart} (95%) rename lib/src/{ => extensions}/playlist_extension.dart (95%) create mode 100644 lib/src/models/channel.dart diff --git a/.gitignore b/.gitignore index 4e28807..ac3c0e0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ doc/api/ .idea/ .vscode/ *.iml +bin/ diff --git a/lib/src/cipher/cipher.dart b/lib/src/cipher/cipher.dart index f56003d..1dca045 100644 --- a/lib/src/cipher/cipher.dart +++ b/lib/src/cipher/cipher.dart @@ -1,7 +1,7 @@ library youtube_explode.cipher; import 'package:http/http.dart' as http; -import '../extensions.dart'; +import '../extensions/helpers_extension.dart'; import 'cipher_operations.dart'; final _deciphererFuncNameExp = RegExp( diff --git a/lib/src/extensions/channel_extension.dart b/lib/src/extensions/channel_extension.dart new file mode 100644 index 0000000..d719b7e --- /dev/null +++ b/lib/src/extensions/channel_extension.dart @@ -0,0 +1,140 @@ +import 'package:html/dom.dart'; +import 'package:html/parser.dart' as html; + +import '../models/models.dart'; +import '../youtube_explode_base.dart'; +import 'helpers_extension.dart'; +import 'playlist_extension.dart'; + +/// Channel extension for YoutubeExplode +extension ChannelExtension on YoutubeExplode { + static final _usernameRegMatchExp = + RegExp(r'youtube\..+?/user/(.*?)(?:\?|&|/|$)'); + + static final _idRegMatchExp = + RegExp(r'youtube\..+?/channel/(.*?)(?:\?|&|/|$)'); + + /// Returns the [Channel] associated with the given channelId. + /// Throws an [ArgumentError] if the channel id is not valid. + Future getChannel(String channelId) async { + if (!validateChannelId(channelId)) { + throw ArgumentError.value( + channelId, 'channelId', 'Invalid YouTube channel id.'); + } + + var channelPageHtml = await _getChannelPageHtml(channelId); + var channelTitle = channelPageHtml + .querySelector('meta[property="og:title"]') + .attributes['content']; + var channelImage = channelPageHtml + .querySelector('meta[property="og:image"]') + .attributes['content']; + + return Channel(channelId, channelTitle, Uri.parse(channelImage)); + } + + /// Get a channel id from a username. + /// Might not work properly. + Future getChannelId(String username) async { + if (!validateUsername(username)) { + throw ArgumentError.value( + username, 'username', 'Invalid YouTube username.'); + } + + var userPageHtml = await _getUserPageHtml(username); + + var channelUrl = userPageHtml + .querySelector('meta[property="og:url"]') + .attributes['content']; + + return channelUrl.replaceFirst('/channel/', ''); + } + + /// Returns all the videos uploaded by a channel up to [maxPages] count. + Future> getChannelUploads(String channelId, + [int maxPages = 5]) async { + if (!validateChannelId(channelId)) { + throw ArgumentError.value( + channelId, 'channelId', 'Invalid YouTube channel id.'); + } + + var playlistId = 'UU${channelId.replaceFirst('UC', '')}'; + var playlist = await getPlaylist(playlistId, maxPages); + + return playlist.videos; + } + + Future _getChannelPageHtml(String channelId) async { + var url = 'https://www.youtube.com/channel/$channelId?hl=en'; + var raw = (await client.get(url)).body; + + return html.parse(raw); + } + + Future _getUserPageHtml(String username) async { + var url = 'https://www.youtube.com/user/$username?hl=en'; + var raw = (await client.get(url)).body; + + return html.parse(raw); + } + + /// Returns true if [username] is a valid Youtube username. + static bool validateUsername(String username) { + if (username.isNullOrWhiteSpace) { + return false; + } + + if (username.length > 20) { + return false; + } + + return !RegExp(r'[^0-9a-zA-Z]').hasMatch(username); + } + + /// Parses a username from an url. + /// Returns null if the username is not found. + static String parseUsername(String url) { + if (url.isNullOrWhiteSpace) { + return null; + } + + var regMatch = _usernameRegMatchExp.firstMatch(url)?.group(1); + if (!regMatch.isNullOrWhiteSpace && validateUsername(regMatch)) { + return regMatch; + } + return null; + } + + /// Returns true if [channelId] is a valid Youtube channel id. + static bool validateChannelId(String channelId) { + if (channelId.isNullOrWhiteSpace) { + return false; + } + + channelId = channelId.toLowerCase(); + + if (!channelId.startsWith('uc')) { + return false; + } + + if (channelId.length != 24) { + return false; + } + + return !RegExp(r'[^0-9a-zA-Z]').hasMatch(channelId); + } + + /// Parses a channel id from an url. + /// Returns null if the username is not found. + static String parseChannelId(String url) { + if (url.isNullOrWhiteSpace) { + return null; + } + + var regMatch = _idRegMatchExp.firstMatch(url)?.group(1); + if (!regMatch.isNullOrWhiteSpace && validateChannelId(regMatch)) { + return regMatch; + } + return null; + } +} diff --git a/lib/src/extensions/extensions.dart b/lib/src/extensions/extensions.dart new file mode 100644 index 0000000..3c893ad --- /dev/null +++ b/lib/src/extensions/extensions.dart @@ -0,0 +1,3 @@ +export 'channel_extension.dart'; +export 'helpers_extension.dart'; +export 'playlist_extension.dart'; diff --git a/lib/src/extensions.dart b/lib/src/extensions/helpers_extension.dart similarity index 95% rename from lib/src/extensions.dart rename to lib/src/extensions/helpers_extension.dart index 4119de0..2a704da 100644 --- a/lib/src/extensions.dart +++ b/lib/src/extensions/helpers_extension.dart @@ -1,4 +1,4 @@ -import 'cipher/cipher_operations.dart'; +import '../cipher/cipher_operations.dart'; /// Utility for Strings. extension StringUtility on String { diff --git a/lib/src/playlist_extension.dart b/lib/src/extensions/playlist_extension.dart similarity index 95% rename from lib/src/playlist_extension.dart rename to lib/src/extensions/playlist_extension.dart index cedab29..45eca25 100644 --- a/lib/src/playlist_extension.dart +++ b/lib/src/extensions/playlist_extension.dart @@ -1,9 +1,9 @@ import 'dart:convert'; -import 'extensions.dart'; -import 'models/models.dart'; -import 'parser.dart' as parser; -import 'youtube_explode_base.dart'; +import '../models/models.dart'; +import '../parser.dart' as parser; +import '../youtube_explode_base.dart'; +import 'helpers_extension.dart'; /// Playlist extension for YoutubeExplode extension PlaylistExtension on YoutubeExplode { @@ -29,8 +29,7 @@ extension PlaylistExtension on YoutubeExplode { /// If the id is not valid an [ArgumentError] is thrown. Future getPlaylist(String playlistId, [int maxPages = 500]) async { if (!validatePlaylistId(playlistId)) { - throw ArgumentError.value( - playlistId, 'videoId', 'Invalid video id'); + throw ArgumentError.value(playlistId, 'videoId', 'Invalid video id'); } Map playlistJson; diff --git a/lib/src/models/channel.dart b/lib/src/models/channel.dart new file mode 100644 index 0000000..4bc01a1 --- /dev/null +++ b/lib/src/models/channel.dart @@ -0,0 +1,14 @@ +/// Information about a YouTube channel. +class Channel { + /// ID of this channel. + final String id; + + /// Title of this channel. + final String title; + + /// Logo image URL of this channel. + final Uri logoUrl; + + /// Initializes an instance of [Channel] + Channel(this.id, this.title, this.logoUrl); +} diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index 278b823..5c035bf 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -2,6 +2,7 @@ library youtube_explode.models; export 'audio_encoding.dart'; export 'audio_stream_info.dart'; +export 'channel.dart'; export 'container.dart'; export 'media_stream_info.dart'; export 'media_stream_info_set.dart'; diff --git a/lib/src/youtube_explode_base.dart b/lib/src/youtube_explode_base.dart index 721e599..70ac299 100644 --- a/lib/src/youtube_explode_base.dart +++ b/lib/src/youtube_explode_base.dart @@ -2,13 +2,13 @@ import 'dart:convert'; import 'dart:io'; import 'package:html/dom.dart'; -import 'package:http/http.dart' as http; import 'package:html/parser.dart' as html; +import 'package:http/http.dart' as http; + import 'cipher/cipher.dart'; -import 'extensions.dart'; +import 'extensions/extensions.dart'; import 'models/models.dart'; import 'parser.dart' as parser; -import 'playlist_extension.dart'; /// YoutubeExplode entry class. class YoutubeExplode { @@ -389,13 +389,33 @@ class YoutubeExplode { return null; } + /// Closes the youtube explode's http client. + void close() { + client.close(); + } + + /* Export the extension static members. */ + /// Parses a playlist [url] returning its id. /// If the [url] is a valid it is returned itself. static String parsePlaylistId(String url) => PlaylistExtension.parsePlaylistId(url); - /// Closes the youtube explode's http client. - void close() { - client.close(); - } + /// Returns true if [username] is a valid Youtube username. + static bool validateUsername(String username) => + ChannelExtension.validateUsername(username); + + /// Parses a username from an url. + /// Returns null if the username is not found. + static String parseUsername(String url) => + ChannelExtension.parseUsername(url); + + /// Returns true if [channelId] is a valid Youtube channel id. + static bool validateChannelId(String channelId) => + ChannelExtension.validateChannelId(channelId); + + /// Parses a channel id from an url. + /// Returns null if the username is not found. + static String parseChannelId(String url) => + ChannelExtension.parseChannelId(url); } diff --git a/lib/youtube_explode_dart.dart b/lib/youtube_explode_dart.dart index f5278e8..bb63240 100644 --- a/lib/youtube_explode_dart.dart +++ b/lib/youtube_explode_dart.dart @@ -1,5 +1,6 @@ library youtube_explode; +export 'src/extensions/extensions.dart' + hide StringUtility, ListDecipher, ListFirst; // Hide helper extensions. export 'src/models/models.dart'; -export 'src/playlist_extension.dart'; export 'src/youtube_explode_base.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index af2c3dd..c08817f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: youtube_explode_dart description: A port in dart of the youtube explode library. Support serveral API functions. -version: 0.0.1 +version: 0.0.2 homepage: https://github.com/Hexer10/youtube_explode_dart environment: