├── .gitattributes ├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── composer.json ├── composer.lock ├── etc ├── links.json └── youtube_urls.txt ├── phpstan.neon.dist ├── public ├── index.html ├── stream.php └── video_info.php ├── src ├── Browser.php ├── DownloadOptions.php ├── Exception │ ├── TooManyRequestsException.php │ ├── VideoNotFoundException.php │ ├── VideoPlayerNotFoundException.php │ └── YouTubeException.php ├── Models │ ├── AbstractModel.php │ ├── SplitStream.php │ ├── StreamFormat.php │ ├── VideoDetails.php │ └── VideoInfo.php ├── Responses │ ├── GetVideoInfo.php │ ├── HttpResponse.php │ ├── VideoPlayerJs.php │ └── WatchVideoPage.php ├── SignatureDecoder.php ├── Utils │ ├── ITagUtils.php │ ├── SerializationUtils.php │ └── Utils.php ├── YouTubeDownloader.php └── YouTubeStreamer.php └── tests ├── ArrayTest.php ├── StreamFormatTest.php ├── TestCase.php ├── YouTubePageTest.php ├── YouTubeTest.php └── YouTubeUrlTest.php /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | .DS_Store 4 | Thumbs.db 5 | /phpunit.xml 6 | /.idea 7 | /.vscode 8 | .phpunit.result.cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: vendor/bin/heroku-php-apache2 public/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![](https://img.shields.io/packagist/dt/Athlon1600/youtube-downloader.svg) ![](https://img.shields.io/github/last-commit/Athlon1600/youtube-downloader.svg) ![](https://img.shields.io/github/license/Athlon1600/youtube-downloader.svg) 4 | 5 |
6 | 7 | # youtube-downloader 8 | 9 | This project was inspired by a very popular youtube-dl python package: 10 | https://github.com/rg3/youtube-dl 11 | 12 | Yes, there are multiple other PHP-based youtube downloaders on the Internet, 13 | but most of them haven't been updated in years, or they depend on youtube-dl itself. 14 | 15 | Pure PHP-based youtube downloaders that work, and are **kept-up-to date** just do not exist. 16 | 17 | This script uses no Javascript interpreters, no calls to shell... nothing but pure PHP with no heavy dependencies either. 18 | 19 | ![](https://i.imgur.com/39LIE0r.png) 20 | 21 | That's all there is to it! 22 | 23 | ## :warning: Legal Disclaimer 24 | 25 | This program is for personal use only. 26 | Downloading copyrighted material without permission is against [YouTube's terms of services](https://www.youtube.com/static?template=terms). 27 | By using this program, you are solely responsible for any copyright violations. 28 | We are not responsible for people who attempt to use this program in any way that breaks YouTube's terms of services. 29 | 30 | ## Demo App 31 | 32 | This may not work at all times, because YouTube puts a short ban on the server if it receives too many requests from it. 33 | 34 | - https://youtube-downloader-v3.herokuapp.com/ 35 | 36 | ![](http://proxynova.s3.us-east-1.amazonaws.com/youtube-downloader-save-video.png) 37 | 38 | 39 | ### Deploy your own App 40 | 41 | on Heroku: 42 | 43 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 44 | 45 | 46 | Create a FREE account first if you do not yet have one: 47 | https://signup.heroku.com/ 48 | 49 | Installation 50 | ------- 51 | 52 | Recommended way of installing this is via [Composer](http://getcomposer.org): 53 | 54 | ```bash 55 | composer require athlon1600/youtube-downloader "^3.0" 56 | ``` 57 | 58 | Run locally: 59 | 60 | ```bash 61 | php -S localhost:8000 -t vendor/athlon1600/youtube-downloader/public 62 | ``` 63 | 64 | # Previous versions 65 | 66 | Instructions for installing & using Version 2 can be found here: 67 | https://github.com/Athlon1600/youtube-downloader/tree/2.x 68 | 69 | # Usage 70 | 71 | 72 | ```php 73 | use YouTube\YouTubeDownloader; 74 | use YouTube\Exception\YouTubeException; 75 | 76 | $youtube = new YouTubeDownloader(); 77 | 78 | try { 79 | $downloadOptions = $youtube->getDownloadLinks("https://www.youtube.com/watch?v=aqz-KE-bpKQ"); 80 | 81 | if ($downloadOptions->getAllFormats()) { 82 | echo $downloadOptions->getFirstCombinedFormat()->url; 83 | } else { 84 | echo 'No links found'; 85 | } 86 | 87 | } catch (YouTubeException $e) { 88 | echo 'Something went wrong: ' . $e->getMessage(); 89 | } 90 | ``` 91 | 92 | `getDownloadLinks` method returns a `DownloadOptions` type object, which holds an array of stream links - some that are audio-only, and some that are both audio and video combined into one. 93 | 94 | For typical usage, you are probably interested in dealing with combined streams, for that case, there is the `getCombinedFormats` method. 95 | 96 | ## Other Features 97 | 98 | - Stream YouTube videos directly from your server: 99 | 100 | ```php 101 | $youtube = new \YouTube\YouTubeStreamer(); 102 | $youtube->stream('https://r4---sn-n4v7knll.googlevideo.com/videoplayback?...'); 103 | ``` 104 | 105 | - Pass in your own cookies/user-agent 106 | 107 | If you try downloading age-restricted videos, YouTube will ask you to login. The only way to make this work, is to login to your YouTube account in your own web-browser, export those newly set cookies from your browser into a file, and then pass it all to youtube-downloader for use. 108 | 109 | ```php 110 | $youtube = new YouTubeDownloader(); 111 | $youtube->getBrowser()->setCookieFile('./your_cookies.txt'); 112 | $youtube->getBrowser()->setUserAgent('Opera 7.6'); 113 | ``` 114 | 115 | See also: 116 | https://github.com/ytdl-org/youtube-dl/blob/master/README.md#how-do-i-pass-cookies-to-youtube-dl 117 | 118 | - Before you continue to YouTube... 119 | 120 | Depending on your region, you might be force redirected to a [page](https://unblockvideos.com/images/before-you-continue-cookies.jpg) that asks you to agree to Google's cookie policy. 121 | You can programmatically agree to those terms, and bypass that warning permanently via `consentCookies` method on your Browser instance. Example: 122 | ```php 123 | $youtube = new YouTubeDownloader(); 124 | $youtube->getBrowser()->consentCookies(); 125 | ``` 126 | 127 | 128 | ## How does it work 129 | 130 | A more detailed explanation on how to download videos from YouTube will be written soon. 131 | For now, there is this: 132 | 133 | https://github.com/Athlon1600/youtube-downloader/pull/25#issuecomment-439373506 134 | 135 | ## Other Links 136 | 137 | - https://github.com/Athlon1600/php-curl-file-downloader 138 | - https://github.com/TeamNewPipe/NewPipeExtractor/blob/d83787a5ca308c4ca4e86e63a8b63c5e7c4708d6/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/YoutubeStreamExtractor.java 139 | - https://github.com/ytdl-org/youtube-dl/blob/master/youtube_dl/extractor/youtube.py 140 | 141 | ## To-do list 142 | 143 | - Add ability to solve YouTube Captcha and avoid `HTTP 429 Too Many Requests` errors. 144 | - Add ability to download video and audio streams separately, and merge the two together using ffmpeg. Just like `youtube-dl` does! 145 | - Optional command that finds ALL video formats. 146 | - ~~Fetch additional metadata about the video without using YouTube API.~~ 147 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PHP YouTube Downloader", 3 | "description": "PHP powered alternative for youtube-dl", 4 | "repository": "https://github.com/Athlon1600/youtube-downloader/", 5 | "keywords": ["youtube downloader", "download youtube", "download youtube videos"] 6 | } 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "athlon1600/youtube-downloader", 3 | "description": "PHP powered alternative for youtube-dl", 4 | "keywords": ["youtube downloader", "download youtube", "download youtube videos"], 5 | "license": "MIT", 6 | "require": { 7 | "php": "^5.6 || ^7.0 || ^8.0", 8 | "ext-curl": "*", 9 | "ext-json": "*", 10 | "athlon1600/php-curl-file-downloader": "^1.0" 11 | }, 12 | "require-dev": { 13 | "phpunit/phpunit": "^5.7|6.2|^7.0|^8.0" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "YouTube\\": "src/" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "YouTube\\Tests\\": "tests" 23 | } 24 | }, 25 | "scripts": { 26 | "test": "phpunit tests" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "ddc378e0c54e28727731638e63b6c50a", 8 | "packages": [ 9 | { 10 | "name": "athlon1600/php-curl-client", 11 | "version": "v1.1.3", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/Athlon1600/php-curl-client.git", 15 | "reference": "2c9a7ac0aea487fcdfc57333dfba6cb684fb64fc" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/Athlon1600/php-curl-client/zipball/2c9a7ac0aea487fcdfc57333dfba6cb684fb64fc", 20 | "reference": "2c9a7ac0aea487fcdfc57333dfba6cb684fb64fc", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-curl": "*", 25 | "ext-json": "*" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^7.0" 29 | }, 30 | "type": "library", 31 | "autoload": { 32 | "psr-4": { 33 | "Curl\\": "src/" 34 | } 35 | }, 36 | "notification-url": "https://packagist.org/downloads/", 37 | "license": [ 38 | "MIT" 39 | ], 40 | "description": "Simple PHP cURL Client", 41 | "keywords": [ 42 | "php curl client" 43 | ], 44 | "support": { 45 | "issues": "https://github.com/Athlon1600/php-curl-client/issues", 46 | "source": "https://github.com/Athlon1600/php-curl-client/tree/v1.1.3" 47 | }, 48 | "time": "2021-02-14T17:10:18+00:00" 49 | }, 50 | { 51 | "name": "athlon1600/php-curl-file-downloader", 52 | "version": "v1.0.2", 53 | "source": { 54 | "type": "git", 55 | "url": "https://github.com/Athlon1600/php-curl-file-downloader.git", 56 | "reference": "66391a0b89e2ed8b42ed6341e111f180697e0c5f" 57 | }, 58 | "dist": { 59 | "type": "zip", 60 | "url": "https://api.github.com/repos/Athlon1600/php-curl-file-downloader/zipball/66391a0b89e2ed8b42ed6341e111f180697e0c5f", 61 | "reference": "66391a0b89e2ed8b42ed6341e111f180697e0c5f", 62 | "shasum": "" 63 | }, 64 | "require": { 65 | "athlon1600/php-curl-client": "^1.0", 66 | "ext-curl": "*" 67 | }, 68 | "require-dev": { 69 | "phpunit/phpunit": "^7.0" 70 | }, 71 | "type": "library", 72 | "autoload": { 73 | "psr-4": { 74 | "CurlDownloader\\": "src/" 75 | } 76 | }, 77 | "notification-url": "https://packagist.org/downloads/", 78 | "support": { 79 | "issues": "https://github.com/Athlon1600/php-curl-file-downloader/issues", 80 | "source": "https://github.com/Athlon1600/php-curl-file-downloader/tree/v1.0.2" 81 | }, 82 | "time": "2021-02-25T20:29:06+00:00" 83 | } 84 | ], 85 | "packages-dev": [ 86 | { 87 | "name": "doctrine/instantiator", 88 | "version": "1.4.0", 89 | "source": { 90 | "type": "git", 91 | "url": "https://github.com/doctrine/instantiator.git", 92 | "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" 93 | }, 94 | "dist": { 95 | "type": "zip", 96 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", 97 | "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", 98 | "shasum": "" 99 | }, 100 | "require": { 101 | "php": "^7.1 || ^8.0" 102 | }, 103 | "require-dev": { 104 | "doctrine/coding-standard": "^8.0", 105 | "ext-pdo": "*", 106 | "ext-phar": "*", 107 | "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", 108 | "phpstan/phpstan": "^0.12", 109 | "phpstan/phpstan-phpunit": "^0.12", 110 | "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" 111 | }, 112 | "type": "library", 113 | "autoload": { 114 | "psr-4": { 115 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 116 | } 117 | }, 118 | "notification-url": "https://packagist.org/downloads/", 119 | "license": [ 120 | "MIT" 121 | ], 122 | "authors": [ 123 | { 124 | "name": "Marco Pivetta", 125 | "email": "ocramius@gmail.com", 126 | "homepage": "https://ocramius.github.io/" 127 | } 128 | ], 129 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 130 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 131 | "keywords": [ 132 | "constructor", 133 | "instantiate" 134 | ], 135 | "support": { 136 | "issues": "https://github.com/doctrine/instantiator/issues", 137 | "source": "https://github.com/doctrine/instantiator/tree/1.4.0" 138 | }, 139 | "funding": [ 140 | { 141 | "url": "https://www.doctrine-project.org/sponsorship.html", 142 | "type": "custom" 143 | }, 144 | { 145 | "url": "https://www.patreon.com/phpdoctrine", 146 | "type": "patreon" 147 | }, 148 | { 149 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", 150 | "type": "tidelift" 151 | } 152 | ], 153 | "time": "2020-11-10T18:47:58+00:00" 154 | }, 155 | { 156 | "name": "myclabs/deep-copy", 157 | "version": "1.10.2", 158 | "source": { 159 | "type": "git", 160 | "url": "https://github.com/myclabs/DeepCopy.git", 161 | "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" 162 | }, 163 | "dist": { 164 | "type": "zip", 165 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", 166 | "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", 167 | "shasum": "" 168 | }, 169 | "require": { 170 | "php": "^7.1 || ^8.0" 171 | }, 172 | "replace": { 173 | "myclabs/deep-copy": "self.version" 174 | }, 175 | "require-dev": { 176 | "doctrine/collections": "^1.0", 177 | "doctrine/common": "^2.6", 178 | "phpunit/phpunit": "^7.1" 179 | }, 180 | "type": "library", 181 | "autoload": { 182 | "psr-4": { 183 | "DeepCopy\\": "src/DeepCopy/" 184 | }, 185 | "files": [ 186 | "src/DeepCopy/deep_copy.php" 187 | ] 188 | }, 189 | "notification-url": "https://packagist.org/downloads/", 190 | "license": [ 191 | "MIT" 192 | ], 193 | "description": "Create deep copies (clones) of your objects", 194 | "keywords": [ 195 | "clone", 196 | "copy", 197 | "duplicate", 198 | "object", 199 | "object graph" 200 | ], 201 | "support": { 202 | "issues": "https://github.com/myclabs/DeepCopy/issues", 203 | "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" 204 | }, 205 | "funding": [ 206 | { 207 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 208 | "type": "tidelift" 209 | } 210 | ], 211 | "time": "2020-11-13T09:40:50+00:00" 212 | }, 213 | { 214 | "name": "phar-io/manifest", 215 | "version": "2.0.1", 216 | "source": { 217 | "type": "git", 218 | "url": "https://github.com/phar-io/manifest.git", 219 | "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" 220 | }, 221 | "dist": { 222 | "type": "zip", 223 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", 224 | "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", 225 | "shasum": "" 226 | }, 227 | "require": { 228 | "ext-dom": "*", 229 | "ext-phar": "*", 230 | "ext-xmlwriter": "*", 231 | "phar-io/version": "^3.0.1", 232 | "php": "^7.2 || ^8.0" 233 | }, 234 | "type": "library", 235 | "extra": { 236 | "branch-alias": { 237 | "dev-master": "2.0.x-dev" 238 | } 239 | }, 240 | "autoload": { 241 | "classmap": [ 242 | "src/" 243 | ] 244 | }, 245 | "notification-url": "https://packagist.org/downloads/", 246 | "license": [ 247 | "BSD-3-Clause" 248 | ], 249 | "authors": [ 250 | { 251 | "name": "Arne Blankerts", 252 | "email": "arne@blankerts.de", 253 | "role": "Developer" 254 | }, 255 | { 256 | "name": "Sebastian Heuer", 257 | "email": "sebastian@phpeople.de", 258 | "role": "Developer" 259 | }, 260 | { 261 | "name": "Sebastian Bergmann", 262 | "email": "sebastian@phpunit.de", 263 | "role": "Developer" 264 | } 265 | ], 266 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 267 | "support": { 268 | "issues": "https://github.com/phar-io/manifest/issues", 269 | "source": "https://github.com/phar-io/manifest/tree/master" 270 | }, 271 | "time": "2020-06-27T14:33:11+00:00" 272 | }, 273 | { 274 | "name": "phar-io/version", 275 | "version": "3.1.0", 276 | "source": { 277 | "type": "git", 278 | "url": "https://github.com/phar-io/version.git", 279 | "reference": "bae7c545bef187884426f042434e561ab1ddb182" 280 | }, 281 | "dist": { 282 | "type": "zip", 283 | "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", 284 | "reference": "bae7c545bef187884426f042434e561ab1ddb182", 285 | "shasum": "" 286 | }, 287 | "require": { 288 | "php": "^7.2 || ^8.0" 289 | }, 290 | "type": "library", 291 | "autoload": { 292 | "classmap": [ 293 | "src/" 294 | ] 295 | }, 296 | "notification-url": "https://packagist.org/downloads/", 297 | "license": [ 298 | "BSD-3-Clause" 299 | ], 300 | "authors": [ 301 | { 302 | "name": "Arne Blankerts", 303 | "email": "arne@blankerts.de", 304 | "role": "Developer" 305 | }, 306 | { 307 | "name": "Sebastian Heuer", 308 | "email": "sebastian@phpeople.de", 309 | "role": "Developer" 310 | }, 311 | { 312 | "name": "Sebastian Bergmann", 313 | "email": "sebastian@phpunit.de", 314 | "role": "Developer" 315 | } 316 | ], 317 | "description": "Library for handling version information and constraints", 318 | "support": { 319 | "issues": "https://github.com/phar-io/version/issues", 320 | "source": "https://github.com/phar-io/version/tree/3.1.0" 321 | }, 322 | "time": "2021-02-23T14:00:09+00:00" 323 | }, 324 | { 325 | "name": "phpdocumentor/reflection-common", 326 | "version": "2.2.0", 327 | "source": { 328 | "type": "git", 329 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 330 | "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" 331 | }, 332 | "dist": { 333 | "type": "zip", 334 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", 335 | "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", 336 | "shasum": "" 337 | }, 338 | "require": { 339 | "php": "^7.2 || ^8.0" 340 | }, 341 | "type": "library", 342 | "extra": { 343 | "branch-alias": { 344 | "dev-2.x": "2.x-dev" 345 | } 346 | }, 347 | "autoload": { 348 | "psr-4": { 349 | "phpDocumentor\\Reflection\\": "src/" 350 | } 351 | }, 352 | "notification-url": "https://packagist.org/downloads/", 353 | "license": [ 354 | "MIT" 355 | ], 356 | "authors": [ 357 | { 358 | "name": "Jaap van Otterdijk", 359 | "email": "opensource@ijaap.nl" 360 | } 361 | ], 362 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 363 | "homepage": "http://www.phpdoc.org", 364 | "keywords": [ 365 | "FQSEN", 366 | "phpDocumentor", 367 | "phpdoc", 368 | "reflection", 369 | "static analysis" 370 | ], 371 | "support": { 372 | "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", 373 | "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" 374 | }, 375 | "time": "2020-06-27T09:03:43+00:00" 376 | }, 377 | { 378 | "name": "phpdocumentor/reflection-docblock", 379 | "version": "5.2.2", 380 | "source": { 381 | "type": "git", 382 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 383 | "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" 384 | }, 385 | "dist": { 386 | "type": "zip", 387 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", 388 | "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", 389 | "shasum": "" 390 | }, 391 | "require": { 392 | "ext-filter": "*", 393 | "php": "^7.2 || ^8.0", 394 | "phpdocumentor/reflection-common": "^2.2", 395 | "phpdocumentor/type-resolver": "^1.3", 396 | "webmozart/assert": "^1.9.1" 397 | }, 398 | "require-dev": { 399 | "mockery/mockery": "~1.3.2" 400 | }, 401 | "type": "library", 402 | "extra": { 403 | "branch-alias": { 404 | "dev-master": "5.x-dev" 405 | } 406 | }, 407 | "autoload": { 408 | "psr-4": { 409 | "phpDocumentor\\Reflection\\": "src" 410 | } 411 | }, 412 | "notification-url": "https://packagist.org/downloads/", 413 | "license": [ 414 | "MIT" 415 | ], 416 | "authors": [ 417 | { 418 | "name": "Mike van Riel", 419 | "email": "me@mikevanriel.com" 420 | }, 421 | { 422 | "name": "Jaap van Otterdijk", 423 | "email": "account@ijaap.nl" 424 | } 425 | ], 426 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 427 | "support": { 428 | "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", 429 | "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" 430 | }, 431 | "time": "2020-09-03T19:13:55+00:00" 432 | }, 433 | { 434 | "name": "phpdocumentor/type-resolver", 435 | "version": "1.4.0", 436 | "source": { 437 | "type": "git", 438 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 439 | "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" 440 | }, 441 | "dist": { 442 | "type": "zip", 443 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", 444 | "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", 445 | "shasum": "" 446 | }, 447 | "require": { 448 | "php": "^7.2 || ^8.0", 449 | "phpdocumentor/reflection-common": "^2.0" 450 | }, 451 | "require-dev": { 452 | "ext-tokenizer": "*" 453 | }, 454 | "type": "library", 455 | "extra": { 456 | "branch-alias": { 457 | "dev-1.x": "1.x-dev" 458 | } 459 | }, 460 | "autoload": { 461 | "psr-4": { 462 | "phpDocumentor\\Reflection\\": "src" 463 | } 464 | }, 465 | "notification-url": "https://packagist.org/downloads/", 466 | "license": [ 467 | "MIT" 468 | ], 469 | "authors": [ 470 | { 471 | "name": "Mike van Riel", 472 | "email": "me@mikevanriel.com" 473 | } 474 | ], 475 | "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", 476 | "support": { 477 | "issues": "https://github.com/phpDocumentor/TypeResolver/issues", 478 | "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" 479 | }, 480 | "time": "2020-09-17T18:55:26+00:00" 481 | }, 482 | { 483 | "name": "phpspec/prophecy", 484 | "version": "1.13.0", 485 | "source": { 486 | "type": "git", 487 | "url": "https://github.com/phpspec/prophecy.git", 488 | "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" 489 | }, 490 | "dist": { 491 | "type": "zip", 492 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", 493 | "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", 494 | "shasum": "" 495 | }, 496 | "require": { 497 | "doctrine/instantiator": "^1.2", 498 | "php": "^7.2 || ~8.0, <8.1", 499 | "phpdocumentor/reflection-docblock": "^5.2", 500 | "sebastian/comparator": "^3.0 || ^4.0", 501 | "sebastian/recursion-context": "^3.0 || ^4.0" 502 | }, 503 | "require-dev": { 504 | "phpspec/phpspec": "^6.0", 505 | "phpunit/phpunit": "^8.0 || ^9.0" 506 | }, 507 | "type": "library", 508 | "extra": { 509 | "branch-alias": { 510 | "dev-master": "1.11.x-dev" 511 | } 512 | }, 513 | "autoload": { 514 | "psr-4": { 515 | "Prophecy\\": "src/Prophecy" 516 | } 517 | }, 518 | "notification-url": "https://packagist.org/downloads/", 519 | "license": [ 520 | "MIT" 521 | ], 522 | "authors": [ 523 | { 524 | "name": "Konstantin Kudryashov", 525 | "email": "ever.zet@gmail.com", 526 | "homepage": "http://everzet.com" 527 | }, 528 | { 529 | "name": "Marcello Duarte", 530 | "email": "marcello.duarte@gmail.com" 531 | } 532 | ], 533 | "description": "Highly opinionated mocking framework for PHP 5.3+", 534 | "homepage": "https://github.com/phpspec/prophecy", 535 | "keywords": [ 536 | "Double", 537 | "Dummy", 538 | "fake", 539 | "mock", 540 | "spy", 541 | "stub" 542 | ], 543 | "support": { 544 | "issues": "https://github.com/phpspec/prophecy/issues", 545 | "source": "https://github.com/phpspec/prophecy/tree/1.13.0" 546 | }, 547 | "time": "2021-03-17T13:42:18+00:00" 548 | }, 549 | { 550 | "name": "phpunit/php-code-coverage", 551 | "version": "7.0.14", 552 | "source": { 553 | "type": "git", 554 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 555 | "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c" 556 | }, 557 | "dist": { 558 | "type": "zip", 559 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bb7c9a210c72e4709cdde67f8b7362f672f2225c", 560 | "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c", 561 | "shasum": "" 562 | }, 563 | "require": { 564 | "ext-dom": "*", 565 | "ext-xmlwriter": "*", 566 | "php": ">=7.2", 567 | "phpunit/php-file-iterator": "^2.0.2", 568 | "phpunit/php-text-template": "^1.2.1", 569 | "phpunit/php-token-stream": "^3.1.1 || ^4.0", 570 | "sebastian/code-unit-reverse-lookup": "^1.0.1", 571 | "sebastian/environment": "^4.2.2", 572 | "sebastian/version": "^2.0.1", 573 | "theseer/tokenizer": "^1.1.3" 574 | }, 575 | "require-dev": { 576 | "phpunit/phpunit": "^8.2.2" 577 | }, 578 | "suggest": { 579 | "ext-xdebug": "^2.7.2" 580 | }, 581 | "type": "library", 582 | "extra": { 583 | "branch-alias": { 584 | "dev-master": "7.0-dev" 585 | } 586 | }, 587 | "autoload": { 588 | "classmap": [ 589 | "src/" 590 | ] 591 | }, 592 | "notification-url": "https://packagist.org/downloads/", 593 | "license": [ 594 | "BSD-3-Clause" 595 | ], 596 | "authors": [ 597 | { 598 | "name": "Sebastian Bergmann", 599 | "email": "sebastian@phpunit.de", 600 | "role": "lead" 601 | } 602 | ], 603 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 604 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 605 | "keywords": [ 606 | "coverage", 607 | "testing", 608 | "xunit" 609 | ], 610 | "support": { 611 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 612 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.14" 613 | }, 614 | "funding": [ 615 | { 616 | "url": "https://github.com/sebastianbergmann", 617 | "type": "github" 618 | } 619 | ], 620 | "time": "2020-12-02T13:39:03+00:00" 621 | }, 622 | { 623 | "name": "phpunit/php-file-iterator", 624 | "version": "2.0.3", 625 | "source": { 626 | "type": "git", 627 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 628 | "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357" 629 | }, 630 | "dist": { 631 | "type": "zip", 632 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357", 633 | "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357", 634 | "shasum": "" 635 | }, 636 | "require": { 637 | "php": ">=7.1" 638 | }, 639 | "require-dev": { 640 | "phpunit/phpunit": "^8.5" 641 | }, 642 | "type": "library", 643 | "extra": { 644 | "branch-alias": { 645 | "dev-master": "2.0.x-dev" 646 | } 647 | }, 648 | "autoload": { 649 | "classmap": [ 650 | "src/" 651 | ] 652 | }, 653 | "notification-url": "https://packagist.org/downloads/", 654 | "license": [ 655 | "BSD-3-Clause" 656 | ], 657 | "authors": [ 658 | { 659 | "name": "Sebastian Bergmann", 660 | "email": "sebastian@phpunit.de", 661 | "role": "lead" 662 | } 663 | ], 664 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 665 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 666 | "keywords": [ 667 | "filesystem", 668 | "iterator" 669 | ], 670 | "support": { 671 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 672 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.3" 673 | }, 674 | "funding": [ 675 | { 676 | "url": "https://github.com/sebastianbergmann", 677 | "type": "github" 678 | } 679 | ], 680 | "time": "2020-11-30T08:25:21+00:00" 681 | }, 682 | { 683 | "name": "phpunit/php-text-template", 684 | "version": "1.2.1", 685 | "source": { 686 | "type": "git", 687 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 688 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 689 | }, 690 | "dist": { 691 | "type": "zip", 692 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 693 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 694 | "shasum": "" 695 | }, 696 | "require": { 697 | "php": ">=5.3.3" 698 | }, 699 | "type": "library", 700 | "autoload": { 701 | "classmap": [ 702 | "src/" 703 | ] 704 | }, 705 | "notification-url": "https://packagist.org/downloads/", 706 | "license": [ 707 | "BSD-3-Clause" 708 | ], 709 | "authors": [ 710 | { 711 | "name": "Sebastian Bergmann", 712 | "email": "sebastian@phpunit.de", 713 | "role": "lead" 714 | } 715 | ], 716 | "description": "Simple template engine.", 717 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 718 | "keywords": [ 719 | "template" 720 | ], 721 | "support": { 722 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues", 723 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" 724 | }, 725 | "time": "2015-06-21T13:50:34+00:00" 726 | }, 727 | { 728 | "name": "phpunit/php-timer", 729 | "version": "2.1.3", 730 | "source": { 731 | "type": "git", 732 | "url": "https://github.com/sebastianbergmann/php-timer.git", 733 | "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662" 734 | }, 735 | "dist": { 736 | "type": "zip", 737 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662", 738 | "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662", 739 | "shasum": "" 740 | }, 741 | "require": { 742 | "php": ">=7.1" 743 | }, 744 | "require-dev": { 745 | "phpunit/phpunit": "^8.5" 746 | }, 747 | "type": "library", 748 | "extra": { 749 | "branch-alias": { 750 | "dev-master": "2.1-dev" 751 | } 752 | }, 753 | "autoload": { 754 | "classmap": [ 755 | "src/" 756 | ] 757 | }, 758 | "notification-url": "https://packagist.org/downloads/", 759 | "license": [ 760 | "BSD-3-Clause" 761 | ], 762 | "authors": [ 763 | { 764 | "name": "Sebastian Bergmann", 765 | "email": "sebastian@phpunit.de", 766 | "role": "lead" 767 | } 768 | ], 769 | "description": "Utility class for timing", 770 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 771 | "keywords": [ 772 | "timer" 773 | ], 774 | "support": { 775 | "issues": "https://github.com/sebastianbergmann/php-timer/issues", 776 | "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3" 777 | }, 778 | "funding": [ 779 | { 780 | "url": "https://github.com/sebastianbergmann", 781 | "type": "github" 782 | } 783 | ], 784 | "time": "2020-11-30T08:20:02+00:00" 785 | }, 786 | { 787 | "name": "phpunit/php-token-stream", 788 | "version": "4.0.4", 789 | "source": { 790 | "type": "git", 791 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 792 | "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3" 793 | }, 794 | "dist": { 795 | "type": "zip", 796 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/a853a0e183b9db7eed023d7933a858fa1c8d25a3", 797 | "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3", 798 | "shasum": "" 799 | }, 800 | "require": { 801 | "ext-tokenizer": "*", 802 | "php": "^7.3 || ^8.0" 803 | }, 804 | "require-dev": { 805 | "phpunit/phpunit": "^9.0" 806 | }, 807 | "type": "library", 808 | "extra": { 809 | "branch-alias": { 810 | "dev-master": "4.0-dev" 811 | } 812 | }, 813 | "autoload": { 814 | "classmap": [ 815 | "src/" 816 | ] 817 | }, 818 | "notification-url": "https://packagist.org/downloads/", 819 | "license": [ 820 | "BSD-3-Clause" 821 | ], 822 | "authors": [ 823 | { 824 | "name": "Sebastian Bergmann", 825 | "email": "sebastian@phpunit.de" 826 | } 827 | ], 828 | "description": "Wrapper around PHP's tokenizer extension.", 829 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 830 | "keywords": [ 831 | "tokenizer" 832 | ], 833 | "support": { 834 | "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", 835 | "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master" 836 | }, 837 | "funding": [ 838 | { 839 | "url": "https://github.com/sebastianbergmann", 840 | "type": "github" 841 | } 842 | ], 843 | "abandoned": true, 844 | "time": "2020-08-04T08:28:15+00:00" 845 | }, 846 | { 847 | "name": "phpunit/phpunit", 848 | "version": "8.5.17", 849 | "source": { 850 | "type": "git", 851 | "url": "https://github.com/sebastianbergmann/phpunit.git", 852 | "reference": "79067856d85421c56d413bd238d4e2cd6b0e54da" 853 | }, 854 | "dist": { 855 | "type": "zip", 856 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/79067856d85421c56d413bd238d4e2cd6b0e54da", 857 | "reference": "79067856d85421c56d413bd238d4e2cd6b0e54da", 858 | "shasum": "" 859 | }, 860 | "require": { 861 | "doctrine/instantiator": "^1.3.1", 862 | "ext-dom": "*", 863 | "ext-json": "*", 864 | "ext-libxml": "*", 865 | "ext-mbstring": "*", 866 | "ext-xml": "*", 867 | "ext-xmlwriter": "*", 868 | "myclabs/deep-copy": "^1.10.0", 869 | "phar-io/manifest": "^2.0.1", 870 | "phar-io/version": "^3.0.2", 871 | "php": ">=7.2", 872 | "phpspec/prophecy": "^1.10.3", 873 | "phpunit/php-code-coverage": "^7.0.12", 874 | "phpunit/php-file-iterator": "^2.0.2", 875 | "phpunit/php-text-template": "^1.2.1", 876 | "phpunit/php-timer": "^2.1.2", 877 | "sebastian/comparator": "^3.0.2", 878 | "sebastian/diff": "^3.0.2", 879 | "sebastian/environment": "^4.2.3", 880 | "sebastian/exporter": "^3.1.2", 881 | "sebastian/global-state": "^3.0.0", 882 | "sebastian/object-enumerator": "^3.0.3", 883 | "sebastian/resource-operations": "^2.0.1", 884 | "sebastian/type": "^1.1.3", 885 | "sebastian/version": "^2.0.1" 886 | }, 887 | "require-dev": { 888 | "ext-pdo": "*" 889 | }, 890 | "suggest": { 891 | "ext-soap": "*", 892 | "ext-xdebug": "*", 893 | "phpunit/php-invoker": "^2.0.0" 894 | }, 895 | "bin": [ 896 | "phpunit" 897 | ], 898 | "type": "library", 899 | "extra": { 900 | "branch-alias": { 901 | "dev-master": "8.5-dev" 902 | } 903 | }, 904 | "autoload": { 905 | "classmap": [ 906 | "src/" 907 | ] 908 | }, 909 | "notification-url": "https://packagist.org/downloads/", 910 | "license": [ 911 | "BSD-3-Clause" 912 | ], 913 | "authors": [ 914 | { 915 | "name": "Sebastian Bergmann", 916 | "email": "sebastian@phpunit.de", 917 | "role": "lead" 918 | } 919 | ], 920 | "description": "The PHP Unit Testing framework.", 921 | "homepage": "https://phpunit.de/", 922 | "keywords": [ 923 | "phpunit", 924 | "testing", 925 | "xunit" 926 | ], 927 | "support": { 928 | "issues": "https://github.com/sebastianbergmann/phpunit/issues", 929 | "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.17" 930 | }, 931 | "funding": [ 932 | { 933 | "url": "https://phpunit.de/donate.html", 934 | "type": "custom" 935 | }, 936 | { 937 | "url": "https://github.com/sebastianbergmann", 938 | "type": "github" 939 | } 940 | ], 941 | "time": "2021-06-23T05:12:43+00:00" 942 | }, 943 | { 944 | "name": "sebastian/code-unit-reverse-lookup", 945 | "version": "1.0.2", 946 | "source": { 947 | "type": "git", 948 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 949 | "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" 950 | }, 951 | "dist": { 952 | "type": "zip", 953 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", 954 | "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", 955 | "shasum": "" 956 | }, 957 | "require": { 958 | "php": ">=5.6" 959 | }, 960 | "require-dev": { 961 | "phpunit/phpunit": "^8.5" 962 | }, 963 | "type": "library", 964 | "extra": { 965 | "branch-alias": { 966 | "dev-master": "1.0.x-dev" 967 | } 968 | }, 969 | "autoload": { 970 | "classmap": [ 971 | "src/" 972 | ] 973 | }, 974 | "notification-url": "https://packagist.org/downloads/", 975 | "license": [ 976 | "BSD-3-Clause" 977 | ], 978 | "authors": [ 979 | { 980 | "name": "Sebastian Bergmann", 981 | "email": "sebastian@phpunit.de" 982 | } 983 | ], 984 | "description": "Looks up which function or method a line of code belongs to", 985 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 986 | "support": { 987 | "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", 988 | "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" 989 | }, 990 | "funding": [ 991 | { 992 | "url": "https://github.com/sebastianbergmann", 993 | "type": "github" 994 | } 995 | ], 996 | "time": "2020-11-30T08:15:22+00:00" 997 | }, 998 | { 999 | "name": "sebastian/comparator", 1000 | "version": "3.0.3", 1001 | "source": { 1002 | "type": "git", 1003 | "url": "https://github.com/sebastianbergmann/comparator.git", 1004 | "reference": "1071dfcef776a57013124ff35e1fc41ccd294758" 1005 | }, 1006 | "dist": { 1007 | "type": "zip", 1008 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758", 1009 | "reference": "1071dfcef776a57013124ff35e1fc41ccd294758", 1010 | "shasum": "" 1011 | }, 1012 | "require": { 1013 | "php": ">=7.1", 1014 | "sebastian/diff": "^3.0", 1015 | "sebastian/exporter": "^3.1" 1016 | }, 1017 | "require-dev": { 1018 | "phpunit/phpunit": "^8.5" 1019 | }, 1020 | "type": "library", 1021 | "extra": { 1022 | "branch-alias": { 1023 | "dev-master": "3.0-dev" 1024 | } 1025 | }, 1026 | "autoload": { 1027 | "classmap": [ 1028 | "src/" 1029 | ] 1030 | }, 1031 | "notification-url": "https://packagist.org/downloads/", 1032 | "license": [ 1033 | "BSD-3-Clause" 1034 | ], 1035 | "authors": [ 1036 | { 1037 | "name": "Sebastian Bergmann", 1038 | "email": "sebastian@phpunit.de" 1039 | }, 1040 | { 1041 | "name": "Jeff Welch", 1042 | "email": "whatthejeff@gmail.com" 1043 | }, 1044 | { 1045 | "name": "Volker Dusch", 1046 | "email": "github@wallbash.com" 1047 | }, 1048 | { 1049 | "name": "Bernhard Schussek", 1050 | "email": "bschussek@2bepublished.at" 1051 | } 1052 | ], 1053 | "description": "Provides the functionality to compare PHP values for equality", 1054 | "homepage": "https://github.com/sebastianbergmann/comparator", 1055 | "keywords": [ 1056 | "comparator", 1057 | "compare", 1058 | "equality" 1059 | ], 1060 | "support": { 1061 | "issues": "https://github.com/sebastianbergmann/comparator/issues", 1062 | "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3" 1063 | }, 1064 | "funding": [ 1065 | { 1066 | "url": "https://github.com/sebastianbergmann", 1067 | "type": "github" 1068 | } 1069 | ], 1070 | "time": "2020-11-30T08:04:30+00:00" 1071 | }, 1072 | { 1073 | "name": "sebastian/diff", 1074 | "version": "3.0.3", 1075 | "source": { 1076 | "type": "git", 1077 | "url": "https://github.com/sebastianbergmann/diff.git", 1078 | "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" 1079 | }, 1080 | "dist": { 1081 | "type": "zip", 1082 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", 1083 | "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", 1084 | "shasum": "" 1085 | }, 1086 | "require": { 1087 | "php": ">=7.1" 1088 | }, 1089 | "require-dev": { 1090 | "phpunit/phpunit": "^7.5 || ^8.0", 1091 | "symfony/process": "^2 || ^3.3 || ^4" 1092 | }, 1093 | "type": "library", 1094 | "extra": { 1095 | "branch-alias": { 1096 | "dev-master": "3.0-dev" 1097 | } 1098 | }, 1099 | "autoload": { 1100 | "classmap": [ 1101 | "src/" 1102 | ] 1103 | }, 1104 | "notification-url": "https://packagist.org/downloads/", 1105 | "license": [ 1106 | "BSD-3-Clause" 1107 | ], 1108 | "authors": [ 1109 | { 1110 | "name": "Sebastian Bergmann", 1111 | "email": "sebastian@phpunit.de" 1112 | }, 1113 | { 1114 | "name": "Kore Nordmann", 1115 | "email": "mail@kore-nordmann.de" 1116 | } 1117 | ], 1118 | "description": "Diff implementation", 1119 | "homepage": "https://github.com/sebastianbergmann/diff", 1120 | "keywords": [ 1121 | "diff", 1122 | "udiff", 1123 | "unidiff", 1124 | "unified diff" 1125 | ], 1126 | "support": { 1127 | "issues": "https://github.com/sebastianbergmann/diff/issues", 1128 | "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3" 1129 | }, 1130 | "funding": [ 1131 | { 1132 | "url": "https://github.com/sebastianbergmann", 1133 | "type": "github" 1134 | } 1135 | ], 1136 | "time": "2020-11-30T07:59:04+00:00" 1137 | }, 1138 | { 1139 | "name": "sebastian/environment", 1140 | "version": "4.2.4", 1141 | "source": { 1142 | "type": "git", 1143 | "url": "https://github.com/sebastianbergmann/environment.git", 1144 | "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0" 1145 | }, 1146 | "dist": { 1147 | "type": "zip", 1148 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", 1149 | "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", 1150 | "shasum": "" 1151 | }, 1152 | "require": { 1153 | "php": ">=7.1" 1154 | }, 1155 | "require-dev": { 1156 | "phpunit/phpunit": "^7.5" 1157 | }, 1158 | "suggest": { 1159 | "ext-posix": "*" 1160 | }, 1161 | "type": "library", 1162 | "extra": { 1163 | "branch-alias": { 1164 | "dev-master": "4.2-dev" 1165 | } 1166 | }, 1167 | "autoload": { 1168 | "classmap": [ 1169 | "src/" 1170 | ] 1171 | }, 1172 | "notification-url": "https://packagist.org/downloads/", 1173 | "license": [ 1174 | "BSD-3-Clause" 1175 | ], 1176 | "authors": [ 1177 | { 1178 | "name": "Sebastian Bergmann", 1179 | "email": "sebastian@phpunit.de" 1180 | } 1181 | ], 1182 | "description": "Provides functionality to handle HHVM/PHP environments", 1183 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1184 | "keywords": [ 1185 | "Xdebug", 1186 | "environment", 1187 | "hhvm" 1188 | ], 1189 | "support": { 1190 | "issues": "https://github.com/sebastianbergmann/environment/issues", 1191 | "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4" 1192 | }, 1193 | "funding": [ 1194 | { 1195 | "url": "https://github.com/sebastianbergmann", 1196 | "type": "github" 1197 | } 1198 | ], 1199 | "time": "2020-11-30T07:53:42+00:00" 1200 | }, 1201 | { 1202 | "name": "sebastian/exporter", 1203 | "version": "3.1.3", 1204 | "source": { 1205 | "type": "git", 1206 | "url": "https://github.com/sebastianbergmann/exporter.git", 1207 | "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e" 1208 | }, 1209 | "dist": { 1210 | "type": "zip", 1211 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e", 1212 | "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e", 1213 | "shasum": "" 1214 | }, 1215 | "require": { 1216 | "php": ">=7.0", 1217 | "sebastian/recursion-context": "^3.0" 1218 | }, 1219 | "require-dev": { 1220 | "ext-mbstring": "*", 1221 | "phpunit/phpunit": "^6.0" 1222 | }, 1223 | "type": "library", 1224 | "extra": { 1225 | "branch-alias": { 1226 | "dev-master": "3.1.x-dev" 1227 | } 1228 | }, 1229 | "autoload": { 1230 | "classmap": [ 1231 | "src/" 1232 | ] 1233 | }, 1234 | "notification-url": "https://packagist.org/downloads/", 1235 | "license": [ 1236 | "BSD-3-Clause" 1237 | ], 1238 | "authors": [ 1239 | { 1240 | "name": "Sebastian Bergmann", 1241 | "email": "sebastian@phpunit.de" 1242 | }, 1243 | { 1244 | "name": "Jeff Welch", 1245 | "email": "whatthejeff@gmail.com" 1246 | }, 1247 | { 1248 | "name": "Volker Dusch", 1249 | "email": "github@wallbash.com" 1250 | }, 1251 | { 1252 | "name": "Adam Harvey", 1253 | "email": "aharvey@php.net" 1254 | }, 1255 | { 1256 | "name": "Bernhard Schussek", 1257 | "email": "bschussek@gmail.com" 1258 | } 1259 | ], 1260 | "description": "Provides the functionality to export PHP variables for visualization", 1261 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1262 | "keywords": [ 1263 | "export", 1264 | "exporter" 1265 | ], 1266 | "support": { 1267 | "issues": "https://github.com/sebastianbergmann/exporter/issues", 1268 | "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.3" 1269 | }, 1270 | "funding": [ 1271 | { 1272 | "url": "https://github.com/sebastianbergmann", 1273 | "type": "github" 1274 | } 1275 | ], 1276 | "time": "2020-11-30T07:47:53+00:00" 1277 | }, 1278 | { 1279 | "name": "sebastian/global-state", 1280 | "version": "3.0.1", 1281 | "source": { 1282 | "type": "git", 1283 | "url": "https://github.com/sebastianbergmann/global-state.git", 1284 | "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b" 1285 | }, 1286 | "dist": { 1287 | "type": "zip", 1288 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/474fb9edb7ab891665d3bfc6317f42a0a150454b", 1289 | "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b", 1290 | "shasum": "" 1291 | }, 1292 | "require": { 1293 | "php": ">=7.2", 1294 | "sebastian/object-reflector": "^1.1.1", 1295 | "sebastian/recursion-context": "^3.0" 1296 | }, 1297 | "require-dev": { 1298 | "ext-dom": "*", 1299 | "phpunit/phpunit": "^8.0" 1300 | }, 1301 | "suggest": { 1302 | "ext-uopz": "*" 1303 | }, 1304 | "type": "library", 1305 | "extra": { 1306 | "branch-alias": { 1307 | "dev-master": "3.0-dev" 1308 | } 1309 | }, 1310 | "autoload": { 1311 | "classmap": [ 1312 | "src/" 1313 | ] 1314 | }, 1315 | "notification-url": "https://packagist.org/downloads/", 1316 | "license": [ 1317 | "BSD-3-Clause" 1318 | ], 1319 | "authors": [ 1320 | { 1321 | "name": "Sebastian Bergmann", 1322 | "email": "sebastian@phpunit.de" 1323 | } 1324 | ], 1325 | "description": "Snapshotting of global state", 1326 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1327 | "keywords": [ 1328 | "global state" 1329 | ], 1330 | "support": { 1331 | "issues": "https://github.com/sebastianbergmann/global-state/issues", 1332 | "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.1" 1333 | }, 1334 | "funding": [ 1335 | { 1336 | "url": "https://github.com/sebastianbergmann", 1337 | "type": "github" 1338 | } 1339 | ], 1340 | "time": "2020-11-30T07:43:24+00:00" 1341 | }, 1342 | { 1343 | "name": "sebastian/object-enumerator", 1344 | "version": "3.0.4", 1345 | "source": { 1346 | "type": "git", 1347 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1348 | "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" 1349 | }, 1350 | "dist": { 1351 | "type": "zip", 1352 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", 1353 | "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", 1354 | "shasum": "" 1355 | }, 1356 | "require": { 1357 | "php": ">=7.0", 1358 | "sebastian/object-reflector": "^1.1.1", 1359 | "sebastian/recursion-context": "^3.0" 1360 | }, 1361 | "require-dev": { 1362 | "phpunit/phpunit": "^6.0" 1363 | }, 1364 | "type": "library", 1365 | "extra": { 1366 | "branch-alias": { 1367 | "dev-master": "3.0.x-dev" 1368 | } 1369 | }, 1370 | "autoload": { 1371 | "classmap": [ 1372 | "src/" 1373 | ] 1374 | }, 1375 | "notification-url": "https://packagist.org/downloads/", 1376 | "license": [ 1377 | "BSD-3-Clause" 1378 | ], 1379 | "authors": [ 1380 | { 1381 | "name": "Sebastian Bergmann", 1382 | "email": "sebastian@phpunit.de" 1383 | } 1384 | ], 1385 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1386 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1387 | "support": { 1388 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", 1389 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4" 1390 | }, 1391 | "funding": [ 1392 | { 1393 | "url": "https://github.com/sebastianbergmann", 1394 | "type": "github" 1395 | } 1396 | ], 1397 | "time": "2020-11-30T07:40:27+00:00" 1398 | }, 1399 | { 1400 | "name": "sebastian/object-reflector", 1401 | "version": "1.1.2", 1402 | "source": { 1403 | "type": "git", 1404 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1405 | "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" 1406 | }, 1407 | "dist": { 1408 | "type": "zip", 1409 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", 1410 | "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", 1411 | "shasum": "" 1412 | }, 1413 | "require": { 1414 | "php": ">=7.0" 1415 | }, 1416 | "require-dev": { 1417 | "phpunit/phpunit": "^6.0" 1418 | }, 1419 | "type": "library", 1420 | "extra": { 1421 | "branch-alias": { 1422 | "dev-master": "1.1-dev" 1423 | } 1424 | }, 1425 | "autoload": { 1426 | "classmap": [ 1427 | "src/" 1428 | ] 1429 | }, 1430 | "notification-url": "https://packagist.org/downloads/", 1431 | "license": [ 1432 | "BSD-3-Clause" 1433 | ], 1434 | "authors": [ 1435 | { 1436 | "name": "Sebastian Bergmann", 1437 | "email": "sebastian@phpunit.de" 1438 | } 1439 | ], 1440 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1441 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1442 | "support": { 1443 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues", 1444 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2" 1445 | }, 1446 | "funding": [ 1447 | { 1448 | "url": "https://github.com/sebastianbergmann", 1449 | "type": "github" 1450 | } 1451 | ], 1452 | "time": "2020-11-30T07:37:18+00:00" 1453 | }, 1454 | { 1455 | "name": "sebastian/recursion-context", 1456 | "version": "3.0.1", 1457 | "source": { 1458 | "type": "git", 1459 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1460 | "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" 1461 | }, 1462 | "dist": { 1463 | "type": "zip", 1464 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", 1465 | "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", 1466 | "shasum": "" 1467 | }, 1468 | "require": { 1469 | "php": ">=7.0" 1470 | }, 1471 | "require-dev": { 1472 | "phpunit/phpunit": "^6.0" 1473 | }, 1474 | "type": "library", 1475 | "extra": { 1476 | "branch-alias": { 1477 | "dev-master": "3.0.x-dev" 1478 | } 1479 | }, 1480 | "autoload": { 1481 | "classmap": [ 1482 | "src/" 1483 | ] 1484 | }, 1485 | "notification-url": "https://packagist.org/downloads/", 1486 | "license": [ 1487 | "BSD-3-Clause" 1488 | ], 1489 | "authors": [ 1490 | { 1491 | "name": "Sebastian Bergmann", 1492 | "email": "sebastian@phpunit.de" 1493 | }, 1494 | { 1495 | "name": "Jeff Welch", 1496 | "email": "whatthejeff@gmail.com" 1497 | }, 1498 | { 1499 | "name": "Adam Harvey", 1500 | "email": "aharvey@php.net" 1501 | } 1502 | ], 1503 | "description": "Provides functionality to recursively process PHP variables", 1504 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1505 | "support": { 1506 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues", 1507 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1" 1508 | }, 1509 | "funding": [ 1510 | { 1511 | "url": "https://github.com/sebastianbergmann", 1512 | "type": "github" 1513 | } 1514 | ], 1515 | "time": "2020-11-30T07:34:24+00:00" 1516 | }, 1517 | { 1518 | "name": "sebastian/resource-operations", 1519 | "version": "2.0.2", 1520 | "source": { 1521 | "type": "git", 1522 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1523 | "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3" 1524 | }, 1525 | "dist": { 1526 | "type": "zip", 1527 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3", 1528 | "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3", 1529 | "shasum": "" 1530 | }, 1531 | "require": { 1532 | "php": ">=7.1" 1533 | }, 1534 | "type": "library", 1535 | "extra": { 1536 | "branch-alias": { 1537 | "dev-master": "2.0-dev" 1538 | } 1539 | }, 1540 | "autoload": { 1541 | "classmap": [ 1542 | "src/" 1543 | ] 1544 | }, 1545 | "notification-url": "https://packagist.org/downloads/", 1546 | "license": [ 1547 | "BSD-3-Clause" 1548 | ], 1549 | "authors": [ 1550 | { 1551 | "name": "Sebastian Bergmann", 1552 | "email": "sebastian@phpunit.de" 1553 | } 1554 | ], 1555 | "description": "Provides a list of PHP built-in functions that operate on resources", 1556 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1557 | "support": { 1558 | "issues": "https://github.com/sebastianbergmann/resource-operations/issues", 1559 | "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2" 1560 | }, 1561 | "funding": [ 1562 | { 1563 | "url": "https://github.com/sebastianbergmann", 1564 | "type": "github" 1565 | } 1566 | ], 1567 | "time": "2020-11-30T07:30:19+00:00" 1568 | }, 1569 | { 1570 | "name": "sebastian/type", 1571 | "version": "1.1.4", 1572 | "source": { 1573 | "type": "git", 1574 | "url": "https://github.com/sebastianbergmann/type.git", 1575 | "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4" 1576 | }, 1577 | "dist": { 1578 | "type": "zip", 1579 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4", 1580 | "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4", 1581 | "shasum": "" 1582 | }, 1583 | "require": { 1584 | "php": ">=7.2" 1585 | }, 1586 | "require-dev": { 1587 | "phpunit/phpunit": "^8.2" 1588 | }, 1589 | "type": "library", 1590 | "extra": { 1591 | "branch-alias": { 1592 | "dev-master": "1.1-dev" 1593 | } 1594 | }, 1595 | "autoload": { 1596 | "classmap": [ 1597 | "src/" 1598 | ] 1599 | }, 1600 | "notification-url": "https://packagist.org/downloads/", 1601 | "license": [ 1602 | "BSD-3-Clause" 1603 | ], 1604 | "authors": [ 1605 | { 1606 | "name": "Sebastian Bergmann", 1607 | "email": "sebastian@phpunit.de", 1608 | "role": "lead" 1609 | } 1610 | ], 1611 | "description": "Collection of value objects that represent the types of the PHP type system", 1612 | "homepage": "https://github.com/sebastianbergmann/type", 1613 | "support": { 1614 | "issues": "https://github.com/sebastianbergmann/type/issues", 1615 | "source": "https://github.com/sebastianbergmann/type/tree/1.1.4" 1616 | }, 1617 | "funding": [ 1618 | { 1619 | "url": "https://github.com/sebastianbergmann", 1620 | "type": "github" 1621 | } 1622 | ], 1623 | "time": "2020-11-30T07:25:11+00:00" 1624 | }, 1625 | { 1626 | "name": "sebastian/version", 1627 | "version": "2.0.1", 1628 | "source": { 1629 | "type": "git", 1630 | "url": "https://github.com/sebastianbergmann/version.git", 1631 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 1632 | }, 1633 | "dist": { 1634 | "type": "zip", 1635 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 1636 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 1637 | "shasum": "" 1638 | }, 1639 | "require": { 1640 | "php": ">=5.6" 1641 | }, 1642 | "type": "library", 1643 | "extra": { 1644 | "branch-alias": { 1645 | "dev-master": "2.0.x-dev" 1646 | } 1647 | }, 1648 | "autoload": { 1649 | "classmap": [ 1650 | "src/" 1651 | ] 1652 | }, 1653 | "notification-url": "https://packagist.org/downloads/", 1654 | "license": [ 1655 | "BSD-3-Clause" 1656 | ], 1657 | "authors": [ 1658 | { 1659 | "name": "Sebastian Bergmann", 1660 | "email": "sebastian@phpunit.de", 1661 | "role": "lead" 1662 | } 1663 | ], 1664 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1665 | "homepage": "https://github.com/sebastianbergmann/version", 1666 | "support": { 1667 | "issues": "https://github.com/sebastianbergmann/version/issues", 1668 | "source": "https://github.com/sebastianbergmann/version/tree/master" 1669 | }, 1670 | "time": "2016-10-03T07:35:21+00:00" 1671 | }, 1672 | { 1673 | "name": "symfony/polyfill-ctype", 1674 | "version": "v1.23.0", 1675 | "source": { 1676 | "type": "git", 1677 | "url": "https://github.com/symfony/polyfill-ctype.git", 1678 | "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce" 1679 | }, 1680 | "dist": { 1681 | "type": "zip", 1682 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce", 1683 | "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce", 1684 | "shasum": "" 1685 | }, 1686 | "require": { 1687 | "php": ">=7.1" 1688 | }, 1689 | "suggest": { 1690 | "ext-ctype": "For best performance" 1691 | }, 1692 | "type": "library", 1693 | "extra": { 1694 | "branch-alias": { 1695 | "dev-main": "1.23-dev" 1696 | }, 1697 | "thanks": { 1698 | "name": "symfony/polyfill", 1699 | "url": "https://github.com/symfony/polyfill" 1700 | } 1701 | }, 1702 | "autoload": { 1703 | "psr-4": { 1704 | "Symfony\\Polyfill\\Ctype\\": "" 1705 | }, 1706 | "files": [ 1707 | "bootstrap.php" 1708 | ] 1709 | }, 1710 | "notification-url": "https://packagist.org/downloads/", 1711 | "license": [ 1712 | "MIT" 1713 | ], 1714 | "authors": [ 1715 | { 1716 | "name": "Gert de Pagter", 1717 | "email": "BackEndTea@gmail.com" 1718 | }, 1719 | { 1720 | "name": "Symfony Community", 1721 | "homepage": "https://symfony.com/contributors" 1722 | } 1723 | ], 1724 | "description": "Symfony polyfill for ctype functions", 1725 | "homepage": "https://symfony.com", 1726 | "keywords": [ 1727 | "compatibility", 1728 | "ctype", 1729 | "polyfill", 1730 | "portable" 1731 | ], 1732 | "support": { 1733 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0" 1734 | }, 1735 | "funding": [ 1736 | { 1737 | "url": "https://symfony.com/sponsor", 1738 | "type": "custom" 1739 | }, 1740 | { 1741 | "url": "https://github.com/fabpot", 1742 | "type": "github" 1743 | }, 1744 | { 1745 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1746 | "type": "tidelift" 1747 | } 1748 | ], 1749 | "time": "2021-02-19T12:13:01+00:00" 1750 | }, 1751 | { 1752 | "name": "theseer/tokenizer", 1753 | "version": "1.2.0", 1754 | "source": { 1755 | "type": "git", 1756 | "url": "https://github.com/theseer/tokenizer.git", 1757 | "reference": "75a63c33a8577608444246075ea0af0d052e452a" 1758 | }, 1759 | "dist": { 1760 | "type": "zip", 1761 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", 1762 | "reference": "75a63c33a8577608444246075ea0af0d052e452a", 1763 | "shasum": "" 1764 | }, 1765 | "require": { 1766 | "ext-dom": "*", 1767 | "ext-tokenizer": "*", 1768 | "ext-xmlwriter": "*", 1769 | "php": "^7.2 || ^8.0" 1770 | }, 1771 | "type": "library", 1772 | "autoload": { 1773 | "classmap": [ 1774 | "src/" 1775 | ] 1776 | }, 1777 | "notification-url": "https://packagist.org/downloads/", 1778 | "license": [ 1779 | "BSD-3-Clause" 1780 | ], 1781 | "authors": [ 1782 | { 1783 | "name": "Arne Blankerts", 1784 | "email": "arne@blankerts.de", 1785 | "role": "Developer" 1786 | } 1787 | ], 1788 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1789 | "support": { 1790 | "issues": "https://github.com/theseer/tokenizer/issues", 1791 | "source": "https://github.com/theseer/tokenizer/tree/master" 1792 | }, 1793 | "funding": [ 1794 | { 1795 | "url": "https://github.com/theseer", 1796 | "type": "github" 1797 | } 1798 | ], 1799 | "time": "2020-07-12T23:59:07+00:00" 1800 | }, 1801 | { 1802 | "name": "webmozart/assert", 1803 | "version": "1.10.0", 1804 | "source": { 1805 | "type": "git", 1806 | "url": "https://github.com/webmozarts/assert.git", 1807 | "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" 1808 | }, 1809 | "dist": { 1810 | "type": "zip", 1811 | "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", 1812 | "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", 1813 | "shasum": "" 1814 | }, 1815 | "require": { 1816 | "php": "^7.2 || ^8.0", 1817 | "symfony/polyfill-ctype": "^1.8" 1818 | }, 1819 | "conflict": { 1820 | "phpstan/phpstan": "<0.12.20", 1821 | "vimeo/psalm": "<4.6.1 || 4.6.2" 1822 | }, 1823 | "require-dev": { 1824 | "phpunit/phpunit": "^8.5.13" 1825 | }, 1826 | "type": "library", 1827 | "extra": { 1828 | "branch-alias": { 1829 | "dev-master": "1.10-dev" 1830 | } 1831 | }, 1832 | "autoload": { 1833 | "psr-4": { 1834 | "Webmozart\\Assert\\": "src/" 1835 | } 1836 | }, 1837 | "notification-url": "https://packagist.org/downloads/", 1838 | "license": [ 1839 | "MIT" 1840 | ], 1841 | "authors": [ 1842 | { 1843 | "name": "Bernhard Schussek", 1844 | "email": "bschussek@gmail.com" 1845 | } 1846 | ], 1847 | "description": "Assertions to validate method input/output with nice error messages.", 1848 | "keywords": [ 1849 | "assert", 1850 | "check", 1851 | "validate" 1852 | ], 1853 | "support": { 1854 | "issues": "https://github.com/webmozarts/assert/issues", 1855 | "source": "https://github.com/webmozarts/assert/tree/1.10.0" 1856 | }, 1857 | "time": "2021-03-09T10:59:23+00:00" 1858 | } 1859 | ], 1860 | "aliases": [], 1861 | "minimum-stability": "stable", 1862 | "stability-flags": [], 1863 | "prefer-stable": false, 1864 | "prefer-lowest": false, 1865 | "platform": { 1866 | "php": "^5.6 || ^7.0 || ^8.0", 1867 | "ext-curl": "*", 1868 | "ext-json": "*" 1869 | }, 1870 | "platform-dev": [], 1871 | "plugin-api-version": "2.1.0" 1872 | } 1873 | -------------------------------------------------------------------------------- /etc/links.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "itag": 18, 4 | "mimeType": "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", 5 | "width": 640, 6 | "height": 360, 7 | "contentLength": "37566943", 8 | "quality": "medium", 9 | "qualityLabel": "360p", 10 | "audioQuality": "AUDIO_QUALITY_LOW", 11 | "audioSampleRate": "44100", 12 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=18&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fmp4&ns=ClVsKdYkdsXEH-wN5BA-tTkF&gir=yes&clen=37566943&ratebypass=yes&dur=667.016&lmt=1612997479356301&mt=1613060937&fvip=3&beids=9466586&c=WEB&txp=5530434&n=B4Y7dhUpKo8z1j&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAP3_51aqK3K3MONvgqDmcthpiM8oniucCXxMs01ebhq2AiEAmfHQaKsiu96gyf9TD7x_EPt3dLFqyWNWBr7ksFP7y40%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 13 | "signatureCipher": null 14 | }, 15 | { 16 | "itag": 22, 17 | "mimeType": "video/mp4; codecs=\"avc1.64001F, mp4a.40.2\"", 18 | "width": 1280, 19 | "height": 720, 20 | "contentLength": null, 21 | "quality": "hd720", 22 | "qualityLabel": "720p", 23 | "audioQuality": "AUDIO_QUALITY_MEDIUM", 24 | "audioSampleRate": "44100", 25 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=22&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fmp4&ns=ClVsKdYkdsXEH-wN5BA-tTkF&cnr=14&ratebypass=yes&dur=667.016&lmt=1613000535641001&mt=1613060937&fvip=3&beids=9466586&c=WEB&txp=5532434&n=B4Y7dhUpKo8z1j&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAKTqGSfCcG2SnSHE-f0ZprCv_BNAdWfdr3wK-ILgux3lAiA6hidm7mqjMR4rYdMTjk7a3A9SLA1PbmAlyEPn_8cD9Q%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 26 | "signatureCipher": null 27 | }, 28 | { 29 | "itag": 137, 30 | "mimeType": "video/mp4; codecs=\"avc1.640028\"", 31 | "width": 1920, 32 | "height": 1080, 33 | "contentLength": "107754305", 34 | "quality": "hd1080", 35 | "qualityLabel": "1080p", 36 | "audioQuality": null, 37 | "audioSampleRate": null, 38 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=137&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fmp4&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=107754305&dur=666.960&lmt=1613001396223569&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAKtEZ6ecG56Eaye7oTYOPZoAlXO2lps9I0KQKpz6UNC9AiEA4qIQEFugWm-DIcjnpwMiX2qS0SMvn08db26wUg2WZ0M%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 39 | "signatureCipher": null 40 | }, 41 | { 42 | "itag": 248, 43 | "mimeType": "video/webm; codecs=\"vp9\"", 44 | "width": 1920, 45 | "height": 1080, 46 | "contentLength": "90902983", 47 | "quality": "hd1080", 48 | "qualityLabel": "1080p", 49 | "audioQuality": null, 50 | "audioSampleRate": null, 51 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=248&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fwebm&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=90902983&dur=666.960&lmt=1613004063059938&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAIoWCawOnzuDhqu4o_y_sVceQZ3ouftr90y8Dzb27Ju4AiBp736HAiWsPgd87Q8yvfHz2pDrtYZ8d3lubJJvjcgRyA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 52 | "signatureCipher": null 53 | }, 54 | { 55 | "itag": 136, 56 | "mimeType": "video/mp4; codecs=\"avc1.4d401f\"", 57 | "width": 1280, 58 | "height": 720, 59 | "contentLength": "31161584", 60 | "quality": "hd720", 61 | "qualityLabel": "720p", 62 | "audioQuality": null, 63 | "audioSampleRate": null, 64 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=136&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fmp4&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=31161584&dur=666.960&lmt=1613000523151160&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhANlyzM2wvDVDNGcVRJBLwZC_XMcIaPpLMojkQWCjQawhAiEA-X2MnprsYj5-98wf-L0CRIDALsXIb4aPNHxL5YSfYMo%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 65 | "signatureCipher": null 66 | }, 67 | { 68 | "itag": 247, 69 | "mimeType": "video/webm; codecs=\"vp9\"", 70 | "width": 1280, 71 | "height": 720, 72 | "contentLength": "33655281", 73 | "quality": "hd720", 74 | "qualityLabel": "720p", 75 | "audioQuality": null, 76 | "audioSampleRate": null, 77 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=247&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fwebm&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=33655281&dur=666.960&lmt=1613004691754283&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAJOCki1v84LbBnTsbpIVf4vgwe1J3E8hWmSCa6431iZMAiEAxXIX7yYAFGLO_oAmbdpNdfgJhHUlf68pXXd8xVlmtM8%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 78 | "signatureCipher": null 79 | }, 80 | { 81 | "itag": 135, 82 | "mimeType": "video/mp4; codecs=\"avc1.4d401e\"", 83 | "width": 854, 84 | "height": 480, 85 | "contentLength": "18188922", 86 | "quality": "large", 87 | "qualityLabel": "480p", 88 | "audioQuality": null, 89 | "audioSampleRate": null, 90 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=135&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fmp4&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=18188922&dur=666.960&lmt=1613000426776116&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAIw_BkZkleH7S-1g5s5e509hRwHnvBuhILxcmZ8R13OoAiEAo8ZVa9hb6CozwI3iaWeC1UenDYqJ2-9Q7mOyxEQsQsU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 91 | "signatureCipher": null 92 | }, 93 | { 94 | "itag": 244, 95 | "mimeType": "video/webm; codecs=\"vp9\"", 96 | "width": 854, 97 | "height": 480, 98 | "contentLength": "21008955", 99 | "quality": "large", 100 | "qualityLabel": "480p", 101 | "audioQuality": null, 102 | "audioSampleRate": null, 103 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=244&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fwebm&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=21008955&dur=666.960&lmt=1613004690905920&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgcc2k0ZzE2DcyN4Y-QmgdFxafB5xs6_RegV6OLb1l0RMCIGxrorv3cHVXdUa9TWXN3orCJK6djUPgcOdNuJ9Mp8gl&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 104 | "signatureCipher": null 105 | }, 106 | { 107 | "itag": 134, 108 | "mimeType": "video/mp4; codecs=\"avc1.4d401e\"", 109 | "width": 640, 110 | "height": 360, 111 | "contentLength": "12491149", 112 | "quality": "medium", 113 | "qualityLabel": "360p", 114 | "audioQuality": null, 115 | "audioSampleRate": null, 116 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=134&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fmp4&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=12491149&dur=666.960&lmt=1613000413521531&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAICCGzhYkEAj2BcLscT8DLNhby4DVpmNqWVBvoSGCLFaAiEAj01K0Sq8Iq-UM7ne0tCDclF2hJCv-nrByxrGrR08P_g%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 117 | "signatureCipher": null 118 | }, 119 | { 120 | "itag": 243, 121 | "mimeType": "video/webm; codecs=\"vp9\"", 122 | "width": 640, 123 | "height": 360, 124 | "contentLength": "14961041", 125 | "quality": "medium", 126 | "qualityLabel": "360p", 127 | "audioQuality": null, 128 | "audioSampleRate": null, 129 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=243&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fwebm&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=14961041&dur=666.960&lmt=1613004699985244&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgBmSbWsRc6Q4ZJurlr63ap-C7IFTshKjKoFs3rquDnbgCIC4nxbVs8q_Ud4r6ReNI4uL4tuGVblSudqLuXQ_ZHusY&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 130 | "signatureCipher": null 131 | }, 132 | { 133 | "itag": 133, 134 | "mimeType": "video/mp4; codecs=\"avc1.4d4015\"", 135 | "width": 426, 136 | "height": 240, 137 | "contentLength": "7255081", 138 | "quality": "small", 139 | "qualityLabel": "240p", 140 | "audioQuality": null, 141 | "audioSampleRate": null, 142 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=133&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fmp4&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=7255081&dur=666.960&lmt=1613000469082024&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgRdHAU9QdSOfmTQcaSH9EY3kPHcpDmR-l7lFGj-FigSUCIQCeUo5Pzt9tjcyHqW6oySazkS73Jjn4RtZuwA7OKrz5UQ%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 143 | "signatureCipher": null 144 | }, 145 | { 146 | "itag": 242, 147 | "mimeType": "video/webm; codecs=\"vp9\"", 148 | "width": 426, 149 | "height": 240, 150 | "contentLength": "9197885", 151 | "quality": "small", 152 | "qualityLabel": "240p", 153 | "audioQuality": null, 154 | "audioSampleRate": null, 155 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=242&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fwebm&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=9197885&dur=666.960&lmt=1613004705046561&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAIKKZ7uwcEaP8vxJLvaPVOjB4SmHpZCc5-sekNHE2yPxAiEAqz4a28sKoau88MYOlC8sy3JjfTqSVjrSWVE0-_N9c20%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 156 | "signatureCipher": null 157 | }, 158 | { 159 | "itag": 160, 160 | "mimeType": "video/mp4; codecs=\"avc1.4d400c\"", 161 | "width": 256, 162 | "height": 144, 163 | "contentLength": "4647707", 164 | "quality": "tiny", 165 | "qualityLabel": "144p", 166 | "audioQuality": null, 167 | "audioSampleRate": null, 168 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=160&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fmp4&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=4647707&dur=666.960&lmt=1613000438648456&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAJKVjdrJivysIF9ps3tSIr1O36bg400lzlFIMbo2rN3uAiA5gvWC_GR8BF-1MumlnppAZNqNxdM2WNRfPjFuFMuu0w%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 169 | "signatureCipher": null 170 | }, 171 | { 172 | "itag": 278, 173 | "mimeType": "video/webm; codecs=\"vp9\"", 174 | "width": 256, 175 | "height": 144, 176 | "contentLength": "6913554", 177 | "quality": "tiny", 178 | "qualityLabel": "144p", 179 | "audioQuality": null, 180 | "audioSampleRate": null, 181 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=278&aitags=133%2C134%2C135%2C136%2C137%2C160%2C242%2C243%2C244%2C247%2C248%2C278&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=video%2Fwebm&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=6913554&dur=666.960&lmt=1613004710486243&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5535434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgaWkZZW57RaxzjlA4Ylzr7XOoDtxVepaByNoPmq9DiicCICyqre6WNrk_045O8GzbOJZhTzga7Tlq5T_oZ3FmOBun&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 182 | "signatureCipher": null 183 | }, 184 | { 185 | "itag": 140, 186 | "mimeType": "audio/mp4; codecs=\"mp4a.40.2\"", 187 | "width": null, 188 | "height": null, 189 | "contentLength": "10795602", 190 | "quality": "tiny", 191 | "qualityLabel": null, 192 | "audioQuality": "AUDIO_QUALITY_MEDIUM", 193 | "audioSampleRate": "44100", 194 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=140&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=audio%2Fmp4&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=10795602&dur=667.016&lmt=1612996129703340&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5532434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAOZ_w4P2y6CCyGivKkIDihpdoF065NQ2xQqYj-McS0YyAiEAlqgbTLzYFCowKm64zQ4HqdaRrU1sBjcCk6BdL3pAWxE%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 195 | "signatureCipher": null 196 | }, 197 | { 198 | "itag": 249, 199 | "mimeType": "audio/webm; codecs=\"opus\"", 200 | "width": null, 201 | "height": null, 202 | "contentLength": "4346683", 203 | "quality": "tiny", 204 | "qualityLabel": null, 205 | "audioQuality": "AUDIO_QUALITY_LOW", 206 | "audioSampleRate": "48000", 207 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=249&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=audio%2Fwebm&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=4346683&dur=666.981&lmt=1612996146279714&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5532434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgD90YBq4kr2phCMTkVs-Fpaw8FyN8O5J2dS9ss3djKYYCIDCd2OSHvSzUatm-qvBqiJ_SQYqqyARvUDcphHmV0hPH&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 208 | "signatureCipher": null 209 | }, 210 | { 211 | "itag": 250, 212 | "mimeType": "audio/webm; codecs=\"opus\"", 213 | "width": null, 214 | "height": null, 215 | "contentLength": "6200435", 216 | "quality": "tiny", 217 | "qualityLabel": null, 218 | "audioQuality": "AUDIO_QUALITY_LOW", 219 | "audioSampleRate": "48000", 220 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=250&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=audio%2Fwebm&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=6200435&dur=666.981&lmt=1612996701287229&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5532434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgQohaUl41PyWAW6gq3ME1G7nueAJ55OCQHoeTHOucf6kCIFK1SaIrJGb9UWqrR4F6PFO2XU80OdeY4A-MvrlvHdxH&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 221 | "signatureCipher": null 222 | }, 223 | { 224 | "itag": 251, 225 | "mimeType": "audio/webm; codecs=\"opus\"", 226 | "width": null, 227 | "height": null, 228 | "contentLength": "11603339", 229 | "quality": "tiny", 230 | "qualityLabel": null, 231 | "audioQuality": "AUDIO_QUALITY_MEDIUM", 232 | "audioSampleRate": "48000", 233 | "url": "https://r3---sn-vgqs7ney.googlevideo.com/videoplayback?expire=1613083175&ei=xl0lYLTsOYuRigTniKewBw&ip=73.44.159.175&id=o-AK5U3YRNvHl6y8UNYeHOuSju2dUkkBqiWNNn4EnczOE3&itag=251&source=youtube&requiressl=yes&mh=ta&mm=31%2C29&mn=sn-vgqs7ney%2Csn-vgqskns7&ms=au%2Crdu&mv=m&mvi=3&pl=15&initcwndbps=1785000&vprv=1&mime=audio%2Fwebm&ns=K_JP2OGgfaFu6evpcZw7POgF&gir=yes&clen=11603339&dur=666.981&lmt=1612996129075535&mt=1613060937&fvip=3&keepalive=yes&beids=9466586&c=WEB&txp=5532434&n=KfWqVJy7kNiAOe&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAMTxBDoQdNOlxqm-JzAbL-bgQEY0_BpuybB5_Kh5yeEEAiBPh2Spw2a1x5OSez1VDB7QHngzJCg8exuWyfU0xm8Ckg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhAN1GHEBLYLpgmMpZsCIpt6f4U7zBPSPkLsP5ouc2oMVWAiEA1Ff2HrdIVYUqV3Vlq_uncsUEebAKU4RjUMziTUjItwU%3D", 234 | "signatureCipher": null 235 | } 236 | ] -------------------------------------------------------------------------------- /etc/youtube_urls.txt: -------------------------------------------------------------------------------- 1 | https://www.youtube.com/watch?v=aqz-KE-bpKQ&ab_channel=Blender 2 | http://www.youtube.com/attribution_link?a=aqz-KE-bpKQ&u=/watch?v=aqz-KE-bpKQ&feature=share 3 | https://www.youtube.com/attribution_link?a=8g8kPrPIi-ecwIsS&u=/watch?v=aqz-KE-bpKQ&feature=em-uploademail 4 | https://www.youtube.com/user/IngridMichaelsonVEVO#p/a/u/1/aqz-KE-bpKQ 5 | https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ?rel=0 6 | http://www.youtube.com/ytscreeningroom?v=aqz-KE-bpKQ 7 | http://www.youtube.com/user/SilkRoadTheatre#p/a/u/2/aqz-KE-bpKQ 8 | http://www.youtube.com/watch?feature=player_embedded&v=aqz-KE-bpKQ 9 | http://www.youtube.com/?feature=player_embedded&v=aqz-KE-bpKQ 10 | http://www.youtube.com/user/IngridMichaelsonVEVO#p/u/11/aqz-KE-bpKQ 11 | http://www.youtube-nocookie.com/v/aqz-KE-bpKQ?version=3&hl=en_US&rel=0 12 | http://www.youtube.com/user/dreamtheater#p/u/1/aqz-KE-bpKQ 13 | http://www.youtube.com/ytscreeningroom?v=aqz-KE-bpKQ -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 6 3 | paths: 4 | - src -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 51 | 52 | Youtube Downloader 53 | 54 | 55 | 56 | 57 |

YouTube Downloader By Maadhav Sharma 58 |

59 |

60 | Source Code: 61 | https://github.com/Maadhav/youtube-downloader-API 63 |

64 |
65 | 67 | 68 | 69 |
70 | 71 | 75 | 76 | 77 | 132 |

133 | Project Originally Forked From: 134 | https://github.com/Athlon1600/youtube-downloader 135 |

136 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /public/stream.php: -------------------------------------------------------------------------------- 1 | stream($url); 15 | -------------------------------------------------------------------------------- /public/video_info.php: -------------------------------------------------------------------------------- 1 | 'No URL provided!' 17 | ]); 18 | } 19 | 20 | $youtube = new \YouTube\YouTubeDownloader(); 21 | 22 | try { 23 | $links = $youtube->getDownloadLinks($url); 24 | 25 | $best = $links->getFirstCombinedFormat(); 26 | 27 | if ($best) { 28 | send_json([ 29 | 'links' => [$best->url] 30 | ]); 31 | } else { 32 | send_json(['error' => 'No links found']); 33 | } 34 | 35 | } catch (\YouTube\Exception\YouTubeException $e) { 36 | 37 | send_json([ 38 | 'error' => $e->getMessage() 39 | ]); 40 | } 41 | -------------------------------------------------------------------------------- /src/Browser.php: -------------------------------------------------------------------------------- 1 | headers['User-Agent'] = $agent; 13 | } 14 | 15 | public function getUserAgent() 16 | { 17 | return Utils::arrayGet($this->headers, 'User-Agent'); 18 | } 19 | 20 | public function followRedirects($enabled) 21 | { 22 | $this->options[CURLOPT_FOLLOWLOCATION] = $enabled ? 1 : 0; 23 | return $this; 24 | } 25 | 26 | public function cachedGet($url) 27 | { 28 | $cache_path = sprintf('%s/%s', static::getStorageDirectory(), $this->getCacheKey($url)); 29 | 30 | if (file_exists($cache_path)) { 31 | 32 | // unserialize could fail on empty file 33 | $str = file_get_contents($cache_path); 34 | return unserialize($str); 35 | } 36 | 37 | $response = $this->get($url); 38 | 39 | // cache only if successful 40 | if (empty($response->error)) { 41 | file_put_contents($cache_path, serialize($response)); 42 | } 43 | 44 | return $response; 45 | } 46 | 47 | protected function getCacheKey($url) 48 | { 49 | return md5($url) . '_v3'; 50 | } 51 | 52 | public function consentCookies() 53 | { 54 | $response = $this->get('https://www.youtube.com/'); 55 | $current_url = $response->info->url; 56 | 57 | // must be missing that special cookie 58 | if (strpos($current_url, 'consent.youtube.com') !== false) { 59 | 60 | $field_names = ['gl', 'm', 'pc', 'continue', 'ca', 'x', 'v', 't', 'hl', 'src', 'uxe']; 61 | 62 | $form_data = []; 63 | 64 | foreach ($field_names as $name) { 65 | $value = Utils::getInputValueByName($response->body, $name); 66 | $form_data[$name] = htmlspecialchars_decode($value); 67 | } 68 | 69 | // this will set that cookie that we need to never see that message again 70 | $this->post('https://consent.youtube.com/s', $form_data); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/DownloadOptions.php: -------------------------------------------------------------------------------- 1 | formats = $formats; 21 | $this->info = $info; 22 | } 23 | 24 | /** 25 | * @return StreamFormat[] 26 | */ 27 | public function getAllFormats() 28 | { 29 | return $this->formats; 30 | } 31 | 32 | /** 33 | * @return VideoDetails|null 34 | */ 35 | public function getInfo() 36 | { 37 | return $this->info; 38 | } 39 | 40 | // Will not include Videos with Audio 41 | public function getVideoFormats() 42 | { 43 | return Utils::arrayFilterReset($this->getAllFormats(), function ($format) { 44 | /** @var $format StreamFormat */ 45 | return strpos($format->mimeType, 'video') === 0 && empty($format->audioQuality); 46 | }); 47 | } 48 | 49 | public function getAudioFormats() 50 | { 51 | return Utils::arrayFilterReset($this->getAllFormats(), function ($format) { 52 | /** @var $format StreamFormat */ 53 | return strpos($format->mimeType, 'audio') === 0; 54 | }); 55 | } 56 | 57 | public function getCombinedFormats() 58 | { 59 | return Utils::arrayFilterReset($this->getAllFormats(), function ($format) { 60 | /** @var $format StreamFormat */ 61 | return strpos($format->mimeType, 'video') === 0 && !empty($format->audioQuality); 62 | }); 63 | } 64 | 65 | /** 66 | * @return StreamFormat|null 67 | */ 68 | public function getFirstCombinedFormat() 69 | { 70 | $combined = $this->getCombinedFormats(); 71 | return count($combined) ? $combined[0] : null; 72 | } 73 | 74 | protected function getLowToHighVideoFormats() 75 | { 76 | $copy = array_values($this->getVideoFormats()); 77 | 78 | usort($copy, function ($a, $b) { 79 | 80 | /** @var StreamFormat $a */ 81 | /** @var StreamFormat $b */ 82 | 83 | return $a->height - $b->height; 84 | }); 85 | 86 | return $copy; 87 | } 88 | 89 | protected function getLowToHighAudioFormats() 90 | { 91 | $copy = array_values($this->getAudioFormats()); 92 | 93 | // just assume higher filesize => higher quality... 94 | usort($copy, function ($a, $b) { 95 | 96 | /** @var StreamFormat $a */ 97 | /** @var StreamFormat $b */ 98 | 99 | return $a->contentLength - $b->contentLength; 100 | }); 101 | 102 | return $copy; 103 | } 104 | 105 | // Combined using: ffmpeg -i video.mp4 -i audio.mp3 output.mp4 106 | public function getSplitFormats($quality = null) 107 | { 108 | // sort formats by quality in desc, and high = first, medium = middle, low = last 109 | $videos = $this->getLowToHighVideoFormats(); 110 | $audio = $this->getLowToHighAudioFormats(); 111 | 112 | if ($quality == 'high' || $quality == 'best') { 113 | 114 | return new SplitStream([ 115 | 'video' => $videos[count($videos) - 1], 116 | 'audio' => $audio[count($audio) - 1] 117 | ]); 118 | 119 | } else if ($quality == 'low' || $quality == 'worst') { 120 | 121 | return new SplitStream([ 122 | 'video' => $videos[0], 123 | 'audio' => $audio[0] 124 | ]); 125 | } 126 | 127 | // something in between! 128 | return new SplitStream([ 129 | 'video' => $videos[floor(count($videos) / 2)], 130 | 'audio' => $audio[floor(count($audio) / 2)] 131 | ]); 132 | } 133 | } -------------------------------------------------------------------------------- /src/Exception/TooManyRequestsException.php: -------------------------------------------------------------------------------- 1 | page = $page; 16 | } 17 | 18 | /** 19 | * @return WatchVideoPage 20 | */ 21 | public function getPage() 22 | { 23 | return $this->page; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Exception/VideoNotFoundException.php: -------------------------------------------------------------------------------- 1 | fillFromArray($array); 11 | } 12 | } 13 | 14 | public static function fromArray($array) 15 | { 16 | return new static($array); 17 | } 18 | 19 | private function fillFromArray($array) 20 | { 21 | foreach ($array as $key => $val) { 22 | if (property_exists($this, $key)) { 23 | $this->{$key} = $val; 24 | } 25 | } 26 | } 27 | 28 | /** 29 | * @return array 30 | */ 31 | public function toArray() 32 | { 33 | return get_object_vars($this); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Models/SplitStream.php: -------------------------------------------------------------------------------- 1 | mimeType)); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Models/VideoDetails.php: -------------------------------------------------------------------------------- 1 | videoDetails = $videoDetails; 14 | } 15 | 16 | /** 17 | * From `videoDetails` array that appears inside JSON on /watch or /get_video_info pages 18 | * @param $array 19 | * @return static 20 | */ 21 | public static function fromPlayerResponseArray($array) 22 | { 23 | return new static(Utils::arrayGet($array, 'videoDetails')); 24 | } 25 | 26 | public function getId() 27 | { 28 | return Utils::arrayGet($this->videoDetails, 'videoId'); 29 | } 30 | 31 | public function getTitle() 32 | { 33 | return Utils::arrayGet($this->videoDetails, 'title'); 34 | } 35 | 36 | public function getKeywords() 37 | { 38 | return Utils::arrayGet($this->videoDetails, 'keywords'); 39 | } 40 | 41 | public function getShortDescription() 42 | { 43 | return Utils::arrayGet($this->videoDetails, 'shortDescription'); 44 | } 45 | 46 | public function getViewCount() 47 | { 48 | return Utils::arrayGet($this->videoDetails, 'viewCount'); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Models/VideoInfo.php: -------------------------------------------------------------------------------- 1 | getResponseBody()); 12 | } 13 | 14 | public function isError() 15 | { 16 | return Utils::arrayGet($this->getJson(), 'errorcode') !== null; 17 | } 18 | 19 | /** 20 | * About same as `player_response` that appears on video pages. 21 | * @return array 22 | */ 23 | public function getPlayerResponse() 24 | { 25 | $playerResponse = Utils::arrayGet($this->getJson(), 'player_response'); 26 | return json_decode($playerResponse, true); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Responses/HttpResponse.php: -------------------------------------------------------------------------------- 1 | response = $response; 20 | $this->json = json_decode($response->body, true); 21 | } 22 | 23 | public function getResponse() 24 | { 25 | return $this->response; 26 | } 27 | 28 | /** 29 | * @return string|null 30 | */ 31 | public function getResponseBody() 32 | { 33 | return $this->response->body; 34 | } 35 | 36 | /** 37 | * @return array|null 38 | */ 39 | public function getJson() 40 | { 41 | return $this->json; 42 | } 43 | 44 | public function isStatusOkay() 45 | { 46 | return $this->getResponse()->status == 200; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Responses/VideoPlayerJs.php: -------------------------------------------------------------------------------- 1 | getResponseBody(), 'We have been receiving a large volume of requests') !== false || 14 | strpos($this->getResponseBody(), 'systems have detected unusual traffic') !== false || 15 | strpos($this->getResponseBody(), '/recaptcha/') !== false; 16 | } 17 | 18 | public function isVideoNotFound() 19 | { 20 | return strpos($this->getResponseBody(), ' - YouTube') !== false; 21 | } 22 | 23 | public function hasPlayableVideo() 24 | { 25 | $playerResponse = $this->getPlayerResponse(); 26 | $playabilityStatus = Utils::arrayGet($playerResponse, 'playabilityStatus.status'); 27 | 28 | return $this->getResponse()->status == 200 && $playabilityStatus == 'OK'; 29 | } 30 | 31 | /** 32 | * Look for a player script URL. E.g: 33 | * 34 | * 35 | * @return string|null 36 | */ 37 | public function getPlayerScriptUrl() 38 | { 39 | // check what player version that video is using 40 | if (preg_match('@getResponseBody(), $matches)) { 41 | return Utils::relativeToAbsoluteUrl($matches[1], 'https://www.youtube.com'); 42 | } 43 | 44 | return null; 45 | } 46 | 47 | public function getPlayerResponse() 48 | { 49 | // $re = '/ytplayer.config\s*=\s*([^\n]+});ytplayer/i'; 50 | // $re = '/player_response":"(.*?)\"}};/'; 51 | $re = '/ytInitialPlayerResponse\s*=\s*({.+?})\s*;/i'; 52 | 53 | if (preg_match($re, $this->getResponseBody(), $matches)) { 54 | $match = $matches[1]; 55 | return json_decode($match, true); 56 | } 57 | 58 | return array(); 59 | } 60 | 61 | protected function getInitialData() 62 | { 63 | // TODO: this does not appear for mobile 64 | if (preg_match('/ytInitialData\s*=\s*({.+?})\s*;/i', $this->getResponseBody(), $matches)) { 65 | $json = $matches[1]; 66 | return json_decode($json, true); 67 | } 68 | 69 | return null; 70 | } 71 | 72 | /** 73 | * @return VideoInfo 74 | */ 75 | public function getVideoInfo() 76 | { 77 | $playerResponse = $this->getPlayerResponse(); 78 | 79 | $thumbnails = Utils::arrayGet($playerResponse, 'videoDetails.thumbnail.thumbnails', []); 80 | 81 | $thumbnail_url = null; 82 | $thumb_max_width = 0; 83 | 84 | foreach ($thumbnails as $thumbnail) { 85 | 86 | if ($thumbnail['width'] > $thumb_max_width) { 87 | $thumbnail_url = $thumbnail['url']; 88 | $thumb_max_width = $thumbnail['width']; 89 | } 90 | } 91 | 92 | $data = array( 93 | 'id' => Utils::arrayGet($playerResponse, 'videoDetails.videoId'), 94 | 'channelId' => Utils::arrayGet($playerResponse, 'videoDetails.channelId'), 95 | 'channelTitle' => Utils::arrayGet($playerResponse, 'videoDetails.author'), 96 | 'title' => Utils::arrayGet($playerResponse, 'videoDetails.title'), 97 | 'description' => Utils::arrayGet($playerResponse, 'videoDetails.shortDescription'), 98 | 'pageUrl' => $this->getResponse()->info->url, 99 | 'uploadDate' => Utils::arrayGet($playerResponse, 'microformat.playerMicroformatRenderer.uploadDate'), 100 | 'viewCount' => Utils::arrayGet($playerResponse, 'videoDetails.viewCount'), 101 | 'thumbnail' => $thumbnail_url, 102 | 'duration' => Utils::arrayGet($playerResponse, 'videoDetails.lengthSeconds'), 103 | 'keywords' => Utils::arrayGet($playerResponse, 'videoDetails.keywords', []), 104 | 'regionsAllowed' => Utils::arrayGet($playerResponse, 'microformat.playerMicroformatRenderer.availableCountries', []) 105 | ); 106 | 107 | return new VideoInfo($data); 108 | } 109 | } -------------------------------------------------------------------------------- /src/SignatureDecoder.php: -------------------------------------------------------------------------------- 1 | parseFunctionName($js_code); 18 | 19 | // PHP instructions 20 | $instructions = (array)$this->parseFunctionCode($func_name, $js_code); 21 | 22 | foreach ($instructions as $opt) { 23 | 24 | $command = $opt[0]; 25 | $value = $opt[1]; 26 | 27 | if ($command == 'swap') { 28 | 29 | $temp = $signature[0]; 30 | $signature[0] = $signature[$value % strlen($signature)]; 31 | $signature[$value] = $temp; 32 | 33 | } elseif ($command == 'splice') { 34 | $signature = substr($signature, $value); 35 | } elseif ($command == 'reverse') { 36 | $signature = strrev($signature); 37 | } 38 | } 39 | 40 | return trim($signature); 41 | } 42 | 43 | public function parseFunctionName($js_code) 44 | { 45 | if (preg_match('@,\s*encodeURIComponent\((\w{2})@is', $js_code, $matches)) { 46 | $func_name = $matches[1]; 47 | $func_name = preg_quote($func_name); 48 | 49 | return $func_name; 50 | 51 | } else if (preg_match('@(?:\b|[^a-zA-Z0-9$])([a-zA-Z0-9$]{2})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)@is', $js_code, $matches)) { 52 | return preg_quote($matches[1]); 53 | } 54 | 55 | return null; 56 | } 57 | 58 | // convert JS code for signature decipher to PHP code 59 | public function parseFunctionCode($func_name, $player_html) 60 | { 61 | // extract code block from that function 62 | // single quote in case function name contains $dollar sign 63 | // xm=function(a){a=a.split("");wm.zO(a,47);wm.vY(a,1);wm.z9(a,68);wm.zO(a,21);wm.z9(a,34);wm.zO(a,16);wm.z9(a,41);return a.join("")}; 64 | if (preg_match('/' . $func_name . '=function\([a-z]+\){(.*?)}/', $player_html, $matches)) { 65 | 66 | $js_code = $matches[1]; 67 | 68 | // extract all relevant statements within that block 69 | // wm.vY(a,1); 70 | if (preg_match_all('/([a-z0-9$]{2})\.([a-z0-9]{2})\([^,]+,(\d+)\)/i', $js_code, $matches) != false) { 71 | 72 | // wm 73 | $obj_list = $matches[1]; 74 | 75 | // vY 76 | $func_list = $matches[2]; 77 | 78 | // extract javascript code for each one of those statement functions 79 | preg_match_all('/(' . implode('|', $func_list) . '):function(.*?)\}/m', $player_html, $matches2, PREG_SET_ORDER); 80 | 81 | $functions = array(); 82 | 83 | // translate each function according to its use 84 | foreach ($matches2 as $m) { 85 | 86 | if (strpos($m[2], 'splice') !== false) { 87 | $functions[$m[1]] = 'splice'; 88 | } elseif (strpos($m[2], 'a.length') !== false) { 89 | $functions[$m[1]] = 'swap'; 90 | } elseif (strpos($m[2], 'reverse') !== false) { 91 | $functions[$m[1]] = 'reverse'; 92 | } 93 | } 94 | 95 | // FINAL STEP! convert it all to instructions set 96 | $instructions = array(); 97 | 98 | foreach ($matches[2] as $index => $name) { 99 | $instructions[] = array($functions[$name], $matches[3][$index]); 100 | } 101 | 102 | return $instructions; 103 | } 104 | } 105 | 106 | return null; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Utils/ITagUtils.php: -------------------------------------------------------------------------------- 1 | $format) { 36 | 37 | $temp = []; 38 | 39 | if (!empty($format['ext'])) { 40 | $temp[] = $format['ext']; 41 | } 42 | 43 | if (!empty($format['vcodec'])) { 44 | $temp[] = 'video'; 45 | } 46 | 47 | if (!empty($format['height'])) { 48 | $temp[] = $format['height'] . 'p'; 49 | } 50 | 51 | if (!empty($format['acodec']) && $format['acodec'] !== 'none') { 52 | $temp[] = 'audio'; 53 | } 54 | 55 | $results[$itag] = implode(', ', $temp); 56 | } 57 | 58 | return $results; 59 | } 60 | 61 | public static function parseItagInfo($itag) 62 | { 63 | if (isset(static::$itag_detailed[$itag])) { 64 | return static::$itag_detailed[$itag]; 65 | } 66 | 67 | return 'Unknown'; 68 | } 69 | 70 | // itag info does not change frequently, that is why we cache it here as a plain static array 71 | private static $itag_detailed = array( 72 | 5 => 'flv, video, 240p, audio', 73 | 6 => 'flv, video, 270p, audio', 74 | 13 => '3gp, video, audio', 75 | 17 => '3gp, video, 144p, audio', 76 | 18 => 'mp4, video, 360p, audio', 77 | 22 => 'mp4, video, 720p, audio', 78 | 34 => 'flv, video, 360p, audio', 79 | 35 => 'flv, video, 480p, audio', 80 | 36 => '3gp, video, audio', 81 | 37 => 'mp4, video, 1080p, audio', 82 | 38 => 'mp4, video, 3072p, audio', 83 | 43 => 'webm, video, 360p, audio', 84 | 44 => 'webm, video, 480p, audio', 85 | 45 => 'webm, video, 720p, audio', 86 | 46 => 'webm, video, 1080p, audio', 87 | 59 => 'mp4, video, 480p, audio', 88 | 78 => 'mp4, video, 480p, audio', 89 | 82 => 'mp4, video, 360p, audio', 90 | 83 => 'mp4, video, 480p, audio', 91 | 84 => 'mp4, video, 720p, audio', 92 | 85 => 'mp4, video, 1080p, audio', 93 | 100 => 'webm, video, 360p, audio', 94 | 101 => 'webm, video, 480p, audio', 95 | 102 => 'webm, video, 720p, audio', 96 | 91 => 'mp4, video, 144p, audio', 97 | 92 => 'mp4, video, 240p, audio', 98 | 93 => 'mp4, video, 360p, audio', 99 | 94 => 'mp4, video, 480p, audio', 100 | 95 => 'mp4, video, 720p, audio', 101 | 96 => 'mp4, video, 1080p, audio', 102 | 132 => 'mp4, video, 240p, audio', 103 | 151 => 'mp4, video, 72p, audio', 104 | 133 => 'mp4, video, 240p', 105 | 134 => 'mp4, video, 360p', 106 | 135 => 'mp4, video, 480p', 107 | 136 => 'mp4, video, 720p', 108 | 137 => 'mp4, video, 1080p', 109 | 138 => 'mp4, video', 110 | 160 => 'mp4, video, 144p', 111 | 212 => 'mp4, video, 480p', 112 | 264 => 'mp4, video, 1440p', 113 | 298 => 'mp4, video, 720p', 114 | 299 => 'mp4, video, 1080p', 115 | 266 => 'mp4, video, 2160p', 116 | 139 => 'm4a, audio', 117 | 140 => 'm4a, audio', 118 | 141 => 'm4a, audio', 119 | 256 => 'm4a, audio', 120 | 258 => 'm4a, audio', 121 | 325 => 'm4a, audio', 122 | 328 => 'm4a, audio', 123 | 167 => 'webm, video, 360p', 124 | 168 => 'webm, video, 480p', 125 | 169 => 'webm, video, 720p', 126 | 170 => 'webm, video, 1080p', 127 | 218 => 'webm, video, 480p', 128 | 219 => 'webm, video, 480p', 129 | 278 => 'webm, video, 144p', 130 | 242 => 'webm, video, 240p', 131 | 243 => 'webm, video, 360p', 132 | 244 => 'webm, video, 480p', 133 | 245 => 'webm, video, 480p', 134 | 246 => 'webm, video, 480p', 135 | 247 => 'webm, video, 720p', 136 | 248 => 'webm, video, 1080p', 137 | 271 => 'webm, video, 1440p', 138 | 272 => 'webm, video, 2160p', 139 | 302 => 'webm, video, 720p', 140 | 303 => 'webm, video, 1080p', 141 | 308 => 'webm, video, 1440p', 142 | 313 => 'webm, video, 2160p', 143 | 315 => 'webm, video, 2160p', 144 | 171 => 'webm, audio', 145 | 172 => 'webm, audio', 146 | 249 => 'webm, audio', 147 | 250 => 'webm, audio', 148 | 251 => 'webm, audio', 149 | 394 => 'video', 150 | 395 => 'video', 151 | 396 => 'video', 152 | 397 => 'video', 153 | ); 154 | } -------------------------------------------------------------------------------- /src/Utils/SerializationUtils.php: -------------------------------------------------------------------------------- 1 | toArray(); 14 | }, $downloadOptions->getAllFormats()); 15 | } 16 | 17 | public static function optionsFromArray($array) 18 | { 19 | $links = array(); 20 | 21 | foreach ($array as $item) { 22 | $links[] = new StreamFormat($item); 23 | } 24 | 25 | return new DownloadOptions($links); 26 | } 27 | 28 | public static function optionsFromFile($path) 29 | { 30 | $contents = @file_get_contents($path); 31 | 32 | if ($contents) { 33 | $json = json_decode($contents, true); 34 | 35 | if ($json) { 36 | return self::optionsFromArray($json); 37 | } 38 | } 39 | 40 | return null; 41 | } 42 | } -------------------------------------------------------------------------------- /src/Utils/Utils.php: -------------------------------------------------------------------------------- 1 | ]+value=(['\"])(.*?)\\2/is", $html, $matches)) { 99 | return $matches[3]; 100 | } 101 | 102 | return null; 103 | } 104 | } -------------------------------------------------------------------------------- /src/YouTubeDownloader.php: -------------------------------------------------------------------------------- 1 | client = new Browser(); 23 | } 24 | 25 | public function getBrowser() 26 | { 27 | return $this->client; 28 | } 29 | 30 | /** 31 | * @param $query 32 | * @return array 33 | */ 34 | public function getSearchSuggestions($query) 35 | { 36 | $query = rawurlencode($query); 37 | 38 | $response = $this->client->get('http://suggestqueries.google.com/complete/search?client=firefox&ds=yt&q=' . $query); 39 | $json = json_decode($response->body, true); 40 | 41 | if (is_array($json) && count($json) >= 2) { 42 | return $json[1]; 43 | } 44 | 45 | return []; 46 | } 47 | 48 | public function getVideoInfo($video_id) 49 | { 50 | $video_id = Utils::extractVideoId($video_id); 51 | 52 | $response = $this->client->get("https://www.youtube.com/get_video_info?" . http_build_query([ 53 | 'html5' => 1, 54 | 'video_id' => $video_id, 55 | 'eurl' => 'https://youtube.googleapis.com/v/' . $video_id, 56 | 'el' => 'embedded', // or detailpage. default: embedded, will fail if video is not embeddable 57 | 'c' => 'TVHTML5', 58 | 'cver' => '6.20180913' 59 | ])); 60 | 61 | return new GetVideoInfo($response); 62 | } 63 | 64 | public function getPage($url) 65 | { 66 | $video_id = Utils::extractVideoId($url); 67 | 68 | // exact params as used by youtube-dl... must be there for a reason 69 | $response = $this->client->get("https://www.youtube.com/watch?" . http_build_query([ 70 | 'v' => $video_id, 71 | 'gl' => 'US', 72 | 'hl' => 'en', 73 | 'has_verified' => 1, 74 | 'bpctr' => 9999999999 75 | ])); 76 | 77 | return new WatchVideoPage($response); 78 | } 79 | 80 | /** 81 | * To parse the links for the video we need two things: 82 | * contents of `player_response` JSON object that appears on video pages 83 | * contents of player.js script file that's included inside video pages 84 | * 85 | * @param array $player_response 86 | * @param VideoPlayerJs $player 87 | * @return array 88 | */ 89 | public function parseLinksFromPlayerResponse($player_response, VideoPlayerJs $player) 90 | { 91 | $js_code = $player->getResponseBody(); 92 | 93 | $formats = Utils::arrayGet($player_response, 'streamingData.formats', []); 94 | 95 | // video only or audio only streams 96 | $adaptiveFormats = Utils::arrayGet($player_response, 'streamingData.adaptiveFormats', []); 97 | 98 | $formats_combined = array_merge($formats, $adaptiveFormats); 99 | 100 | // final response 101 | $return = array(); 102 | 103 | foreach ($formats_combined as $format) { 104 | 105 | // appear as either "cipher" or "signatureCipher" 106 | $cipher = Utils::arrayGet($format, 'cipher', Utils::arrayGet($format, 'signatureCipher', '')); 107 | 108 | // some videos do not need to be decrypted! 109 | if (isset($format['url'])) { 110 | $return[] = new StreamFormat($format); 111 | continue; 112 | } 113 | 114 | $cipherArray = Utils::parseQueryString($cipher); 115 | 116 | $url = Utils::arrayGet($cipherArray, 'url'); 117 | $sp = Utils::arrayGet($cipherArray, 'sp'); // used to be 'sig' 118 | $signature = Utils::arrayGet($cipherArray, 's'); 119 | 120 | $decoded_signature = (new SignatureDecoder())->decode($signature, $js_code); 121 | 122 | $decoded_url = $url . '&' . $sp . '=' . $decoded_signature; 123 | 124 | $streamUrl = new StreamFormat($format); 125 | $streamUrl->url = $decoded_url; 126 | 127 | $return[] = $streamUrl; 128 | } 129 | 130 | return $return; 131 | } 132 | 133 | /** 134 | * @param $video_id 135 | * @param array $options 136 | * @return DownloadOptions 137 | * @throws TooManyRequestsException 138 | * @throws YouTubeException 139 | */ 140 | public function getDownloadLinks($video_id, $options = array()) 141 | { 142 | $page = $this->getPage($video_id); 143 | 144 | if ($page->isTooManyRequests()) { 145 | throw new TooManyRequestsException($page); 146 | } elseif (!$page->isStatusOkay()) { 147 | throw new YouTubeException('Page failed to load. HTTP error: ' . $page->getResponse()->error); 148 | } elseif ($page->isVideoNotFound()) { 149 | throw new VideoNotFoundException(); 150 | } 151 | // get JSON encoded parameters that appear on video pages 152 | $player_response = $page->getPlayerResponse(); 153 | 154 | // it may ask you to "Sign in to confirm your age" 155 | // we can bypass that by querying /get_video_info 156 | if (!$page->hasPlayableVideo()) { 157 | $player_response = $this->getVideoInfo($video_id)->getPlayerResponse(); 158 | } 159 | 160 | if (empty($player_response)) { 161 | throw new VideoPlayerNotFoundException(); 162 | } 163 | 164 | // get player.js location that holds signature function 165 | $player_url = $page->getPlayerScriptUrl(); 166 | $response = $this->getBrowser()->cachedGet($player_url); 167 | $player = new VideoPlayerJs($response); 168 | 169 | $links = $this->parseLinksFromPlayerResponse($player_response, $player); 170 | 171 | // since we already have that information anyways... 172 | $info = VideoDetails::fromPlayerResponseArray($player_response); 173 | 174 | return new DownloadOptions($links, $info); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/YouTubeStreamer.php: -------------------------------------------------------------------------------- 1 | debug) { 18 | var_dump($header); 19 | } else { 20 | header($header); 21 | } 22 | } 23 | 24 | public function headerCallback($ch, $data) 25 | { 26 | // this should be first line 27 | if (preg_match('/HTTP\/[\d.]+\s*(\d+)/', $data, $matches)) { 28 | $status_code = $matches[1]; 29 | 30 | // if Forbidden or Not Found -> those are "valid" statuses too 31 | if ($status_code == 200 || $status_code == 206 || $status_code == 403 || $status_code == 404) { 32 | $this->headers_sent = true; 33 | $this->sendHeader(rtrim($data)); 34 | } 35 | 36 | } else { 37 | 38 | // only headers we wish to forward back to the client 39 | $forward = array('content-type', 'content-length', 'accept-ranges', 'content-range'); 40 | 41 | $parts = explode(':', $data, 2); 42 | 43 | if ($this->headers_sent && count($parts) == 2 && in_array(trim(strtolower($parts[0])), $forward)) { 44 | $this->sendHeader(rtrim($data)); 45 | } 46 | } 47 | 48 | return strlen($data); 49 | } 50 | 51 | public function bodyCallback($ch, $data) 52 | { 53 | if (true) { 54 | echo $data; 55 | flush(); 56 | } 57 | 58 | return strlen($data); 59 | } 60 | 61 | public function stream($url) 62 | { 63 | $ch = curl_init(); 64 | 65 | $headers = array(); 66 | $headers[] = 'User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0'; 67 | 68 | if (isset($_SERVER['HTTP_RANGE'])) { 69 | $headers[] = 'Range: ' . $_SERVER['HTTP_RANGE']; 70 | } 71 | 72 | // otherwise you get weird "OpenSSL SSL_read: No error" 73 | curl_setopt($ch, CURLOPT_FORBID_REUSE, 1); 74 | curl_setopt($ch, CURLOPT_FRESH_CONNECT, 1); 75 | 76 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 77 | 78 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); 79 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); 80 | 81 | curl_setopt($ch, CURLOPT_BUFFERSIZE, $this->buffer_size); 82 | curl_setopt($ch, CURLOPT_URL, $url); 83 | 84 | //curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); 85 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 86 | 87 | // we deal with this ourselves 88 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 0); 89 | curl_setopt($ch, CURLOPT_HEADER, 0); 90 | 91 | curl_setopt($ch, CURLOPT_HEADERFUNCTION, [$this, 'headerCallback']); 92 | 93 | // if response is empty - this never gets called 94 | curl_setopt($ch, CURLOPT_WRITEFUNCTION, [$this, 'bodyCallback']); 95 | 96 | $ret = curl_exec($ch); 97 | 98 | // TODO: $this->logError($ch); 99 | $error = ($ret === false) ? sprintf('curl error: %s, num: %s', curl_error($ch), curl_errno($ch)) : null; 100 | 101 | curl_close($ch); 102 | 103 | // if we are still here by now, then all must be okay 104 | return true; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/ArrayTest.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'two' => ['three' => 33] 13 | ], 14 | 'simple' => [1, 2, 3], 15 | 'one_2' => 12, 16 | 1, 17 | 2, 18 | 3 19 | ]; 20 | 21 | public function test_array_get() 22 | { 23 | $this->assertEquals(33, Utils::arrayGet($this->array, "one.two.three", "default")); 24 | } 25 | 26 | public function test_when_value_is_array() 27 | { 28 | $this->assertEquals([1, 2, 3], Utils::arrayGet($this->array, 'simple')); 29 | } 30 | 31 | public function test_non_existing_index() 32 | { 33 | $this->assertEquals("default", Utils::arrayGet($this->array, "one.two.four", "default")); 34 | $this->assertEquals(null, Utils::arrayGet($this->array, "one.two.four")); 35 | } 36 | 37 | public function test_deeper_level_than_exists() 38 | { 39 | $this->assertEquals(null, Utils::arrayGet($this->array, "one.two.three.four")); 40 | } 41 | } -------------------------------------------------------------------------------- /tests/StreamFormatTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($utils instanceof DownloadOptions); 18 | 19 | $this->assertEquals(18, count($utils->getAllFormats())); 20 | 21 | $first = $utils->getAllFormats()[0]; 22 | $this->assertEquals("video/mp4", $first->getCleanMimeType()); 23 | 24 | $this->assertTrue(true); 25 | } 26 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | getBrowser()->setUserAgent($agent); 21 | 22 | $page = $youtube->getPage(self::BUNNY_VIDEO_ID); 23 | 24 | $info = $page->getVideoInfo(); 25 | 26 | // we should be able to parse at least these 27 | $this->assertNotEmpty($info->id); 28 | $this->assertNotEmpty($info->title); 29 | $this->assertNotEmpty($info->channelTitle); 30 | $this->assertNotEmpty($info->description); 31 | 32 | $this->assertNotEmpty($info->pageUrl); 33 | $this->assertNotEmpty($info->uploadDate); 34 | $this->assertNotEmpty($info->thumbnail); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /tests/YouTubeTest.php: -------------------------------------------------------------------------------- 1 | get('https://httpbin.org/cookies/set?name=random_value'); 20 | 21 | // list cookies 22 | $list = $browser->get('https://httpbin.org/cookies'); 23 | 24 | $this->assertContains('random_value', $list->body); 25 | } 26 | 27 | public function test_get_download_links() 28 | { 29 | $downloader = new YouTubeDownloader(); 30 | 31 | $ret = $downloader->getDownloadLinks(self::BUNNY); 32 | 33 | $this->assertGreaterThan(0, count($ret->getAllFormats())); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/YouTubeUrlTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(self::BUNNY_VIDEO_ID, $id); 22 | } 23 | } 24 | 25 | public function test_channel_parsing() 26 | { 27 | $tests = [ 28 | 'https://www.youtube.com/channel/UCkRfArvrzheW2E7b6SVT7vQ' => 'UCkRfArvrzheW2E7b6SVT7vQ', 29 | // 'https://www.youtube.com/user/creatoracademy' => 'creatoracademy', 30 | 'https://www.youtube.com/c/YouTubeCreators' => 'YouTubeCreators', 31 | 'https://www.youtube.com/c/youtubecreators/videos' => 'youtubecreators', 32 | 'https://www.youtube.com/feed/explore' => null, 33 | '' => null 34 | ]; 35 | 36 | $this->assertNull(Utils::extractChannel(null)); 37 | 38 | foreach ($tests as $url => $expected) { 39 | $this->assertEquals($expected, Utils::extractChannel($url)); 40 | } 41 | } 42 | } --------------------------------------------------------------------------------