├── .github └── workflows │ └── gradle.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── settings.gradle └── src ├── main └── java │ └── com │ └── github │ └── felipeucelli │ └── javatube │ ├── CaptionQuery.java │ ├── Captions.java │ ├── Channel.java │ ├── Cipher.java │ ├── FilterBuilder.java │ ├── InnerTube.java │ ├── JsInterpreter.java │ ├── Playlist.java │ ├── Protobuf.java │ ├── Request.java │ ├── Search.java │ ├── Stream.java │ ├── StreamQuery.java │ ├── Youtube.java │ └── exceptions │ ├── AgeRestrictedError.java │ ├── BotDetectionError.java │ ├── LiveStreamError.java │ ├── LiveStreamOffline.java │ ├── MembersOnlyError.java │ ├── RecordingUnavailableError.java │ ├── RegexMatchError.java │ ├── UnknownVideoError.java │ ├── VideoPrivateError.java │ ├── VideoRegionBlockedError.java │ └── VideoUnavailableError.java └── test ├── java └── com │ └── github │ └── felipeucelli │ └── javatube │ ├── CipherTest.java │ └── JsInterpreterTest.java └── resources └── com └── github └── felipeucelli └── javatube └── base ├── 019a2dc2-player-ias-vflset_en_US.txt ├── 1f8742dc-player_ias.vflset-en_US.txt ├── 20830619-player_ias.vflset-en_US.txt ├── 20830619-player_ias_tce.vflset-en_US.txt ├── 20dfca59-player_ias.vflset-en_US.txt ├── 21812a9c-player_ias.vflset-en_US.txt ├── 23604418-player_ias.vflset-en_US.txt ├── 2f1832d2-player_ias.vflset-en_US.txt ├── 2f238d39-player_ias.vflset-en_US.txt ├── 31e0b6d9-player_ias.vflset-en_US.txt ├── 3bb1f723-player_ias.vflset-en_US.txt ├── 3cd2d050-player-plasma-ias-tablet-en_US.vflset.txt ├── 3ffefd71-player_ias.vflset-en_US.txt ├── 42a553e1-player_ias.vflset-en_US.txt ├── 4fcd6e4a-player_ias.vflset-en_US.txt ├── 59b252b9-player_ias.vflset-en_US.txt ├── 5b22937f-player_ias.vflset-en_US.txt ├── 5bdfe6d5-player_ias.vflset-en_US.txt ├── 6450230e-player_ias.vflset-en_US.txt ├── 71547d26-player_ias.vflset-en_US.txt ├── 74e4bb46-player_ias_tce.vflset-en_US.txt ├── 91201489-player_ias_tce.vflset-en_US.txt ├── b12cc44b-player_ias.vflset-en_US.txt ├── b22ef6e7-player_ias.vflset-en_US.txt ├── b7240855-player_ias.vflset-en_US.txt ├── bc657243-player_ias.vflset-en_US.txt ├── c153b631-player-plasma-ias-tablet-en_US.vflset.txt ├── d8a5aa5e-player_ias.vflset-en_US.txt ├── da7c2a60-player_ias.vflset-en_US.txt ├── e7567ecf-player_ias_tce.vflset-en_US.txt ├── f3d47b5a-player_ias.vflset-en_US.txt ├── f980f2a9-player-plasma-ias-tablet-en_US.vflset.txt └── f980f2a9-player_ias.vflset-en_US.txt /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle 7 | 8 | name: Gradle 9 | 10 | on: 11 | push: 12 | branches: [ "master" ] 13 | pull_request: 14 | branches: [ "master" ] 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up JDK 17 22 | uses: actions/setup-java@v3 23 | with: 24 | java-version: '17' 25 | distribution: 'adopt' 26 | - name: Change wrapper permissions 27 | run: chmod +x ./gradlew 28 | - name: Build with Gradle 29 | run: ./gradlew build 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/ 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | !**/src/main/**/out/ 14 | !**/src/test/**/out/ 15 | 16 | /src/test/java/test.java 17 | /src/test/java/com/github/felipeucelli/javatube/CaptionsTest.java 18 | /src/test/java/com/github/felipeucelli/javatube/ChannelTest.java 19 | /src/test/java/com/github/felipeucelli/javatube/InnerTubeTest.java 20 | /src/test/java/com/github/felipeucelli/javatube/PlaylistTest.java 21 | /src/test/java/com/github/felipeucelli/javatube/SearchTest.java 22 | /src/test/java/com/github/felipeucelli/javatube/StreamQueryTest.java 23 | /src/test/java/com/github/felipeucelli/javatube/YoutubeTest.java 24 | 25 | ### Eclipse ### 26 | .apt_generated 27 | .classpath 28 | .factorypath 29 | .project 30 | .settings 31 | .springBeans 32 | .sts4-cache 33 | bin/ 34 | !**/src/main/**/bin/ 35 | !**/src/test/**/bin/ 36 | 37 | ### NetBeans ### 38 | /nbproject/private/ 39 | /nbbuild/ 40 | /dist/ 41 | /nbdist/ 42 | /.nb-gradle/ 43 | 44 | ### VS Code ### 45 | .vscode/ 46 | 47 | ### Mac OS ### 48 | .DS_Store 49 | .idea/gradle.xml 50 | 51 | ### File Type #### 52 | *.mp4 53 | *.mp3 54 | *.webm 55 | *.srt 56 | 57 | .cache/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 - 2024 Felipe Ucelli 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavaTube 2 | ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/felipeucelli/JavaTube/gradle.yml) 3 | 4 | [![JDK](https://img.shields.io/badge/JDK-17%2B-blue.svg)](https://www.oracle.com/java/technologies/downloads/#java17) 5 | [![](https://jitpack.io/v/felipeucelli/javatube.svg)](https://jitpack.io/#felipeucelli/javatube) 6 | 7 | _JavaTube_ is a YouTube video download library based on [pytube](https://github.com/pytube/pytube) library. 8 | 9 | _JavaTube_ is a library written in java and aims to be highly reliable. 10 | 11 | ## Features 12 | * Support for downloading the full playlist 13 | * Support for progressive and adaptive streams 14 | * Interaction with channels (Videos, YouTube Shorts, lives and Playlists) 15 | * onProgress callback register 16 | * Keyword search support 17 | * Search using filters 18 | * Ability to get video details (Title, Description, Publish Date, Length, Thumbnail Url, Views, Author and Keywords) 19 | * Subtitle generator for .srt format 20 | * Support downloading yt_otf streams 21 | * Possibility to choose the client (WEB, ANDROID, IOS) 22 | * Native js interpreter 23 | * PoToken support 24 | 25 | ## Contribution 26 | Currently this project is maintained by only one person. Feel free to create issues with questions, bug reports or improvement ideas. 27 | 28 | ## WARNING 29 | This code is for educational purposes and must not be used in any way for commercial purposes. 30 | 31 | Downloading videos from YouTube without proper authorization may violate the [terms of the platform](https://www.youtube.com/static?template=terms). 32 | 33 | Downloading copyrighted videos may infringe on the creators' intellectual property. 34 | 35 | I reaffirm not to use this software to violate any laws. 36 | 37 | ## Using JavaTube 38 | 39 | To download YouTube videos you need to import the YouTube class and pass a YouTube video url to access the streams. 40 | 41 | The streams() method returns a StreamQuery object that lists the properly handled streams. 42 | 43 | You must only get one stream to be able to download it. You can use the methods: 44 | 45 | * `getHighestResolution()` 46 | * `getLowestResolution() ` 47 | * `getOnlyAudio() ` 48 | 49 | You can also manually select the stream using `.get("index")`. 50 | 51 | The download() method must receive the path that the stream will be downloaded. 52 | 53 | ```java 54 | public static void main(String[] args) throws Exception { 55 | Youtube yt = new Youtube("https://www.youtube.com/watch?v=2lAe1cqCOXo"); 56 | yt.streams().getHighestResolution().download("./"); 57 | } 58 | ``` 59 | 60 | or 61 | 62 | ```java 63 | public static void main(String[] args) throws Exception { 64 | new Youtube("https://www.youtube.com/watch?v=2lAe1cqCOXo").streams().get(1).download("./"); 65 | } 66 | } 67 | ``` 68 | 69 | ### Downloading videos with multiple audio tracks 70 | Videos with multiple progressive audio tracks come with the original audio, which is why we must choose the adaptive types. 71 | 72 | Because the dubbed audio tracks have the same tag, we have to filter by name. 73 | 74 | This will only list tracks dubbed in the chosen language: 75 | 76 | ```java 77 | public static void main(String[] args) throws Exception { 78 | for(Stream s : new Youtube("https://www.youtube.com/watch?v=g_VxOIlg7q8").streams().getExtraAudioTracksByName("English").getAll()){ 79 | System.out.println(s.getItag() + " " + s.getAudioTrackName() + " " + s.getAbr() + " " + s.getUrl()); 80 | } 81 | } 82 | ``` 83 | You can check the dubbed tracks using: 84 | 85 | ```java 86 | public static void main(String[] args) throws Exception { 87 | for(Stream s : new Youtube("https://www.youtube.com/watch?v=g_VxOIlg7q8").streams().getExtraAudioTracks().getAll()){ 88 | System.out.println(s.getItag() + " " + s.getAudioTrackName() + " " + s.getAbr() + " " + s.getUrl()); 89 | } 90 | } 91 | ``` 92 | 93 | ### Download using filters 94 | 95 | You must pass a HashMap String with the filter you want to use and its respective value 96 | 97 | ```java 98 | public static void main(String[] args) throws Exception { 99 | Youtube yt = new Youtube("https://www.youtube.com/watch?v=2lAe1cqCOXo"); 100 | 101 | HashMap filters = new HashMap<>(); 102 | filters.put("progressive", "true"); 103 | filters.put("subType", "mp4"); 104 | 105 | yt.streams().filter(filters).getFirst().download("./"); 106 | 107 | } 108 | ``` 109 | 110 | ### Download with callback function 111 | 112 | If no parameter is passed, a download percentage string will be printed to the terminal 113 | ```java 114 | public static void progress(Long value){ 115 | System.out.println(value); 116 | } 117 | 118 | public static void main(String[] args) throws Exception { 119 | Youtube yt = new Youtube("https://www.youtube.com/watch?v=2lAe1cqCOXo"); 120 | yt.streams().getHighestResolution().download("./", Download::progress); 121 | } 122 | ``` 123 | 124 | ### Downloading a playlist 125 | 126 | The `getVideos()` method will return an ArrayList with the links extracted from the playlist url (YouTube mix not supported) 127 | 128 | ```java 129 | public static void main(String[] args) throws Exception { 130 | for(String pl : new Playlist("https://www.youtube.com/playlist?list=PLS1QulWo1RIbfTjQvTdj8Y6yyq4R7g-Al").getVideos()){ 131 | new Youtube(pl).streams().getHighestResolution().download("./"); 132 | } 133 | } 134 | ``` 135 | 136 | ### Using the search feature 137 | 138 | * `getResults()`: method will return an ArrayList with links to videos, shorts, playlists and channels. 139 | 140 | 141 | * `getVideoResults()`: method returns an ArrayList of Youtube objects, containing videos. 142 | 143 | 144 | * `getShortsResults()`: method returns an ArrayList of Youtube objects, containing YouTube Shorts. 145 | 146 | 147 | * `getChannelsResults()`: method returns an ArrayList of Channel objects, containing the channels. 148 | 149 | 150 | * `getPlaylistsResults()`: method returns an ArrayList of Playlist objects, containing the playlists. 151 | 152 | 153 | * `getCompletionSuggestions()`: method returns a list containing search suggestions. 154 | 155 | 156 | * `generateContinuation()`: method will not return anything, just add the continuation of the items to their respective lists. 157 | 158 | 159 | If no match was found the method will return empty, other than `getCompletionSuggestions()` which returns null. 160 | ```java 161 | public static void main(String[] args) throws Exception { 162 | for(String yt : new Search("Java").getResults()){ 163 | System.out.println(yt); 164 | } 165 | } 166 | ``` 167 | 168 | ### Search using filters 169 | 170 | YouTube allows you to filter a search just by passing a parameter encoded in protobuf. 171 | This parameter consists of dictionaries where each key and value represents a category and filter respectively. 172 | With these parameters we can combine several filters to create a personalized search. 173 | 174 | It wouldn't be very practical for the user or developer to have to manually retrieve the custom filter from YouTube whenever they want to do a search, 175 | so the `FilterBuilder` class will do all the work of providing all the available filters, combining them, coding them in protobuf and send to the `Search` class, 176 | all we need to do is import it and create a dictionary with the necessary filters: 177 | 178 | ```java 179 | public static void main(String[] args) throws Exception { 180 | Map filter = new HashMap<>(); 181 | 182 | filter.put(FilterBuilder.Filter.TYPE, FilterBuilder.Type.VIDEO); 183 | filter.put(FilterBuilder.Filter.UPLOAD_DATE, FilterBuilder.UploadDate.TODAY); 184 | filter.put(FilterBuilder.Filter.DURATION, FilterBuilder.Duration.UNDER_4_MIN); 185 | filter.put(FilterBuilder.Filter.FEATURES, List.of(FilterBuilder.Feature.CREATIVE_COMMONS, FilterBuilder.Feature._4K)); 186 | filter.put(FilterBuilder.Filter.SORT_BY, FilterBuilder.SortBy.UPLOAD_DATE); 187 | 188 | Search s = new Search("music", filter); 189 | System.out.println(s.getResults()); 190 | } 191 | ``` 192 | This will return all videos published *today* in *4K* and *Creative Commons* *under 4 minutes* organized by *upload date*. 193 | 194 | Note that the FEATURES category is the only one that supports combining several filters, you can send a list with all the necessary filters or just a single filter like the other categories. 195 | 196 | ### Interacting with channels 197 | 198 | * `getVideos()`: method returns an ArrayList containing the channel's videos. 199 | 200 | 201 | * `getShorts()`: method returns an ArrayList containing the channel's YouTube Shorts. 202 | 203 | 204 | * `getLives()`: method returns an ArrayList containing the channel's lives. 205 | 206 | 207 | * `getPlaylists()`: method returns an ArrayList containing the channel's playlists. 208 | 209 | 210 | ```java 211 | public static void main(String[] args) throws Exception { 212 | for(String c : new Channel("https://www.youtube.com/channel/UCmRtPmgnQ04CMUpSUqPfhxQ").getVideos()){ 213 | System.out.println(new Youtube(c).getTitle()); 214 | } 215 | } 216 | ``` 217 | 218 | ### Using the subtitles feature 219 | 220 | See available languages. 221 | 222 | ```java 223 | public static void main(String[] args) throws Exception { 224 | for(Captions caption: new Youtube("https://www.youtube.com/watch?v=2lAe1cqCOXo&t=1s").getCaptionTracks()){ 225 | System.out.println(caption.getCode()); 226 | } 227 | } 228 | ``` 229 | 230 | Write to console in .srt format. 231 | 232 | ```java 233 | public static void main(String[] args) throws Exception { 234 | System.out.println(new Youtube("https://www.youtube.com/watch?v=2lAe1cqCOXo&t=1s").getCaptions().getByCode("en").xmlCaptionToSrt()); 235 | } 236 | ``` 237 | 238 | Download it in .srt format (if the .srt format is not informed, the xml will be downloaded). 239 | 240 | ```java 241 | public static void main(String[] args) throws Exception { 242 | new Youtube("https://www.youtube.com/watch?v=2lAe1cqCOXo&t=1s").getCaptions().getByCode("en").download("caption.srt", "./") 243 | } 244 | ``` 245 | 246 | ## PoToken 247 | The proof of origin (PO) token is a parameter that YouTube requires to be sent with video playback requests from some clients. Without it, format URL requests from affected customers may return HTTP error 403, error with bot detection, or result in your account or IP address being blocked. 248 | 249 | This token is generated by BotGuard (Web) / DroidGuard (Android) to attest the requests are coming from a genuine client. 250 | 251 | ### Manually acquiring a PO Token from a browser for use when logged out 252 | This process involves manually obtaining a PO token generated from YouTube in a web browser and then manually passing it to JavaTube via the usePoToken=True argument. Steps: 253 | 254 | 1. Open a browser and go to any video on YouTube Music or YouTube Embedded (e.g. https://www.youtube.com/embed/2lAe1cqCOXo). Make sure you are not logged in to any account! 255 | 256 | 2. Open the developer console (F12), then go to the "Network" tab and filter by v1/player 257 | 258 | 3. Click the video to play and a player request will appear in the network tab 259 | 260 | 4. In the request payload JSON, find the PO Token at serviceIntegrityDimensions.poToken and save that value 261 | 262 | 5. In the request payload JSON, find the visitorData at context.client.visitorData and save that value 263 | 264 | 6. In your code, pass the parameter usePoToken=True, to send the visitorData and PoToken: 265 | 266 | ```java 267 | public static void main(String[] args) throws Exception { 268 | Youtube yt = new Youtube("https://www.youtube.com/watch?v=2lAe1cqCOXo", true); 269 | yt.streams().getHighestResolution().download("./"); 270 | } 271 | ``` 272 | The terminal will ask you to insert the tokens. 273 | 274 | If you want to save the token in cache, just add one more argument `true`, this will create a _tokens.json_ file where the visitorData and poToken will be stored. 275 | 276 | The _tokens.json_ will be created in the temporary folder of your operating system, you can delete it using: `Youtube.resetCache();`. 277 | 278 | ## Stream filters Parameters: 279 | * `"res"` The video resolution (e.g.: "360p", "720p") 280 | 281 | 282 | * `"fps"` The frames per second (e.g.: "24fps", "60fps") 283 | 284 | 285 | * `"mineType"` Two-part identifier for file formats and format contents composed of a “type”, a “subtype” (e.g.: "video/mp4", "audio/mp4") 286 | 287 | 288 | * `"type"` Type part of the mineType (e.g.: audio, video) 289 | 290 | 291 | * `"subType"` Sub-type part of the mineType (e.g.: mp4, webm) 292 | 293 | 294 | * `"abr"` Average bitrate (e.g.: "128kbps", "192kbps") 295 | 296 | 297 | * `"videoCodec"` Video compression format 298 | 299 | 300 | * `"audioCodec"` Audio compression format 301 | 302 | 303 | * `"onlyAudio"` Excludes streams with video tracks (e.g.: "true" or "false") 304 | 305 | 306 | * `"onlyVideo"` Excludes streams with audio tracks (e.g.: "true" or "false") 307 | 308 | 309 | * `"progressive"` Excludes adaptive streams (one file contains both audio and video tracks) (e.g.: "true" or "false") 310 | 311 | 312 | * `"adaptive"` Excludes progressive streams (audio and video are on separate tracks) (e.g.: "true" or "false") 313 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | } 4 | 5 | group 'com.github.felipeucelli.javatube' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' 13 | testImplementation 'junit:junit:4.13.1' 14 | testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.2' 15 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' 16 | implementation 'org.json:json:20231013' 17 | } 18 | 19 | test { 20 | useJUnitPlatform() 21 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/felipeucelli/JavaTube/46d9ebf0e9302d6e293c5bca9d0203f040bb4c94/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | before_install: 4 | - sdk install java 17-open 5 | - sdk use java 17-open -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'JavaTube' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/CaptionQuery.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import java.util.*; 4 | 5 | public class CaptionQuery { 6 | Map langCodeIndex = new HashMap<>(); 7 | 8 | @Override 9 | public String toString(){ 10 | return langCodeIndex.toString(); 11 | } 12 | 13 | public CaptionQuery(ArrayList captions){ 14 | for(Captions code : captions){ 15 | langCodeIndex.put(code.getCode(), code); 16 | } 17 | } 18 | public Captions getByCode(String code){ 19 | return langCodeIndex.get(code); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/Captions.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | import java.io.File; 7 | import java.io.FileWriter; 8 | import java.io.IOException; 9 | import java.io.UnsupportedEncodingException; 10 | import java.net.URLDecoder; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.regex.Matcher; 15 | import java.util.regex.Pattern; 16 | 17 | 18 | public class Captions { 19 | private final String url; 20 | private final String code; 21 | private final String name; 22 | 23 | public Captions(JSONObject captionTrack) throws JSONException { 24 | url = captionTrack.getString("baseUrl"); 25 | String vssId = captionTrack.getString("vssId"); 26 | code = vssId.startsWith(".") ? vssId.replace(".", "") : vssId; 27 | JSONObject nameContent = captionTrack.getJSONObject("name"); 28 | name = nameContent.has("simpleText") ? nameContent.getString("simpleText") : nameContent 29 | .getJSONArray("runs") 30 | .getJSONObject(0) 31 | .getString("text"); 32 | } 33 | 34 | @Override 35 | public String toString(){ 36 | return ""; 37 | } 38 | 39 | public String getUrl(){ 40 | return url; 41 | } 42 | public String getCode(){ 43 | return code; 44 | } 45 | public String getName(){ 46 | return name; 47 | } 48 | 49 | private String getXmlCaptions() throws Exception { 50 | Map header = new HashMap<>(); 51 | header.put("User-Agent", "\"Mozilla/5.0\""); 52 | return Request.get(url, header).toString(StandardCharsets.UTF_8.name()).replaceAll("(')|(&#39;)", "'"); 53 | } 54 | 55 | private String generateSrtCaptions() throws Exception { 56 | return getXmlCaptions(); 57 | } 58 | 59 | private String decodeString(String s) throws UnsupportedEncodingException { 60 | return URLDecoder.decode(s, StandardCharsets.UTF_8.name()); 61 | } 62 | 63 | private String srtTimeFormat(Float d){ 64 | 65 | Float ms = (d % 1); 66 | int round = Integer.parseInt(String.valueOf((d - ms)).replace(".", "") + "00"); 67 | Integer seconds = ((round / 1000 ) % 60); 68 | Integer minutes = ( round / 60000 ) % 60; 69 | Integer hours = round / 3600000; 70 | return String.format("%02d:%02d:%02d,", hours, minutes, seconds) + String.format("%.3f", ms).replaceAll("0[.,]", ""); 71 | } 72 | 73 | public String xmlCaptionToSrt() throws Exception { 74 | String root = generateSrtCaptions(); 75 | 76 | int i = 0; 77 | StringBuilder segments = new StringBuilder(); 78 | 79 | String[] pattern = { 80 | "start=\\\"(.*?)\\\".*?dur=\\\"(.*?)\\\">(.*?)<", 81 | "t=\\\"(.*?)\\\".*?d=\\\"(.*?)\\\">(.*?)<" 82 | }; 83 | for(String s : pattern){ 84 | Pattern regex = Pattern.compile(s); 85 | Matcher matcher = regex.matcher(root); 86 | while (matcher.find()) { 87 | Float start = Float.parseFloat(matcher.group(1)); 88 | Float duration = Float.parseFloat(matcher.group(2)); 89 | String caption = decodeString(matcher.group(3)); 90 | 91 | Float end = start + duration; 92 | int sequenceNumber = i + 1; 93 | 94 | String line = sequenceNumber + "\n" + srtTimeFormat(start) + " --> " + srtTimeFormat(end) + "\n" + caption + "\n\n"; 95 | 96 | segments.append(line); 97 | 98 | i++; 99 | } 100 | } 101 | 102 | return segments.toString(); 103 | } 104 | 105 | public void download(String filename, String savePath) { 106 | String fullPath; 107 | 108 | if(savePath.endsWith("/")){ 109 | fullPath = savePath + filename; 110 | }else{ 111 | fullPath = savePath + "/" + filename; 112 | } 113 | 114 | 115 | if(filename.endsWith(".srt")){ 116 | try { 117 | File file = new File(fullPath); 118 | FileWriter write = new FileWriter(file); 119 | write.write(xmlCaptionToSrt()); 120 | write.close(); 121 | } 122 | catch (IOException ex) { 123 | System.out.print("Invalid Path"); 124 | } catch (Exception e) { 125 | throw new RuntimeException(e); 126 | } 127 | }else{ 128 | try { 129 | File file = new File(fullPath); 130 | FileWriter write = new FileWriter(file); 131 | write.write(getXmlCaptions()); 132 | write.close(); 133 | } 134 | catch (IOException ex) { 135 | System.out.print("Invalid Path"); 136 | } catch (Exception e) { 137 | throw new RuntimeException(e); 138 | } 139 | } 140 | 141 | } 142 | 143 | } -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/Channel.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import com.github.felipeucelli.javatube.exceptions.RegexMatchError; 4 | import org.json.JSONArray; 5 | import org.json.JSONException; 6 | import org.json.JSONObject; 7 | 8 | import java.util.*; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | public class Channel extends Playlist{ 13 | 14 | private final String channelUrl; 15 | private final String featuredUrl; 16 | private final String videosUrl; 17 | private final String shortsUrl; 18 | private final String streamsUrl; 19 | private final String releasesUrl; 20 | private final String playlistUrl; 21 | private final String communityUrl; 22 | private final String featuredChannelUrl; 23 | private final String aboutUrl; 24 | private final String url; 25 | private String htmlPage; 26 | private String visitorData; 27 | private int attempts = 0; 28 | 29 | public Channel(String inputUrl) throws Exception { 30 | super(inputUrl); 31 | url = inputUrl; 32 | channelUrl = "https://www.youtube.com" + extractUrl(); 33 | 34 | featuredUrl = channelUrl + "/featured"; 35 | videosUrl = channelUrl + "/videos"; 36 | shortsUrl = channelUrl + "/shorts"; 37 | streamsUrl = channelUrl + "/streams"; 38 | releasesUrl = channelUrl + "/releases"; 39 | playlistUrl = channelUrl + "/playlists"; 40 | communityUrl = channelUrl + "/community"; 41 | featuredChannelUrl = channelUrl + "/channels"; 42 | aboutUrl = channelUrl + "/about"; 43 | } 44 | 45 | @Override 46 | public String toString(){ 47 | try { 48 | return ""; 49 | } catch (Exception e) { 50 | throw new RuntimeException(e); 51 | } 52 | } 53 | 54 | private String extractUrl() throws Exception { 55 | ArrayList re = new ArrayList<>(); 56 | re.add("(?:\\/(c)\\/([%\\d\\w_\\-]+)(\\/.*)?)"); 57 | re.add("(?:\\/(channel)\\/([%\\w\\d_\\-]+)(\\/.*)?)"); 58 | re.add("(?:\\/(u)\\/([%\\d\\w_\\-]+)(\\/.*)?)"); 59 | re.add("(?:\\/(user)\\/([%\\w\\d_\\-]+)(\\/.*)?)"); 60 | re.add("(?:\\/(\\@)([%\\d\\w_\\-\\.]+)(\\/.*)?)"); 61 | 62 | for (String regex : re) { 63 | Pattern pattern = Pattern.compile(regex); 64 | Matcher matcher = pattern.matcher(url); 65 | if (matcher.find()){ 66 | if (Objects.equals(matcher.group(1), "@")){ 67 | return "/@" + matcher.group(2); 68 | }else { 69 | return "/" + matcher.group(1) + "/" + matcher.group(2); 70 | } 71 | } 72 | } 73 | throw new RegexMatchError("extractUrl: Unable to find match on: " + url); 74 | } 75 | 76 | private void setHtmlUrl(String url){ 77 | if(!Objects.equals(htmlPage, url)){ 78 | htmlPage = url; 79 | html = null; 80 | json = null; 81 | } 82 | } 83 | 84 | private String getHtmlUrl(){ 85 | return htmlPage; 86 | } 87 | 88 | @Override 89 | protected String setHtml() throws Exception { 90 | return Request.get(getHtmlUrl(), null, innerTube.getClientHeaders()).toString(); 91 | } 92 | 93 | private JSONObject getActiveTab(JSONObject rawJson) throws JSONException{ 94 | JSONObject activeTab = new JSONObject(); 95 | JSONArray tabs = rawJson.getJSONObject("contents") 96 | .getJSONObject("twoColumnBrowseResultsRenderer") 97 | .getJSONArray("tabs"); 98 | for(int i = 0; tabs.length() > i; i ++ ) { 99 | if(tabs.getJSONObject(i).has("tabRenderer")){ 100 | String tabUrl = tabs.getJSONObject(i).getJSONObject("tabRenderer") 101 | .getJSONObject("endpoint") 102 | .getJSONObject("commandMetadata") 103 | .getJSONObject("webCommandMetadata") 104 | .getString("url"); 105 | if (tabUrl.substring(tabUrl.lastIndexOf("/") + 1).equals(getHtmlUrl().substring(getHtmlUrl().lastIndexOf("/") + 1))) { 106 | activeTab = tabs.getJSONObject(i); 107 | break; 108 | } 109 | } 110 | } 111 | return activeTab; 112 | } 113 | 114 | private JSONArray getImportantContent(JSONObject activeTab) throws JSONException { 115 | try { 116 | return activeTab.getJSONObject("tabRenderer") 117 | .getJSONObject("content") 118 | .getJSONObject("richGridRenderer") 119 | .getJSONArray("contents"); 120 | } catch (JSONException e) { 121 | return getImportantContentFromSectionList(activeTab); 122 | } 123 | } 124 | 125 | private JSONArray getImportantContentFromSectionList(JSONObject activeTab) throws JSONException { 126 | try { 127 | JSONObject firstItem = activeTab.getJSONObject("tabRenderer") 128 | .getJSONObject("content") 129 | .getJSONObject("sectionListRenderer") 130 | .getJSONArray("contents") 131 | .getJSONObject(0) 132 | .getJSONObject("itemSectionRenderer") 133 | .getJSONArray("contents") 134 | .getJSONObject(0) 135 | .getJSONObject("gridRenderer"); 136 | return firstItem.getJSONArray("items"); 137 | } catch (JSONException e) { 138 | return getImportantContentFromShelfRenderers(activeTab); 139 | } 140 | } 141 | 142 | private JSONArray getImportantContentFromShelfRenderers(JSONObject activeTab) throws JSONException { 143 | JSONArray contents = activeTab.getJSONObject("tabRenderer") 144 | .getJSONObject("content") 145 | .getJSONObject("sectionListRenderer") 146 | .getJSONArray("contents"); 147 | JSONArray items = new JSONArray(); 148 | for (int i = 0; i < contents.length(); i++) { 149 | JSONObject shelfRenderer = contents.getJSONObject(i) 150 | .getJSONObject("itemSectionRenderer") 151 | .getJSONArray("contents") 152 | .getJSONObject(0) 153 | .getJSONObject("shelfRenderer") 154 | .getJSONObject("content") 155 | .getJSONObject("horizontalListRenderer"); 156 | 157 | for(int j = 0; shelfRenderer.getJSONArray("items").length() > j; j++){ 158 | items.put(shelfRenderer.getJSONArray("items").getJSONObject(j)); 159 | } 160 | } 161 | return items; 162 | } 163 | 164 | @Override 165 | protected JSONArray buildContinuationUrl(String continuation) throws Exception { 166 | String data = "{" + 167 | "\"continuation\": \"" + continuation + "\"," + 168 | "\"context\": {" + 169 | "\"client\": {" + 170 | "\"visitorData\": \"" + visitorData + "\"" + 171 | "}" + 172 | "}" + 173 | "}"; 174 | return extractVideos(innerTube.browse(new JSONObject(data))); 175 | } 176 | 177 | private JSONArray extractHomePage(JSONObject rawJson){ 178 | JSONArray items = new JSONArray(); 179 | try{ 180 | JSONArray contents = getActiveTab(rawJson) 181 | .getJSONObject("tabRenderer") 182 | .getJSONObject("content") 183 | .getJSONObject("sectionListRenderer") 184 | .getJSONArray("contents"); 185 | 186 | for(int i = 0; i < contents.length(); i ++){ 187 | JSONObject itemSectionRenderer = contents.getJSONObject(i) 188 | .getJSONObject("itemSectionRenderer") 189 | .getJSONArray("contents") 190 | .getJSONObject(0); 191 | 192 | // Skip the presentation videos for non-subscribers 193 | if(itemSectionRenderer.has("channelVideoPlayerRenderer")){ 194 | continue; 195 | } 196 | 197 | // Skip presentation videos for subscribers 198 | if(itemSectionRenderer.has("channelFeaturedContentRenderer")){ 199 | continue; 200 | } 201 | 202 | // Skip the list with channel members 203 | if(itemSectionRenderer.has("recognitionShelfRenderer")){ 204 | continue; 205 | } 206 | 207 | // Get the horizontal shorts 208 | if(itemSectionRenderer.has("reelShelfRenderer")){ 209 | for(int k = 0; k < itemSectionRenderer.length(); k ++){ 210 | items.put(itemSectionRenderer.getJSONObject("reelShelfRenderer") 211 | .getJSONArray("items")); 212 | } 213 | } 214 | 215 | // Get videos, playlist and horizontal channels 216 | if(itemSectionRenderer.has("shelfRenderer")){ 217 | if(itemSectionRenderer 218 | .getJSONObject("shelfRenderer") 219 | .getJSONObject("content") 220 | .has("horizontalListRenderer")){ 221 | 222 | JSONArray obj = itemSectionRenderer.getJSONObject("shelfRenderer") 223 | .getJSONObject("content") 224 | .getJSONObject("horizontalListRenderer") 225 | .getJSONArray("items"); 226 | 227 | for(int j = 0; obj.length() > j; j++){ 228 | items.put(obj.getJSONObject(j)); 229 | } 230 | } 231 | 232 | } 233 | } 234 | }catch (Exception e){ 235 | return new JSONArray(); 236 | } 237 | return items; 238 | } 239 | 240 | @Override 241 | protected JSONArray extractVideos(JSONObject rawJson){ 242 | JSONArray swap = new JSONArray(); 243 | try { 244 | JSONArray importantContent = new JSONArray(); 245 | if(rawJson.has("contents")) { 246 | JSONObject activeTab = getActiveTab(rawJson); 247 | 248 | visitorData = rawJson.getJSONObject("responseContext") 249 | .getJSONObject("webResponseContextExtensionData") 250 | .getJSONObject("ytConfigData") 251 | .getString("visitorData"); 252 | 253 | importantContent = getImportantContent(activeTab); 254 | 255 | }else if (rawJson.has("onResponseReceivedActions")){ 256 | importantContent = rawJson.getJSONArray("onResponseReceivedActions") 257 | .getJSONObject(0) 258 | .getJSONObject("appendContinuationItemsAction") 259 | .getJSONArray("continuationItems"); 260 | 261 | }else if (attempts < 3){ 262 | // YouTube is blocking very quick requests for shorts, so we wait and repeat the request 263 | Thread.sleep(1000); 264 | swap = extractContinuationItems(importantContent); 265 | attempts += 1; 266 | } 267 | 268 | if(importantContent.getJSONObject(importantContent.length() - 1).has("continuationItemRenderer")){ 269 | setContinuationToken(importantContent); 270 | swap = extractContinuationItems(importantContent); 271 | } else { 272 | for(int i = 0; i < importantContent.length(); i++){ 273 | swap.put(importantContent.get(i)); 274 | } 275 | } 276 | 277 | } catch (Exception ignored) { 278 | } 279 | return swap; 280 | } 281 | 282 | public ArrayList getHome() throws Exception { 283 | setHtmlUrl(featuredUrl); 284 | return extractId(extractHomePage(getJson())); 285 | } 286 | 287 | @Override 288 | public ArrayList getVideos() throws Exception { 289 | setHtmlUrl(videosUrl); 290 | return extractId(extractVideos(getJson())); 291 | } 292 | 293 | public ArrayList getShorts() throws Exception { 294 | setHtmlUrl(shortsUrl); 295 | return extractId(extractVideos(getJson())); 296 | } 297 | 298 | public ArrayList getLives() throws Exception { 299 | setHtmlUrl(streamsUrl); 300 | return extractId(extractVideos(getJson())); 301 | } 302 | 303 | public ArrayList getReleases() throws Exception { 304 | setHtmlUrl(releasesUrl); 305 | return extractId(extractVideos(getJson())); 306 | } 307 | 308 | public ArrayList getPlaylists() throws Exception { 309 | setHtmlUrl(playlistUrl); 310 | return extractId(extractVideos(getJson())); 311 | } 312 | 313 | private ArrayList extractId(JSONArray obj) throws Exception { 314 | ArrayList objId = new ArrayList<>(); 315 | 316 | for(int i = 0; i < obj.length(); i++){ 317 | try { 318 | if(!obj.getJSONObject(i).has("continuationItemRenderer")) { 319 | objId.add(getVideoId(obj.getJSONObject(i))); 320 | } 321 | }catch (JSONException ignored){ 322 | } 323 | } 324 | return unify(objId); 325 | } 326 | 327 | private String getVideoId(JSONObject ids) throws JSONException{ 328 | try{ 329 | return "https://www.youtube.com/watch?v=" + ids.getJSONObject("richItemRenderer") 330 | .getJSONObject("content") 331 | .getJSONObject("videoRenderer") 332 | .getString("videoId"); 333 | }catch (JSONException e){ 334 | return getShortId(ids); 335 | } 336 | } 337 | 338 | private String getShortId(JSONObject ids) throws JSONException{ 339 | try{ 340 | JSONObject content = ids.getJSONObject("richItemRenderer").getJSONObject("content"); 341 | String videoId; 342 | if (content.has("shortsLockupViewModel")){ 343 | videoId = content 344 | .getJSONObject("shortsLockupViewModel") 345 | .getJSONObject("onTap") 346 | .getJSONObject("innertubeCommand") 347 | .getJSONObject("reelWatchEndpoint") 348 | .getString("videoId"); 349 | }else { 350 | videoId = content 351 | .getJSONObject("reelItemRenderer") 352 | .getString("videoId"); 353 | } 354 | return "https://www.youtube.com/watch?v=" + videoId; 355 | }catch (JSONException e){ 356 | return getReleasesId(ids); 357 | } 358 | } 359 | 360 | private String getReleasesId(JSONObject ids) throws JSONException{ 361 | try{ 362 | return "https://www.youtube.com/playlist?list=" + ids.getJSONObject("richItemRenderer") 363 | .getJSONObject("content") 364 | .getJSONObject("playlistRenderer") 365 | .getString("playlistId"); 366 | }catch (JSONException e){ 367 | return getVideoIdFromHome(ids); 368 | } 369 | } 370 | 371 | private String getVideoIdFromHome(JSONObject ids) throws JSONException { 372 | try{ 373 | return "https://www.youtube.com/watch?v=" + ids.getJSONObject("gridVideoRenderer") 374 | .getString("videoId"); 375 | }catch (JSONException e){ 376 | return getPlaylistId(ids); 377 | } 378 | } 379 | 380 | private String getPlaylistId(JSONObject ids) throws JSONException{ 381 | try{ 382 | return "https://www.youtube.com/playlist?list=" + ids.getJSONObject("gridPlaylistRenderer") 383 | .getString("playlistId"); 384 | }catch (JSONException e){ 385 | return getChannelIdFromHome(ids); 386 | } 387 | } 388 | 389 | private String getChannelIdFromHome(JSONObject ids) throws JSONException{ 390 | try{ 391 | return "https://www.youtube.com/playlist?list=" + ids.getJSONObject("gridChannelRenderer") 392 | .getString("channelId"); 393 | }catch (JSONException e){ 394 | return getCompactStationRenderer(ids); 395 | } 396 | } 397 | 398 | private String getCompactStationRenderer(JSONObject ids) throws JSONException{ 399 | try{ 400 | return "https://www.youtube.com/playlist?list=" + ids.getJSONObject("compactStationRenderer") 401 | .getJSONObject("navigationEndpoint") 402 | .getJSONObject("watchPlaylistEndpoint") 403 | .getString("playlistId"); 404 | }catch (JSONException e){ 405 | return getLockupViewModel(ids); 406 | } 407 | } 408 | 409 | private String getLockupViewModel(JSONObject ids) throws JSONException{ 410 | try{ 411 | return "https://www.youtube.com/playlist?list=" + ids.getJSONObject("lockupViewModel") 412 | .getString("contentId"); 413 | }catch (JSONException e){ 414 | throw new JSONException(e); 415 | } 416 | } 417 | 418 | @Override 419 | public String getUrl() { 420 | return channelUrl; 421 | } 422 | 423 | @Override 424 | public String getTitle() throws Exception { 425 | return getChannelName(); 426 | } 427 | 428 | @Override 429 | public String getLastUpdated() throws Exception { 430 | setHtmlUrl(videosUrl); 431 | JSONObject lastVideoContent; 432 | try{ 433 | lastVideoContent = getActiveTab(getJson()).getJSONObject("tabRenderer") 434 | .getJSONObject("content") 435 | .getJSONObject("richGridRenderer") 436 | .getJSONArray("contents") 437 | .getJSONObject(0) 438 | .getJSONObject("richItemRenderer") 439 | .getJSONObject("content") 440 | .getJSONObject("videoRenderer"); 441 | }catch (JSONException e){ 442 | return null; 443 | } 444 | String videoId = lastVideoContent.getString("videoId"); 445 | JSONObject response = new InnerTube("WEB").player(videoId); 446 | try { 447 | return response.getJSONObject("microformat") 448 | .getJSONObject("playerMicroformatRenderer") 449 | .getString("publishDate"); 450 | 451 | }catch (JSONException j){ 452 | return lastVideoContent.getJSONObject("publishedTimeText") 453 | .getString("simpleText"); 454 | } 455 | } 456 | 457 | @Override 458 | public String length() throws Exception { 459 | setHtmlUrl(getChannelUrl()); 460 | try { 461 | JSONObject header = getJson().getJSONObject("header"); 462 | 463 | if(header.has("c4TabbedHeaderRenderer")){ 464 | return header.getJSONObject("c4TabbedHeaderRenderer") 465 | .getJSONObject("videosCountText") 466 | .getJSONArray("runs") 467 | .getJSONObject(0) 468 | .getString("text"); 469 | }else { 470 | return header.getJSONObject("pageHeaderRenderer") 471 | .getJSONObject("content") 472 | .getJSONObject("pageHeaderViewModel") 473 | .getJSONObject("metadata") 474 | .getJSONObject("contentMetadataViewModel") 475 | .getJSONArray("metadataRows") 476 | .getJSONObject(1) 477 | .getJSONArray("metadataParts") 478 | .getJSONObject(1) 479 | .getJSONObject("text") 480 | .getString("content"); 481 | } 482 | }catch (JSONException j){ 483 | return null; 484 | } 485 | 486 | } 487 | 488 | public String getChannelName() throws Exception { 489 | setHtmlUrl(channelUrl); 490 | return getJson().getJSONObject("metadata") 491 | .getJSONObject("channelMetadataRenderer") 492 | .getString("title"); 493 | } 494 | 495 | public String getChannelId() throws Exception { 496 | setHtmlUrl(channelUrl); 497 | return getJson().getJSONObject("metadata") 498 | .getJSONObject("channelMetadataRenderer") 499 | .getString("externalId"); 500 | } 501 | 502 | public String getVanityUrl() throws Exception { 503 | setHtmlUrl(channelUrl); 504 | return getJson().getJSONObject("metadata") 505 | .getJSONObject("channelMetadataRenderer") 506 | .getString("vanityChannelUrl"); 507 | } 508 | 509 | @Override 510 | public String getDescription() throws Exception { 511 | setHtmlUrl(channelUrl); 512 | return getJson().getJSONObject("metadata") 513 | .getJSONObject("channelMetadataRenderer") 514 | .getString("description"); 515 | } 516 | 517 | public String getSubscribers() throws Exception { 518 | setHtmlUrl(getChannelUrl()); 519 | try{ 520 | JSONObject header = getJson().getJSONObject("header"); 521 | 522 | if(header.has("c4TabbedHeaderRenderer")){ 523 | return getJson().getJSONObject("header") 524 | .getJSONObject("c4TabbedHeaderRenderer") 525 | .getJSONObject("subscriberCountText") 526 | .getString("simpleText"); 527 | }else { 528 | return header.getJSONObject("pageHeaderRenderer") 529 | .getJSONObject("content") 530 | .getJSONObject("pageHeaderViewModel") 531 | .getJSONObject("metadata") 532 | .getJSONObject("contentMetadataViewModel") 533 | .getJSONArray("metadataRows") 534 | .getJSONObject(1) 535 | .getJSONArray("metadataParts") 536 | .getJSONObject(0) 537 | .getJSONObject("text") 538 | .getString("content"); 539 | } 540 | 541 | }catch (JSONException e){ 542 | return null; 543 | } 544 | } 545 | 546 | @Deprecated 547 | public String getBiography() throws Exception { 548 | setHtmlUrl(aboutUrl); 549 | try { 550 | return getActiveTab(getJson()).getJSONObject("tabRenderer") 551 | .getJSONObject("content") 552 | .getJSONObject("sectionListRenderer") 553 | .getJSONArray("contents") 554 | .getJSONObject(0) 555 | .getJSONObject("itemSectionRenderer") 556 | .getJSONArray("contents") 557 | .getJSONObject(0) 558 | .getJSONObject("channelAboutFullMetadataRenderer") 559 | .getJSONObject("artistBio") 560 | .getString("simpleText"); 561 | }catch (JSONException e){ 562 | return null; 563 | } 564 | } 565 | 566 | @Override 567 | public String getViews() throws Exception { 568 | setHtmlUrl(aboutUrl); 569 | try { 570 | return getJson().getJSONArray("onResponseReceivedEndpoints") 571 | .getJSONObject(0) 572 | .getJSONObject("showEngagementPanelEndpoint") 573 | .getJSONObject("engagementPanel") 574 | .getJSONObject("engagementPanelSectionListRenderer") 575 | .getJSONObject("content") 576 | .getJSONObject("sectionListRenderer") 577 | .getJSONArray("contents") 578 | .getJSONObject(0) 579 | .getJSONObject("itemSectionRenderer") 580 | .getJSONArray("contents") 581 | .getJSONObject(0) 582 | .getJSONObject("aboutChannelRenderer") 583 | .getJSONObject("metadata") 584 | .getJSONObject("aboutChannelViewModel") 585 | .getString("viewCountText"); 586 | }catch (JSONException e){ 587 | return null; 588 | } 589 | } 590 | 591 | public String getKeywords() throws Exception { 592 | setHtmlUrl(channelUrl); 593 | return getJson().getJSONObject("metadata") 594 | .getJSONObject("channelMetadataRenderer") 595 | .getString("keywords"); 596 | } 597 | 598 | public JSONArray getAvailableCountryCodes() throws Exception { 599 | setHtmlUrl(channelUrl); 600 | return getJson().getJSONObject("metadata") 601 | .getJSONObject("channelMetadataRenderer") 602 | .getJSONArray("availableCountryCodes"); 603 | } 604 | 605 | public String getThumbnailUrl() throws Exception { 606 | setHtmlUrl(channelUrl); 607 | return getJson().getJSONObject("metadata") 608 | .getJSONObject("channelMetadataRenderer") 609 | .getJSONObject("avatar") 610 | .getJSONArray("thumbnails") 611 | .getJSONObject(0) 612 | .getString("url"); 613 | } 614 | 615 | public String getChannelUrl(){ 616 | return channelUrl; 617 | } 618 | public String getVideosUrl(){ 619 | return videosUrl; 620 | } 621 | 622 | public String getShortsUrl(){ 623 | return shortsUrl; 624 | } 625 | 626 | public String getStreamsUrl(){ 627 | return streamsUrl; 628 | } 629 | 630 | public String getReleasesUrl(){ 631 | return releasesUrl; 632 | } 633 | 634 | public String getPlaylistUrl(){ 635 | return playlistUrl; 636 | } 637 | 638 | public String getCommunityUrl(){ 639 | return communityUrl; 640 | } 641 | 642 | public String getFeaturedChannelUrl(){ 643 | return featuredChannelUrl; 644 | } 645 | 646 | public String getAboutUrl(){ 647 | return aboutUrl; 648 | } 649 | 650 | @Deprecated 651 | @Override 652 | public String getOwner(){ 653 | return null; 654 | } 655 | 656 | @Deprecated 657 | @Override 658 | public String getOwnerId(){ 659 | return null; 660 | } 661 | 662 | @Deprecated 663 | @Override 664 | public String getOwnerUrl(){ 665 | return null; 666 | } 667 | 668 | } -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/Cipher.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import com.github.felipeucelli.javatube.exceptions.RegexMatchError; 4 | 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | public class Cipher { 11 | 12 | private static String playerJs; 13 | private static JsInterpreter jsInterpreter; 14 | private static String signatureFunctionName; 15 | private static String throttlingFunctionName; 16 | 17 | public Cipher(String js, String ytPlayerJs) throws Exception { 18 | playerJs = ytPlayerJs; 19 | jsInterpreter = new JsInterpreter(js); 20 | signatureFunctionName = getInitialFunctionName(js); 21 | throttlingFunctionName = getThrottlingFunctionName(js); 22 | } 23 | private static String getInitialFunctionName(String js) throws Exception { 24 | String[] functionPattern = { 25 | "(?[a-zA-Z0-9_$]+)\\s*=\\s*function\\(\\s*(?[a-zA-Z0-9_$]+)\\s*\\)\\s*\\{\\s*(\\k)\\s*=\\s*(\\k)\\.split\\(\\s*[a-zA-Z0-9_\\$\\\"\\[\\]]+\\s*\\)\\s*;\\s*[^}]+;\\s*return\\s+(\\k)\\.join\\(\\s*[a-zA-Z0-9_\\$\\\"\\[\\]]+\\s*\\)", 26 | "\\b(?[a-zA-Z0-9_$]+)&&\\((\\k)=(?[a-zA-Z0-9_$]{2,})\\(decodeURIComponent\\((\\k)\\)\\)", 27 | "(?:\\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*\\)(?:;[a-zA-Z0-9_$]{2}\\.[a-zA-Z0-9_$]{2}\\(a,\\d+\\))?" 28 | }; 29 | for(String pattern : functionPattern){ 30 | Pattern regex = Pattern.compile(pattern); 31 | Matcher matcher = regex.matcher(js); 32 | if (matcher.find()) { 33 | return matcher.group("sig"); 34 | } 35 | } 36 | throw new RegexMatchError("getInitialFunctionName: Could not find function name in playerJs:" + playerJs); 37 | } 38 | private String getThrottlingFunctionName(String js) throws Exception { 39 | 40 | try { 41 | String[] globalVar = jsInterpreter.extractPlayerJsGlobalVar(js); 42 | String name = globalVar[0]; 43 | String code = globalVar[1]; 44 | String value = globalVar[2]; 45 | if(code != null) { 46 | Object array = jsInterpreter.interpretExpression(value, new LocalNameSpace(new HashMap<>()), 100); 47 | if(array instanceof ArrayList){ 48 | @SuppressWarnings("unchecked") 49 | ArrayList globalArray = (ArrayList) array; 50 | for(int i = 0; i < globalArray.size(); i ++){ 51 | if (globalArray.get(i).endsWith("_w8_")){ 52 | String pattern = 53 | "(?xs)" 54 | + "[;\\n](?:" 55 | + "(?function\\s+)|" 56 | + "(?:var\\s+)?" 57 | + ")(?[a-zA-Z0-9_$]+)\\s*((f)|=\\s*function\\s*)" 58 | + "\\((?[a-zA-Z0-9_$]+)\\)\\s*\\{" 59 | + "(?:(?!\\}[;\\n]).)+" 60 | + "\\}\\s*catch\\(\\s*[a-zA-Z0-9_$]+\\s*\\)\\s*" 61 | + "\\{\\s*return\\s+" + name + "\\[" + i + "\\]\\s*\\+\\s*(\\k)\\s*\\}\\s*return\\s+[^}]+\\}[;\\n]" 62 | ; 63 | Pattern regex = Pattern.compile(pattern); 64 | Matcher matcher = regex.matcher(js); 65 | if(matcher.find()){ 66 | return matcher.group("funcname"); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | }catch (Exception e){} 73 | 74 | String functionPattern = """ 75 | (?x) 76 | (?: 77 | \\.get\\(\\"n\\"\\)\\)&&\\(b=| 78 | (?: 79 | b=String\\.fromCharCode\\(110\\)| 80 | (?[a-zA-Z0-9_$.]+)&&\\(b=\\"nn\\"\\[\\+(\\k)\\] 81 | ) 82 | (?: 83 | ,[a-zA-Z0-9_$]+\\(a\\))?,c=a\\. 84 | (?: 85 | get\\(b\\)| 86 | [a-zA-Z0-9_$]+\\[b\\]\\|\\|null 87 | )\\)&&\\(c=| 88 | \\b(?[a-zA-Z0-9_$]+)= 89 | )(?[a-zA-Z0-9_$]+)(?:\\[(?\\d+)\\])?\\([a-zA-Z]\\) 90 | ((var)|,[a-zA-Z0-9_$]+\\.set\\((?:\\"n+\\"|[a-zA-Z0-9_$]+)\\,(\\k)\\))"""; 91 | 92 | Pattern regex = Pattern.compile(functionPattern); 93 | Matcher matcher = regex.matcher(js); 94 | if (matcher.find()){ 95 | String funName = Pattern.quote(matcher.group("nfunc")); 96 | String idx = matcher.group("idx"); 97 | if(!idx.isEmpty()){ 98 | String pattern2 = "var " + funName + "\\s*=\\s*\\[(.+?)]"; 99 | Pattern regex2 = Pattern.compile(pattern2); 100 | Matcher nFuncFound = regex2.matcher(js); 101 | if (nFuncFound.find()){ 102 | return nFuncFound.group(1); 103 | }else { 104 | throw new RegexMatchError("getThrottlingFunctionName: Could not find function name " + pattern2 + " in playerJs: " + playerJs); 105 | } 106 | } 107 | } 108 | 109 | throw new RegexMatchError("getThrottlingFunctionName: Could not find function name in playerJs: " + playerJs); 110 | } 111 | 112 | public String getSignatureFunctionName(){ 113 | return signatureFunctionName; 114 | } 115 | 116 | public String getThrottlingFunctionName(){ 117 | return throttlingFunctionName; 118 | } 119 | 120 | public String getSignature(String cipherSignature) throws Exception { 121 | return (String) jsInterpreter.callFunction(signatureFunctionName, cipherSignature); 122 | } 123 | 124 | public String getNSig(String n) throws Exception { 125 | return (String) jsInterpreter.callFunction(throttlingFunctionName, n); 126 | } 127 | } -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/FilterBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | 4 | import java.util.*; 5 | 6 | public class FilterBuilder { 7 | 8 | public enum Filter { 9 | TYPE, 10 | UPLOAD_DATE, 11 | DURATION, 12 | FEATURES, 13 | SORT_BY 14 | } 15 | 16 | public enum Type { 17 | VIDEO("Video"), 18 | CHANNEL("Channel"), 19 | PLAYLIST("Playlist"), 20 | MOVIE("Movie"); 21 | 22 | public final String label; 23 | Type(String label) { this.label = label; } 24 | } 25 | 26 | public enum UploadDate { 27 | LAST_HOUR("Last Hour"), 28 | TODAY("Today"), 29 | THIS_WEEK("This week"), 30 | THIS_MONTH("This month"), 31 | THIS_YEAR("This year"); 32 | 33 | public final String label; 34 | UploadDate(String label) { this.label = label; } 35 | } 36 | 37 | public enum Duration { 38 | UNDER_4_MIN("Under 4 minutes"), 39 | OVER_20_MIN("Over 20 minutes"), 40 | BETWEEN_4_20("4 - 20 minutes"); 41 | 42 | public final String label; 43 | Duration(String label) { this.label = label; } 44 | } 45 | 46 | public enum Feature { 47 | LIVE("Live"), 48 | _4K("4K"), 49 | HD("HD"), 50 | SUBTITLES("Subtitles/CC"), 51 | CREATIVE_COMMONS("Creative Commons"), 52 | _360("360"), 53 | VR180("VR180"), 54 | _3D("3D"), 55 | HDR("HDR"), 56 | LOCATION("Location"), 57 | PURCHASED("Purchased"); 58 | 59 | public final String label; 60 | Feature(String label) { this.label = label; } 61 | } 62 | 63 | public enum SortBy { 64 | RELEVANCE("Relevance"), 65 | UPLOAD_DATE("Upload date"), 66 | VIEW_COUNT("View count"), 67 | RATING("Rating"); 68 | 69 | public final String label; 70 | SortBy(String label) { this.label = label; } 71 | } 72 | 73 | private static final Map>> UPLOAD_DATE = Map.of( 74 | "Last Hour", Map.of(2, Map.of(1, 1)), 75 | "Today", Map.of(2, Map.of(1, 2)), 76 | "This week", Map.of(2, Map.of(1, 3)), 77 | "This month", Map.of(2, Map.of(1, 4)), 78 | "This year", Map.of(2, Map.of(1, 5)) 79 | ); 80 | 81 | private static final Map>> TYPE = Map.of( 82 | "Video", Map.of(2, Map.of(2, 1)), 83 | "Channel", Map.of(2, Map.of(2, 2)), 84 | "Playlist", Map.of(2, Map.of(2, 3)), 85 | "Movie", Map.of(2, Map.of(2, 4)) 86 | ); 87 | 88 | private static final Map>> DURATION = Map.of( 89 | "Under 4 minutes", Map.of(2, Map.of(3, 1)), 90 | "Over 20 minutes", Map.of(2, Map.of(3, 2)), 91 | "4 - 20 minutes", Map.of(2, Map.of(3, 3)) 92 | ); 93 | 94 | private static final Map>> FEATURES = Map.ofEntries( 95 | Map.entry("Live", Map.of(2, Map.of(8, 1))), 96 | Map.entry("4K", Map.of(2, Map.of(14, 1))), 97 | Map.entry("HD", Map.of(2, Map.of(4, 1))), 98 | Map.entry("Subtitles/CC", Map.of(2, Map.of(5, 1))), 99 | Map.entry("Creative Commons", Map.of(2, Map.of(6, 1))), 100 | Map.entry("360", Map.of(2, Map.of(15, 1))), 101 | Map.entry("VR180", Map.of(2, Map.of(26, 1))), 102 | Map.entry("3D", Map.of(2, Map.of(7, 1))), 103 | Map.entry("HDR", Map.of(2, Map.of(25, 1))), 104 | Map.entry("Location", Map.of(2, Map.of(23, 1))), 105 | Map.entry("Purchased", Map.of(2, Map.of(9, 1))) 106 | ); 107 | 108 | private static final Map> SORT_BY = Map.of( 109 | "Relevance", Map.of(1, 0), 110 | "Upload date", Map.of(1, 2), 111 | "View count", Map.of(1, 3), 112 | "Rating", Map.of(1, 1) 113 | ); 114 | 115 | public static String buildFilter(Map filters) { 116 | Map result = new LinkedHashMap<>(); 117 | 118 | if (filters.containsKey(Filter.SORT_BY)) { 119 | SortBy sortBy = (SortBy) filters.get(Filter.SORT_BY); 120 | result.putAll(SORT_BY.get(sortBy.label)); 121 | } 122 | 123 | for (Map.Entry entry : filters.entrySet()) { 124 | Filter filterType = entry.getKey(); 125 | Object value = entry.getValue(); 126 | 127 | if (filterType == Filter.SORT_BY) continue; 128 | 129 | switch (filterType) { 130 | case UPLOAD_DATE -> { 131 | UploadDate param = (UploadDate) value; 132 | mergeMap(result, getMapFromLabel(UPLOAD_DATE, param.label)); 133 | } 134 | case TYPE -> { 135 | Type param = (Type) value; 136 | mergeMap(result, getMapFromLabel(TYPE, param.label)); 137 | } 138 | case DURATION -> { 139 | Duration param = (Duration) value; 140 | mergeMap(result, getMapFromLabel(DURATION, param.label)); 141 | } 142 | case FEATURES -> { 143 | if (value instanceof List){ 144 | @SuppressWarnings("unchecked") 145 | List features = (List) value; 146 | for (Feature f : features) { 147 | mergeMap(result, getMapFromLabel(FEATURES, f.label)); 148 | } 149 | }else if(value instanceof Feature param) 150 | mergeMap(result, getMapFromLabel(FEATURES, param.label)); 151 | } 152 | } 153 | } 154 | 155 | return mapToString(result); 156 | } 157 | 158 | 159 | private static Map> getMapFromLabel( 160 | Map>> source, String label) { 161 | return source.getOrDefault(label, Map.of()); 162 | } 163 | 164 | private static void mergeMap(Map target, Map> toMerge) { 165 | for (Map.Entry> entry : toMerge.entrySet()) { 166 | Integer outerKey = entry.getKey(); 167 | Map innerMap = entry.getValue(); 168 | 169 | if (!target.containsKey(outerKey)) { 170 | target.put(outerKey, new LinkedHashMap<>(innerMap)); 171 | } else { 172 | @SuppressWarnings("unchecked") 173 | Map existingMap = (Map) target.get(outerKey); 174 | existingMap.putAll(innerMap); 175 | } 176 | } 177 | } 178 | 179 | private static String mapToString(Map map) { 180 | StringBuilder sb = new StringBuilder("{"); 181 | boolean first = true; 182 | for (Map.Entry entry : map.entrySet()) { 183 | if (!first) sb.append(", "); 184 | sb.append(entry.getKey()).append(": "); 185 | if (entry.getValue() instanceof Map nestedMap) { 186 | sb.append("{"); 187 | boolean nestedFirst = true; 188 | for (Map.Entry nestedEntry : nestedMap.entrySet()) { 189 | if (!nestedFirst) sb.append(", "); 190 | sb.append(nestedEntry.getKey()).append(": ").append(nestedEntry.getValue()); 191 | nestedFirst = false; 192 | } 193 | sb.append("}"); 194 | } else { 195 | sb.append(entry.getValue()); 196 | } 197 | first = false; 198 | } 199 | sb.append("}"); 200 | return sb.toString(); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/InnerTube.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | import java.io.UnsupportedEncodingException; 9 | import java.net.URLEncoder; 10 | import java.nio.charset.StandardCharsets; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.util.*; 15 | 16 | public class InnerTube{ 17 | private static JSONObject innerTubeContext; 18 | private static boolean requireJsPlayer; 19 | private static boolean requirePoToken; 20 | private static JSONObject header; 21 | private static String apiKey; 22 | 23 | private final boolean usePoToken; 24 | private String accessPoToken; 25 | private String accessVisitorData; 26 | 27 | JSONObject defaultClient = new JSONObject(""" 28 | { 29 | "WEB": { 30 | "innerTubeContext": { 31 | "context": { 32 | "client": { 33 | "clientName": "WEB", 34 | "osName": "Windows", 35 | "osVersion": "10.0", 36 | "clientVersion": "2.20250122.01.00", 37 | "platform": "DESKTOP" 38 | } 39 | } 40 | }, 41 | "header": { 42 | "User-Agent": "Mozilla/5.0", 43 | "X-Youtube-Client-Name": "1", 44 | "X-Youtube-Client-Version": "2.20250122.01.00" 45 | }, 46 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 47 | "requireJsPlayer": "true", 48 | "requirePoToken": "true" 49 | }, 50 | 51 | "WEB_EMBED": { 52 | "innerTubeContext": { 53 | "context": { 54 | "client": { 55 | "clientName": "WEB_EMBEDDED_PLAYER", 56 | "osName": "Windows", 57 | "osVersion": "10.0", 58 | "clientVersion": "2.20240530.02.00", 59 | "clientScreen": "EMBED" 60 | } 61 | } 62 | }, 63 | "header": { 64 | "User-Agent": "Mozilla/5.0", 65 | "X-Youtube-Client-Name": "56" 66 | }, 67 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 68 | "requireJsPlayer": "true", 69 | "requirePoToken": "true" 70 | }, 71 | 72 | "WEB_MUSIC": { 73 | "innerTubeContext": { 74 | "context": { 75 | "client": { 76 | "clientName": "WEB_REMIX", 77 | "clientVersion": "1.20240403.01.00" 78 | } 79 | } 80 | }, 81 | "header": { 82 | "User-Agent": "Mozilla/5.0", 83 | "X-Youtube-Client-Name": "67" 84 | }, 85 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 86 | "requireJsPlayer": "true", 87 | "requirePoToken": "false" 88 | }, 89 | 90 | "WEB_CREATOR": { 91 | "innerTubeContext": { 92 | "context": { 93 | "client": { 94 | "clientName": "WEB_CREATOR", 95 | "clientVersion": "1.20220726.00.00" 96 | } 97 | } 98 | }, 99 | "header": { 100 | "User-Agent": "Mozilla/5.0", 101 | "X-Youtube-Client-Name": "62" 102 | }, 103 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 104 | "requireJsPlayer": "true", 105 | "requirePoToken": "false" 106 | }, 107 | 108 | "WEB_SAFARI": { 109 | "innerTubeContext": { 110 | "context": { 111 | "client": { 112 | "clientName": "WEB", 113 | "clientVersion": "2.20240726.00.00" 114 | } 115 | } 116 | }, 117 | "header": { 118 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)", 119 | "X-Youtube-Client-Name": "1" 120 | }, 121 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 122 | "requireJsPlayer": "true", 123 | "requirePoToken": "true" 124 | }, 125 | 126 | "MWEB": { 127 | "innerTubeContext": { 128 | "context": { 129 | "client": { 130 | "clientName": "MWEB", 131 | "clientVersion": "2.20241202.07.00" 132 | } 133 | } 134 | }, 135 | "header": { 136 | "User-Agent": "Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)", 137 | "X-Youtube-Client-Name": "2" 138 | }, 139 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 140 | "requireJsPlayer": "true", 141 | "requirePoToken": "true" 142 | }, 143 | 144 | "WEB_KIDS": { 145 | "innerTubeContext": { 146 | "context": { 147 | "client": { 148 | "clientName": "WEB_KIDS", 149 | "osName": "Windows", 150 | "osVersion": "10.0", 151 | "clientVersion": "2.20241125.00.00", 152 | "platform": "DESKTOP" 153 | } 154 | } 155 | }, 156 | "header": { 157 | "User-Agent": "Mozilla/5.0" 158 | }, 159 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 160 | "requireJsPlayer": "true", 161 | "requirePoToken": "false" 162 | }, 163 | 164 | 165 | "ANDROID": { 166 | "innerTubeContext": { 167 | "context": { 168 | "client": { 169 | "clientName": "ANDROID", 170 | "clientVersion": "19.44.38", 171 | "platform": "MOBILE", 172 | "osName": "Android", 173 | "osVersion": "14", 174 | "androidSdkVersion": "34" 175 | } 176 | }, 177 | "params": "2AMB" 178 | }, 179 | "header": { 180 | "User-Agent": "com.google.android.youtube/", 181 | "X-Youtube-Client-Name": "3" 182 | }, 183 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 184 | "requireJsPlayer": "false", 185 | "requirePoToken": "true" 186 | }, 187 | 188 | "ANDROID_VR": { 189 | "innerTubeContext": { 190 | "context": { 191 | "client": { 192 | "clientName": "ANDROID_VR", 193 | "clientVersion": "1.60.19", 194 | "deviceMake": "Oculus", 195 | "deviceModel": "Quest 3", 196 | "osName": "Android", 197 | "osVersion": "12L", 198 | "androidSdkVersion": "32" 199 | } 200 | } 201 | }, 202 | "header": { 203 | "User-Agent": "com.google.android.apps.youtube.vr.oculus/1.60.19 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip", 204 | "X-Youtube-Client-Name": "28" 205 | }, 206 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 207 | "requireJsPlayer": "false", 208 | "requirePoToken": "false" 209 | }, 210 | 211 | "ANDROID_MUSIC": { 212 | "innerTubeContext": { 213 | "context": { 214 | "client": { 215 | "clientName": "ANDROID_MUSIC", 216 | "clientVersion": "7.27.52", 217 | "androidSdkVersion": "30", 218 | "osName": "Android", 219 | "osVersion": "11" 220 | } 221 | } 222 | }, 223 | "header": { 224 | "User-Agent": "com.google.android.apps.youtube.music/7.27.52 (Linux; U; Android 11) gzip", 225 | "X-Youtube-Client-Name": "21" 226 | }, 227 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 228 | "requireJsPlayer": "false", 229 | "requirePoToken": "false" 230 | }, 231 | 232 | "ANDROID_CREATOR": { 233 | "innerTubeContext": { 234 | "context": { 235 | "client": { 236 | "clientName": "ANDROID_CREATOR", 237 | "clientVersion": "24.45.100", 238 | "androidSdkVersion": "30", 239 | "osName": "Android", 240 | "osVersion": "11" 241 | } 242 | } 243 | }, 244 | "header": { 245 | "User-Agent": "com.google.android.apps.youtube.creator/24.45.100 (Linux; U; Android 11) gzip", 246 | "X-Youtube-Client-Name": "14" 247 | }, 248 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 249 | "requireJsPlayer": "false", 250 | "requirePoToken": "false" 251 | }, 252 | 253 | "ANDROID_TESTSUITE": { 254 | "innerTubeContext": { 255 | "context": { 256 | "client": { 257 | "clientName": "ANDROID_TESTSUITE", 258 | "clientVersion": "1.9", 259 | "platform": "MOBILE", 260 | "osName": "Android", 261 | "osVersion": "14", 262 | "androidSdkVersion": "34" 263 | } 264 | } 265 | }, 266 | "header": { 267 | "User-Agent": "com.google.android.youtube/", 268 | "X-Youtube-Client-Name": "30", 269 | "X-Youtube-Client-Version": "1.9" 270 | }, 271 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 272 | "requireJsPlayer": "false", 273 | "requirePoToken": "false" 274 | }, 275 | 276 | "ANDROID_PRODUCER": { 277 | "innerTubeContext": { 278 | "context": { 279 | "client": { 280 | "clientName": "ANDROID_PRODUCER", 281 | "clientVersion": "0.111.1", 282 | "androidSdkVersion": "30", 283 | "osName": "Android", 284 | "osVersion": "11" 285 | } 286 | } 287 | }, 288 | "header": { 289 | "User-Agent": "com.google.android.apps.youtube.producer/0.111.1 (Linux; U; Android 11) gzip", 290 | "X-Youtube-Client-Name": "91" 291 | }, 292 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 293 | "requireJsPlayer": "false", 294 | "requirePoToken": "false" 295 | }, 296 | 297 | "ANDROID_KIDS": { 298 | "innerTubeContext": { 299 | "context": { 300 | "client": { 301 | "clientName": "ANDROID_KIDS", 302 | "clientVersion": "7.36.1", 303 | "osName": "Android", 304 | "osVersion": "11", 305 | "androidSdkVersion": "30" 306 | } 307 | } 308 | }, 309 | "header": { 310 | "User-Agent": "com.google.android.youtube/" 311 | }, 312 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 313 | "requireJsPlayer": "false", 314 | "requirePoToken": "false" 315 | }, 316 | 317 | "IOS": { 318 | "innerTubeContext": { 319 | "context": { 320 | "client": { 321 | "clientName": "IOS", 322 | "clientVersion": "19.45.4", 323 | "deviceMake": "Apple", 324 | "platform": "MOBILE", 325 | "osName": "iPhone", 326 | "osVersion": "18.1.0.22B83", 327 | "deviceModel": "iPhone16,2" 328 | } 329 | } 330 | }, 331 | "header": { 332 | "User-Agent": "com.google.ios.youtube/19.45.4 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X;)", 333 | "X-Youtube-Client-Name": "5" 334 | }, 335 | "apiKey": "AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc", 336 | "requireJsPlayer": "false", 337 | "requirePoToken": "false" 338 | }, 339 | 340 | "IOS_MUSIC": { 341 | "innerTubeContext": { 342 | "context": { 343 | "client": { 344 | "clientName": "IOS_MUSIC", 345 | "clientVersion": "7.27.0", 346 | "deviceMake": "Apple", 347 | "platform": "MOBILE", 348 | "osName": "iPhone", 349 | "osVersion": "18.1.0.22B83", 350 | "deviceModel": "iPhone16,2" 351 | } 352 | } 353 | }, 354 | "header": { 355 | "User-Agent": "com.google.ios.youtubemusic/7.27.0 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X;)", 356 | "X-Youtube-Client-Name": "26" 357 | }, 358 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 359 | "requireJsPlayer": "false", 360 | "requirePoToken": "false" 361 | }, 362 | 363 | "IOS_CREATOR": { 364 | "innerTubeContext": { 365 | "context": { 366 | "client": { 367 | "clientName": "IOS_CREATOR", 368 | "clientVersion": "24.45.100", 369 | "deviceMake": "Apple", 370 | "deviceModel": "iPhone16,2", 371 | "osName": "iPhone", 372 | "osVersion": "18.1.0.22B83" 373 | } 374 | } 375 | }, 376 | "header": { 377 | "User-Agent": "com.google.ios.ytcreator/24.45.100 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X;)", 378 | "X-Youtube-Client-Name": "15" 379 | }, 380 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 381 | "requireJsPlayer": "false", 382 | "requirePoToken": "false" 383 | }, 384 | 385 | "IOS_KIDS": { 386 | "innerTubeContext": { 387 | "context": { 388 | "client": { 389 | "clientName": "IOS_KIDS", 390 | "clientVersion": "7.36.1", 391 | "deviceMake": "Apple", 392 | "platform": "MOBILE", 393 | "osName": "iPhone", 394 | "osVersion": "18.1.0.22B83", 395 | "deviceModel": "iPhone16,2" 396 | } 397 | } 398 | }, 399 | "header": { 400 | "User-Agent": "com.google.ios.youtube/19.45.4 (iPhone16,2; U; CPU iOS 18_1_0 like Mac OS X;)" 401 | }, 402 | "apiKey": "AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc", 403 | "requireJsPlayer": "false", 404 | "requirePoToken": "false" 405 | }, 406 | 407 | "TV": { 408 | "innerTubeContext": { 409 | "context": { 410 | "client": { 411 | "clientName": "TVHTML5", 412 | "clientVersion": "7.20240813.07.00", 413 | "platform": "TV" 414 | } 415 | } 416 | }, 417 | "header": { 418 | "User-Agent": "Mozilla/5.0", 419 | "X-Youtube-Client-Name": "7" 420 | }, 421 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 422 | "requireJsPlayer": "true", 423 | "requirePoToken": "false" 424 | }, 425 | 426 | "TV_EMBED": { 427 | "innerTubeContext": { 428 | "context": { 429 | "client": { 430 | "clientName": "TVHTML5_SIMPLY_EMBEDDED_PLAYER", 431 | "clientVersion": "2.0", 432 | "clientScreen": "EMBED", 433 | "platform": "TV" 434 | } 435 | } 436 | }, 437 | "header": { 438 | "User-Agent": "Mozilla/5.0", 439 | "X-Youtube-Client-Name": "85" 440 | }, 441 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 442 | "requireJsPlayer": "true", 443 | "requirePoToken": "false" 444 | }, 445 | 446 | "MEDIA_CONNECT": { 447 | "innerTubeContext": { 448 | "context": { 449 | "client": { 450 | "clientName": "MEDIA_CONNECT_FRONTEND", 451 | "clientVersion": "0.1" 452 | } 453 | } 454 | }, 455 | "header": { 456 | "User-Agent": "Mozilla/5.0", 457 | "X-Youtube-Client-Name": "95" 458 | }, 459 | "apiKey": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", 460 | "requireJsPlayer": "false", 461 | "requirePoToken": "false" 462 | } 463 | } 464 | """); 465 | 466 | 467 | /** 468 | * @Clients: 469 | * WEB, 470 | * WEB_EMBED, 471 | * WEB_MUSIC, 472 | * WEB_CREATOR, 473 | * WEB_SAFARI, 474 | * MWEB, 475 | * ANDROID, 476 | * ANDROID_VR, 477 | * ANDROID_MUSIC, 478 | * ANDROID_CREATOR, 479 | * ANDROID_TESTSUITE, 480 | * ANDROID_PRODUCER, 481 | * IOS, 482 | * IOS_MUSIC, 483 | * IOS_CREATOR, 484 | * TV_EMBED, 485 | * MEDIA_CONNECT 486 | * */ 487 | public InnerTube(String client, boolean usePoToken, boolean allowCache) throws JSONException { 488 | 489 | innerTubeContext = defaultClient.getJSONObject(client).getJSONObject("innerTubeContext"); 490 | requireJsPlayer = defaultClient.getJSONObject(client).getBoolean("requireJsPlayer"); 491 | requirePoToken = defaultClient.getJSONObject(client).getBoolean("requirePoToken"); 492 | header = defaultClient.getJSONObject(client).getJSONObject("header"); 493 | 494 | // API keys are not required, see: https://github.com/TeamNewPipe/NewPipeExtractor/pull/1168 495 | apiKey = defaultClient.getJSONObject(client).getString("apiKey"); 496 | 497 | this.usePoToken = usePoToken; 498 | 499 | try { 500 | String tempDir = System.getProperty("java.io.tmpdir"); 501 | Path path = Paths.get(tempDir, "tokens.json"); 502 | 503 | if (usePoToken && allowCache && Files.exists(path)) { 504 | String content = new String(Files.readAllBytes(path)); 505 | JSONObject jsonObject = new JSONObject(content); 506 | accessVisitorData = jsonObject.getString("visitorData"); 507 | accessPoToken = jsonObject.getString("poToken"); 508 | } 509 | } catch (IOException e) { 510 | e.printStackTrace(); 511 | } 512 | } 513 | 514 | /** 515 | * @Clients: 516 | * WEB, 517 | * WEB_EMBED, 518 | * WEB_MUSIC, 519 | * WEB_CREATOR, 520 | * WEB_SAFARI, 521 | * MWEB, 522 | * ANDROID, 523 | * ANDROID_VR, 524 | * ANDROID_MUSIC, 525 | * ANDROID_CREATOR, 526 | * ANDROID_TESTSUITE, 527 | * ANDROID_PRODUCER, 528 | * IOS, 529 | * IOS_MUSIC, 530 | * IOS_CREATOR, 531 | * TV_EMBED, 532 | * MEDIA_CONNECT 533 | * */ 534 | public InnerTube(String client, boolean usePoToken) throws JSONException { 535 | this(client, usePoToken, false); 536 | } 537 | 538 | /** 539 | * @Clients: 540 | * WEB, 541 | * WEB_EMBED, 542 | * WEB_MUSIC, 543 | * WEB_CREATOR, 544 | * WEB_SAFARI, 545 | * MWEB, 546 | * ANDROID, 547 | * ANDROID_VR, 548 | * ANDROID_MUSIC, 549 | * ANDROID_CREATOR, 550 | * ANDROID_TESTSUITE, 551 | * ANDROID_PRODUCER, 552 | * IOS, 553 | * IOS_MUSIC, 554 | * IOS_CREATOR, 555 | * TV_EMBED, 556 | * MEDIA_CONNECT 557 | * */ 558 | public InnerTube(String client) throws JSONException { 559 | this(client, false, false); 560 | } 561 | 562 | public JSONObject getInnerTubeContext() throws JSONException { 563 | return innerTubeContext; 564 | } 565 | public void updateInnerTubeContext(JSONObject innerTubeContext, JSONObject extraInfo) throws JSONException { 566 | for (Iterator it = extraInfo.keys(); it.hasNext(); ) { 567 | String key = it.next(); 568 | if (innerTubeContext.has(key) && innerTubeContext.get(key) instanceof JSONObject) { 569 | updateInnerTubeContext(innerTubeContext.getJSONObject(key), extraInfo.getJSONObject(key)); 570 | } else { 571 | innerTubeContext.put(key, extraInfo.get(key)); 572 | } 573 | } 574 | } 575 | public Map getClientHeaders() throws JSONException { 576 | return getHeaderMap(); 577 | } 578 | @Deprecated 579 | public String getClientApiKey() throws JSONException { 580 | return apiKey; 581 | } 582 | public boolean getRequireJsPlayer(){ 583 | return requireJsPlayer; 584 | } 585 | 586 | public boolean getRequirePoToken(){ 587 | return requirePoToken; 588 | } 589 | 590 | public String getVisitorData(){ 591 | return accessVisitorData; 592 | } 593 | 594 | public String getPoToken(){ 595 | return accessPoToken; 596 | } 597 | 598 | private String getBaseUrl(){ 599 | return "https://www.youtube.com/youtubei/v1"; 600 | } 601 | 602 | private String getBaseParam(){ 603 | return "{prettyPrint: \"false\"}"; 604 | } 605 | 606 | private String[] defaultPoTokenVerifier(){ 607 | Scanner scanner = new Scanner(System.in); 608 | System.out.println("You can use the tool: https://github.com/YunzheZJU/youtube-po-token-generator, to get the token"); 609 | System.out.print("Enter with your visitorData: "); 610 | String visitorData = scanner.nextLine(); 611 | System.out.print("Enter with your PoToken: "); 612 | String poToken = scanner.nextLine(); 613 | return new String[]{visitorData, poToken}; 614 | } 615 | 616 | public void cacheTokens() throws JSONException { 617 | if (usePoToken){ 618 | JSONObject data = new JSONObject( 619 | "{" + 620 | "\"visitorData\": \"" + accessVisitorData + "\"," + 621 | "\"poToken\": \"" + accessPoToken + "\"" + 622 | "}" 623 | ); 624 | 625 | try { 626 | String tempDir = System.getProperty("java.io.tmpdir"); 627 | Path path = Paths.get(tempDir, "tokens.json"); 628 | Files.write(path, data.toString(4).getBytes()); 629 | } catch (IOException e) { 630 | e.printStackTrace(); 631 | } 632 | } 633 | } 634 | 635 | public void insertVisitorData(String visitorData) throws JSONException { 636 | JSONObject context = new JSONObject( 637 | "{" + 638 | "\"context\": {" + 639 | "\"client\": {" + 640 | "\"visitorData\": \"" + visitorData + "\"" + 641 | "}"+ 642 | "}" + 643 | "}" 644 | ); 645 | updateInnerTubeContext(innerTubeContext, context); 646 | } 647 | 648 | public void insetPoToken() throws JSONException { 649 | JSONObject context = new JSONObject( 650 | "{" + 651 | "\"context\": {" + 652 | "\"client\": {" + 653 | "\"visitorData\": \"" + accessVisitorData + "\"" + 654 | "}"+ 655 | "}," + 656 | "\"serviceIntegrityDimensions\": {" + 657 | "\"poToken\": \"" + accessPoToken + "\"" + 658 | "}" + 659 | "}" 660 | ); 661 | updateInnerTubeContext(innerTubeContext, context); 662 | } 663 | 664 | public void insetPoToken(String poToken, String visitorData) throws JSONException { 665 | JSONObject context = new JSONObject( 666 | "{" + 667 | "\"context\": {" + 668 | "\"client\": {" + 669 | "\"visitorData\": \"" + visitorData + "\"" + 670 | "}"+ 671 | "}," + 672 | "\"serviceIntegrityDimensions\": {" + 673 | "\"poToken\": \"" + poToken + "\"" + 674 | "}" + 675 | "}" 676 | ); 677 | updateInnerTubeContext(innerTubeContext, context); 678 | } 679 | 680 | public void fetchPoToken() throws JSONException { 681 | String[] token = defaultPoTokenVerifier(); 682 | accessVisitorData = token[0]; 683 | accessPoToken = token[1]; 684 | cacheTokens(); 685 | insetPoToken(); 686 | } 687 | 688 | private String urlEncode(JSONObject json) throws JSONException, UnsupportedEncodingException { 689 | StringBuilder query = new StringBuilder(); 690 | for (Iterator it = json.keys(); it.hasNext(); ) { 691 | String key = it.next(); 692 | String value = json.getString(key); 693 | query.append(URLEncoder.encode(key, StandardCharsets.UTF_8.name())); 694 | query.append("="); 695 | query.append(URLEncoder.encode(value, StandardCharsets.UTF_8.name())); 696 | query.append("&"); 697 | } 698 | if (query.length() != 0) { 699 | query.setLength(query.length() - 1); 700 | } 701 | return query.toString(); 702 | } 703 | 704 | private Map getHeaderMap() throws JSONException { 705 | HashMap headers = new HashMap<>(); 706 | Iterator keys = header.keys(); 707 | while (keys.hasNext()) { 708 | String key = keys.next(); 709 | String value = header.getString(key); 710 | headers.put(key, value); 711 | } 712 | return headers; 713 | } 714 | 715 | private JSONObject callApi(String endpoint, JSONObject query) throws Exception { 716 | 717 | String endpointUrl = endpoint + "?" + urlEncode(query); 718 | 719 | if(usePoToken){ 720 | if(accessPoToken != null){ 721 | insetPoToken(); 722 | }else { 723 | fetchPoToken(); 724 | } 725 | } 726 | 727 | ByteArrayOutputStream response = Request.post(endpointUrl, getInnerTubeContext().toString(), getHeaderMap()); 728 | return new JSONObject(response.toString()); 729 | } 730 | 731 | public JSONObject player(String videoId) throws Exception { 732 | String endpoint = getBaseUrl() + "/player"; 733 | JSONObject query = new JSONObject(getBaseParam()); 734 | JSONObject context = new JSONObject("{videoId: " + videoId + ", " + "contentCheckOk: \"true\"" + "}"); 735 | updateInnerTubeContext(getInnerTubeContext(), context); 736 | return callApi(endpoint, query); 737 | } 738 | 739 | public JSONObject browse(JSONObject data) throws Exception { 740 | String endpoint = getBaseUrl() + "/browse"; 741 | JSONObject query = new JSONObject(getBaseParam()); 742 | updateInnerTubeContext(getInnerTubeContext(), data); 743 | return callApi(endpoint, query); 744 | } 745 | 746 | public JSONObject search(String searchQuery, JSONObject data) throws Exception { 747 | String endpoint = getBaseUrl() + "/search"; 748 | JSONObject query = new JSONObject(getBaseParam()); 749 | JSONObject contextQuery = new JSONObject("{query: " + searchQuery + "}"); 750 | updateInnerTubeContext(getInnerTubeContext(), contextQuery); 751 | 752 | if (data.length() > 0){ 753 | updateInnerTubeContext(getInnerTubeContext(), data); 754 | } 755 | return callApi(endpoint, query); 756 | } 757 | } -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/Playlist.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import com.github.felipeucelli.javatube.exceptions.RegexMatchError; 4 | import org.json.JSONArray; 5 | import org.json.JSONException; 6 | import org.json.JSONObject; 7 | 8 | import java.util.*; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | public class Playlist { 13 | private final String url; 14 | protected String html = null; 15 | protected JSONObject json = null; 16 | protected String continuationToken = null; 17 | InnerTube innerTube; 18 | 19 | public Playlist(String InputUrl) throws JSONException { 20 | url = InputUrl; 21 | innerTube = new InnerTube("WEB"); 22 | } 23 | 24 | @Override 25 | public String toString(){ 26 | try { 27 | return ""; 28 | } catch (Exception e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | 33 | private String getPlaylistId() throws Exception { 34 | Pattern pattern = Pattern.compile("list=([a-zA-Z0-9_\\-]*)"); 35 | Matcher matcher = pattern.matcher(url); 36 | if (matcher.find()){ 37 | return matcher.group(1); 38 | }else { 39 | throw new RegexMatchError("getPlaylistId: Unable to find match on: " + url); 40 | } 41 | } 42 | 43 | private String getPlaylistUrl() throws Exception { 44 | return "https://www.youtube.com/playlist?list=" + getPlaylistId(); 45 | } 46 | 47 | protected String setHtml() throws Exception { 48 | return Request.get(getPlaylistUrl(), null, innerTube.getClientHeaders()).toString(); 49 | } 50 | protected String getHtml() throws Exception { 51 | if(html == null){ 52 | html = setHtml(); 53 | } 54 | return html; 55 | } 56 | 57 | protected JSONObject setJson() throws Exception { 58 | Pattern pattern = Pattern.compile("ytInitialData\\s=\\s(\\{\\\"responseContext\\\":.*\\});"); 59 | Matcher matcher = pattern.matcher(getHtml()); 60 | if(matcher.find()){ 61 | return new JSONObject(matcher.group(1)); 62 | }else { 63 | throw new RegexMatchError("setJson: " + pattern); 64 | } 65 | } 66 | 67 | protected JSONObject getJson() throws Exception { 68 | if(json == null){ 69 | json = setJson(); 70 | } 71 | return json; 72 | } 73 | 74 | protected void setContinuationToken(JSONArray importantContent) throws JSONException { 75 | continuationToken = importantContent.getJSONObject(importantContent.length() - 1) 76 | .getJSONObject("continuationItemRenderer") 77 | .getJSONObject("continuationEndpoint") 78 | .getJSONObject("continuationCommand") 79 | .getString("token"); 80 | } 81 | 82 | protected JSONArray extractContinuationItems(JSONArray importantContent) throws Exception { 83 | JSONArray swap = new JSONArray(); 84 | 85 | JSONArray continuationEnd = buildContinuationUrl(continuationToken); 86 | 87 | for(int i = 0; i < importantContent.length(); i++){ 88 | swap.put(importantContent.get(i)); 89 | } 90 | 91 | for(int i = 0; i < continuationEnd.length(); i++){ 92 | swap.put(continuationEnd.get(i)); 93 | } 94 | return swap; 95 | } 96 | 97 | protected JSONArray buildContinuationUrl(String continuation) throws Exception { 98 | String data = "{" + 99 | "\"continuation\": \"" + continuation + "\"" + 100 | "}"; 101 | return extractVideos(innerTube.browse(new JSONObject(data))); 102 | } 103 | 104 | protected JSONArray extractVideos(JSONObject rawJson) { 105 | JSONArray swap = new JSONArray(); 106 | try { 107 | JSONArray importantContent; 108 | try { 109 | JSONObject tabs = rawJson.getJSONObject("contents") 110 | .getJSONObject("twoColumnBrowseResultsRenderer") 111 | .getJSONArray("tabs") 112 | .getJSONObject(0) 113 | .getJSONObject("tabRenderer") 114 | .getJSONObject("content") 115 | .getJSONObject("sectionListRenderer") 116 | .getJSONArray("contents") 117 | .getJSONObject(0); 118 | 119 | JSONObject renderer = tabs.getJSONObject("itemSectionRenderer") 120 | .getJSONArray("contents") 121 | .getJSONObject(0); 122 | if (renderer.has("richGridRenderer")){ 123 | importantContent = renderer 124 | .getJSONObject("richGridRenderer") 125 | .getJSONArray("contents"); 126 | }else { 127 | importantContent = renderer 128 | .getJSONObject("playlistVideoListRenderer") 129 | .getJSONArray("contents"); 130 | } 131 | 132 | }catch (JSONException e){ 133 | importantContent = rawJson.getJSONArray("onResponseReceivedActions") 134 | .getJSONObject(0) 135 | .getJSONObject("appendContinuationItemsAction") 136 | .getJSONArray("continuationItems"); 137 | } 138 | if(importantContent.getJSONObject(importantContent.length() - 1).has("continuationItemRenderer")){ 139 | setContinuationToken(importantContent); 140 | swap = extractContinuationItems(importantContent); 141 | } else { 142 | for(int i = 0; i < importantContent.length(); i++){ 143 | swap.put(importantContent.get(i)); 144 | } 145 | } 146 | 147 | }catch (Exception ignored){ 148 | } 149 | return swap; 150 | } 151 | 152 | protected ArrayList unify(ArrayList list){ 153 | LinkedHashSet unifiedList = new LinkedHashSet<>(list); 154 | list.clear(); 155 | list.addAll(unifiedList); 156 | return list; 157 | } 158 | 159 | public ArrayList getVideos() throws Exception { 160 | JSONArray video = extractVideos(getJson()); 161 | ArrayList videosId = new ArrayList<>(); 162 | try { 163 | for(int i = 0; i < video.length(); i++){ 164 | try{ 165 | if (video.getJSONObject(i).has("richItemRenderer")){ 166 | videosId.add("https://www.youtube.com/watch?v=" + video.getJSONObject(i) 167 | .getJSONObject("richItemRenderer") 168 | .getJSONObject("content") 169 | .getJSONObject("shortsLockupViewModel") 170 | .getJSONObject("onTap") 171 | .getJSONObject("innertubeCommand") 172 | .getJSONObject("reelWatchEndpoint") 173 | .getString("videoId")); 174 | }else { 175 | videosId.add("https://www.youtube.com/watch?v=" + video.getJSONObject(i) 176 | .getJSONObject("playlistVideoRenderer") 177 | .getString("videoId")); 178 | } 179 | }catch (Exception ignored){ 180 | } 181 | } 182 | return unify(videosId); 183 | } catch (Exception e) { 184 | throw new Error(e); 185 | } 186 | } 187 | 188 | private JSONObject getSidebarInfo(Integer i) throws Exception { 189 | return getJson().getJSONObject("sidebar") 190 | .getJSONObject("playlistSidebarRenderer") 191 | .getJSONArray("items") 192 | .getJSONObject(i); 193 | } 194 | 195 | public String getUrl() throws Exception { 196 | return getPlaylistUrl(); 197 | } 198 | 199 | public String getTitle() throws Exception { 200 | return getSidebarInfo(0).getJSONObject("playlistSidebarPrimaryInfoRenderer") 201 | .getJSONObject("title") 202 | .getJSONArray("runs") 203 | .getJSONObject(0) 204 | .getString("text"); 205 | } 206 | 207 | public String getDescription() throws Exception { 208 | try { 209 | try { 210 | return getSidebarInfo(0).getJSONObject("playlistSidebarPrimaryInfoRenderer") 211 | .getJSONObject("description") 212 | .getString("simpleText"); 213 | }catch (JSONException e) { 214 | return getSidebarInfo(0).getJSONObject("playlistSidebarPrimaryInfoRenderer") 215 | .getJSONObject("description") 216 | .getJSONArray("runs") 217 | .getJSONObject(0) 218 | .getString("text"); 219 | } 220 | }catch (Exception e){ 221 | return null; 222 | } 223 | } 224 | 225 | public String getViews() throws Exception { 226 | return getSidebarInfo(0).getJSONObject("playlistSidebarPrimaryInfoRenderer") 227 | .getJSONArray("stats") 228 | .getJSONObject(1) 229 | .getString("simpleText"); 230 | } 231 | 232 | public String getLastUpdated() throws Exception { 233 | try { 234 | return getSidebarInfo(0).getJSONObject("playlistSidebarPrimaryInfoRenderer") 235 | .getJSONArray("stats").getJSONObject(2) 236 | .getJSONArray("runs").getJSONObject(1) 237 | .getString("text"); 238 | }catch (JSONException e){ 239 | return getSidebarInfo(0).getJSONObject("playlistSidebarPrimaryInfoRenderer") 240 | .getJSONArray("stats") 241 | .getJSONObject(2) 242 | .getJSONArray("runs") 243 | .getJSONObject(0) 244 | .getString("text"); 245 | } 246 | } 247 | 248 | public String getOwner() throws Exception { 249 | return getSidebarInfo(1).getJSONObject("playlistSidebarSecondaryInfoRenderer") 250 | .getJSONObject("videoOwner") 251 | .getJSONObject("videoOwnerRenderer") 252 | .getJSONObject("title") 253 | .getJSONArray("runs") 254 | .getJSONObject(0) 255 | .getString("text"); 256 | } 257 | 258 | public String getOwnerId() throws Exception { 259 | return getSidebarInfo(1).getJSONObject("playlistSidebarSecondaryInfoRenderer") 260 | .getJSONObject("videoOwner") 261 | .getJSONObject("videoOwnerRenderer") 262 | .getJSONObject("title") 263 | .getJSONArray("runs") 264 | .getJSONObject(0) 265 | .getJSONObject("navigationEndpoint") 266 | .getJSONObject("browseEndpoint") 267 | .getString("browseId"); 268 | } 269 | 270 | public String getOwnerUrl() throws Exception { 271 | return "https://www.youtube.com/channel/" + getOwnerId(); 272 | } 273 | 274 | public String length() throws Exception { 275 | return getSidebarInfo(0).getJSONObject("playlistSidebarPrimaryInfoRenderer") 276 | .getJSONArray("stats") 277 | .getJSONObject(0) 278 | .getJSONArray("runs") 279 | .getJSONObject(0) 280 | .getString("text"); 281 | } 282 | 283 | } -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/Protobuf.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import java.io.*; 4 | import java.util.*; 5 | 6 | public class Protobuf { 7 | 8 | enum WireType { 9 | VARINT(0), I64(1), LEN(2), SGROUP(3), EGROUP(4), I32(5); 10 | final int value; 11 | WireType(int value) { this.value = value; } 12 | static WireType from(int value) { return values()[value]; } 13 | } 14 | 15 | public static String encodeProtobuf(String input) throws IOException { 16 | Map map = parseMap(new StringReader(input.trim())); 17 | ByteArrayOutputStream output = new ByteArrayOutputStream(); 18 | encode(map, output); 19 | return Base64.getEncoder().encodeToString(output.toByteArray()); 20 | } 21 | 22 | public static String decodeProtobuf(String base64) throws IOException { 23 | byte[] data = Base64.getDecoder().decode(base64); 24 | Map map = decode(new ByteArrayInputStream(data)); 25 | return mapToString(map); 26 | } 27 | 28 | private static Map parseMap(StringReader reader) throws IOException { 29 | Map map = new LinkedHashMap<>(); 30 | int c; 31 | skipWhitespace(reader); // Skip opening '{' 32 | if (reader.read() != '{') throw new IllegalArgumentException("Expected '{'"); 33 | 34 | while (true) { 35 | skipWhitespace(reader); 36 | c = reader.read(); 37 | if (c == '}') break; 38 | if (c == -1) throw new EOFException("Unexpected end of input"); 39 | 40 | reader.skip(-1); // unread 41 | int key = readInt(reader); 42 | skipWhitespace(reader); 43 | if (reader.read() != ':') throw new IllegalArgumentException("Expected ':'"); 44 | skipWhitespace(reader); 45 | 46 | c = reader.read(); 47 | if (c == '{') { 48 | reader.skip(-1); 49 | Object value = parseMap(reader); 50 | map.put(key, value); 51 | } else { 52 | StringBuilder number = new StringBuilder(); 53 | number.append((char) c); 54 | while ((c = reader.read()) != -1 && Character.isDigit(c)) { 55 | number.append((char) c); 56 | } 57 | if (c != -1) reader.skip(-1); // unread non-digit 58 | map.put(key, Integer.parseInt(number.toString())); 59 | } 60 | 61 | skipWhitespace(reader); 62 | c = reader.read(); 63 | if (c == ',') continue; 64 | if (c == '}') break; 65 | if (c == -1) break; 66 | throw new IllegalArgumentException("Unexpected character: " + (char) c); 67 | } 68 | 69 | return map; 70 | } 71 | 72 | private static void skipWhitespace(StringReader reader) throws IOException { 73 | reader.mark(1); 74 | int c; 75 | while ((c = reader.read()) != -1) { 76 | if (!Character.isWhitespace(c)) { 77 | reader.reset(); 78 | return; 79 | } 80 | reader.mark(1); 81 | } 82 | } 83 | 84 | private static int readInt(StringReader reader) throws IOException { 85 | StringBuilder sb = new StringBuilder(); 86 | int c; 87 | while ((c = reader.read()) != -1 && Character.isDigit(c)) { 88 | sb.append((char) c); 89 | } 90 | if (c != -1) reader.skip(-1); // unread 91 | return Integer.parseInt(sb.toString()); 92 | } 93 | 94 | private static void encode(Map data, OutputStream out) throws IOException { 95 | for (Map.Entry entry : data.entrySet()) { 96 | encodeRecord(entry.getValue(), entry.getKey(), out); 97 | } 98 | } 99 | 100 | private static Map decode(InputStream in) throws IOException { 101 | Map> result = new LinkedHashMap<>(); 102 | while (in.available() > 0) { 103 | int tag = readVarint(in); 104 | int wireId = tag >> 3; 105 | WireType type = WireType.from(tag & 0b111); 106 | 107 | Object value; 108 | switch (type) { 109 | case VARINT: 110 | value = readVarint(in); 111 | break; 112 | case I64: 113 | value = in.readNBytes(8); 114 | break; 115 | case I32: 116 | value = in.readNBytes(4); 117 | break; 118 | case LEN: 119 | int length = readVarint(in); 120 | byte[] sub = in.readNBytes(length); 121 | try (ByteArrayInputStream subIn = new ByteArrayInputStream(sub)) { 122 | value = decode(subIn); 123 | } catch (Exception e) { 124 | value = sub; 125 | } 126 | break; 127 | default: 128 | throw new IllegalArgumentException("Unsupported wire type"); 129 | } 130 | 131 | result.computeIfAbsent(wireId, k -> new ArrayList<>()).add(value); 132 | } 133 | 134 | Map finalResult = new LinkedHashMap<>(); 135 | for (Map.Entry> entry : result.entrySet()) { 136 | if (entry.getValue().size() == 1) { 137 | finalResult.put(entry.getKey(), entry.getValue().get(0)); 138 | } else { 139 | finalResult.put(entry.getKey(), entry.getValue()); 140 | } 141 | } 142 | 143 | return finalResult; 144 | } 145 | @SuppressWarnings("unchecked") 146 | private static void encodeRecord(Object value, int wireId, OutputStream out) throws IOException { 147 | if (value instanceof Integer) { 148 | int v = (Integer) value; 149 | if (v < 0) v = signedToZigZag(v); 150 | writeVarint((wireId << 3) | WireType.VARINT.value, out); 151 | writeVarint(v, out); 152 | } else if (value instanceof Map) { 153 | ByteArrayOutputStream nested = new ByteArrayOutputStream(); 154 | encode((Map) value, nested); 155 | byte[] bytes = nested.toByteArray(); 156 | writeVarint((wireId << 3) | WireType.LEN.value, out); 157 | writeVarint(bytes.length, out); 158 | out.write(bytes); 159 | } else { 160 | throw new IllegalArgumentException("Unsupported value type: " + value.getClass()); 161 | } 162 | } 163 | 164 | private static int signedToZigZag(int value) { 165 | return (value << 1) ^ (value >> 31); 166 | } 167 | 168 | private static void writeVarint(int value, OutputStream out) throws IOException { 169 | while ((value & ~0x7F) != 0) { 170 | out.write((value & 0x7F) | 0x80); 171 | value >>>= 7; 172 | } 173 | out.write(value); 174 | } 175 | 176 | private static int readVarint(InputStream in) throws IOException { 177 | int shift = 0, result = 0; 178 | int b; 179 | do { 180 | b = in.read(); 181 | if (b == -1) throw new EOFException(); 182 | result |= (b & 0x7F) << shift; 183 | shift += 7; 184 | } while ((b & 0x80) != 0); 185 | return result; 186 | } 187 | 188 | @SuppressWarnings("unchecked") 189 | private static String mapToString(Map map) { 190 | StringBuilder sb = new StringBuilder(); 191 | sb.append("{"); 192 | Iterator> it = map.entrySet().iterator(); 193 | while (it.hasNext()) { 194 | var entry = it.next(); 195 | sb.append(entry.getKey()).append(": "); 196 | if (entry.getValue() instanceof Map) { 197 | sb.append(mapToString((Map) entry.getValue())); 198 | } else { 199 | sb.append(entry.getValue()); 200 | } 201 | if (it.hasNext()) sb.append(", "); 202 | } 203 | sb.append("}"); 204 | return sb.toString(); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/Request.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import java.io.*; 4 | import java.net.HttpURLConnection; 5 | import java.net.SocketTimeoutException; 6 | import java.net.URL; 7 | import java.util.Map; 8 | 9 | class Request{ 10 | private static ByteArrayOutputStream executeRequest(String url, String method, String data, Map headers) throws IOException { 11 | ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); 12 | int attempts = 0; 13 | int maxAttempts = 3; 14 | 15 | while (attempts <= maxAttempts) { 16 | try { 17 | URL urlObj = new URL(url); 18 | HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection(); 19 | connection.setRequestMethod(method); 20 | connection.setConnectTimeout(5000); 21 | connection.setReadTimeout(10000); 22 | connection.setRequestProperty("accept-language", "en-US,en"); 23 | 24 | if (headers != null) { 25 | for (Map.Entry entry : headers.entrySet()) { 26 | connection.setRequestProperty(entry.getKey(), entry.getValue()); 27 | } 28 | } 29 | if (data != null && method.equalsIgnoreCase("POST")) { 30 | connection.setDoOutput(true); 31 | connection.setRequestProperty("Content-Type", "application/json"); 32 | connection.setRequestProperty("Content-Length", String.valueOf(data.length())); 33 | connection.getOutputStream().write(data.getBytes()); 34 | } 35 | 36 | int responseCode = connection.getResponseCode(); 37 | if (responseCode == HttpURLConnection.HTTP_OK) { 38 | InputStream in = new BufferedInputStream(connection.getInputStream()); 39 | byte[] buf = new byte[1024]; 40 | int n; 41 | while ((n = in.read(buf)) != -1) { 42 | responseStream.write(buf, 0, n); 43 | } 44 | responseStream.close(); 45 | in.close(); 46 | } else { 47 | throw new IOException("HTTP request failed with response code: " + responseCode); 48 | } 49 | connection.disconnect(); 50 | break; 51 | } catch (SocketTimeoutException e) { 52 | if (attempts == maxAttempts) { 53 | throw new IOException("Timeout occurred during the request.", e); 54 | } 55 | attempts += 1; 56 | } catch (IOException e) { 57 | if (attempts == maxAttempts) { 58 | throw e; 59 | } 60 | attempts += 1; 61 | } 62 | } 63 | return responseStream; 64 | } 65 | 66 | public static ByteArrayOutputStream get(String url, String data, Map headers) throws Exception { 67 | return executeRequest(url, "GET", data, headers); 68 | } 69 | public static ByteArrayOutputStream get(String url) throws Exception { 70 | return executeRequest(url, "GET", null, null); 71 | } 72 | public static ByteArrayOutputStream get(String url, String data) throws Exception { 73 | return executeRequest(url, "GET", data, null); 74 | } 75 | public static ByteArrayOutputStream get(String url, Map headers) throws Exception { 76 | return executeRequest(url, "GET", null, headers); 77 | } 78 | 79 | public static ByteArrayOutputStream post(String url, String data, Map headers) throws Exception { 80 | return executeRequest(url, "POST", data, headers); 81 | } 82 | public static ByteArrayOutputStream post(String url) throws Exception { 83 | return executeRequest(url, "POST", null, null); 84 | } 85 | public static ByteArrayOutputStream post(String url, String data) throws Exception { 86 | return executeRequest(url, "POST", data, null); 87 | } 88 | public static ByteArrayOutputStream post(String url, Map headers) throws Exception { 89 | return executeRequest(url, "POST", null, headers); 90 | } 91 | 92 | public static ByteArrayOutputStream postChunk(String chunk) throws IOException { 93 | URL url = new URL(chunk); 94 | InputStream in = new BufferedInputStream(url.openStream()); 95 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 96 | byte[] buf = new byte[1024]; 97 | int n; 98 | while (-1!=(n=in.read(buf))) { 99 | out.write(buf, 0, n); 100 | } 101 | out.close(); 102 | in.close(); 103 | 104 | return out; 105 | } 106 | } -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/Search.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | 7 | import java.io.UnsupportedEncodingException; 8 | import java.net.URLEncoder; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.*; 11 | 12 | public class Search { 13 | 14 | private final String query; 15 | private Map filter = null; 16 | private JSONObject jsonResult = null; 17 | private final InnerTube innerTubeClient = new InnerTube("WEB"); 18 | private Map> results = new HashMap<>(); 19 | private String continuationToken = ""; 20 | 21 | public Search(String query) throws JSONException { 22 | this.query = query; 23 | } 24 | 25 | public Search(String query, Map filter) throws JSONException { 26 | this.query = query; 27 | this.filter = filter; 28 | } 29 | 30 | private String safeQuery() throws UnsupportedEncodingException { 31 | return URLEncoder.encode(this.query, StandardCharsets.UTF_8.name()); 32 | } 33 | 34 | private JSONObject getJsonResult() throws Exception { 35 | if(jsonResult == null){ 36 | jsonResult = setJson(); 37 | } 38 | return jsonResult; 39 | } 40 | 41 | private JSONObject setJson() throws Exception { 42 | JSONObject data = new JSONObject(); 43 | if (filter != null && Objects.equals(continuationToken, "")){ 44 | data.put("params", Protobuf.encodeProtobuf(FilterBuilder.buildFilter(filter))); 45 | } 46 | if (!Objects.equals(continuationToken, "")){ 47 | data.put("continuation", continuationToken); 48 | } 49 | return innerTubeClient.search(safeQuery(), data); 50 | } 51 | 52 | private ArrayList extractShelfRenderer(JSONArray items) throws JSONException { 53 | ArrayList ids = new ArrayList<>(); 54 | for(int i = 0; items.length() > i; i++){ 55 | String vidId = items.getJSONObject(i).getJSONObject("videoRenderer").getString("videoId"); 56 | ids.add("https://www.youtube.com/watch?v=" + vidId); 57 | } 58 | return ids; 59 | } 60 | 61 | private ArrayList extractReelShelfRenderer(JSONArray items) throws JSONException { 62 | ArrayList ids = new ArrayList<>(); 63 | for(int i = 0; items.length() > i; i++){ 64 | String vidId; 65 | if (items.getJSONObject(i).has("reelItemRenderer")){ 66 | vidId = items.getJSONObject(i) 67 | .getJSONObject("reelItemRenderer") 68 | .getString("videoId"); 69 | }else { 70 | vidId = items.getJSONObject(i) 71 | .getJSONObject("shortsLockupViewModel") 72 | .getJSONObject("onTap") 73 | .getJSONObject("innertubeCommand") 74 | .getJSONObject("reelWatchEndpoint") 75 | .getString("videoId"); 76 | } 77 | 78 | ids.add("https://www.youtube.com/shorts/" + vidId); 79 | } 80 | return ids; 81 | } 82 | 83 | private Map> fetchAndParse() throws Exception { 84 | JSONObject rawResults = getJsonResult(); 85 | JSONArray sections; 86 | try { 87 | sections = rawResults.getJSONObject("contents") 88 | .getJSONObject("twoColumnSearchResultsRenderer") 89 | .getJSONObject("primaryContents") 90 | .getJSONObject("sectionListRenderer") 91 | .getJSONArray("contents"); 92 | }catch (JSONException e){ 93 | sections = rawResults.getJSONArray("onResponseReceivedCommands") 94 | .getJSONObject(0) 95 | .getJSONObject("appendContinuationItemsAction") 96 | .getJSONArray("continuationItems"); 97 | } 98 | 99 | JSONArray rawVideoList = new JSONArray(); 100 | for(int i = 0; i < sections.length(); i++){ 101 | if(sections.getJSONObject(i).has("itemSectionRenderer")){ 102 | rawVideoList = sections.getJSONObject(i).getJSONObject("itemSectionRenderer") 103 | .getJSONArray("contents"); 104 | } 105 | if(sections.getJSONObject(i).has("continuationItemRenderer")){ 106 | continuationToken = sections.getJSONObject(i).getJSONObject("continuationItemRenderer") 107 | .getJSONObject("continuationEndpoint") 108 | .getJSONObject("continuationCommand") 109 | .getString("token"); 110 | } 111 | } 112 | 113 | ArrayList videos = new ArrayList<>(); 114 | ArrayList shorts = new ArrayList<>(); 115 | ArrayList channel = new ArrayList<>(); 116 | ArrayList playlist = new ArrayList<>(); 117 | 118 | for(int i = 0; i < rawVideoList.length() - 1; i++) { 119 | 120 | // Get videos results 121 | if (rawVideoList.getJSONObject(i).has("videoRenderer")) { 122 | JSONObject vidRenderer = rawVideoList.getJSONObject(i).getJSONObject("videoRenderer"); 123 | String vidId = vidRenderer.getString("videoId"); 124 | videos.add("https://www.youtube.com/watch?v=" + vidId); 125 | 126 | } else if (rawVideoList.getJSONObject(i).has("shelfRenderer")) { 127 | JSONObject contents = rawVideoList.getJSONObject(i).getJSONObject("shelfRenderer") 128 | .getJSONObject("content"); 129 | if(contents.has("verticalListRenderer")){ 130 | videos.addAll(extractShelfRenderer(contents.getJSONObject("verticalListRenderer") 131 | .getJSONArray("items"))); 132 | } 133 | 134 | // Get shorts results 135 | } else if (rawVideoList.getJSONObject(i).has("reelShelfRenderer")) { 136 | shorts.addAll(extractReelShelfRenderer(rawVideoList.getJSONObject(i) 137 | .getJSONObject("reelShelfRenderer") 138 | .getJSONArray("items"))); 139 | 140 | // Get channel results 141 | } else if (rawVideoList.getJSONObject(i).has("channelRenderer")) { 142 | String channelId = rawVideoList.getJSONObject(i).getJSONObject("channelRenderer") 143 | .getString("channelId"); 144 | channel.add("https://www.youtube.com/channel/" + channelId); 145 | 146 | // Get playlist results 147 | } else if (rawVideoList.getJSONObject(i).has("playlistRenderer")) { 148 | String playlistId = rawVideoList.getJSONObject(i).getJSONObject("playlistRenderer") 149 | .getString("playlistId"); 150 | playlist.add("https://www.youtube.com/playlist?list=" + playlistId); 151 | } 152 | } 153 | 154 | Map> results = new HashMap<>(); 155 | results.put("videos", videos); 156 | results.put("shorts", shorts); 157 | results.put("channel", channel); 158 | results.put("playlist", playlist); 159 | 160 | return results; 161 | } 162 | 163 | public void generateContinuation() throws Exception { 164 | if(!Objects.equals(continuationToken, "")){ 165 | jsonResult = null; 166 | Map> result = fetchAndParse(); 167 | results.get("videos").addAll(result.get("videos")); 168 | results.get("shorts").addAll(result.get("shorts")); 169 | results.get("channel").addAll(result.get("channel")); 170 | results.get("playlist").addAll(result.get("playlist")); 171 | } 172 | } 173 | 174 | public List getCompletionSuggestions() throws Exception { 175 | List result = new ArrayList<>(); 176 | try { 177 | JSONArray refinements = getJsonResult().getJSONArray("refinements"); 178 | for(int i = 0; refinements.length() > i; i ++){ 179 | result.add(refinements.getString(i)); 180 | } 181 | return result; 182 | }catch (JSONException e){ 183 | return null; 184 | } 185 | } 186 | 187 | public ArrayList getResults() throws Exception { 188 | ArrayList result = new ArrayList<>(); 189 | if(results.isEmpty()){ 190 | results = fetchAndParse(); 191 | } 192 | result.addAll(results.get("videos")); 193 | result.addAll(results.get("shorts")); 194 | result.addAll(results.get("channel")); 195 | result.addAll(results.get("playlist")); 196 | return result; 197 | } 198 | 199 | public ArrayList getVideosResults() throws Exception { 200 | ArrayList result = new ArrayList<>(); 201 | if(results.isEmpty()){ 202 | results = fetchAndParse(); 203 | } 204 | for(String ids : results.get("videos")){ 205 | result.add(new Youtube(ids)); 206 | } 207 | return result; 208 | } 209 | 210 | public ArrayList getShortsResults() throws Exception { 211 | ArrayList result = new ArrayList<>(); 212 | if(results.isEmpty()){ 213 | results = fetchAndParse(); 214 | } 215 | for(String ids : results.get("shorts")){ 216 | result.add(new Youtube(ids)); 217 | } 218 | return result; 219 | } 220 | 221 | public ArrayList getChannelsResults() throws Exception { 222 | ArrayList result = new ArrayList<>(); 223 | if(results.isEmpty()){ 224 | results = fetchAndParse(); 225 | } 226 | for(String ids : results.get("channel")){ 227 | result.add(new Channel(ids)); 228 | } 229 | return result; 230 | } 231 | 232 | public ArrayList getPlaylistsResults() throws Exception { 233 | ArrayList result = new ArrayList<>(); 234 | if(results.isEmpty()){ 235 | results = fetchAndParse(); 236 | } 237 | for(String ids : results.get("playlist")){ 238 | result.add(new Playlist(ids)); 239 | } 240 | return result; 241 | } 242 | 243 | } -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/Stream.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import com.github.felipeucelli.javatube.exceptions.RegexMatchError; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | 7 | import java.io.*; 8 | import java.net.*; 9 | import java.util.*; 10 | import java.util.function.Consumer; 11 | import java.util.regex.Matcher; 12 | import java.util.regex.Pattern; 13 | import static java.lang.Math.min; 14 | 15 | public class Stream{ 16 | 17 | private final String title; 18 | private final String url; 19 | private final Integer itag; 20 | private final String mimeType; 21 | private final String codecs; 22 | private final String type; 23 | private final String subType; 24 | private final String videoCodec; 25 | private final String audioCodec; 26 | private final Integer bitrate; 27 | private final Boolean isOtf; 28 | private final long fileSize; 29 | private final Map itagProfile; 30 | private final String abr; 31 | private Integer fps = null; 32 | private final String resolution; 33 | private final JSONObject multipleAudioTracks; 34 | private final boolean defaultAudioTrack; 35 | private String audioTrackName = null; 36 | private String audioTrackId = null; 37 | 38 | 39 | public Stream(JSONObject stream, String videoTitle) throws Exception { 40 | title = videoTitle; 41 | url = stream.getString("url"); 42 | itag = stream.getInt("itag"); 43 | mimeType = mimeTypeCodec(stream.getString("mimeType")).group(1); 44 | codecs = mimeTypeCodec(stream.getString("mimeType")).group(2); 45 | type = Arrays.asList(mimeType.split("/")).get(0); 46 | subType = Arrays.asList(mimeType.split("/")).get(1); 47 | videoCodec = parseCodecs().get(0); 48 | audioCodec = parseCodecs().get(1); 49 | bitrate = stream.getInt("bitrate"); 50 | isOtf = setIsOtf(stream); 51 | fileSize = setFileSize(stream.has("contentLength") ? stream.getString("contentLength") : null); 52 | itagProfile = getFormatProfile(); 53 | abr = itagProfile.get("abr"); 54 | if(stream.has("fps")){ 55 | fps = stream.getInt("fps"); 56 | } 57 | resolution = itagProfile.get("resolution"); 58 | multipleAudioTracks = stream.has("audioTrack") ? stream.getJSONObject("audioTrack") : null; 59 | if(includesMultipleAudioTracks()){ 60 | defaultAudioTrack = multipleAudioTracks.getBoolean("audioIsDefault"); 61 | audioTrackName = Arrays.asList(multipleAudioTracks.getString("displayName").split(" ")).get(0); 62 | audioTrackId = multipleAudioTracks.getString("id"); 63 | }else { 64 | defaultAudioTrack = includeAudioTrack() && !includeVideoTrack(); 65 | } 66 | } 67 | 68 | @Override 69 | public String toString(){ 70 | ArrayList parts = new ArrayList<>(Arrays.asList( 71 | "itag=\"" + itag + "\"", "mime_type=\"" + mimeType + "\"")); 72 | if(includeVideoTrack()){ 73 | parts.addAll(Arrays.asList("res=\"" + resolution + "\"", "fps=\"" + fps + "\"")); 74 | if(!isAdaptive()){ 75 | parts.addAll(Arrays.asList("vcodec=\"" + videoCodec + "\"", "acodec=\"" + audioCodec + "\"")); 76 | }else { 77 | parts.add("vcodec=\"" + videoCodec + "\""); 78 | } 79 | } 80 | else { 81 | parts.addAll(Arrays.asList("abr=\"" + abr + "\"", "acodec=\"" + audioCodec + "\"")); 82 | } 83 | parts.addAll(Arrays.asList("progressive=\"" + isProgressive() + "\"", "type=\"" + type + "\"")); 84 | return ""; 85 | } 86 | 87 | private long setFileSize(String size) throws IOException { 88 | if (Objects.equals(size, null)) { 89 | if(!isOtf){ 90 | URL url = new URL(this.url); 91 | HttpURLConnection http = (HttpURLConnection) url.openConnection(); 92 | http.setRequestMethod("HEAD"); 93 | 94 | try { 95 | size = http.getHeaderFields().get("Content-Length").get(0); 96 | } catch (NullPointerException e) { 97 | size = "0"; 98 | } 99 | http.disconnect(); 100 | }else { 101 | size = "0"; 102 | } 103 | return Long.parseLong(size); 104 | } 105 | return Long.parseLong(size); 106 | } 107 | 108 | private boolean setIsOtf(JSONObject stream) throws JSONException { 109 | if(stream.has("type")){ 110 | return Objects.equals(stream.getString("type"), "FORMAT_STREAM_TYPE_OTF"); 111 | }else{ 112 | return false; 113 | } 114 | } 115 | 116 | public Boolean isAdaptive(){ 117 | return (Arrays.asList(codecs.split(",")).size() % 2) == 1; 118 | } 119 | 120 | public Boolean isProgressive(){ 121 | return !isAdaptive(); 122 | } 123 | 124 | public Boolean includeAudioTrack(){ 125 | return isProgressive() || Objects.equals(type, "audio"); 126 | } 127 | 128 | public Boolean includeVideoTrack() { return isProgressive() || Objects.equals(type, "video"); } 129 | 130 | public Boolean includesMultipleAudioTracks(){ 131 | return multipleAudioTracks != null; 132 | } 133 | 134 | private ArrayList parseCodecs(){ 135 | ArrayList array = new ArrayList<>(); 136 | String video = null, audio = null; 137 | if(!isAdaptive()){ 138 | video = Arrays.asList(codecs.split(",")).get(0); 139 | audio = Arrays.asList(codecs.split(",")).get(1); 140 | }else if(includeVideoTrack()){ 141 | video = Arrays.asList(codecs.split(",")).get(0); 142 | } else if (includeAudioTrack()) { 143 | audio = Arrays.asList(codecs.split(",")).get(0); 144 | } 145 | array.add(video); 146 | array.add(audio); 147 | 148 | return array; 149 | } 150 | 151 | private Matcher mimeTypeCodec(String mimeTypeCodec) throws Exception { 152 | Pattern pattern = Pattern.compile("(\\w+/\\w+);\\scodecs=\"([a-zA-Z-0-9.,\\s]*)\""); 153 | Matcher matcher = pattern.matcher(mimeTypeCodec); 154 | if (matcher.find()) { 155 | return matcher; 156 | }else { 157 | throw new RegexMatchError("mimeTypeCodec: " + pattern); 158 | } 159 | } 160 | 161 | private String safeFileName(String s){ 162 | return s.replaceAll("[\"'#$%*,.:;<>?\\\\^|~/]", " "); 163 | } 164 | 165 | private void checkFile(String filePath) throws IOException { 166 | File file = new File(filePath); 167 | if(file.exists()){ 168 | if(!file.delete()){ 169 | throw new IOException("Failed to delete existing output file: " + file.getName()); 170 | } 171 | } 172 | } 173 | 174 | public static void onProgress(long value){ 175 | System.out.println(value + "%"); 176 | } 177 | public void download(String path) throws Exception { 178 | startDownload(path, title, Stream::onProgress); 179 | } 180 | public void download(String path, Consumer progress) throws Exception { 181 | startDownload(path, title, progress); 182 | } 183 | public void download(String path, String fileName) throws Exception { 184 | startDownload(path, fileName, Stream::onProgress); 185 | } 186 | public void download(String path, String fileName, Consumer progress) throws Exception { 187 | startDownload(path, fileName, progress); 188 | } 189 | private void startDownload(String path, String fileName, Consumer progress) throws Exception { 190 | String savePath = path + safeFileName(fileName) + "." + subType; 191 | if(!isOtf){ 192 | long startSize = 0; 193 | long stopPos; 194 | int defaultRange = 1048576; 195 | long progressPercentage; 196 | long lastPrintedProgress = 0; 197 | byte[] chunkReceived; 198 | 199 | checkFile(savePath); 200 | do { 201 | stopPos = min(startSize + defaultRange, fileSize); 202 | if (stopPos >= fileSize) { 203 | stopPos = fileSize; 204 | } 205 | String chunk = url + "&range=" + startSize + "-" + stopPos; 206 | chunkReceived = Request.get(chunk).toByteArray(); 207 | 208 | progressPercentage = (stopPos * 100L) / (fileSize); 209 | 210 | if (progressPercentage != lastPrintedProgress) { 211 | lastPrintedProgress = progressPercentage; 212 | progress.accept(progressPercentage); 213 | } 214 | startSize = startSize + chunkReceived.length; 215 | try (FileOutputStream fos = new FileOutputStream(savePath, true)) { 216 | fos.write(chunkReceived); 217 | } 218 | } while (stopPos != fileSize); 219 | }else { 220 | downloadOtf(savePath, progress); 221 | } 222 | } 223 | 224 | private void downloadOtf(String savePath, Consumer progress) throws Exception { 225 | int countChunk = 0; 226 | byte[] chunkReceived; 227 | int lastChunk = 0; 228 | 229 | checkFile(savePath); 230 | do { 231 | String chunk = url + "&sq=" + countChunk; 232 | 233 | chunkReceived = Request.postChunk(chunk).toByteArray(); 234 | 235 | if(countChunk == 0){ 236 | Pattern pattern = Pattern.compile("Segment-Count: (\\d*)"); 237 | Matcher matcher = pattern.matcher(new String(chunkReceived)); 238 | if (matcher.find()){ 239 | lastChunk = Integer.parseInt(matcher.group(1)); 240 | }else{ 241 | throw new RegexMatchError("downloadOtf: " + pattern); 242 | } 243 | } 244 | progress.accept((countChunk * 100L) / (lastChunk)); 245 | countChunk = countChunk + 1; 246 | try (FileOutputStream fos = new FileOutputStream(savePath, true)) { 247 | fos.write(chunkReceived); 248 | } 249 | }while (countChunk <= lastChunk); 250 | } 251 | 252 | private Map getFormatProfile(){ 253 | Map> itags = new HashMap<>(); 254 | 255 | // progressive video 256 | itags.put(5, new ArrayList<>(Arrays.asList("240p", "64kbps"))); 257 | itags.put(6, new ArrayList<>(Arrays.asList("270p", "64kbps"))); 258 | itags.put(13, new ArrayList<>(Arrays.asList("144p", null))); 259 | itags.put(17, new ArrayList<>(Arrays.asList("144p", "24kbps"))); 260 | itags.put(18, new ArrayList<>(Arrays.asList("360p", "96kbps"))); 261 | itags.put(22, new ArrayList<>(Arrays.asList("720p", "192kbps"))); 262 | itags.put(34, new ArrayList<>(Arrays.asList("360p", "128kbps"))); 263 | itags.put(35, new ArrayList<>(Arrays.asList("480p", "128kbps"))); 264 | itags.put(36, new ArrayList<>(Arrays.asList("240p", null))); 265 | itags.put(37, new ArrayList<>(Arrays.asList("1080p", "192kbps"))); 266 | itags.put(38, new ArrayList<>(Arrays.asList("3072p", "192kbps"))); 267 | itags.put(43, new ArrayList<>(Arrays.asList("360p", "128kbps"))); 268 | itags.put(44, new ArrayList<>(Arrays.asList("480p", "128kbps"))); 269 | itags.put(45, new ArrayList<>(Arrays.asList("720p", "192kbps"))); 270 | itags.put(46, new ArrayList<>(Arrays.asList("1080p", "192kbps"))); 271 | itags.put(59, new ArrayList<>(Arrays.asList("480p", "128kbps"))); 272 | itags.put(78, new ArrayList<>(Arrays.asList("480p", "128kbps"))); 273 | itags.put(82, new ArrayList<>(Arrays.asList("360p", "128kbps"))); 274 | itags.put(83, new ArrayList<>(Arrays.asList("480p", "128kbps"))); 275 | itags.put(84, new ArrayList<>(Arrays.asList("720p", "192kbps"))); 276 | itags.put(85, new ArrayList<>(Arrays.asList("1080p", "192kbps"))); 277 | itags.put(91, new ArrayList<>(Arrays.asList("144p", "48kbps"))); 278 | itags.put(92, new ArrayList<>(Arrays.asList("240p", "48kbps"))); 279 | itags.put(93, new ArrayList<>(Arrays.asList("360p", "128kbps"))); 280 | itags.put(94, new ArrayList<>(Arrays.asList("480p", "128kbps"))); 281 | itags.put(95, new ArrayList<>(Arrays.asList("720p", "256kbps"))); 282 | itags.put(96, new ArrayList<>(Arrays.asList("1080p", "256kbps"))); 283 | itags.put(100, new ArrayList<>(Arrays.asList("360p", "128kbps"))); 284 | itags.put(101, new ArrayList<>(Arrays.asList("480p", "192kbps"))); 285 | itags.put(102, new ArrayList<>(Arrays.asList("720p", "192kbps"))); 286 | itags.put(132, new ArrayList<>(Arrays.asList("240p", "48kbps"))); 287 | itags.put(151, new ArrayList<>(Arrays.asList("720p", "24kbps"))); 288 | itags.put(300, new ArrayList<>(Arrays.asList("720p", "128kbps"))); 289 | itags.put(301, new ArrayList<>(Arrays.asList("1080p", "128kbps"))); 290 | 291 | // dash video 292 | itags.put(133, new ArrayList<>(Arrays.asList("240p", null))); // MP4 293 | itags.put(134, new ArrayList<>(Arrays.asList("360p", null))); // MP4 294 | itags.put(135, new ArrayList<>(Arrays.asList("480p", null))); // MP4 295 | itags.put(136, new ArrayList<>(Arrays.asList("720p", null))); // MP4 296 | itags.put(137, new ArrayList<>(Arrays.asList("1080p", null))); // MP4 297 | itags.put(138, new ArrayList<>(Arrays.asList("2160p", null))); // MP4 298 | itags.put(160, new ArrayList<>(Arrays.asList("144p", null))); // MP4 299 | itags.put(167, new ArrayList<>(Arrays.asList("360p", null))); // WEBM 300 | itags.put(168, new ArrayList<>(Arrays.asList("480p", null))); // WEBM 301 | itags.put(169, new ArrayList<>(Arrays.asList("720p", null))); // WEBM 302 | itags.put(170, new ArrayList<>(Arrays.asList("1080p", null))); // WEBM 303 | itags.put(212, new ArrayList<>(Arrays.asList("480p", null))); // MP4 304 | itags.put(218, new ArrayList<>(Arrays.asList("480p", null))); // WEBM 305 | itags.put(219, new ArrayList<>(Arrays.asList("480p", null))); // WEBM 306 | itags.put(242, new ArrayList<>(Arrays.asList("240p", null))); // WEBM 307 | itags.put(243, new ArrayList<>(Arrays.asList("360p", null))); // WEBM 308 | itags.put(244, new ArrayList<>(Arrays.asList("480p", null))); // WEBM 309 | itags.put(245, new ArrayList<>(Arrays.asList("480p", null))); // WEBM 310 | itags.put(246, new ArrayList<>(Arrays.asList("480p", null))); // WEBM 311 | itags.put(247, new ArrayList<>(Arrays.asList("720p", null))); // WEBM 312 | itags.put(248, new ArrayList<>(Arrays.asList("1080p", null))); // WEBM 313 | itags.put(264, new ArrayList<>(Arrays.asList("1440p", null))); // MP4 314 | itags.put(266, new ArrayList<>(Arrays.asList("2160p", null))); // MP4 315 | itags.put(271, new ArrayList<>(Arrays.asList("1440p", null))); // WEBM 316 | itags.put(272, new ArrayList<>(Arrays.asList("4320p", null))); // WEBM 317 | itags.put(278, new ArrayList<>(Arrays.asList("144p", null))); // WEBM 318 | itags.put(298, new ArrayList<>(Arrays.asList("720p", null))); // MP4 319 | itags.put(299, new ArrayList<>(Arrays.asList("1080p", null))); // MP4 320 | itags.put(302, new ArrayList<>(Arrays.asList("720p", null))); // WEBM 321 | itags.put(303, new ArrayList<>(Arrays.asList("1080p", null))); // WEBM 322 | itags.put(308, new ArrayList<>(Arrays.asList("1440p", null))); // WEBM 323 | itags.put(313, new ArrayList<>(Arrays.asList("2160p", null))); // WEBM 324 | itags.put(315, new ArrayList<>(Arrays.asList("2160p", null))); // WEBM 325 | itags.put(330, new ArrayList<>(Arrays.asList("144p", null))); // WEBM 326 | itags.put(331, new ArrayList<>(Arrays.asList("240p", null))); // WEBM 327 | itags.put(332, new ArrayList<>(Arrays.asList("360p", null))); // WEBM 328 | itags.put(333, new ArrayList<>(Arrays.asList("480p", null))); // WEBM 329 | itags.put(334, new ArrayList<>(Arrays.asList("720p", null))); // WEBM 330 | itags.put(335, new ArrayList<>(Arrays.asList("1080p", null))); // WEBM 331 | itags.put(336, new ArrayList<>(Arrays.asList("1440p", null))); // WEBM 332 | itags.put(337, new ArrayList<>(Arrays.asList("2160p", null))); // WEBM 333 | itags.put(394, new ArrayList<>(Arrays.asList("144p", null))); // MP4 334 | itags.put(395, new ArrayList<>(Arrays.asList("240p", null))); // MP4 335 | itags.put(396, new ArrayList<>(Arrays.asList("360p", null))); // MP4 336 | itags.put(397, new ArrayList<>(Arrays.asList("480p", null))); // MP4 337 | itags.put(398, new ArrayList<>(Arrays.asList("720p", null))); // MP4 338 | itags.put(399, new ArrayList<>(Arrays.asList("1080p", null))); // MP4 339 | itags.put(400, new ArrayList<>(Arrays.asList("1440p", null))); // MP4 340 | itags.put(401, new ArrayList<>(Arrays.asList("2160p", null))); // MP4 341 | itags.put(402, new ArrayList<>(Arrays.asList("4320p", null))); // MP4 342 | itags.put(571, new ArrayList<>(Arrays.asList("4320p", null))); // MP4 343 | itags.put(597, new ArrayList<>(Arrays.asList(null, null))); // MP4 344 | itags.put(598, new ArrayList<>(Arrays.asList(null, null))); // WEBM 345 | itags.put(694, new ArrayList<>(Arrays.asList("144p", null))); // MP4 346 | itags.put(695, new ArrayList<>(Arrays.asList("240p", null))); // MP4 347 | itags.put(696, new ArrayList<>(Arrays.asList("360p", null))); // MP4 348 | itags.put(697, new ArrayList<>(Arrays.asList("480p", null))); // MP4 349 | itags.put(698, new ArrayList<>(Arrays.asList("720p", null))); // MP4 350 | itags.put(699, new ArrayList<>(Arrays.asList("1080p", null))); // MP4 351 | itags.put(700, new ArrayList<>(Arrays.asList("1440p", null))); // MP4 352 | itags.put(701, new ArrayList<>(Arrays.asList("2160p", null))); // MP4 353 | itags.put(702, new ArrayList<>(Arrays.asList("4320p", null))); // MP4 354 | 355 | // dash audio 356 | itags.put(139, new ArrayList<>(Arrays.asList(null, "48kbps"))); // MP4 357 | itags.put(140, new ArrayList<>(Arrays.asList(null, "128kbps"))); // MP4 358 | itags.put(141, new ArrayList<>(Arrays.asList(null, "256kbps"))); // MP4 359 | itags.put(171, new ArrayList<>(Arrays.asList(null, "128kbps"))); // WEBM 360 | itags.put(172, new ArrayList<>(Arrays.asList(null, "256kbps"))); // WEBM 361 | itags.put(249, new ArrayList<>(Arrays.asList(null, "50kbps"))); // WEBM 362 | itags.put(250, new ArrayList<>(Arrays.asList(null, "70kbps"))); // WEBM 363 | itags.put(251, new ArrayList<>(Arrays.asList(null, "160kbps"))); // WEBM 364 | itags.put(256, new ArrayList<>(Arrays.asList(null, "192kbps"))); // MP4 365 | itags.put(258, new ArrayList<>(Arrays.asList(null, "384kbps"))); // MP4 366 | itags.put(325, new ArrayList<>(Arrays.asList(null, null))); // MP4 367 | itags.put(328, new ArrayList<>(Arrays.asList(null, null))); // MP4 368 | itags.put(599, new ArrayList<>(Arrays.asList(null, null))); // MP4 369 | itags.put(600, new ArrayList<>(Arrays.asList(null, null))); // webm 370 | 371 | 372 | String res, bitrate; 373 | if(itags.containsKey(itag)){ 374 | res = itags.get(itag).get(0); 375 | bitrate = itags.get(itag).get(1); 376 | }else{ 377 | res = null; 378 | bitrate = null; 379 | } 380 | 381 | Map returnItags = new HashMap<>(); 382 | 383 | returnItags.put("resolution", res); 384 | returnItags.put("abr", bitrate); 385 | 386 | return returnItags; 387 | } 388 | public String getTitle(){ 389 | return title; 390 | } 391 | public String getUrl(){ 392 | return url; 393 | } 394 | public Integer getItag(){ 395 | return itag; 396 | } 397 | public String getMimeType(){ 398 | return mimeType; 399 | } 400 | public String getCodecs(){ 401 | return codecs; 402 | } 403 | public String getType(){ 404 | return type; 405 | } 406 | public String getSubType(){ 407 | return subType; 408 | } 409 | public String getVideoCodec(){ 410 | return videoCodec; 411 | } 412 | public String getAudioCodec(){ 413 | return audioCodec; 414 | } 415 | public Integer getBitrate(){ 416 | return bitrate; 417 | } 418 | public Boolean getIsOtf(){ 419 | return isOtf; 420 | } 421 | public long getFileSize(){ 422 | return fileSize; 423 | } 424 | public Map getItagProfile(){ 425 | return itagProfile; 426 | } 427 | public String getAbr(){ 428 | return abr; 429 | } 430 | public Integer getFps(){ 431 | return fps; 432 | } 433 | public String getResolution(){ 434 | return resolution; 435 | } 436 | public boolean isDefaultAudioTrack(){ 437 | return defaultAudioTrack; 438 | } 439 | public String getAudioTrackName(){ 440 | return audioTrackName; 441 | } 442 | public String getAudioTrackId(){ 443 | return audioTrackId; 444 | } 445 | 446 | } 447 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/StreamQuery.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import java.util.*; 4 | 5 | public class StreamQuery{ 6 | private final ArrayList fmtStreams; 7 | Map itagIndex = new HashMap<>(); 8 | public StreamQuery(ArrayList fmt_streams){ 9 | fmtStreams = fmt_streams; 10 | for (Stream fmt_stream : fmt_streams) { 11 | itagIndex.put(fmt_stream.getItag(), fmt_stream); 12 | } 13 | } 14 | @Override 15 | public String toString(){ 16 | try { 17 | return fmtStreams.toString(); 18 | } catch (Exception e) { 19 | throw new RuntimeException(e); 20 | } 21 | } 22 | 23 | public ArrayList getAll(){ 24 | return fmtStreams; 25 | } 26 | 27 | public Stream get(int index){ 28 | return fmtStreams.get(index); 29 | } 30 | 31 | public StreamQuery filter(HashMap filters){ 32 | 33 | ArrayList streamFilter = new ArrayList<>(); 34 | if(filters.containsKey("res")){ 35 | if(!streamFilter.isEmpty()){ 36 | streamFilter.retainAll(new ArrayList<>(getResolution(filters.get("res")))); 37 | }else{ 38 | streamFilter.addAll(getResolution(filters.get("res"))); 39 | } 40 | if(streamFilter.isEmpty()){ 41 | filters.clear(); 42 | } 43 | } 44 | 45 | if(filters.containsKey("fps")){ 46 | if(!streamFilter.isEmpty()){ 47 | streamFilter.retainAll(new ArrayList<>(getFps(filters.get("fps")))); 48 | }else{ 49 | streamFilter.addAll(getFps(filters.get("fps"))); 50 | } 51 | if(streamFilter.isEmpty()){ 52 | filters.clear(); 53 | } 54 | } 55 | 56 | if(filters.containsKey("mineType")){ 57 | if(!streamFilter.isEmpty()){ 58 | streamFilter.retainAll(new ArrayList<>(getMineType(filters.get("mineType")))); 59 | }else{ 60 | streamFilter.addAll(getMineType(filters.get("mineType"))); 61 | } 62 | if(streamFilter.isEmpty()){ 63 | filters.clear(); 64 | } 65 | } 66 | 67 | if(filters.containsKey("type")){ 68 | if(!streamFilter.isEmpty()){ 69 | streamFilter.retainAll(new ArrayList<>(getType(filters.get("type")))); 70 | }else{ 71 | streamFilter.addAll(getType(filters.get("type"))); 72 | } 73 | if(streamFilter.isEmpty()){ 74 | filters.clear(); 75 | } 76 | } 77 | 78 | if(filters.containsKey("subType")){ 79 | if(!streamFilter.isEmpty()){ 80 | streamFilter.retainAll(new ArrayList<>(getSubtype(filters.get("subType")))); 81 | }else{ 82 | streamFilter.addAll(getSubtype(filters.get("subType"))); 83 | } 84 | if(streamFilter.isEmpty()){ 85 | filters.clear(); 86 | } 87 | } 88 | 89 | if(filters.containsKey("abr")){ 90 | if(!streamFilter.isEmpty()){ 91 | streamFilter.retainAll(new ArrayList<>(getAbr(filters.get("abr")))); 92 | }else{ 93 | streamFilter.addAll(getAbr(filters.get("abr"))); 94 | } 95 | if(streamFilter.isEmpty()){ 96 | filters.clear(); 97 | } 98 | 99 | } 100 | 101 | if(filters.containsKey("videoCodec")){ 102 | if(!streamFilter.isEmpty()){ 103 | streamFilter.retainAll(new ArrayList<>(getVideoCodec(filters.get("videoCodec")))); 104 | }else{ 105 | streamFilter.addAll(getVideoCodec(filters.get("videoCodec"))); 106 | } 107 | if(streamFilter.isEmpty()){ 108 | filters.clear(); 109 | } 110 | } 111 | if(filters.containsKey("audioCodec")){ 112 | if(!streamFilter.isEmpty()){ 113 | streamFilter.retainAll(new ArrayList<>(getAudioCodec(filters.get("audioCodec")))); 114 | }else{ 115 | streamFilter.addAll(getAudioCodec(filters.get("audioCodec"))); 116 | } 117 | if(streamFilter.isEmpty()){ 118 | filters.clear(); 119 | } 120 | } 121 | if(filters.containsKey("onlyAudio")){ 122 | if(Objects.equals(filters.get("onlyAudio"), "true")){ 123 | if(!streamFilter.isEmpty()){ 124 | streamFilter.retainAll(new ArrayList<>(onlyAudio())); 125 | }else{ 126 | streamFilter.addAll(onlyAudio()); 127 | } 128 | if(streamFilter.isEmpty()){ 129 | filters.clear(); 130 | } 131 | } 132 | } 133 | if(filters.containsKey("onlyVideo")){ 134 | if(Objects.equals(filters.get("onlyVideo"), "true")){ 135 | if(!streamFilter.isEmpty()){ 136 | streamFilter.retainAll(new ArrayList<>(onlyVideo())); 137 | }else{ 138 | streamFilter.addAll(onlyVideo()); 139 | } 140 | if(streamFilter.isEmpty()){ 141 | filters.clear(); 142 | } 143 | } 144 | } 145 | if(filters.containsKey("progressive")){ 146 | if(Objects.equals(filters.get("progressive"), "true")){ 147 | if(!streamFilter.isEmpty()){ 148 | streamFilter.retainAll(new ArrayList<>(progressive())); 149 | }else{ 150 | streamFilter.addAll(progressive()); 151 | } 152 | if(streamFilter.isEmpty()){ 153 | filters.clear(); 154 | } 155 | }else if (Objects.equals(filters.get("progressive"), "false")){ 156 | if(!streamFilter.isEmpty()){ 157 | streamFilter.retainAll(new ArrayList<>(adaptive())); 158 | }else{ 159 | streamFilter.addAll(adaptive()); 160 | } 161 | if(streamFilter.isEmpty()){ 162 | filters.clear(); 163 | } 164 | } 165 | } 166 | if(filters.containsKey("adaptive")){ 167 | if(Objects.equals(filters.get("adaptive"), "true")){ 168 | if(!streamFilter.isEmpty()){ 169 | streamFilter.retainAll(new ArrayList<>(adaptive())); 170 | }else{ 171 | streamFilter.addAll(adaptive()); 172 | } 173 | if(streamFilter.isEmpty()){ 174 | filters.clear(); 175 | } 176 | } else if (Objects.equals(filters.get("adaptive"), "false")) { 177 | if(!streamFilter.isEmpty()){ 178 | streamFilter.retainAll(new ArrayList<>(progressive())); 179 | }else{ 180 | streamFilter.addAll(progressive()); 181 | } 182 | if(streamFilter.isEmpty()){ 183 | filters.clear(); 184 | } 185 | } 186 | } 187 | 188 | return new StreamQuery(streamFilter); 189 | } 190 | 191 | private ArrayList getResolution(String re){ 192 | ArrayList filter = new ArrayList<>(); 193 | for(Stream st : fmtStreams){ 194 | if(Objects.equals(st.getResolution(), re)){ 195 | filter.add(st); 196 | } 197 | } 198 | return filter; 199 | } 200 | 201 | private ArrayList getFps(String fps){ 202 | ArrayList filter = new ArrayList<>(); 203 | for(Stream st : fmtStreams){ 204 | if(Objects.equals(st.getFps(), Integer.parseInt(fps))){ 205 | filter.add(st); 206 | } 207 | } 208 | return filter; 209 | } 210 | 211 | private ArrayList getMineType(String mineType){ 212 | ArrayList filter = new ArrayList<>(); 213 | for(Stream st : fmtStreams){ 214 | if(Objects.equals(st.getMimeType(), mineType)){ 215 | filter.add(st); 216 | } 217 | } 218 | return filter; 219 | } 220 | 221 | private ArrayList getType(String type){ 222 | ArrayList filter = new ArrayList<>(); 223 | for(Stream st : fmtStreams){ 224 | if(Objects.equals(st.getType(), type)){ 225 | filter.add(st); 226 | } 227 | } 228 | return filter; 229 | } 230 | 231 | private ArrayList getSubtype(String subtype){ 232 | ArrayList filter = new ArrayList<>(); 233 | for(Stream st : fmtStreams){ 234 | if(Objects.equals(st.getSubType(), subtype)){ 235 | filter.add(st); 236 | } 237 | } 238 | return filter; 239 | } 240 | 241 | private ArrayList getAbr(String abr){ 242 | ArrayList filter = new ArrayList<>(); 243 | for(Stream st : fmtStreams){ 244 | if(Objects.equals(st.getAbr(), abr)){ 245 | filter.add(st); 246 | } 247 | } 248 | return filter; 249 | } 250 | 251 | private ArrayList getVideoCodec(String videoCodec){ 252 | ArrayList filter = new ArrayList<>(); 253 | for(Stream st : fmtStreams){ 254 | if(Objects.equals(st.getVideoCodec(), videoCodec)){ 255 | filter.add(st); 256 | } 257 | } 258 | return filter; 259 | } 260 | 261 | private ArrayList getAudioCodec(String audioCodec){ 262 | ArrayList filter = new ArrayList<>(); 263 | for(Stream st : fmtStreams){ 264 | if(Objects.equals(st.getAudioCodec(), audioCodec)){ 265 | filter.add(st); 266 | } 267 | } 268 | return filter; 269 | } 270 | 271 | private ArrayList onlyAudio(){ 272 | ArrayList filter = new ArrayList<>(); 273 | for(Stream st : fmtStreams){ 274 | if((st.includeAudioTrack()) && (!st.includeVideoTrack())){ 275 | filter.add(st); 276 | } 277 | } 278 | return filter; 279 | } 280 | 281 | private ArrayList onlyVideo(){ 282 | ArrayList filter = new ArrayList<>(); 283 | for(Stream st : fmtStreams){ 284 | if((st.includeVideoTrack() && (!st.includeAudioTrack()))){ 285 | filter.add(st); 286 | } 287 | } 288 | return filter; 289 | } 290 | 291 | private ArrayList progressive(){ 292 | ArrayList filter = new ArrayList<>(); 293 | for(Stream st : fmtStreams){ 294 | if(st.isProgressive()){ 295 | filter.add(st); 296 | } 297 | } 298 | return filter; 299 | } 300 | 301 | private ArrayList adaptive(){ 302 | ArrayList filter = new ArrayList<>(); 303 | for(Stream st : fmtStreams){ 304 | if(st.isAdaptive()){ 305 | filter.add(st); 306 | } 307 | } 308 | return filter; 309 | } 310 | 311 | private ArrayList reverseArrayList(ArrayList aList) { 312 | ArrayList revArrayList = new ArrayList<>(); 313 | for (int i = aList.size() - 1; i >= 0; i--) { 314 | revArrayList.add(aList.get(i)); 315 | } 316 | return revArrayList; 317 | } 318 | 319 | private static ArrayList sortByValue(HashMap hm) { 320 | List> list = new LinkedList<>(hm.entrySet()); 321 | list.sort(Map.Entry.comparingByValue()); 322 | ArrayList ordered = new ArrayList<>(); 323 | for (Map.Entry aa : list) { 324 | ordered.add(aa.getKey()); 325 | } 326 | return ordered; 327 | } 328 | 329 | public StreamQuery orderBy(String by) throws Exception { 330 | HashMap map = new HashMap<>(); 331 | for(Stream s : fmtStreams){ 332 | if(Objects.equals(by, "res")){ 333 | if(s.getResolution() != null){ 334 | map.put(s, Integer.parseInt(s.getResolution().replace("p", ""))); 335 | } 336 | } else if (Objects.equals(by, "abr")) { 337 | if(s.getAbr() != null){ 338 | map.put(s, Integer.parseInt(s.getAbr().replace("kbps", ""))); 339 | } 340 | } else if (Objects.equals(by, "fps")) { 341 | if(s.getFps() != null){ 342 | map.put(s, s.getFps()); 343 | } 344 | }else{ 345 | throw new Exception("InvalidParameter"); 346 | } 347 | } 348 | return new StreamQuery(sortByValue(map)); 349 | } 350 | 351 | public StreamQuery getOtf(Boolean otf){ 352 | ArrayList filter = new ArrayList<>(); 353 | for(Stream s : fmtStreams){ 354 | if(otf){ 355 | if(s.getIsOtf()){ 356 | filter.add(s); 357 | } 358 | }else { 359 | if(!s.getIsOtf()){ 360 | filter.add(s); 361 | } 362 | } 363 | } 364 | return new StreamQuery(filter); 365 | } 366 | 367 | public StreamQuery getDesc(){ 368 | return new StreamQuery(reverseArrayList(fmtStreams)); 369 | } 370 | 371 | public StreamQuery getAsc(){ 372 | return new StreamQuery(fmtStreams); 373 | } 374 | 375 | public Stream getFirst(){ 376 | return fmtStreams.get(0); 377 | } 378 | 379 | public Stream getLast(){ 380 | return fmtStreams.get(fmtStreams.size() - 1); 381 | } 382 | 383 | public StreamQuery getProgressive(){ 384 | return new StreamQuery(progressive()); 385 | } 386 | 387 | public StreamQuery getAdaptive(){ 388 | return new StreamQuery(adaptive()); 389 | } 390 | 391 | public StreamQuery getDefaultAudioTracks(){ 392 | ArrayList filter = new ArrayList<>(); 393 | for(Stream st : fmtStreams){ 394 | if(st.isDefaultAudioTrack() ){ 395 | filter.add(st); 396 | } 397 | } 398 | return new StreamQuery(filter); 399 | } 400 | 401 | public StreamQuery getExtraAudioTracks(){ 402 | ArrayList filter = new ArrayList<>(); 403 | for(Stream st : fmtStreams){ 404 | if(!st.isDefaultAudioTrack() && st.includeAudioTrack() && !st.includeVideoTrack()){ 405 | filter.add(st); 406 | } 407 | } 408 | return new StreamQuery(filter); 409 | } 410 | 411 | public StreamQuery getExtraAudioTracksById(String id){ 412 | ArrayList filter = new ArrayList<>(); 413 | for(Stream st : fmtStreams){ 414 | if(st.includesMultipleAudioTracks()) { 415 | if (Objects.equals(st.getAudioTrackId(), id)) { 416 | filter.add(st); 417 | } 418 | } 419 | } 420 | return new StreamQuery(filter); 421 | } 422 | 423 | public StreamQuery getExtraAudioTracksByName(String name){ 424 | ArrayList filter = new ArrayList<>(); 425 | for(Stream st : fmtStreams){ 426 | if(st.includesMultipleAudioTracks()){ 427 | if(Objects.equals(st.getAudioTrackName(), name)){ 428 | filter.add(st); 429 | } 430 | } 431 | } 432 | return new StreamQuery(filter); 433 | } 434 | 435 | public Stream getOnlyAudio(){ 436 | HashMap filters = new HashMap<>(); 437 | filters.put("onlyAudio", "true"); 438 | filters.put("subType", "mp4"); 439 | return filter(filters).getLast(); 440 | } 441 | 442 | public Stream getLowestResolution(){ 443 | HashMap filters = new HashMap<>(); 444 | filters.put("progressive", "true"); 445 | filters.put("subType", "mp4"); 446 | return filter(filters).getFirst(); 447 | } 448 | 449 | public Stream getHighestResolution(){ 450 | HashMap filters = new HashMap<>(); 451 | filters.put("progressive", "true"); 452 | filters.put("subType", "mp4"); 453 | return filter(filters).getLast(); 454 | } 455 | 456 | } 457 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/Youtube.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | import java.io.IOException; 4 | import java.io.UnsupportedEncodingException; 5 | import java.net.URLDecoder; 6 | import java.nio.charset.StandardCharsets; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.Paths; 10 | import java.util.*; 11 | import java.util.regex.*; 12 | 13 | import com.github.felipeucelli.javatube.exceptions.*; 14 | import org.json.*; 15 | 16 | 17 | public class Youtube { 18 | 19 | private final String urlVideo; 20 | private final String watchUrl; 21 | private String videoId = null; 22 | private InnerTube innerTube = null; 23 | private String client = null; 24 | private JSONObject vidInfo = null; 25 | private String html = null; 26 | private JSONObject initialData = null; 27 | private String js = null; 28 | private JSONObject ytCfg = null; 29 | private JSONObject signatureTimestamp = null; 30 | private String visitorData = null; 31 | private String poToken = null; 32 | private String playerJs = null; 33 | private final boolean usePoToken; 34 | private final boolean allowCache; 35 | 36 | /** 37 | * Default client: ANDROID_VR 38 | * */ 39 | public Youtube(String url) throws Exception { 40 | this(url, "ANDROID_VR", false, false); 41 | } 42 | /** 43 | * @Clients: 44 | * WEB, 45 | * WEB_EMBED, 46 | * WEB_MUSIC, 47 | * WEB_CREATOR, 48 | * WEB_SAFARI, 49 | * MWEB, 50 | * ANDROID, 51 | * ANDROID_VR, 52 | * ANDROID_MUSIC, 53 | * ANDROID_CREATOR, 54 | * ANDROID_TESTSUITE, 55 | * ANDROID_PRODUCER, 56 | * IOS, 57 | * IOS_MUSIC, 58 | * IOS_CREATOR, 59 | * TV_EMBED, 60 | * MEDIA_CONNECT 61 | * */ 62 | public Youtube(String url, String clientName) throws Exception { 63 | this(url, clientName, false, false); 64 | } 65 | /** 66 | * Default client: WEB 67 | * */ 68 | public Youtube(String url, boolean usePoToken) throws Exception { 69 | this(url, "ANDROID_VR", usePoToken, false); 70 | } 71 | /** 72 | * Default client: WEB 73 | * */ 74 | public Youtube(String url, boolean usePoToken, boolean allowCache) throws Exception { 75 | this(url, "ANDROID_VR", usePoToken, allowCache); 76 | } 77 | /** 78 | * @Clients: 79 | * WEB, 80 | * WEB_EMBED, 81 | * WEB_MUSIC, 82 | * WEB_CREATOR, 83 | * WEB_SAFARI, 84 | * MWEB, 85 | * ANDROID, 86 | * ANDROID_VR, 87 | * ANDROID_MUSIC, 88 | * ANDROID_CREATOR, 89 | * ANDROID_TESTSUITE, 90 | * ANDROID_PRODUCER, 91 | * IOS, 92 | * IOS_MUSIC, 93 | * IOS_CREATOR, 94 | * TV_EMBED, 95 | * MEDIA_CONNECT 96 | * */ 97 | public Youtube(String url, String clientName, boolean usePoToken, boolean allowCache) throws Exception { 98 | client = usePoToken ? "WEB" : clientName; 99 | this.usePoToken = usePoToken; 100 | this.allowCache = allowCache; 101 | innerTube = new InnerTube(client, usePoToken, allowCache); 102 | urlVideo = url; 103 | watchUrl = "https://www.youtube.com/watch?v=" + getVideoId(); 104 | } 105 | 106 | private String setVideoId() throws RegexMatchError { 107 | Pattern pattern = Pattern.compile("(?:v=|/)([0-9A-Za-z_-]{11}).*"); 108 | Matcher matcher = pattern.matcher(urlVideo); 109 | if (matcher.find()) { 110 | return matcher.group(1); 111 | }else { 112 | throw new RegexMatchError("videoId: could not find match for " + pattern); 113 | } 114 | } 115 | 116 | private String getVideoId() throws Exception { 117 | if (videoId == null){ 118 | videoId = setVideoId(); 119 | } 120 | return videoId; 121 | } 122 | 123 | @Override 124 | public String toString(){ 125 | try { 126 | return ""; 127 | } catch (Exception e) { 128 | throw new RuntimeException(e); 129 | } 130 | } 131 | 132 | public static void resetCache(){ 133 | try { 134 | String tempDir = System.getProperty("java.io.tmpdir"); 135 | Path path = Paths.get(tempDir, "tokens.json"); 136 | 137 | if (Files.exists(path)) { 138 | Files.delete(path); 139 | } 140 | } catch (IOException e) { 141 | e.printStackTrace(); 142 | } 143 | } 144 | 145 | private String setHtml() throws Exception { 146 | Map headers = !client.contains("WEB") ? null : innerTube.getClientHeaders(); 147 | return Request.get(watchUrl, headers).toString(StandardCharsets.UTF_8.name()).replace("\n", ""); 148 | } 149 | 150 | public String getHtml() throws Exception { 151 | if(html == null){ 152 | html = setHtml(); 153 | } 154 | return html; 155 | } 156 | 157 | private String setSignatureTimestamp() throws Exception { 158 | String pattern = "signatureTimestamp:(\\d*)"; 159 | Pattern regex = Pattern.compile(pattern); 160 | Matcher matcher = regex.matcher(getJs()); 161 | if(matcher.find()){ 162 | return matcher.group(1); 163 | }else { 164 | throw new RegexMatchError("setSignatureTimestamp: Unable to find signatureTimestamp in playerJs: " + getYtPlayerJs()); 165 | } 166 | } 167 | public JSONObject getSignatureTimestamp() throws Exception { 168 | if(signatureTimestamp == null){ 169 | signatureTimestamp = new JSONObject( 170 | "{" + 171 | "\"playbackContext\": {" + 172 | "\"contentPlaybackContext\": {" + 173 | "\"signatureTimestamp\": " + setSignatureTimestamp() + 174 | "}" + 175 | "}" + 176 | "}" 177 | ); 178 | } 179 | return signatureTimestamp; 180 | } 181 | 182 | private JSONObject setYtCfg() throws Exception { 183 | Pattern pattern = Pattern.compile("window\\.ytplayer=\\{};ytcfg\\.set\\((\\{.*?\\})\\);"); 184 | Matcher matcher = pattern.matcher(getHtml()); 185 | if(matcher.find()){ 186 | return new JSONObject(matcher.group(1)); 187 | }else { 188 | throw new RegexMatchError("setYtCfg: Could not find ytCfg: " + pattern); 189 | } 190 | } 191 | 192 | public JSONObject getYtCfg() throws Exception { 193 | if(ytCfg == null){ 194 | ytCfg = setYtCfg(); 195 | } 196 | return ytCfg; 197 | } 198 | 199 | private String setVisitorData() throws Exception { 200 | InnerTube inner_tube = new InnerTube(client); 201 | if(inner_tube.getRequirePoToken()){ 202 | String[] functionPatterns = { 203 | "\\{\"key\":\"visitor_data\",\"value\":\"([a-zA-Z0-9%-_]+)\"\\}", 204 | "\\{\"value\":\"([a-zA-Z0-9%-_]+)\",\"key\":\"visitor_data\"\\}" 205 | 206 | }; 207 | for (String pattern : functionPatterns){ 208 | Pattern regex = Pattern.compile(pattern); 209 | Matcher matcher = regex.matcher(getInitialData().getJSONObject("responseContext").toString()); 210 | if(matcher.find()){ 211 | return matcher.group(1); 212 | } 213 | } 214 | 215 | }else { 216 | JSONObject innerTubeResponse = inner_tube.player(getVideoId()); 217 | try { 218 | return innerTubeResponse.getJSONObject("responseContext").getString("visitorData"); 219 | }catch (JSONException e){ 220 | return innerTubeResponse.getJSONObject("responseContext").getJSONArray("serviceTrackingParams").getJSONObject(0).getJSONArray("params").getJSONObject(6).getString("value"); 221 | } 222 | } 223 | throw new RegexMatchError("setVisitorData: Unable to find setVisitorData"); 224 | } 225 | 226 | public String getVisitorData() throws Exception { 227 | if(visitorData == null){ 228 | visitorData = setVisitorData(); 229 | } 230 | 231 | return visitorData; 232 | } 233 | 234 | private JSONObject setInitialData() throws Exception { 235 | String pattern = "ytInitialPlayerResponse\\s=\\s(\\{\"responseContext\":.*?\\});(?:var|)"; 236 | Pattern regex = Pattern.compile(pattern); 237 | Matcher matcher = regex.matcher(getHtml()); 238 | if(matcher.find()){ 239 | return new JSONObject(matcher.group(1)); 240 | }else { 241 | throw new RegexMatchError("setInitialData: Unable to find InitialData: " + pattern); 242 | } 243 | } 244 | 245 | public JSONObject getInitialData() throws Exception { 246 | if(initialData == null){ 247 | initialData = setInitialData(); 248 | } 249 | return initialData; 250 | } 251 | 252 | private String setYtPlayerJs() throws Exception { 253 | Pattern pattern = Pattern.compile("(/s/player/[\\w\\d]+/[\\w\\d_/.\\-]+/base\\.js)"); 254 | Matcher matcher = pattern.matcher(getHtml()); 255 | if (matcher.find()) { 256 | return "https://youtube.com" + matcher.group(1); 257 | }else { 258 | throw new RegexMatchError("setYtPlayerJs: Could not find playerJs: " + pattern); 259 | } 260 | } 261 | public String getYtPlayerJs() throws Exception { 262 | if(playerJs == null){ 263 | playerJs = setYtPlayerJs(); 264 | } 265 | return playerJs; 266 | } 267 | 268 | private String setJs() throws Exception { 269 | return Request.get(getYtPlayerJs()).toString().replace("\n", ""); 270 | } 271 | public String getJs() throws Exception { 272 | if(js == null){ 273 | js = setJs(); 274 | } 275 | return js; 276 | } 277 | 278 | public String getUrl(){ 279 | return watchUrl; 280 | } 281 | 282 | private JSONObject callInnerTube() throws Exception { 283 | if (innerTube.getRequireJsPlayer()) { 284 | innerTube.updateInnerTubeContext(innerTube.getInnerTubeContext(), getSignatureTimestamp()); 285 | } 286 | if(!usePoToken && !innerTube.getRequirePoToken()){ 287 | innerTube.insertVisitorData(getVisitorData()); 288 | } 289 | 290 | return innerTube.player(getVideoId()); 291 | } 292 | 293 | private JSONObject getVidInfo() throws Exception { 294 | List fallbackClients = Arrays.asList("IOS", "WEB"); 295 | 296 | if (vidInfo != null) { 297 | return vidInfo; 298 | } 299 | JSONObject innerTubeResponse = callInnerTube(); 300 | for(String client : fallbackClients) { 301 | 302 | JSONObject playabilityStatus = innerTubeResponse.getJSONObject("playabilityStatus"); 303 | 304 | if (Objects.equals(playabilityStatus.getString("status"), "UNPLAYABLE")) { 305 | if (playabilityStatus.has("reason") && Objects.equals(playabilityStatus.getString("reason"), "This video is not available")) { 306 | innerTube = new InnerTube(client, usePoToken, allowCache); 307 | innerTubeResponse = callInnerTube(); 308 | } 309 | }else{ 310 | break; 311 | } 312 | } 313 | vidInfo = innerTubeResponse; 314 | 315 | return vidInfo; 316 | } 317 | 318 | 319 | private List extractAvailability(JSONObject playabilityStatus) throws JSONException { 320 | String status = ""; 321 | String reason = ""; 322 | 323 | if (playabilityStatus.has("status")){ 324 | status = playabilityStatus.getString("status"); 325 | 326 | if (playabilityStatus.has("reason")){ 327 | reason = playabilityStatus.getString("reason"); 328 | 329 | } else if (playabilityStatus.has("messages")){ 330 | reason = playabilityStatus.getJSONArray("messages").getString(0); 331 | } 332 | } 333 | 334 | return Arrays.asList(status, reason); 335 | } 336 | 337 | void checkAvailability() throws Exception { 338 | JSONObject playabilityStatus = getVidInfo().getJSONObject("playabilityStatus"); 339 | 340 | List availability = extractAvailability(playabilityStatus); 341 | 342 | String status = availability.get(0); 343 | String reason = availability.get(1); 344 | 345 | if (playabilityStatus.has("status")){ 346 | status = playabilityStatus.getString("status"); 347 | 348 | if (playabilityStatus.has("reason")){ 349 | reason = playabilityStatus.getString("reason"); 350 | 351 | } else if (playabilityStatus.has("messages")){ 352 | reason = playabilityStatus.getJSONArray("messages").getString(0); 353 | } 354 | } 355 | 356 | switch (status) { 357 | case "UNPLAYABLE" -> { 358 | if (reason.equals("Join this channel to get access to members-only content like this video, and other exclusive perks.")) { 359 | throw new MembersOnlyError(getVideoId()); 360 | 361 | } else if (reason.equals("This live stream recording is not available.")){ 362 | throw new RecordingUnavailableError(getVideoId()); 363 | 364 | } else if(reason.equals("The uploader has not made this video available in your country")){ 365 | throw new VideoRegionBlockedError(getVideoId()); 366 | 367 | } else { 368 | throw new VideoUnavailableError(getVideoId()); 369 | } 370 | } 371 | case "LOGIN_REQUIRED" -> { 372 | if (reason.equals("Sign in to confirm your age") || reason.equals("This video may be inappropriate for some users.")) { 373 | throw new AgeRestrictedError(getVideoId()); 374 | 375 | } else if (reason.equals("Sign in to confirm you’re not a bot")){ 376 | throw new BotDetectionError(getVideoId()); 377 | 378 | }else { 379 | throw new VideoPrivateError(getVideoId()); 380 | } 381 | } 382 | 383 | case "LIVE_STREAM_OFFLINE" -> throw new LiveStreamOffline(getVideoId(), reason); 384 | 385 | case "ERROR" -> { 386 | if (reason.equals("Video unavailable")) { 387 | throw new VideoUnavailableError(getVideoId()); 388 | 389 | }else if(reason.equals("This video is private")){ 390 | throw new VideoPrivateError(getVideoId()); 391 | 392 | }else if (reason.equals("This video is unavailable")){ 393 | throw new VideoUnavailableError(getVideoId()); 394 | 395 | }else if (reason.equals("This video has been removed by the uploader")){ 396 | throw new VideoUnavailableError(getVideoId()); 397 | 398 | }else if (reason.equals("This video is no longer available because the YouTube account associated with this video has been terminated.")){ 399 | throw new VideoUnavailableError(getVideoId()); 400 | 401 | }else { 402 | throw new UnknownVideoError(getVideoId(), status, reason); 403 | } 404 | } 405 | } 406 | if (getVidInfo().getJSONObject("videoDetails").has("isLive")){ 407 | throw new LiveStreamError(getVideoId()); 408 | } 409 | } 410 | 411 | JSONObject streamData() throws Exception { 412 | checkAvailability(); 413 | return getVidInfo().getJSONObject("streamingData"); 414 | } 415 | 416 | private String decodeURL(String s) throws UnsupportedEncodingException { 417 | return URLDecoder.decode(s, StandardCharsets.UTF_8.name()); 418 | } 419 | 420 | private static JSONArray applyDescrambler(JSONObject streamData) throws JSONException{ 421 | JSONArray formats = new JSONArray(); 422 | if(streamData.has("formats")){ 423 | for(int i = 0; i < streamData.getJSONArray("formats").length(); i ++){ 424 | formats.put(streamData.getJSONArray("formats").get(i)); 425 | } 426 | } 427 | if(streamData.has("adaptiveFormats")){ 428 | for(int i = 0; i < streamData.getJSONArray("adaptiveFormats").length(); i ++){ 429 | formats.put(streamData.getJSONArray("adaptiveFormats").get(i)); 430 | } 431 | } 432 | for(int i = 0; i < formats.length(); i++){ 433 | if(formats.getJSONObject(i).has("signatureCipher")){ 434 | String rawSig = formats.getJSONObject(i).getString("signatureCipher").replace("sp=sig", ""); 435 | for(int j = 0; j < rawSig.split("&").length; j++){ 436 | if(Arrays.asList(rawSig.split("&")).get(j).startsWith("url")){ 437 | formats.getJSONObject(i).put("url", Arrays.asList(rawSig.split("&")).get(j).replace("url=", "")); 438 | }else if(Arrays.asList(rawSig.split("&")).get(j).startsWith("s")){ 439 | formats.getJSONObject(i).put("s", Arrays.asList(rawSig.split("&")).get(j).replace("s=", "")); 440 | } 441 | } 442 | } 443 | } 444 | return formats; 445 | } 446 | 447 | private ArrayList fmtStreams() throws Exception { 448 | 449 | JSONArray streamManifest = applyDescrambler(streamData()); 450 | 451 | ArrayList fmtStream = new ArrayList<>(); 452 | String title = getTitle(); 453 | Stream video; 454 | 455 | if(innerTube == null || innerTube.getRequireJsPlayer()){ 456 | applySignature(streamManifest); 457 | } 458 | for (int i = 0; streamManifest.length() > i; i++) { 459 | video = new Stream(streamManifest.getJSONObject(i), title); 460 | fmtStream.add(video); 461 | } 462 | return fmtStream; 463 | } 464 | 465 | private void applySignature(JSONArray streamManifest) throws Exception { 466 | Cipher cipher = new Cipher(getJs(), getYtPlayerJs()); 467 | Pattern nSigPattern = Pattern.compile("&n=(.*?)&"); 468 | Map discoveredNSig = new HashMap<>(); 469 | for (int i = 0; streamManifest.length() > i; i++) { 470 | if (streamManifest.getJSONObject(i).has("signatureCipher")) { 471 | String oldUrl = decodeURL(streamManifest.getJSONObject(i).getString("url")); 472 | streamManifest.getJSONObject(i).remove("url"); 473 | String sig = streamManifest.getJSONObject(i).getString("s"); 474 | streamManifest.getJSONObject(i).put("url", oldUrl + "&sig=" + cipher.getSignature(decodeURL(sig))); 475 | } 476 | 477 | String oldUrl = streamManifest.getJSONObject(i).getString("url"); 478 | Matcher matcher = nSigPattern.matcher(oldUrl); 479 | if (matcher.find()) { 480 | String nSig = matcher.group(1); 481 | if(!discoveredNSig.containsKey(nSig)){ 482 | discoveredNSig.put(nSig, cipher.getNSig(nSig)); 483 | } 484 | String newUrl = oldUrl.replaceFirst("&n=(.*?)&", "&n=" + discoveredNSig.get(nSig) + "&"); 485 | streamManifest.getJSONObject(i).put("url", newUrl); 486 | } 487 | if(usePoToken){ 488 | oldUrl = streamManifest.getJSONObject(i).getString("url"); 489 | String newUrl = oldUrl + "&pot=" + innerTube.getPoToken(); 490 | streamManifest.getJSONObject(i).put("url", newUrl); 491 | } 492 | } 493 | } 494 | 495 | public String getTitle() throws Exception { 496 | return getVidInfo().getJSONObject("videoDetails") 497 | .getString("title"); 498 | } 499 | 500 | public String getDescription() throws Exception { 501 | return getVidInfo().getJSONObject("videoDetails") 502 | .getString("shortDescription"); 503 | } 504 | 505 | public String getPublishDate() throws Exception { 506 | Pattern pattern = Pattern.compile("(?<=itemprop=\"datePublished\" content=\")\\d{4}-\\d{2}-\\d{2}"); 507 | Matcher matcher = pattern.matcher(getHtml()); 508 | if (matcher.find()) { 509 | return matcher.group(0); 510 | }else { 511 | throw new RegexMatchError("getPublishDate: Unable to find publication date: " + pattern); 512 | } 513 | } 514 | 515 | public Integer length() throws Exception { 516 | return getVidInfo().getJSONObject("videoDetails") 517 | .getInt("lengthSeconds"); 518 | } 519 | 520 | public String getThumbnailUrl() throws Exception { 521 | JSONArray thumbnails = new InnerTube("WEB").player(getVideoId()).getJSONObject("videoDetails") 522 | .getJSONObject("thumbnail") 523 | .getJSONArray("thumbnails"); 524 | return thumbnails.getJSONObject(thumbnails.length() - 1).getString("url"); 525 | } 526 | 527 | public Long getViews() throws Exception { 528 | return Long.parseLong(getVidInfo().getJSONObject("videoDetails") 529 | .getString("viewCount")); 530 | } 531 | 532 | public String getAuthor() throws Exception { 533 | return getVidInfo().getJSONObject("videoDetails") 534 | .getString("author"); 535 | } 536 | 537 | public ArrayList getCaptionTracks() throws Exception { 538 | try{ 539 | JSONArray rawTracks = new InnerTube("WEB").player(getVideoId()).getJSONObject("captions") 540 | .getJSONObject("playerCaptionsTracklistRenderer") 541 | .getJSONArray("captionTracks"); 542 | ArrayList captions = new ArrayList<>(); 543 | for(int i = 0; i < rawTracks.length(); i++){ 544 | captions.add(new Captions(rawTracks.getJSONObject(i))); 545 | } 546 | return captions; 547 | } catch (JSONException e) { 548 | return null; 549 | } 550 | } 551 | 552 | public CaptionQuery getCaptions() throws Exception { 553 | return new CaptionQuery(getCaptionTracks()); 554 | } 555 | 556 | public JSONArray getKeywords() throws Exception { 557 | try { 558 | return getVidInfo().getJSONObject("videoDetails") 559 | .getJSONArray("keywords"); 560 | }catch (JSONException e){ 561 | return null; 562 | } 563 | } 564 | 565 | public StreamQuery streams() throws Exception { 566 | return new StreamQuery(fmtStreams()); 567 | } 568 | } -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/exceptions/AgeRestrictedError.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube.exceptions; 2 | 3 | public class AgeRestrictedError extends Exception { 4 | public AgeRestrictedError(String videoId) { 5 | super("Video ID = " + videoId + " is age restricted, and can't be accessed without logging in"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/exceptions/BotDetectionError.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube.exceptions; 2 | 3 | public class BotDetectionError extends Exception { 4 | public BotDetectionError(String videoId) { 5 | super(videoId + " This request was detected as a bot. Use `usePoToken=True` to view"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/exceptions/LiveStreamError.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube.exceptions; 2 | 3 | public class LiveStreamError extends Exception{ 4 | public LiveStreamError(String videoId) { 5 | super(videoId + " is streaming live and cannot be loaded"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/exceptions/LiveStreamOffline.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube.exceptions; 2 | 3 | public class LiveStreamOffline extends Exception{ 4 | public LiveStreamOffline(String videoId, String reason){ 5 | super(videoId + " " + reason); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/exceptions/MembersOnlyError.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube.exceptions; 2 | 3 | public class MembersOnlyError extends Exception{ 4 | public MembersOnlyError(String videoId) { 5 | super(videoId + " is a members-only video"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/exceptions/RecordingUnavailableError.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube.exceptions; 2 | 3 | public class RecordingUnavailableError extends Exception{ 4 | public RecordingUnavailableError(String videoId) { 5 | super(videoId + " does not have a live stream recording available"); 6 | } 7 | } -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/exceptions/RegexMatchError.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube.exceptions; 2 | 3 | public class RegexMatchError extends Exception{ 4 | public RegexMatchError(String msg) { 5 | super(msg); 6 | } 7 | } -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/exceptions/UnknownVideoError.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube.exceptions; 2 | 3 | public class UnknownVideoError extends Exception{ 4 | public UnknownVideoError(String videoId, String status, String reason){ 5 | super("Unknown Video Error, VideoId: " + videoId + " Status: " + status + " Reason: " + reason); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/exceptions/VideoPrivateError.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube.exceptions; 2 | 3 | public class VideoPrivateError extends Exception{ 4 | public VideoPrivateError(String videoId) { 5 | super(videoId + " is a private video"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/exceptions/VideoRegionBlockedError.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube.exceptions; 2 | 3 | public class VideoRegionBlockedError extends Exception { 4 | public VideoRegionBlockedError(String videoId) { 5 | super(videoId + " is not available in your region"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/felipeucelli/javatube/exceptions/VideoUnavailableError.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube.exceptions; 2 | 3 | public class VideoUnavailableError extends Exception{ 4 | public VideoUnavailableError(String videoId) { 5 | super(videoId + " is unavailable"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/com/github/felipeucelli/javatube/CipherTest.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.MethodSource; 8 | 9 | import java.io.IOException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.stream.Stream; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | import static org.junit.jupiter.api.Assertions.assertTrue; 17 | 18 | public class CipherTest { 19 | static java.util.stream.Stream fileNames() { 20 | return Stream.of( 21 | "f980f2a9-player_ias.vflset-en_US.txt", 22 | "71547d26-player_ias.vflset-en_US.txt", 23 | "23604418-player_ias.vflset-en_US.txt", 24 | "f980f2a9-player-plasma-ias-tablet-en_US.vflset.txt", 25 | "3cd2d050-player-plasma-ias-tablet-en_US.vflset.txt", 26 | "da7c2a60-player_ias.vflset-en_US.txt", 27 | "21812a9c-player_ias.vflset-en_US.txt", 28 | "c153b631-player-plasma-ias-tablet-en_US.vflset.txt", 29 | "5bdfe6d5-player_ias.vflset-en_US.txt", 30 | "31e0b6d9-player_ias.vflset-en_US.txt", 31 | "5b22937f-player_ias.vflset-en_US.txt", 32 | "b22ef6e7-player_ias.vflset-en_US.txt", 33 | "1f8742dc-player_ias.vflset-en_US.txt", 34 | "20dfca59-player_ias.vflset-en_US.txt", 35 | "2f238d39-player_ias.vflset-en_US.txt", 36 | "b7240855-player_ias.vflset-en_US.txt", 37 | "3bb1f723-player_ias.vflset-en_US.txt", 38 | "2f1832d2-player_ias.vflset-en_US.txt", 39 | "f3d47b5a-player_ias.vflset-en_US.txt", 40 | "e7567ecf-player_ias_tce.vflset-en_US.txt", 41 | "91201489-player_ias_tce.vflset-en_US.txt", 42 | "20830619-player_ias_tce.vflset-en_US.txt" 43 | ); 44 | } 45 | private String readFileContent(String fileName) throws IOException { 46 | Path filePath = Paths.get("src/test/resources/com/github/felipeucelli/javatube/base", fileName); 47 | assertTrue(Files.exists(filePath), "File " + fileName + " not found."); 48 | 49 | return new String(Files.readAllBytes(filePath)); 50 | } 51 | @Test 52 | public void testExtractCipher() throws Exception { 53 | Youtube yt = new Youtube("https://www.youtube.com/watch?v=O3ElohqHEzQ"); 54 | Cipher cipher = new Cipher(yt.getJs(), yt.getYtPlayerJs()); 55 | Assertions.assertNotNull(cipher.getSignatureFunctionName()); 56 | Assertions.assertNotNull(cipher.getThrottlingFunctionName()); 57 | } 58 | @ParameterizedTest 59 | @MethodSource("fileNames") 60 | public void testGetNSigName(String fileName) throws Exception { 61 | String fileContent = readFileContent(fileName); 62 | String funName = getNSigFunName(fileName); 63 | 64 | assertEquals(funName, new Cipher(fileContent, fileName).getThrottlingFunctionName()); 65 | } 66 | @ParameterizedTest 67 | @MethodSource("fileNames") 68 | public void testGetSigName(String fileName) throws Exception { 69 | String fileContent = readFileContent(fileName); 70 | String funName = getSigFunName(fileName); 71 | 72 | assertEquals(funName, new Cipher(fileContent, fileName).getSignatureFunctionName()); 73 | } 74 | 75 | private String getNSigFunName(String fileName) { 76 | return switch (fileName) { 77 | case "f980f2a9-player_ias.vflset-en_US.txt", "da7c2a60-player_ias.vflset-en_US.txt" -> "Ula"; 78 | case "71547d26-player_ias.vflset-en_US.txt" -> "ema"; 79 | case "23604418-player_ias.vflset-en_US.txt", "5bdfe6d5-player_ias.vflset-en_US.txt" -> "fma"; 80 | case "f980f2a9-player-plasma-ias-tablet-en_US.vflset.txt" -> "bq"; 81 | case "3cd2d050-player-plasma-ias-tablet-en_US.vflset.txt" -> "$p"; 82 | case "21812a9c-player_ias.vflset-en_US.txt" -> "Tla"; 83 | case "c153b631-player-plasma-ias-tablet-en_US.vflset.txt" -> "dq"; 84 | case "31e0b6d9-player_ias.vflset-en_US.txt" -> "kma"; 85 | case "5b22937f-player_ias.vflset-en_US.txt" -> "$ma"; 86 | case "b22ef6e7-player_ias.vflset-en_US.txt" -> "Ima"; 87 | case "1f8742dc-player_ias.vflset-en_US.txt", "20dfca59-player_ias.vflset-en_US.txt" -> "rma"; 88 | case "2f238d39-player_ias.vflset-en_US.txt" -> "Xma"; 89 | case "b7240855-player_ias.vflset-en_US.txt" -> "Zma"; 90 | case "3bb1f723-player_ias.vflset-en_US.txt" -> "fyn"; 91 | case "2f1832d2-player_ias.vflset-en_US.txt" -> "dCH"; 92 | case "f3d47b5a-player_ias.vflset-en_US.txt" -> "xyN"; 93 | case "e7567ecf-player_ias_tce.vflset-en_US.txt" -> "X_S"; 94 | case "91201489-player_ias_tce.vflset-en_US.txt" -> "K48"; 95 | case "20830619-player_ias_tce.vflset-en_US.txt" -> "e2E"; 96 | default -> ""; 97 | }; 98 | } 99 | private String getSigFunName(String fileName) { 100 | return switch (fileName) { 101 | case "f980f2a9-player_ias.vflset-en_US.txt" -> "bua"; 102 | case "71547d26-player_ias.vflset-en_US.txt" -> "Hsa"; 103 | case "23604418-player_ias.vflset-en_US.txt" -> "Isa"; 104 | case "f980f2a9-player-plasma-ias-tablet-en_US.vflset.txt" -> "Nka"; 105 | case "3cd2d050-player-plasma-ias-tablet-en_US.vflset.txt" -> "jka"; 106 | case "da7c2a60-player_ias.vflset-en_US.txt" -> "Zta"; 107 | case "21812a9c-player_ias.vflset-en_US.txt" -> "$ya"; 108 | case "c153b631-player-plasma-ias-tablet-en_US.vflset.txt" -> "Qja"; 109 | case "5bdfe6d5-player_ias.vflset-en_US.txt" -> "ZKa"; 110 | case "31e0b6d9-player_ias.vflset-en_US.txt" -> "zLa"; 111 | case "5b22937f-player_ias.vflset-en_US.txt" -> "cQa"; 112 | case "b22ef6e7-player_ias.vflset-en_US.txt" -> "LPa"; 113 | case "1f8742dc-player_ias.vflset-en_US.txt" -> "HBa"; 114 | case "20dfca59-player_ias.vflset-en_US.txt" -> "EBa"; 115 | case "2f238d39-player_ias.vflset-en_US.txt" -> "mCa"; 116 | case "b7240855-player_ias.vflset-en_US.txt" -> "pCa"; 117 | case "3bb1f723-player_ias.vflset-en_US.txt" -> "pen"; 118 | case "2f1832d2-player_ias.vflset-en_US.txt" -> "B_H"; 119 | case "f3d47b5a-player_ias.vflset-en_US.txt" -> "ouU"; 120 | case "e7567ecf-player_ias_tce.vflset-en_US.txt" -> "$oW"; 121 | case "91201489-player_ias_tce.vflset-en_US.txt" -> "N4a"; 122 | case "20830619-player_ias_tce.vflset-en_US.txt" -> "X8$"; 123 | default -> ""; 124 | }; 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/test/java/com/github/felipeucelli/javatube/JsInterpreterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.felipeucelli.javatube; 2 | 3 | 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | import org.junit.jupiter.params.provider.MethodSource; 7 | 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.nio.file.Paths; 11 | import java.util.List; 12 | import java.util.stream.Stream; 13 | 14 | import java.nio.file.Path; 15 | 16 | import static org.junit.jupiter.api.Assertions.*; 17 | 18 | public class JsInterpreterTest { 19 | static Stream fileNames() { 20 | return Stream.of( 21 | "f980f2a9-player_ias.vflset-en_US.txt", 22 | "71547d26-player_ias.vflset-en_US.txt", 23 | "23604418-player_ias.vflset-en_US.txt", 24 | "f980f2a9-player-plasma-ias-tablet-en_US.vflset.txt", 25 | "3cd2d050-player-plasma-ias-tablet-en_US.vflset.txt", 26 | "da7c2a60-player_ias.vflset-en_US.txt", 27 | "21812a9c-player_ias.vflset-en_US.txt", 28 | "c153b631-player-plasma-ias-tablet-en_US.vflset.txt", 29 | "019a2dc2-player-ias-vflset_en_US.txt", 30 | "5bdfe6d5-player_ias.vflset-en_US.txt", 31 | "31e0b6d9-player_ias.vflset-en_US.txt", 32 | "42a553e1-player_ias.vflset-en_US.txt", 33 | "3ffefd71-player_ias.vflset-en_US.txt", 34 | "bc657243-player_ias.vflset-en_US.txt", 35 | "b22ef6e7-player_ias.vflset-en_US.txt", 36 | "1f8742dc-player_ias.vflset-en_US.txt", 37 | "20dfca59-player_ias.vflset-en_US.txt", 38 | "b12cc44b-player_ias.vflset-en_US.txt", 39 | "3bb1f723-player_ias.vflset-en_US.txt", 40 | "2f1832d2-player_ias.vflset-en_US.txt", 41 | "e7567ecf-player_ias_tce.vflset-en_US.txt", 42 | "74e4bb46-player_ias_tce.vflset-en_US.txt", 43 | "4fcd6e4a-player_ias.vflset-en_US.txt", 44 | "20830619-player_ias.vflset-en_US.txt", 45 | "6450230e-player_ias.vflset-en_US.txt", 46 | "59b252b9-player_ias.vflset-en_US.txt" 47 | ); 48 | } 49 | private String readFileContent(String fileName) throws IOException { 50 | Path filePath = Paths.get("src/test/resources/com/github/felipeucelli/javatube/base", fileName); 51 | assertTrue(Files.exists(filePath), "File " + fileName + " not found."); 52 | 53 | return new String(Files.readAllBytes(filePath)); 54 | } 55 | 56 | @Test 57 | public void testInterpreterCurrentSig() throws Exception { 58 | Youtube yt = new Youtube("https://www.youtube.com/watch?v=O3ElohqHEzQ"); 59 | Cipher c = new Cipher(yt.getJs(), yt.getYtPlayerJs()); 60 | 61 | String sig = "AOq0QJ8wRQIhANGlXiqWj4dne3ftJz6RMy5hK5Xe3QP3oC7MzEWXWYhWAiBOlqAWYj6ZOU-jlBaNLTTUGFOuR%3Dm397tElCFtpaC8jw%3Db"; 62 | 63 | new JsInterpreter(yt.getJs()).callFunction(c.getSignatureFunctionName(), sig); 64 | } 65 | 66 | @Test 67 | public void testInterpreterCurrentNSig() throws Exception { 68 | Youtube yt = new Youtube("https://www.youtube.com/watch?v=O3ElohqHEzQ"); 69 | Cipher c = new Cipher(yt.getJs(), yt.getYtPlayerJs()); 70 | 71 | String Nsig = "70QzMb0nhneLLS6BN"; 72 | 73 | String result = (String) new JsInterpreter(yt.getJs()).callFunction(c.getThrottlingFunctionName(), Nsig); 74 | 75 | assertFalse(result.startsWith("enhanced_except") || result.endsWith(Nsig)); 76 | } 77 | 78 | @ParameterizedTest 79 | @MethodSource("fileNames") 80 | public void testNSigInterpreter(String fileName) throws Exception { 81 | String fileContent = readFileContent(fileName); 82 | List params = getNSigParams(fileName); 83 | 84 | assertEquals(params.get(0), new JsInterpreter(fileContent).callFunction(params.get(1), params.get(2))); 85 | } 86 | @ParameterizedTest 87 | @MethodSource("fileNames") 88 | public void testSigInterpreter(String fileName) throws Exception { 89 | String fileContent = readFileContent(fileName); 90 | List params = getSigParams(fileName); 91 | 92 | assertEquals(params.get(0), new JsInterpreter(fileContent).callFunction(params.get(1), params.get(2))); 93 | } 94 | 95 | private List getNSigParams(String fileName) { 96 | return switch (fileName) { 97 | case "f980f2a9-player_ias.vflset-en_US.txt" -> List.of("4Dvzk8E8Iz-9xQ", "Ula", "t0yYMGhLCCndDH7_oH"); 98 | case "71547d26-player_ias.vflset-en_US.txt" -> List.of("mCqsuKWEbNMz4A", "ema", "w5Ur1wQ_oojYWTNH4z"); 99 | case "23604418-player_ias.vflset-en_US.txt" -> List.of("BTSM6hSdKLn-rw", "fma", "8UGuO2Tv_wq_lCb"); 100 | case "f980f2a9-player-plasma-ias-tablet-en_US.vflset.txt" -> 101 | List.of("a2K_xA_CfqvTUg", "bq", "tMZ1SVtCu1G7wBJrH-"); 102 | case "3cd2d050-player-plasma-ias-tablet-en_US.vflset.txt" -> 103 | List.of("rvxf4lqA9UC6Y", "$p", "tMZ1SVtCu1G7wBJrH-"); 104 | case "da7c2a60-player_ias.vflset-en_US.txt" -> List.of("kV6feunzajGyGaPo", "Ula", "tMZ1SVtCu1G7wBJrH-"); 105 | case "21812a9c-player_ias.vflset-en_US.txt" -> List.of("M5pXBNVBZf01MA", "Tla", "70QzMb0nhneLLS6BN"); 106 | case "c153b631-player-plasma-ias-tablet-en_US.vflset.txt" -> 107 | List.of("AAnFkOJ694_Uew", "dq", "NL7YwUhStcFhEdqJ"); 108 | case "019a2dc2-player-ias-vflset_en_US.txt" -> List.of("6giJCNZ6QlHatA", "Ula", "mSyHU9iB6viPu5-"); 109 | case "5bdfe6d5-player_ias.vflset-en_US.txt" -> List.of("6WVNa9oCSHok", "fma", "6giJCNZ6QlHatA"); 110 | case "31e0b6d9-player_ias.vflset-en_US.txt" -> List.of("BVP_Rb2aDs", "kma", "6giJCNZ6QlHatA"); 111 | case "42a553e1-player_ias.vflset-en_US.txt" -> List.of("t5Jp2AyA1Jog2", "ema", "70QzMb0nhneLLS6BN"); 112 | case "3ffefd71-player_ias.vflset-en_US.txt" -> List.of("Qp4YR89PTr-vmb", "ima", "70QzMb0nhneLLS6BN"); 113 | case "d8a5aa5e-player_ias.vflset-en_US.txt" -> List.of("KvdthMya5dxi87dY", "ema", "70QzMb0nhneLLS6BN"); 114 | case "bc657243-player_ias.vflset-en_US.txt" -> List.of("l_slyNHt1evTOm", "Ula", "70QzMb0nhneLLS6BN"); 115 | case "b22ef6e7-player_ias.vflset-en_US.txt" -> List.of("3imMGazkXNJnWK", "Ima", "70QzMb0nhneLLS6BN"); 116 | case "1f8742dc-player_ias.vflset-en_US.txt" -> List.of("wx4GPH8bp1v7A9", "rma", "70QzMb0nhneLLS6BN"); 117 | case "20dfca59-player_ias.vflset-en_US.txt" -> List.of("zmLIydxjzkWeBA9", "rma", "70QzMb0nhneLLS6BN"); 118 | case "b12cc44b-player_ias.vflset-en_US.txt" -> List.of("FnDBX5UEM1NHNyr", "Ema", "70QzMb0nhneLLS6BN"); 119 | case "3bb1f723-player_ias.vflset-en_US.txt" -> List.of("-zeqduSAj2ON5", "fyn", "70QzMb0nhneLLS6BN"); 120 | case "2f1832d2-player_ias.vflset-en_US.txt" -> List.of("e7hn0bMSQ", "B_H", "70QzMb0nhneLLS6BN"); 121 | case "e7567ecf-player_ias_tce.vflset-en_US.txt" -> List.of("eGJT0Dt7IGpKgk", "X_S", "70QzMb0nhneLLS6BN"); 122 | case "74e4bb46-player_ias_tce.vflset-en_US.txt" -> List.of("YRvnhiNcKsUj", "Bzs", "70QzMb0nhneLLS6BN"); 123 | case "4fcd6e4a-player_ias.vflset-en_US.txt" -> List.of("DtBH24Jm4Vu4ga", "gGu", "70QzMb0nhneLLS6BN"); 124 | case "20830619-player_ias.vflset-en_US.txt" -> List.of("R5zfbyrpEoUHl", "e2E", "70QzMb0nhneLLS6BN"); 125 | case "6450230e-player_ias.vflset-en_US.txt" -> List.of("yaMQah-K-J8VJ0-", "J5S", "70QzMb0nhneLLS6BN"); 126 | case "59b252b9-player_ias.vflset-en_US.txt" -> List.of("CnHxxVCaFYf", "m85", "70QzMb0nhneLLS6BN"); 127 | default -> List.of("", "", ""); 128 | }; 129 | } 130 | 131 | private List getSigParams(String fileName) { 132 | return switch (fileName) { 133 | case "f980f2a9-player_ias.vflset-en_US.txt" -> 134 | List.of("AOq0QJ8wRQIhANGlXiqWj4dne3ftJz6RMy5hK5Xe3QP3oC7MzEWXWYhWAiBOlqAWYj6ZOU-jlBaNLTTUGFOuR%3Dm397tElCFtpaC8jw%3Db", "bua", "AAOq0QJ8wRQIhANGlXiqWj4dne3ftJz6RMy5hK5Xe3QP3oC7MzEWXWYhWAiBOlqAWYj6ZOUbjlBaNLTTUGFOuR%3Dm397tElCFtpaC8jw%3D-"); 135 | case "71547d26-player_ias.vflset-en_US.txt" -> 136 | List.of("AOq0QJ8wRQIhAInqO2_vdMNMCbbHOW_X71EL1Mj-ayWA0MagUI10cAI0AiAIYB_QAseLg8dk54Sz0bnappC3213iiq24WJRLJF5hew==", "Hsa", "====weh5FJLRJW42qii3123Cppanb0zS45kd8gLesAQ_BYIAiA0IAb01IUgaM0AWya-jM1LE17X_WcOAbCMNMdv_2OqnIAhIQRw8JQ0qOcqOH"); 137 | case "23604418-player_ias.vflset-en_US.txt" -> 138 | List.of("WQJ8wRQIhANGlXiqWj4dne3ftJz6RMyAhK5Xe3QP3oC7MzEWXWYh0qiBOlqAWYj6ZOUbjlBaNLTTUGDOuR%3Dm397tElCFtpaC8jw%3F", "Isa", "AAOq0QJ8wRQIhANGlXiqWj4dne3ftJz6RMy5hK5Xe3QP3oC7MzEWXWYhWAiBOlqAWYj6ZOUbjlBaNLTTUGFOuR%3Dm397tElCFtpaC8jw%3D-"); 139 | case "f980f2a9-player-plasma-ias-tablet-en_US.vflset.txt" -> 140 | List.of("AOq0QJ8wRgIhAMulJffBQeJA-jLY-DOAkazwLV1KrW6AsWPC8ne1iceUAiEA5T2QA0vOX6FGwONJg4szsLoT6u4b6I1iVkMDO9HwSJw=", "Nka", "AAOq0QJ8wRgIhAMulJffBQeJA-jLY-DOAkazwLV1KrW6AsWPC8ne1iceUAiEA5T2QA0=OX6FGwONJg4szsLoT6u4b6I1iVkMDO9HwSJwv"); 141 | case "3cd2d050-player-plasma-ias-tablet-en_US.vflset.txt" -> 142 | List.of("AOq0QJ8wRgIhAMulJffBQeJA-jLY-DOAkazwLV1KrW6AsWPC8ne1iceUAiEA5T2QA0=OX6FGwONJg4szsLoT6v4b6I1iVkMDO9HwSJwu", "jka", "AAOq0QJ8wRgIhAMulJffBQeJA-jLY-DOAkazwLV1KrW6AsWPC8ne1iceUAiEA5T2QA0=OX6FGwONJg4szsLoT6u4b6I1iVkMDO9HwSJwv"); 143 | case "da7c2a60-player_ias.vflset-en_US.txt" -> 144 | List.of("Oq0QJ8wAgIhAMulJffBQeJA-jLY-DOAkazwLV1KrW6AsWPC8ne1RceUAiEA5T2QA0=OX6FGwONJg4szsLoT6u4b6I1iVkMDO9HwSJwv", "Zta", "AAOq0QJ8wRgIhAMulJffBQeJA-jLY-DOAkazwLV1KrW6AsWPC8ne1iceUAiEA5T2QA0=OX6FGwONJg4szsLoT6u4b6I1iVkMDO9HwSJwv"); 145 | case "21812a9c-player_ias.vflset-en_US.txt" -> 146 | List.of("TSwH9ODMkVi1I6b4u6JoLszs4gJNOwGF6XO=0AQ2T5AEiAUeci1en8COWsA6WrK1VLwzakAOD-YLj-AJeQBffJluMAhIgRw8JQ0qP", "$ya", "AAOq0QJ8wRgIhAMulJffBQeJA-jLY-DOAkazwLV1KrW6AsWPC8ne1iceUAiEA5T2QA0=OX6FGwONJg4szsLoT6u4b6I1iVkMDO9HwSJwv"); 147 | case "c153b631-player-plasma-ias-tablet-en_US.vflset.txt" -> 148 | List.of("4wH9ODMkVi1I6bSu6ToLszs4gJNOwGF6XO=0AQ2T5AEiAUeci1en8CPWsA6WrK1VLwzakAOD-YLj-AJeQBffJluMAhIgRw8JQ0qOAA", "Qja", "AAOq0QJ8wRgIhAMulJffBQeJA-jLY-DOAkazwLV1KrW6AsWPC8ne1iceUAiEA5T2QA0=OX6FGwONJg4szsLoT6u4b6I1iVkMDO9HwSJwv"); 149 | case "019a2dc2-player-ias-vflset_en_US.txt" -> 150 | List.of("AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO", "AKa", "AOqAOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo9Oyib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO3"); 151 | case "5bdfe6d5-player_ias.vflset-en_US.txt" -> List.of("tTxUgoDDREeR36Kj5wrO_NjqCHEmy6PGNIGWF9GKnFEIC4P07pOGh54LI6biy39oUrq5mTnRI3mE0gJchXqWRWNIgIARw8JQ0q", "ZKa", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 152 | case "31e0b6d9-player_ias.vflset-en_US.txt" -> List.of("OTxUgoDDREeR36Kj5wrO_NjqCHEmy6PGNIGTF9GKnFEIC4P07ptGh54LI6biy39oUrq5m0nRI3mE0gJchXqWRWNIgIARw8JQA", "zLa", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 153 | case "42a553e1-player_ias.vflset-en_US.txt" -> List.of("xUgoDDREeR36Kj5wrObNjqCHEmy6PGNIGWF9GKnFEIC4P07ptGh54AI6Oiy39oUrq5mTnRI3mE0gJchXqWRWNIgIARw8JQ0qO", "YLa", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 154 | case "3ffefd71-player_ias.vflset-en_US.txt" -> List.of("UgoDDREeR36Kj5wrO_NjqCHEmy6PGNIGWO9GKnFEIC4P0JptGh54LI6biy39oUrq5mTnRI3mE0gJchXqWRWNIgIARw8", "CMa", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 155 | case "d8a5aa5e-player_ias.vflset-en_US.txt" -> List.of("gxUToDDREeR36Kj5wrO_NjqCHEqy6PGNIGWF9GKnFEIC4P07ptGh54LI6biy39oUrq5mTnRImmE0gJchXqWRWNIgIARw8JQ03", "MNa", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 156 | case "bc657243-player_ias.vflset-en_US.txt" -> List.of("pOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGt970P4CIEFnKGOFWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTA", "GOa", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 157 | case "b22ef6e7-player_ias.vflset-en_US.txt" -> List.of("goDDREeR36Kj5wrO_NjqCHEmy6PANTGWF9GKnFEIC4P07ptGh54LI6biy39oUrq5ITnRI3mE0gJchXqWRWNIgIARw8JQ0qOG", "LPa", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 158 | case "1f8742dc-player_ias.vflset-en_US.txt" -> List.of("mQJ8wRAIgINW9WqXhcJg0E03IRnTm5qrUoA3yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO", "HBa", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 159 | case "20dfca59-player_ias.vflset-en_US.txt" -> List.of("U0QJ8wRAIgINWRWqXhcJg0qm3IRnTm5qrEo93yib6IL45hGtp70P4CTEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxI", "EBa", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 160 | case "b12cc44b-player_ias.vflset-en_US.txt" -> List.of("EJ8wRAIgINWRWqXhcJg0Am3IRnTm5qrUo93yib6IL45hGtp70P4CQEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDoOU", "PBa", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 161 | case "3bb1f723-player_ias.vflset-en_US.txt" -> List.of("iwR8IgIN5RWqXhcJg0Em3IRATm5qrUo93yAb6IL40hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO", "pen", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 162 | case "2f1832d2-player_ias.vflset-en_US.txt" -> List.of("KpC5ut2EHrCarJWeXtsVBjE-IM3YQwOpfz8kcrr8emyEL62UySqmFPy6MPf8wVOXd41yKQhDHvEx_8f-Gu75ltKNohkk-", "dCH", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 163 | case "e7567ecf-player_ias_tce.vflset-en_US.txt" -> List.of("UgoDDREOR36Kj5wrO_NjqCHEmy6PGNIGWF9GKnFEIC4P0eptGh54LI6biy39oUrq5mTnRI3mE0gJchXqWRWNIgIARw8JQ0qO", "$oW", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 164 | case "74e4bb46-player_ias_tce.vflset-en_US.txt" -> List.of("TxUgoDDREeR36Kj5wrO_NjqCHEmy6PGNIGWF9GKnFEIC4P07ptGh54LI6biy39oUrqAmTnRIOmE0gJchXqWRWNIgIARw8JQ0q3", "bol", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 165 | case "4fcd6e4a-player_ias.vflset-en_US.txt" -> List.of("eTxUgoDDREOR36Kj5wrO_NjqCHEmy6PGNIGWq9GKnFEIC4P07ptGh54LI6biy39oUrq5mTnRI3mE0gJchXqWRWNIgIARw8JQ0", "YqR", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 166 | case "20830619-player_ias.vflset-en_US.txt" -> List.of("IUgoDDREeR36Kj5wrO_NjqCOEmy6PGNIGWF9GKnFExCAP07ptGh54LI6biy39oUrq5mTnRI3mE0gJchXqWRWNIgIARw8JQ0qO4", "X8$", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 167 | case "6450230e-player_ias.vflset-en_US.txt" -> List.of("qOG0QJ8wRAIgINWRWqXhcJg0Em3IRnAm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWTINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO", "L1S", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 168 | case "59b252b9-player_ias.vflset-en_US.txt" -> List.of("pOq0QJ8wRAIgINWRWEXhcJg0Em3IRnTm5qrUo93yib6ILA5hGtP70q4CI4FnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxT", "Had", "AOq0QJ8wRAIgINWRWqXhcJg0Em3IRnTm5qrUo93yib6IL45hGtp70P4CIEFnKG9FWGINGP6ymEHCqjN_Orw5jK63ReERDDogUxTO"); 169 | default -> List.of("", "", ""); 170 | }; 171 | 172 | } 173 | } 174 | --------------------------------------------------------------------------------