├── .gitignore ├── .travis.yml ├── src ├── Facades │ └── Youtube.php ├── config │ └── youtube.php ├── YoutubeServiceProvider.php ├── Rules │ └── ValidYoutubeVideo.php └── Youtube.php ├── phpunit.xml ├── composer.json ├── LICENSE ├── .github └── workflows │ └── tests.yml ├── README.md └── tests └── YoutubeTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | .idea 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.0 5 | - 7.1 6 | - 8.0 7 | 8 | #branches: 9 | # only: 10 | # - master 11 | 12 | before_script: 13 | - travis_retry composer self-update 14 | - travis_retry composer install --no-interaction --prefer-source 15 | 16 | script: 17 | - ./vendor/bin/phpunit 18 | -------------------------------------------------------------------------------- /src/Facades/Youtube.php: -------------------------------------------------------------------------------- 1 | env('YOUTUBE_API_KEY', 'YOUR_API_KEY') 14 | ]; 15 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./tests/ 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alaouy/youtube", 3 | "description": "Laravel PHP Facade/Wrapper for the Youtube Data API v3", 4 | "keywords": ["youtube", "api", "video", "laravel", "alaouy"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Mustapha Alaouy", 9 | "email": "alaouym@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.0|^8.0", 14 | "ext-curl": "*" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^6.1|^9.3" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "Alaouy\\Youtube\\": "src" 22 | } 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "Alaouy\\Youtube\\Tests\\": "tests" 27 | } 28 | }, 29 | "extra": { 30 | "laravel": { 31 | "providers": [ 32 | "Alaouy\\Youtube\\YoutubeServiceProvider" 33 | ], 34 | "aliases": { 35 | "Youtube": "Alaouy\\Youtube\\Facades\\Youtube" 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/YoutubeServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([$source => config_path('youtube.php')]); 19 | 20 | $this->mergeConfigFrom($source, 'youtube'); 21 | } 22 | 23 | /** 24 | * Register the service provider. 25 | * 26 | * @return void 27 | */ 28 | public function register() 29 | { 30 | $this->app->bind(Youtube::class, function () { 31 | return new Youtube(config('youtube.key')); 32 | }); 33 | 34 | $this->app->alias(Youtube::class, 'youtube'); 35 | } 36 | 37 | /** 38 | * Get the services provided by the provider. 39 | * 40 | * @return array 41 | */ 42 | public function provides() 43 | { 44 | return [Youtube::class]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mustapha Alaouy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/Rules/ValidYoutubeVideo.php: -------------------------------------------------------------------------------- 1 | Alaouy\Youtube\Facades\Youtube::class, 33 | ``` 34 | 35 | Publish config settings: 36 | ```bach 37 | $ php artisan vendor:publish --provider="Alaouy\Youtube\YoutubeServiceProvider" 38 | ``` 39 | 40 | Set your YouTube API key in the file: 41 | 42 | ```shell 43 | /config/youtube.php 44 | ``` 45 | 46 | Or in the .env file 47 | ```shell 48 | YOUTUBE_API_KEY = KEY 49 | ``` 50 | 51 | Or you can set the key programmatically at run time : 52 | ```php 53 | Youtube::setApiKey('KEY'); 54 | ``` 55 | 56 | ## Usage 57 | 58 | ```php 59 | // use Alaouy\Youtube\Facades\Youtube; 60 | 61 | 62 | // Return an STD PHP object 63 | $video = Youtube::getVideoInfo('rie-hPVJ7Sw'); 64 | 65 | // Get multiple videos info from an array 66 | $videoList = Youtube::getVideoInfo(['rie-hPVJ7Sw','iKHTawgyKWQ']); 67 | 68 | // Get localized video info 69 | $video = Youtube::getLocalizedVideoInfo('vjF9GgrY9c0', 'pl'); 70 | 71 | // Get comment threads by videoId 72 | $commentThreads = Youtube::getCommentThreadsByVideoId('zwiUB_Lh3iA'); 73 | 74 | // Get popular videos in a country, return an array of PHP objects 75 | $videoList = Youtube::getPopularVideos('us'); 76 | 77 | // Search playlists, channels and videos. return an array of PHP objects 78 | $results = Youtube::search('Android'); 79 | 80 | // Only search videos, return an array of PHP objects 81 | $videoList = Youtube::searchVideos('Android'); 82 | 83 | // Search only videos in a given channel, return an array of PHP objects 84 | $videoList = Youtube::searchChannelVideos('keyword', 'UCk1SpWNzOs4MYmr0uICEntg', 40); 85 | 86 | // List videos in a given channel, return an array of PHP objects 87 | $videoList = Youtube::listChannelVideos('UCk1SpWNzOs4MYmr0uICEntg', 40); 88 | 89 | $results = Youtube::searchAdvanced([ /* params */ ]); 90 | 91 | // Get channel data by channel handle (like https://www.youtube.com/@google), return an STD PHP object 92 | $channel = Youtube::getChannelByHandle('google'); 93 | 94 | // Get channel data by channel name, return an STD PHP object 95 | $channel = Youtube::getChannelByName('xdadevelopers'); 96 | 97 | // Get channel data by channel ID, return an STD PHP object 98 | $channel = Youtube::getChannelById('UCk1SpWNzOs4MYmr0uICEntg'); 99 | 100 | // Get playlist by ID, return an STD PHP object 101 | $playlist = Youtube::getPlaylistById('PL590L5WQmH8fJ54F369BLDSqIwcs-TCfs'); 102 | 103 | // Get playlists by multiple ID's, return an array of STD PHP objects 104 | $playlists = Youtube::getPlaylistById(['PL590L5WQmH8fJ54F369BLDSqIwcs-TCfs', 'PL590L5WQmH8cUsRyHkk1cPGxW0j5kmhm0']); 105 | 106 | // Get playlist by channel ID, return an array of PHP objects 107 | $playlists = Youtube::getPlaylistsByChannelId('UCk1SpWNzOs4MYmr0uICEntg'); 108 | 109 | // Get items in a playlist by playlist ID, return an array of PHP objects 110 | $playlistItems = Youtube::getPlaylistItemsByPlaylistId('PL590L5WQmH8fJ54F369BLDSqIwcs-TCfs'); 111 | 112 | // Get channel activities by channel ID, return an array of PHP objects 113 | $activities = Youtube::getActivitiesByChannelId('UCk1SpWNzOs4MYmr0uICEntg'); 114 | 115 | // Retrieve video ID from original YouTube URL 116 | $videoId = Youtube::parseVidFromURL('https://www.youtube.com/watch?v=moSFlvxnbgk'); 117 | // result: moSFlvxnbgk 118 | ``` 119 | 120 | ## Validation Rules 121 | 122 | ```php 123 | // use Alaouy\Youtube\Rules\ValidYoutubeVideo; 124 | 125 | 126 | // Validate a YouTube Video URL 127 | [ 128 | 'youtube_video_url' => ['bail', 'required', new ValidYoutubeVideo] 129 | ]; 130 | ``` 131 | 132 | You can use the bail rule in conjunction with this in order to prevent unnecessary queries. 133 | 134 | ## Basic Search Pagination 135 | 136 | ```php 137 | // Set default parameters 138 | $params = [ 139 | 'q' => 'Android', 140 | 'type' => 'video', 141 | 'part' => 'id, snippet', 142 | 'maxResults' => 50 143 | ]; 144 | 145 | // Make intial call. with second argument to reveal page info such as page tokens 146 | $search = Youtube::searchAdvanced($params, true); 147 | 148 | // Check if we have a pageToken 149 | if (isset($search['info']['nextPageToken'])) { 150 | $params['pageToken'] = $search['info']['nextPageToken']; 151 | } 152 | 153 | // Make another call and repeat 154 | $search = Youtube::searchAdvanced($params, true); 155 | 156 | // Add results key with info parameter set 157 | print_r($search['results']); 158 | 159 | /* Alternative approach with new built-in paginateResults function */ 160 | 161 | // Same params as before 162 | $params = [ 163 | 'q' => 'Android', 164 | 'type' => 'video', 165 | 'part' => 'id, snippet', 166 | 'maxResults' => 50 167 | ]; 168 | 169 | // An array to store page tokens so we can go back and forth 170 | $pageTokens = []; 171 | 172 | // Make inital search 173 | $search = Youtube::paginateResults($params, null); 174 | 175 | // Store token 176 | $pageTokens[] = $search['info']['nextPageToken']; 177 | 178 | // Go to next page in result 179 | $search = Youtube::paginateResults($params, $pageTokens[0]); 180 | 181 | // Store token 182 | $pageTokens[] = $search['info']['nextPageToken']; 183 | 184 | // Go to next page in result 185 | $search = Youtube::paginateResults($params, $pageTokens[1]); 186 | 187 | // Store token 188 | $pageTokens[] = $search['info']['nextPageToken']; 189 | 190 | // Go back a page 191 | $search = Youtube::paginateResults($params, $pageTokens[0]); 192 | 193 | // Add results key with info parameter set 194 | print_r($search['results']); 195 | ``` 196 | 197 | The pagination above is quite basic. Depending on what you are trying to achieve you may want to create a recursive function that traverses the results. 198 | 199 | ## Manual Class Instantiation 200 | 201 | ```php 202 | // Directly call the YouTube constructor 203 | $youtube = new Youtube(config('YOUTUBE_API_KEY')); 204 | 205 | // By default, if the $_SERVER['HTTP_HOST'] header is set, 206 | // it will be used as the `Referer` header. To override 207 | // this setting, set 'use-http-host' to false during 208 | // object construction: 209 | $youtube = new Youtube(config('YOUTUBE_API_KEY'), ['use-http-host' => false]); 210 | 211 | // This setting can also be set after the object was created 212 | $youtube->useHttpHost(false); 213 | ``` 214 | 215 | ## Run Unit Test 216 | If you have PHPUnit installed in your environment, run: 217 | 218 | ```bash 219 | $ phpunit 220 | ``` 221 | 222 | If you don't have PHPUnit installed, you can run the following: 223 | 224 | ```bash 225 | $ composer update 226 | $ ./vendor/bin/phpunit 227 | ``` 228 | 229 | ## Format of returned data 230 | The returned JSON is decoded as PHP objects (not Array). 231 | Please read the ["Reference" section](https://developers.google.com/youtube/v3/docs/) of the Official API doc. 232 | 233 | 234 | ## YouTube Data API v3 235 | - [YouTube Data API v3 Doc](https://developers.google.com/youtube/v3/) 236 | - [Obtain API key from Google API Console](https://console.developers.google.com) 237 | 238 | ## Donation 239 | If you find this project to be of use to you please consider buying me a cup of tea :) 240 | 241 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.me/malaouy) 242 | ## Credits 243 | Built on code from Madcoda's [php-youtube-api](https://github.com/madcoda/php-youtube-api). 244 | -------------------------------------------------------------------------------- /tests/YoutubeTest.php: -------------------------------------------------------------------------------- 1 | youtube = new Youtube(getenv("YOUTUBE_API_KEY")); 16 | } 17 | 18 | public function tearDown(): void 19 | { 20 | $this->youtube = null; 21 | } 22 | 23 | public function urlProvider() 24 | { 25 | return [ 26 | ['https://'], 27 | ['http://www.yuotube.com'], 28 | ]; 29 | } 30 | 31 | public function testConstructorFail() 32 | { 33 | $this->expectException(\Exception::class); 34 | 35 | $this->youtube = new Youtube(array()); 36 | } 37 | 38 | 39 | public function testConstructorFail2() 40 | { 41 | $this->expectException(\Exception::class); 42 | $this->youtube = new Youtube(''); 43 | } 44 | 45 | public function testSetApiKey() 46 | { 47 | $this->youtube->setApiKey('new_api_key'); 48 | 49 | $this->assertEquals($this->youtube->getApiKey(), 'new_api_key'); 50 | } 51 | 52 | 53 | public function testInvalidApiKey() 54 | { 55 | $this->expectException(\Exception::class); 56 | 57 | $this->youtube = new Youtube(array('key' => 'nonsense')); 58 | $vID = 'rie-hPVJ7Sw'; 59 | $this->youtube->getVideoInfo($vID); 60 | } 61 | 62 | public function testGetCategories() 63 | { 64 | $region = 'US'; 65 | $part = ['snippet']; 66 | $response = $this->youtube->getCategories($region,$part); 67 | 68 | $this->assertNotNull('response'); 69 | $this->assertEquals('youtube#videoCategory', $response[0]->kind); 70 | //add all these assertions here in case the api is changed, 71 | //we can detect it instantly 72 | $this->assertObjectHasProperty('snippet', $response[0]); 73 | } 74 | 75 | public function testGetCommentThreadsByVideoId() 76 | { 77 | $videoId = 'rie-hPVJ7Sw'; 78 | $response = $this->youtube->getCommentThreadsByVideoId($videoId); 79 | 80 | $this->assertNotNull('response'); 81 | $this->assertEquals('youtube#commentThread', $response[0]->kind); 82 | //add all these assertions here in case the api is changed, 83 | //we can detect it instantly 84 | $this->assertObjectHasProperty('etag', $response[0]); 85 | $this->assertObjectHasProperty('id', $response[0]); 86 | $this->assertObjectHasProperty('snippet', $response[0]); 87 | } 88 | 89 | public function testGetVideoInfo() 90 | { 91 | $vID = 'rie-hPVJ7Sw'; 92 | $response = $this->youtube->getVideoInfo($vID); 93 | 94 | $this->assertEquals($vID, $response->id); 95 | $this->assertNotNull('response'); 96 | $this->assertEquals('youtube#video', $response->kind); 97 | //add all these assertions here in case the api is changed, 98 | //we can detect it instantly 99 | $this->assertObjectHasProperty('statistics', $response); 100 | $this->assertObjectHasProperty('status', $response); 101 | $this->assertObjectHasProperty('snippet', $response); 102 | $this->assertObjectHasProperty('contentDetails', $response); 103 | } 104 | 105 | public function testGetLocalizedVideoInfo() 106 | { 107 | $videoId = 'vjF9GgrY9c0'; 108 | $language = 'pl'; 109 | 110 | $response = $this->youtube->getLocalizedVideoInfo($videoId, $language); 111 | 112 | $this->assertNotNull('response'); 113 | $this->assertEquals('youtube#video', $response->kind); 114 | //add all these assertions here in case the api is changed, 115 | //we can detect it instantly 116 | $this->assertObjectHasProperty('statistics', $response); 117 | $this->assertObjectHasProperty('status', $response); 118 | $this->assertObjectHasProperty('snippet', $response); 119 | $this->assertObjectHasProperty('contentDetails', $response); 120 | } 121 | 122 | public function testGetVideoInfoMultiple() 123 | { 124 | $vIDs = ['rie-hPVJ7Sw', 'iKHTawgyKWQ']; 125 | $response = $this->youtube->getVideoInfo($vIDs); 126 | 127 | $this->assertEquals($vIDs[0], $response[0]->id); 128 | $this->assertNotNull('response'); 129 | $this->assertEquals('youtube#video', $response[0]->kind); 130 | //add all these assertions here in case the api is changed, 131 | //we can detect it instantly 132 | $this->assertObjectHasProperty('statistics', $response[0]); 133 | $this->assertObjectHasProperty('status', $response[0]); 134 | $this->assertObjectHasProperty('snippet', $response[0]); 135 | $this->assertObjectHasProperty('contentDetails', $response[0]); 136 | } 137 | 138 | public function testGetPopularVideos() 139 | { 140 | $maxResult = rand(10, 30); 141 | $regionCode = 'us'; 142 | $videoCategoryId = 0; 143 | $part = ['id', 'snippet', 'contentDetails', 'player', 'statistics', 'status']; 144 | $response = $this->youtube->getPopularVideos($regionCode, $maxResult, $part, $videoCategoryId); 145 | 146 | $this->assertNotNull('response'); 147 | $this->assertEquals($maxResult, count($response)); 148 | $this->assertEquals('youtube#video', $response[0]->kind); 149 | $this->assertObjectHasProperty('statistics', $response[0]); 150 | $this->assertObjectHasProperty('status', $response[0]); 151 | $this->assertObjectHasProperty('snippet', $response[0]); 152 | $this->assertObjectHasProperty('contentDetails', $response[0]); 153 | } 154 | 155 | public function testSearch() 156 | { 157 | $limit = rand(3, 10); 158 | $response = $this->youtube->search('Android', $limit); 159 | // $this->assertEquals($limit, count($response)); 160 | $this->assertEquals('youtube#searchResult', $response[0]->kind); 161 | } 162 | 163 | public function testSearchVideos() 164 | { 165 | $limit = rand(3, 10); 166 | $response = $this->youtube->searchVideos('Android', $limit); 167 | $this->assertEquals($limit, count($response)); 168 | $this->assertEquals('youtube#searchResult', $response[0]->kind); 169 | $this->assertEquals('youtube#video', $response[0]->id->kind); 170 | } 171 | 172 | public function testSearchChannelVideos() 173 | { 174 | $limit = rand(3, 10); 175 | $response = $this->youtube->searchChannelVideos('Android', 'UCVHFbqXqoYvEWM1Ddxl0QDg', $limit); 176 | $this->assertEquals($limit, count($response)); 177 | $this->assertEquals('youtube#searchResult', $response[0]->kind); 178 | $this->assertEquals('youtube#video', $response[0]->id->kind); 179 | } 180 | 181 | public function testListChannelVideos() 182 | { 183 | $limit = rand(3, 10); 184 | $response = $this->youtube->listChannelVideos('UCVHFbqXqoYvEWM1Ddxl0QDg', $limit); 185 | // $this->assertEquals($limit, count($response)); 186 | $this->assertEquals('youtube#searchResult', $response[0]->kind); 187 | $this->assertEquals('youtube#video', $response[0]->id->kind); 188 | } 189 | 190 | public function testSearchAdvanced() 191 | { 192 | //TODO 193 | } 194 | 195 | public function testGetChannelByHandle() 196 | { 197 | $response = $this->youtube->getChannelByHandle('google'); 198 | 199 | $this->assertEquals('youtube#channel', $response->kind); 200 | //This is not a safe Assertion because the name can change, but include it anyway 201 | $this->assertEquals('Google', $response->snippet->title); 202 | //add all these assertions here in case the api is changed, 203 | //we can detect it instantly 204 | $this->assertObjectHasProperty('snippet', $response); 205 | $this->assertObjectHasProperty('contentDetails', $response); 206 | $this->assertObjectHasProperty('statistics', $response); 207 | } 208 | 209 | public function testGetChannelByName() 210 | { 211 | $response = $this->youtube->getChannelByName('Google'); 212 | 213 | $this->assertEquals('youtube#channel', $response->kind); 214 | //This is not a safe Assertion because the name can change, but include it anyway 215 | $this->assertEquals('Google', $response->snippet->title); 216 | //add all these assertions here in case the api is changed, 217 | //we can detect it instantly 218 | $this->assertObjectHasProperty('snippet', $response); 219 | $this->assertObjectHasProperty('contentDetails', $response); 220 | $this->assertObjectHasProperty('statistics', $response); 221 | } 222 | 223 | public function testGetChannelById() 224 | { 225 | $channelId = 'UCk1SpWNzOs4MYmr0uICEntg'; 226 | $response = $this->youtube->getChannelById($channelId); 227 | 228 | $this->assertEquals('youtube#channel', $response->kind); 229 | $this->assertEquals($channelId, $response->id); 230 | $this->assertObjectHasProperty('snippet', $response); 231 | $this->assertObjectHasProperty('contentDetails', $response); 232 | $this->assertObjectHasProperty('statistics', $response); 233 | } 234 | 235 | public function testGetPlaylistsByChannelId() 236 | { 237 | $channelId = 'UCK8sQmJBp8GCxrOtXWBpyEA'; 238 | $response = $this->youtube->getPlaylistsByChannelId($channelId); 239 | 240 | $this->assertTrue(count($response) > 0); 241 | $this->assertEquals('youtube#playlist', $response['results'][0]->kind); 242 | $this->assertEquals('Google', $response['results'][0]->snippet->channelTitle); 243 | } 244 | 245 | public function testGetPlaylistById() 246 | { 247 | //get one of the playlist 248 | $channelId = 'UCK8sQmJBp8GCxrOtXWBpyEA'; 249 | $response = $this->youtube->getPlaylistsByChannelId($channelId); 250 | $playlist = $response['results'][0]; 251 | 252 | $response = $this->youtube->getPlaylistById($playlist->id); 253 | $this->assertEquals('youtube#playlist', $response->kind); 254 | } 255 | 256 | public function testGetPlaylistByMultipleIds() 257 | { 258 | //get one of the playlist 259 | $channelId = 'UCK8sQmJBp8GCxrOtXWBpyEA'; 260 | $response = $this->youtube->getPlaylistsByChannelId($channelId); 261 | $playlists = $response['results']; 262 | 263 | $response = $this->youtube->getPlaylistById([$playlists[0]->id, $playlists[1]->id]); 264 | $this->assertEquals('youtube#playlist', $response[0]->kind); 265 | $this->assertEquals('youtube#playlist', $response[1]->kind); 266 | } 267 | 268 | public function testGetPlaylistItemsByPlaylistId() 269 | { 270 | $GOOGLE_ZEITGEIST_PLAYLIST = 'PL590L5WQmH8fJ54F369BLDSqIwcs-TCfs'; 271 | $response = $this->youtube->getPlaylistItemsByPlaylistId($GOOGLE_ZEITGEIST_PLAYLIST); 272 | 273 | $data = $response['results']; 274 | $this->assertTrue(count($data) > 0); 275 | $this->assertEquals('youtube#playlistItem', $data[0]->kind); 276 | } 277 | 278 | /** 279 | * @dataProvider urlProvider 280 | */ 281 | public function testParseVIdFromURLException($url) 282 | { 283 | $this->expectException(\Exception::class); 284 | $vId = $this->youtube->parseVidFromURL($url); 285 | } 286 | 287 | public function testParseVIdException() 288 | { 289 | $this->expectException(\Exception::class); 290 | $vId = $this->youtube->parseVidFromURL('http://www.facebook.com'); 291 | } 292 | 293 | public function testGetActivitiesByChannelId() 294 | { 295 | $channelId = 'UCK8sQmJBp8GCxrOtXWBpyEA'; 296 | $response = $this->youtube->getActivitiesByChannelId($channelId); 297 | $this->assertTrue(count($response) > 0); 298 | $this->assertEquals('youtube#activity', $response[0]->kind); 299 | // $this->assertEquals('Google', $response[0]->snippet->channelTitle); 300 | } 301 | 302 | 303 | public function testGetActivitiesByChannelIdException() 304 | { 305 | $channelId = ''; 306 | 307 | $this->expectException(\InvalidArgumentException::class); 308 | 309 | $response = $this->youtube->getActivitiesByChannelId($channelId); 310 | } 311 | 312 | public function testGetChannelFromURL() 313 | { 314 | $urls = [ 315 | 'https://www.youtube.com/account_notifications' => false, 316 | 'https://www.youtube.com/ads/' => false, 317 | 'https://www.youtube.com/c/Ecolinguist' => false, 318 | 'https://www.youtube.com/feed/library' => false, 319 | 'https://www.youtube.com/gaming' => false, 320 | 'https://www.youtube.com/howyoutubeworks' => false, 321 | 'https://www.youtube.com/howyoutubeworks/product-features/search/' => false, 322 | 'https://www.youtube.com/shorts/lXSwVeKW1QE' => false, 323 | 'https://www.youtube.com/results' => false, 324 | 'https://www.youtube.com/results?search_query=laravel' => false, 325 | 'https://www.youtube.com/t/terms' => false, 326 | 'https://www.youtube.com/upload' => false, 327 | 'https://www.youtube.com/user/Google' => 'UCK8sQmJBp8GCxrOtXWBpyEA', 328 | 'https://www.youtube.com/yt' => false, 329 | 'https://www.youtube.com/yt/about/policies/' => false, 330 | ]; 331 | 332 | foreach ($urls as $url => $result) { 333 | try { 334 | $channel = $this->youtube->getChannelFromURL($url); 335 | $this->assertEquals($channel->id, $result); 336 | } catch (\Exception $e) { 337 | $this->assertEquals(false, $result); 338 | } 339 | } 340 | } 341 | 342 | /** 343 | * Test skipped for now, since the API returns Error 500 344 | */ 345 | public function testNotFoundAPICall() 346 | { 347 | $vID = 'Utn7NBtbHL4'; //an deleted video 348 | $response = $this->youtube->getVideoInfo($vID); 349 | $this->assertFalse($response); 350 | } 351 | 352 | /** 353 | * Test skipped for now, since the API returns Error 500 354 | * 355 | */ 356 | public function testNotFoundAPICall2() 357 | { 358 | $channelId = 'non_exist_channelid'; 359 | 360 | $this->expectException(\Exception::class); 361 | 362 | $response = $this->youtube->getPlaylistsByChannelId($channelId); 363 | $this->assertEquals($response->getStatusCode(), 404); 364 | } 365 | 366 | /** 367 | * @dataProvider youtubeUrlProvider 368 | */ 369 | public function testParseVidFromURL($url, $expectedVideoId) 370 | { 371 | $vId = $this->youtube->parseVidFromURL($url); 372 | $this->assertEquals($expectedVideoId, $vId); 373 | } 374 | 375 | public function youtubeUrlProvider() 376 | { 377 | return [ 378 | ['http://www.youtube.com/watch?v=1FJHYqE0RDg', '1FJHYqE0RDg'], 379 | ['http://youtu.be/1FJHYqE0RDg', '1FJHYqE0RDg'], 380 | ['http://youtube.com/embed/1FJHYqE0RDg', '1FJHYqE0RDg'], 381 | ['https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'dQw4w9WgXcQ'], 382 | ['https://youtu.be/dQw4w9WgXcQ', 'dQw4w9WgXcQ'], 383 | ['https://youtu.be/ymNFyxvIdaM?si=7Ei20mEhPzrS_nZs', 'ymNFyxvIdaM'], 384 | ['https://www.youtube.com/watch?v=ymNFyxvIdaM&ab_channel=BomfunkMCsVEVO', 'ymNFyxvIdaM'], 385 | ['https://www.youtube.com/embed/dQw4w9WgXcQ', 'dQw4w9WgXcQ'], 386 | ['https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=PL590L5WQmH8dKahcfw2Uw8Ud28p4C9XDP', 'dQw4w9WgXcQ'], 387 | ['https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=60', 'dQw4w9WgXcQ'], 388 | ['https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=youtu.be&t=90', 'dQw4w9WgXcQ'], 389 | ['https://www.youtube.com/v/dQw4w9WgXcQ?version=3&autohide=1', 'dQw4w9WgXcQ'], 390 | ['https://www.youtube.com/live/JAkh_0QKtMg?si=roYDVwBJ4csi6df_', 'JAkh_0QKtMg'], 391 | ['https://www.youtube.com/shorts/lXSwVeKW1QE', 'lXSwVeKW1QE'], 392 | ['https://www.youtube.com/shorts/abc123_-XYZ', 'abc123_-XYZ'], 393 | ['https://youtube.com/shorts/dQw4w9WgXcQ', 'dQw4w9WgXcQ'] 394 | ]; 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/Youtube.php: -------------------------------------------------------------------------------- 1 | 'https://www.googleapis.com/youtube/v3/videoCategories', 18 | 'videos.list' => 'https://www.googleapis.com/youtube/v3/videos', 19 | 'search.list' => 'https://www.googleapis.com/youtube/v3/search', 20 | 'channels.list' => 'https://www.googleapis.com/youtube/v3/channels', 21 | 'playlists.list' => 'https://www.googleapis.com/youtube/v3/playlists', 22 | 'playlistItems.list' => 'https://www.googleapis.com/youtube/v3/playlistItems', 23 | 'activities' => 'https://www.googleapis.com/youtube/v3/activities', 24 | 'commentThreads.list' => 'https://www.googleapis.com/youtube/v3/commentThreads', 25 | ]; 26 | 27 | /** 28 | * @var array 29 | */ 30 | public $youtube_reserved_urls = [ 31 | '\/about\b', 32 | '\/account\b', 33 | '\/account_(.*)', 34 | '\/ads\b', 35 | '\/creators\b', 36 | '\/feed\b', 37 | '\/feed\/(.*)', 38 | '\/gaming\b', 39 | '\/gaming\/(.*)', 40 | '\/howyoutubeworks\b', 41 | '\/howyoutubeworks\/(.*)', 42 | '\/new\b', 43 | '\/playlist\b', 44 | '\/playlist\/(.*)', 45 | '\/reporthistory', 46 | '\/results\b', 47 | '\/shorts\b', 48 | '\/shorts\/(.*)', 49 | '\/t\/(.*)', 50 | '\/upload\b', 51 | '\/yt\/(.*)', 52 | ]; 53 | 54 | /** 55 | * @var array 56 | */ 57 | public $page_info = []; 58 | 59 | /** 60 | * @var array 61 | */ 62 | protected $config = []; 63 | 64 | /** 65 | * Constructor 66 | * $youtube = new Youtube(['key' => 'KEY HERE']) 67 | * 68 | * @param string $key 69 | * @throws \Exception 70 | */ 71 | public function __construct($key, $config = []) 72 | { 73 | if (is_string($key) && !empty($key)) { 74 | $this->youtube_key = $key; 75 | } else { 76 | throw new \Exception('Google API key is Required, please visit https://console.developers.google.com/'); 77 | } 78 | $this->config['use-http-host'] = isset($config['use-http-host']) ? $config['use-http-host'] : false; 79 | } 80 | 81 | /** 82 | * @param $setting 83 | * @return Youtube 84 | */ 85 | public function useHttpHost($setting) 86 | { 87 | $this->config['use-http-host'] = !!$setting; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * @param $key 94 | * @return Youtube 95 | */ 96 | public function setApiKey($key) 97 | { 98 | $this->youtube_key = $key; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * @return string 105 | */ 106 | public function getApiKey() 107 | { 108 | return $this->youtube_key; 109 | } 110 | 111 | /** 112 | * @param $regionCode 113 | * @return \StdClass 114 | * @throws \Exception 115 | */ 116 | public function getCategories($regionCode = 'US', $part = ['snippet']) 117 | { 118 | $API_URL = $this->getApi('categories.list'); 119 | $params = [ 120 | 'key' => $this->youtube_key, 121 | 'part' => implode(',', $part), 122 | 'regionCode' => $regionCode 123 | ]; 124 | 125 | $apiData = $this->api_get($API_URL, $params); 126 | return $this->decodeMultiple($apiData); 127 | } 128 | 129 | /** 130 | * @param string $videoId Instructs the API to return comment threads containing comments about the specified channel. (The response will not include comments left on videos that the channel uploaded.) 131 | * @param integer $maxResults Specifies the maximum number of items that should be returned in the result set. Acceptable values are 1 to 100, inclusive. The default value is 20. 132 | * @param string $order Specifies the order in which the API response should list comment threads. Valid values are: time, relevance. 133 | * @param array $part Specifies a list of one or more commentThread resource properties that the API response will include. 134 | * @param bool $pageInfo Add page info to returned array. 135 | * @return array 136 | * @throws \Exception 137 | */ 138 | public function getCommentThreadsByVideoId($videoId = null, $maxResults = 20, $order = null, $part = ['id', 'replies', 'snippet'], $pageInfo = false) { 139 | 140 | return $this->getCommentThreads(null, null, $videoId, $maxResults, $order, $part, $pageInfo); 141 | } 142 | 143 | /** 144 | * @param string $channelId Instructs the API to return comment threads containing comments about the specified channel. (The response will not include comments left on videos that the channel uploaded.) 145 | * @param string $id Specifies a comma-separated list of comment thread IDs for the resources that should be retrieved. 146 | * @param string $videoId Instructs the API to return comment threads containing comments about the specified channel. (The response will not include comments left on videos that the channel uploaded.) 147 | * @param integer $maxResults Specifies the maximum number of items that should be returned in the result set. Acceptable values are 1 to 100, inclusive. The default value is 20. 148 | * @param string $order Specifies the order in which the API response should list comment threads. Valid values are: time, relevance. 149 | * @param array $part Specifies a list of one or more commentThread resource properties that the API response will include. 150 | * @param bool $pageInfo Add page info to returned array. 151 | * @return array 152 | * @throws \Exception 153 | */ 154 | public function getCommentThreads($channelId = null, $id = null, $videoId = null, $maxResults = 20, $order = null, $part = ['id', 'replies', 'snippet'], $pageInfo = false) 155 | { 156 | $API_URL = $this->getApi('commentThreads.list'); 157 | 158 | $params = array_filter([ 159 | 'channelId' => $channelId, 160 | 'id' => $id, 161 | 'videoId' => $videoId, 162 | 'maxResults' => $maxResults, 163 | 'part' => implode(',', $part), 164 | 'order' => $order, 165 | ]); 166 | 167 | $apiData = $this->api_get($API_URL, $params); 168 | 169 | if ($pageInfo) { 170 | return [ 171 | 'results' => $this->decodeList($apiData), 172 | 'info' => $this->page_info, 173 | ]; 174 | } else { 175 | return $this->decodeList($apiData); 176 | } 177 | } 178 | 179 | /** 180 | * @param $vId 181 | * @param array $part 182 | * @return \StdClass 183 | * @throws \Exception 184 | */ 185 | public function getVideoInfo($vId, $part = ['id', 'snippet', 'contentDetails', 'player', 'statistics', 'status']) 186 | { 187 | $API_URL = $this->getApi('videos.list'); 188 | $params = [ 189 | 'id' => is_array($vId) ? implode(',', $vId) : $vId, 190 | 'key' => $this->youtube_key, 191 | 'part' => implode(',', $part), 192 | ]; 193 | 194 | $apiData = $this->api_get($API_URL, $params); 195 | 196 | if (is_array($vId)) { 197 | return $this->decodeMultiple($apiData); 198 | } 199 | 200 | return $this->decodeSingle($apiData); 201 | } 202 | 203 | /** 204 | * Gets localized video info by language (f.ex. de) by adding this parameter after video id 205 | * Youtube::getLocalizedVideoInfo($video->url, 'de') 206 | * 207 | * @param $vId 208 | * @param $language 209 | * @param array $part 210 | * @return \StdClass 211 | * @throws \Exception 212 | */ 213 | 214 | public function getLocalizedVideoInfo($vId, $language, $part = ['id', 'snippet', 'contentDetails', 'player', 'statistics', 'status']) { 215 | 216 | $API_URL = $this->getApi('videos.list'); 217 | $params = [ 218 | 'id' => is_array($vId) ? implode(',', $vId) : $vId, 219 | 'key' => $this->youtube_key, 220 | 'hl' => $language, 221 | 'part' => implode(',', $part), 222 | ]; 223 | 224 | $apiData = $this->api_get($API_URL, $params); 225 | 226 | if (is_array($vId)) { 227 | return $this->decodeMultiple($apiData); 228 | } 229 | 230 | return $this->decodeSingle($apiData); 231 | } 232 | 233 | /** 234 | * Gets popular videos for a specific region (ISO 3166-1 alpha-2) 235 | * 236 | * @param $regionCode 237 | * @param integer $maxResults 238 | * @param array $part 239 | * @return array 240 | */ 241 | public function getPopularVideos($regionCode, $maxResults = 10, $part = ['id', 'snippet', 'contentDetails', 'player', 'statistics', 'status'], $videoCategoryId = 0) 242 | { 243 | $API_URL = $this->getApi('videos.list'); 244 | $params = [ 245 | 'chart' => 'mostPopular', 246 | 'part' => implode(',', $part), 247 | 'regionCode' => $regionCode, 248 | 'videoCategoryId' => $videoCategoryId, 249 | 'maxResults' => $maxResults, 250 | ]; 251 | 252 | $apiData = $this->api_get($API_URL, $params); 253 | 254 | return $this->decodeList($apiData); 255 | } 256 | 257 | /** 258 | * Simple search interface, this search all stuffs 259 | * and order by relevance 260 | * 261 | * @param $q 262 | * @param integer $maxResults 263 | * @param array $part 264 | * @return array 265 | */ 266 | public function search($q, $maxResults = 10, $part = ['id', 'snippet']) 267 | { 268 | $params = [ 269 | 'q' => $q, 270 | 'part' => implode(',', $part), 271 | 'maxResults' => $maxResults, 272 | ]; 273 | 274 | return $this->searchAdvanced($params); 275 | } 276 | 277 | /** 278 | * Search only videos 279 | * 280 | * @param string $q Query 281 | * @param integer $maxResults number of results to return 282 | * @param string $order Order by 283 | * @param array $part 284 | * @return \StdClass API results 285 | */ 286 | public function searchVideos($q, $maxResults = 10, $order = null, $part = ['id']) 287 | { 288 | $params = [ 289 | 'q' => $q, 290 | 'type' => 'video', 291 | 'part' => implode(',', $part), 292 | 'maxResults' => $maxResults, 293 | ]; 294 | if (!empty($order)) { 295 | $params['order'] = $order; 296 | } 297 | 298 | return $this->searchAdvanced($params); 299 | } 300 | 301 | /** 302 | * Search only videos in the channel 303 | * 304 | * @param string $q 305 | * @param string $channelId 306 | * @param integer $maxResults 307 | * @param string $order 308 | * @param array $part 309 | * @param $pageInfo 310 | * @return array 311 | */ 312 | public function searchChannelVideos($q, $channelId, $maxResults = 10, $order = null, $part = ['id', 'snippet'], $pageInfo = false) 313 | { 314 | $params = [ 315 | 'q' => $q, 316 | 'type' => 'video', 317 | 'channelId' => $channelId, 318 | 'part' => implode(',', $part), 319 | 'maxResults' => $maxResults, 320 | ]; 321 | if (!empty($order)) { 322 | $params['order'] = $order; 323 | } 324 | 325 | return $this->searchAdvanced($params, $pageInfo); 326 | } 327 | 328 | /** 329 | * List videos in the channel 330 | * 331 | * @param string $channelId 332 | * @param integer $maxResults 333 | * @param string $order 334 | * @param array $part 335 | * @param $pageInfo 336 | * @return array 337 | */ 338 | public function listChannelVideos($channelId, $maxResults = 10, $order = null, $part = ['id', 'snippet'], $pageInfo = false) 339 | { 340 | $params = [ 341 | 'type' => 'video', 342 | 'channelId' => $channelId, 343 | 'part' => implode(',', $part), 344 | 'maxResults' => $maxResults, 345 | ]; 346 | if (!empty($order)) { 347 | $params['order'] = $order; 348 | } 349 | 350 | return $this->searchAdvanced($params, $pageInfo); 351 | } 352 | 353 | /** 354 | * Generic Search interface, use any parameters specified in 355 | * the API reference 356 | * 357 | * @param $params 358 | * @param $pageInfo 359 | * @return array 360 | * @throws \Exception 361 | */ 362 | public function searchAdvanced($params, $pageInfo = false) 363 | { 364 | $API_URL = $this->getApi('search.list'); 365 | 366 | if (empty($params) || (!isset($params['q']) && !isset($params['channelId']) && !isset($params['videoCategoryId']))) { 367 | throw new \InvalidArgumentException('at least the Search query or Channel ID or videoCategoryId must be supplied'); 368 | } 369 | 370 | $apiData = $this->api_get($API_URL, $params); 371 | if ($pageInfo) { 372 | return [ 373 | 'results' => $this->decodeList($apiData), 374 | 'info' => $this->page_info, 375 | ]; 376 | } else { 377 | return $this->decodeList($apiData); 378 | } 379 | } 380 | 381 | /** 382 | * Generic Search Paginator, use any parameters specified in 383 | * the API reference and pass through nextPageToken as $token if set. 384 | * 385 | * @param $params 386 | * @param $token 387 | * @return array 388 | */ 389 | public function paginateResults($params, $token = null) 390 | { 391 | if (!is_null($token)) { 392 | $params['pageToken'] = $token; 393 | } 394 | 395 | if (!empty($params)) { 396 | return $this->searchAdvanced($params, true); 397 | } 398 | } 399 | 400 | /** 401 | * @param $username 402 | * @param array $optionalParams 403 | * @param array $part 404 | * @return \StdClass 405 | * @throws \Exception 406 | */ 407 | public function getChannelByName($username, $optionalParams = [], $part = ['id', 'snippet', 'contentDetails', 'statistics']) 408 | { 409 | $API_URL = $this->getApi('channels.list'); 410 | $params = [ 411 | 'forUsername' => $username, 412 | 'part' => implode(',', $part), 413 | ]; 414 | 415 | $params = array_merge($params, $optionalParams); 416 | 417 | $apiData = $this->api_get($API_URL, $params); 418 | 419 | return $this->decodeSingle($apiData); 420 | } 421 | 422 | /** 423 | * @param $handleName 424 | * @param array $optionalParams 425 | * @param array $part 426 | * @return \StdClass 427 | * @throws \Exception 428 | */ 429 | public function getChannelByHandle($handleName, $optionalParams = [], $part = ['id', 'snippet', 'contentDetails', 'statistics']) 430 | { 431 | $API_URL = $this->getApi('channels.list'); 432 | $params = [ 433 | 'forHandle' => $handleName, 434 | 'part' => implode(',', $part), 435 | ]; 436 | 437 | $params = array_merge($params, $optionalParams); 438 | 439 | $apiData = $this->api_get($API_URL, $params); 440 | 441 | return $this->decodeSingle($apiData); 442 | } 443 | 444 | /** 445 | * @param $username 446 | * @param $maxResults 447 | * @param $part 448 | * @return false|\StdClass 449 | * @throws \Exception 450 | */ 451 | public function searchChannelByName($username, $maxResults = 1, $part = ['id', 'snippet']) 452 | { 453 | $params = [ 454 | 'q' => $username, 455 | 'part' => implode(',', $part), 456 | 'type' => 'channel', 457 | 'maxResults' => $maxResults, 458 | ]; 459 | 460 | $search = $this->searchAdvanced($params); 461 | 462 | if (!empty($search[0]->snippet->channelId)) { 463 | $channelId = $search[0]->snippet->channelId; 464 | return $this->getChannelById($channelId); 465 | } 466 | } 467 | 468 | /** 469 | * @param $id 470 | * @param array $optionalParams 471 | * @param array $part 472 | * @return \StdClass 473 | * @throws \Exception 474 | */ 475 | public function getChannelById($id, $optionalParams = [], $part = ['id', 'snippet', 'contentDetails', 'statistics']) 476 | { 477 | $API_URL = $this->getApi('channels.list'); 478 | $params = [ 479 | 'id' => is_array($id) ? implode(',', $id) : $id, 480 | 'part' => implode(',', $part), 481 | ]; 482 | 483 | $params = array_merge($params, $optionalParams); 484 | 485 | $apiData = $this->api_get($API_URL, $params); 486 | 487 | if (is_array($id)) { 488 | return $this->decodeMultiple($apiData); 489 | } 490 | 491 | return $this->decodeSingle($apiData); 492 | } 493 | 494 | /** 495 | * @param string $channelId 496 | * @param array $optionalParams 497 | * @param array $part 498 | * @return array 499 | * @throws \Exception 500 | */ 501 | public function getPlaylistsByChannelId($channelId, $optionalParams = [], $part = ['id', 'snippet', 'status']) 502 | { 503 | $API_URL = $this->getApi('playlists.list'); 504 | $params = [ 505 | 'channelId' => $channelId, 506 | 'part' => implode(',', $part) 507 | ]; 508 | 509 | $params = array_merge($params, $optionalParams); 510 | 511 | $apiData = $this->api_get($API_URL, $params); 512 | 513 | $result = ['results' => $this->decodeList($apiData)]; 514 | $result['info']['totalResults'] = (isset($this->page_info['totalResults']) ? $this->page_info['totalResults'] : 0); 515 | $result['info']['nextPageToken'] = (isset($this->page_info['nextPageToken']) ? $this->page_info['nextPageToken'] : false); 516 | $result['info']['prevPageToken'] = (isset($this->page_info['prevPageToken']) ? $this->page_info['prevPageToken'] : false); 517 | 518 | return $result; 519 | } 520 | 521 | /** 522 | * @param $id 523 | * @param $part 524 | * @return \StdClass 525 | * @throws \Exception 526 | */ 527 | public function getPlaylistById($id, $part = ['id', 'snippet', 'status']) 528 | { 529 | $API_URL = $this->getApi('playlists.list'); 530 | $params = [ 531 | 'id' => is_array($id)? implode(',', $id) : $id, 532 | 'part' => implode(',', $part), 533 | ]; 534 | $apiData = $this->api_get($API_URL, $params); 535 | 536 | if (is_array($id)) { 537 | return $this->decodeMultiple($apiData); 538 | } 539 | 540 | return $this->decodeSingle($apiData); 541 | } 542 | 543 | /** 544 | * @param string $playlistId 545 | * @param string $pageToken 546 | * @param integer $maxResults 547 | * @param array $part 548 | * @return array 549 | * @throws \Exception 550 | */ 551 | public function getPlaylistItemsByPlaylistId($playlistId, $pageToken = '', $maxResults = 50, $part = ['id', 'snippet', 'contentDetails', 'status']) 552 | { 553 | $API_URL = $this->getApi('playlistItems.list'); 554 | $params = [ 555 | 'playlistId' => $playlistId, 556 | 'part' => implode(',', $part), 557 | 'maxResults' => $maxResults, 558 | ]; 559 | 560 | // Pass page token if it is given, an empty string won't change the api response 561 | $params['pageToken'] = $pageToken; 562 | 563 | $apiData = $this->api_get($API_URL, $params); 564 | $result = ['results' => $this->decodeList($apiData)]; 565 | $result['info']['totalResults'] = (isset($this->page_info['totalResults']) ? $this->page_info['totalResults'] : 0); 566 | $result['info']['nextPageToken'] = (isset($this->page_info['nextPageToken']) ? $this->page_info['nextPageToken'] : false); 567 | $result['info']['prevPageToken'] = (isset($this->page_info['prevPageToken']) ? $this->page_info['prevPageToken'] : false); 568 | 569 | return $result; 570 | } 571 | 572 | /** 573 | * @param $channelId 574 | * @param array $part 575 | * @param integer $maxResults 576 | * @param $pageInfo 577 | * @param $pageToken 578 | * @return array 579 | * @throws \Exception 580 | */ 581 | public function getActivitiesByChannelId($channelId, $part = ['id', 'snippet', 'contentDetails'], $maxResults = 5, $pageInfo = false, $pageToken = '') 582 | { 583 | if (empty($channelId)) { 584 | throw new \InvalidArgumentException('ChannelId must be supplied'); 585 | } 586 | $API_URL = $this->getApi('activities'); 587 | $params = [ 588 | 'channelId' => $channelId, 589 | 'part' => implode(',', $part), 590 | 'maxResults' => $maxResults, 591 | 'pageToken' => $pageToken, 592 | ]; 593 | $apiData = $this->api_get($API_URL, $params); 594 | 595 | if ($pageInfo) { 596 | return [ 597 | 'results' => $this->decodeList($apiData), 598 | 'info' => $this->page_info, 599 | ]; 600 | } else { 601 | return $this->decodeList($apiData); 602 | } 603 | } 604 | 605 | /** 606 | * Parse a YouTube URL to get the YouTube Video ID. 607 | * Supports full URL (www.youtube.com), short URL (youtu.be), 608 | * embed URL, and live stream URL. 609 | * 610 | * @param string $youtube_url 611 | * @throws \Exception 612 | * @return string Video ID 613 | */ 614 | public static function parseVidFromURL($youtube_url) 615 | { 616 | // Parse the URL into its components 617 | $parsedUrl = parse_url($youtube_url); 618 | 619 | // Check if it's a valid YouTube URL 620 | if (isset($parsedUrl['host'], $parsedUrl['path'])) { 621 | 622 | // Handle full YouTube URL (www.youtube.com) 623 | if (strpos($parsedUrl['host'], 'youtube.com') !== false) { 624 | 625 | // Handle embed URLs (e.g., https://www.youtube.com/embed/{video_id}) 626 | if (strpos($parsedUrl['path'], '/embed/') === 0) { 627 | $path = static::_parse_url_path($youtube_url); 628 | $vid = substr($path, strlen('/embed/')); 629 | 630 | return $vid; 631 | } 632 | 633 | // Handle live URLs (e.g., https://www.youtube.com/live/{video_id}) 634 | if (strpos($parsedUrl['path'], '/live/') === 0) { 635 | $path = static::_parse_url_path($youtube_url); 636 | $vid = substr($path, strlen('/live/')); 637 | 638 | return $vid; 639 | } 640 | 641 | // Handle YouTube Shorts URLs (e.g., https://www.youtube.com/shorts/{video_id}) 642 | if (preg_match('#/shorts/([a-zA-Z0-9_-]+)#', $parsedUrl['path'], $matches)) { 643 | return $matches[1]; 644 | } 645 | 646 | // Handle /v/ URLs (e.g., https://www.youtube.com/v/{video_id}) 647 | if (strpos($parsedUrl['path'], '/v/') === 0) { 648 | $path = static::_parse_url_path($youtube_url); 649 | $vid = substr($path, strlen('/v/')); 650 | 651 | return $vid; 652 | } 653 | 654 | // Handle standard YouTube video URLs (e.g., https://www.youtube.com/watch?v={video_id}) 655 | $params = static::_parse_url_query($youtube_url); 656 | if (isset($params['v'])) { 657 | return $params['v']; 658 | } 659 | 660 | // Handle short YouTube URLs (e.g., https://youtu.be/{video_id}) 661 | } else if (strpos($parsedUrl['host'], 'youtu.be') !== false) { 662 | $path = static::_parse_url_path($youtube_url); 663 | $vid = substr($path, 1); // Remove the leading '/' 664 | 665 | return $vid; 666 | } 667 | } 668 | 669 | // If no valid video ID is found, throw an exception 670 | throw new \Exception('The supplied URL does not look like a valid YouTube URL'); 671 | } 672 | 673 | /** 674 | * Get the channel object by supplying the URL of the channel page 675 | * 676 | * @param string $youtube_url 677 | * @throws \Exception 678 | * @return object Channel object 679 | */ 680 | public function getChannelFromURL($youtube_url) 681 | { 682 | if (strpos($youtube_url, 'youtube.com') === false) { 683 | throw new \Exception('The supplied URL does not look like a Youtube URL'); 684 | } 685 | 686 | $path = static::_parse_url_path($youtube_url); 687 | $segments = explode('/', $path); 688 | 689 | if (strpos($path, '/channel/') === 0) { 690 | $channelId = $segments[count($segments) - 1]; 691 | $channel = $this->getChannelById($channelId); 692 | } else if (strpos($path, '/user/') === 0) { 693 | $username = $segments[count($segments) - 1]; 694 | $channel = $this->getChannelByName($username); 695 | } else if (strpos($path, '/c/') === 0) { 696 | $username = $segments[count($segments) - 1]; 697 | $channel = $this->searchChannelByName($username); 698 | } else if (strpos($path, '/@') === 0) { 699 | $username = str_replace('@', '', $segments[count($segments) - 1]); 700 | $channel = $this->searchChannelByName($username); 701 | } else { 702 | foreach ($this->youtube_reserved_urls as $r) { 703 | if (preg_match('/'.$r.'/', $path)) { 704 | throw new \Exception('The supplied URL does not look like a Youtube Channel URL'); 705 | } 706 | } 707 | 708 | $username = $segments[1]; 709 | $channel = $this->searchChannelByName($username); 710 | } 711 | 712 | return $channel; 713 | } 714 | 715 | /* 716 | * Internally used Methods, set visibility to public to enable more flexibility 717 | */ 718 | 719 | /** 720 | * @param $name 721 | * @return mixed 722 | */ 723 | public function getApi($name) 724 | { 725 | return $this->APIs[$name]; 726 | } 727 | 728 | /** 729 | * Decode the response from youtube, extract the single resource object. 730 | * (Don't use this to decode the response containing list of objects) 731 | * 732 | * @param string $apiData the api response from youtube 733 | * @throws \Exception 734 | * @return \StdClass an Youtube resource object 735 | */ 736 | public function decodeSingle(&$apiData) 737 | { 738 | $resObj = json_decode($apiData); 739 | if (isset($resObj->error)) { 740 | $msg = "Error " . $resObj->error->code . " " . $resObj->error->message; 741 | if (isset($resObj->error->errors[0])) { 742 | $msg .= " : " . $resObj->error->errors[0]->reason; 743 | } 744 | 745 | throw new \Exception($msg); 746 | } else { 747 | if(isset($resObj->items)){ 748 | $itemsArray = $resObj->items; 749 | if (!is_array($itemsArray) || count($itemsArray) == 0) { 750 | return false; 751 | } else { 752 | return $itemsArray[0]; 753 | } 754 | } 755 | return false; 756 | } 757 | } 758 | 759 | /** 760 | * Decode the response from youtube, extract the multiple resource object. 761 | * 762 | * @param string $apiData the api response from youtube 763 | * @throws \Exception 764 | * @return \StdClass an Youtube resource object 765 | */ 766 | public function decodeMultiple(&$apiData) 767 | { 768 | $resObj = json_decode($apiData); 769 | if (isset($resObj->error)) { 770 | $msg = "Error " . $resObj->error->code . " " . $resObj->error->message; 771 | if (isset($resObj->error->errors[0])) { 772 | $msg .= " : " . $resObj->error->errors[0]->reason; 773 | } 774 | 775 | throw new \Exception($msg); 776 | } else { 777 | 778 | if(isset($resObj->items)) { 779 | $itemsArray = $resObj->items; 780 | if (!is_array($itemsArray) || count($itemsArray) == 0) { 781 | return false; 782 | } else { 783 | return $itemsArray; 784 | } 785 | } 786 | return false; 787 | } 788 | } 789 | 790 | /** 791 | * Decode the response from youtube, extract the list of resource objects 792 | * 793 | * @param string $apiData response string from youtube 794 | * @throws \Exception 795 | * @return array Array of StdClass objects 796 | */ 797 | public function decodeList(&$apiData) 798 | { 799 | $resObj = json_decode($apiData); 800 | if (isset($resObj->error)) { 801 | $msg = "Error " . $resObj->error->code . " " . $resObj->error->message; 802 | if (isset($resObj->error->errors[0])) { 803 | $msg .= " : " . $resObj->error->errors[0]->reason; 804 | } 805 | 806 | throw new \Exception($msg); 807 | } else { 808 | $this->page_info = [ 809 | 'kind' => $resObj->kind, 810 | 'etag' => $resObj->etag, 811 | 'prevPageToken' => null, 812 | 'nextPageToken' => null, 813 | ]; 814 | 815 | if (isset($resObj->pageInfo)) { 816 | $this->page_info['resultsPerPage'] = $resObj->pageInfo->resultsPerPage; 817 | $this->page_info['totalResults'] = $resObj->pageInfo->totalResults; 818 | } 819 | 820 | if (isset($resObj->prevPageToken)) { 821 | $this->page_info['prevPageToken'] = $resObj->prevPageToken; 822 | } 823 | 824 | if (isset($resObj->nextPageToken)) { 825 | $this->page_info['nextPageToken'] = $resObj->nextPageToken; 826 | } 827 | 828 | if(isset($resObj->items)) { 829 | $itemsArray = $resObj->items; 830 | if (!is_array($itemsArray) || count($itemsArray) == 0) { 831 | return false; 832 | } else { 833 | return $itemsArray; 834 | } 835 | } 836 | return false; 837 | } 838 | } 839 | 840 | /** 841 | * Using CURL to issue a GET request 842 | * 843 | * @param $url 844 | * @param $params 845 | * @return mixed 846 | * @throws \Exception 847 | */ 848 | public function api_get($url, $params) 849 | { 850 | //set the youtube key 851 | $params['key'] = $this->youtube_key; 852 | 853 | //boilerplates for CURL 854 | $tuCurl = curl_init(); 855 | 856 | if (isset($_SERVER['HTTP_HOST']) && $this->config['use-http-host']) { 857 | curl_setopt($tuCurl, CURLOPT_HEADER, array('Referer' => $_SERVER['HTTP_HOST'])); 858 | } 859 | 860 | curl_setopt($tuCurl, CURLOPT_URL, $url . (strpos($url, '?') === false ? '?' : '') . http_build_query($params)); 861 | if (strpos($url, 'https') === false) { 862 | curl_setopt($tuCurl, CURLOPT_PORT, 80); 863 | } else { 864 | curl_setopt($tuCurl, CURLOPT_PORT, 443); 865 | } 866 | 867 | curl_setopt($tuCurl, CURLOPT_RETURNTRANSFER, 1); 868 | $tuData = curl_exec($tuCurl); 869 | if (curl_errno($tuCurl)) { 870 | throw new \Exception('Curl Error : ' . curl_error($tuCurl)); 871 | } 872 | 873 | return $tuData; 874 | } 875 | 876 | /** 877 | * Parse the input url string and return just the path part 878 | * 879 | * @param string $url the URL 880 | * @return string the path string 881 | */ 882 | public static function _parse_url_path($url) 883 | { 884 | $array = parse_url($url); 885 | 886 | return $array['path']; 887 | } 888 | 889 | /** 890 | * Parse the input url string and return an array of query params 891 | * 892 | * @param string $url the URL 893 | * @return array array of query params 894 | */ 895 | public static function _parse_url_query($url) 896 | { 897 | $array = parse_url($url); 898 | $query = $array['query']; 899 | 900 | $queryParts = explode('&', $query); 901 | 902 | $params = []; 903 | foreach ($queryParts as $param) { 904 | $item = explode('=', $param); 905 | $params[$item[0]] = empty($item[1]) ? '' : $item[1]; 906 | } 907 | 908 | return $params; 909 | } 910 | } 911 | --------------------------------------------------------------------------------