├── .github └── workflows │ ├── automatic-testing.yml │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── asyncExample.py ├── setup.py ├── syncExample.py ├── tests ├── async │ ├── extras.py │ ├── playlists.py │ └── search.py └── sync │ ├── extras.py │ ├── playlists.py │ └── search.py └── youtubesearchpython ├── __future__ ├── README.md ├── __init__.py ├── extras.py ├── search.py └── streamurlfetcher.py ├── __init__.py ├── core ├── __init__.py ├── channel.py ├── channelsearch.py ├── comments.py ├── componenthandler.py ├── constants.py ├── hashtag.py ├── playlist.py ├── requests.py ├── search.py ├── streamurlfetcher.py ├── suggestions.py ├── transcript.py ├── utils.py └── video.py ├── extras.py ├── handlers ├── componenthandler.py └── requesthandler.py ├── legacy └── __init__.py ├── search.py └── streamurlfetcher.py /.github/workflows/automatic-testing.yml: -------------------------------------------------------------------------------- 1 | name: Test with Python 3.8 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 5 * * 1" 8 | 9 | jobs: 10 | extras: 11 | name: Test "Extras" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: '3.8' 19 | - name: Install dependencies 20 | run: python -m pip install httpx yt-dlp . 21 | - name: Sync 22 | run: python tests/sync/extras.py 23 | - name: Async 24 | run: python tests/async/extras.py 25 | playlists: 26 | name: Test "Playlists" 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Set up Python 31 | uses: actions/setup-python@v2 32 | with: 33 | python-version: '3.8' 34 | - name: Install dependencies 35 | run: python -m pip install httpx . 36 | - name: Sync 37 | run: python tests/sync/playlists.py 38 | - name: Async 39 | run: python tests/async/playlists.py 40 | search: 41 | name: Test "Search" 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v2 45 | - name: Set up Python 46 | uses: actions/setup-python@v2 47 | with: 48 | python-version: '3.8' 49 | - name: Install dependencies 50 | run: python -m pip install httpx . 51 | - name: Sync 52 | run: python tests/sync/search.py 53 | - name: Async 54 | run: python tests/async/search.py 55 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package to PyPI 5 | 6 | on: 7 | release: 8 | types: 9 | - released 10 | 11 | jobs: 12 | deploy: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | ref: main 20 | - name: Set up Python 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: '3.x' 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install setuptools wheel twine 28 | - name: Build and publish 29 | env: 30 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 31 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 32 | run: | 33 | python setup.py sdist bdist_wheel 34 | twine upload dist/* 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | 3 | .vscode 4 | 5 | # python 6 | 7 | __pycache__ 8 | dist 9 | 10 | # testing 11 | 12 | test.py 13 | test2.py 14 | *.json 15 | *.html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hitesh Kumar Saini 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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include youtubesearchpython *.py -------------------------------------------------------------------------------- /asyncExample.py: -------------------------------------------------------------------------------- 1 | from youtubesearchpython.__future__ import * 2 | import asyncio 3 | 4 | async def main(): 5 | ''' 6 | Searches for all types of results like videos, channels & playlists in YouTube. 7 | 'type' key in the JSON/Dictionary may be used to differentiate between the types of result. 8 | ''' 9 | search = Search('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 10 | result = await search.next() 11 | print(result) 12 | 13 | 14 | 15 | 16 | ''' 17 | Searches only for videos in YouTube. 18 | ''' 19 | videosSearch = VideosSearch('NoCopyrightSounds', limit = 10, language = 'en', region = 'US') 20 | videosResult = await videosSearch.next() 21 | print(videosResult) 22 | 23 | 24 | 25 | 26 | ''' 27 | Searches only for channels in YouTube. 28 | ''' 29 | channelsSearch = ChannelsSearch('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 30 | channelsResult = await channelsSearch.next() 31 | print(channelsResult) 32 | 33 | 34 | 35 | 36 | ''' 37 | Searches only for playlists in YouTube. 38 | ''' 39 | playlistsSearch = PlaylistsSearch('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 40 | playlistsResult = await playlistsSearch.next() 41 | print(playlistsResult) 42 | 43 | 44 | playlist = Playlist('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 45 | while playlist.hasMoreVideos: 46 | print('Getting more videos...') 47 | await playlist.getNextVideos() 48 | print(f'Videos Retrieved: {len(playlist.videos)}') 49 | 50 | print('Found all the videos.') 51 | 52 | 53 | 54 | 55 | ''' 56 | Can be used to get search results with custom defined filters. 57 | 58 | Setting second parameter as VideoSortOrder.uploadDate, to get video results sorted according to upload date. 59 | 60 | Few of the predefined filters for you to use are: 61 | SearchMode.videos 62 | VideoUploadDateFilter.lastHour 63 | VideoDurationFilter.long 64 | VideoSortOrder.viewCount 65 | There are many other for you to check out. 66 | 67 | If this much control isn't enough then, you may pass custom string yourself by seeing the YouTube query in any web browser e.g. 68 | "EgQIBRAB" from "https://www.youtube.com/results?search_query=NoCopyrightSounds&sp=EgQIBRAB" may be passed as second parameter to get only videos, which are uploaded this year. 69 | ''' 70 | customSearch = CustomSearch('NoCopyrightSounds', VideoSortOrder.uploadDate, language = 'en', region = 'US') 71 | customResult = await customSearch.next() 72 | print(customResult) 73 | 74 | 75 | search = ChannelSearch('Watermelon Sugar', "UCZFWPqqPkFlNwIxcpsLOwew") 76 | result = await search.next() 77 | print(result) 78 | 79 | 80 | 81 | 82 | ''' 83 | Getting search results from the next pages on YouTube. 84 | Generally you'll get maximum of 20 videos in one search, for getting subsequent results, you may call `next` method. 85 | ''' 86 | search = VideosSearch('NoCopyrightSounds') 87 | index = 0 88 | ''' Getting result on 1st page ''' 89 | result = await search.next() 90 | ''' Displaying the result ''' 91 | for video in result['result']: 92 | index += 1 93 | print(f'{index} - {video["title"]}') 94 | ''' Getting result on 2nd page ''' 95 | result = await search.next() 96 | ''' Displaying the result ''' 97 | for video in result['result']: 98 | index += 1 99 | print(f'{index} - {video["title"]}') 100 | ''' Getting result on 3rd page ''' 101 | result = await search.next() 102 | ''' Displaying the result ''' 103 | for video in result['result']: 104 | index += 1 105 | print(f'{index} - {video["title"]}') 106 | 107 | 108 | 109 | 110 | ''' 111 | Getting information about video or its formats using video link or video ID. 112 | 113 | `Video.get` method will give both information & formats of the video 114 | `Video.getInfo` method will give only information about the video. 115 | `Video.getFormats` method will give only formats of the video. 116 | 117 | You may either pass link or ID, method will take care itself. 118 | ''' 119 | video = await Video.get('https://www.youtube.com/watch?v=z0GKGpObgPY', get_upload_date=True) 120 | print(video) 121 | videoInfo = await Video.getInfo('https://youtu.be/z0GKGpObgPY') 122 | print(videoInfo) 123 | videoFormats = await Video.getFormats('z0GKGpObgPY') 124 | print(videoFormats) 125 | 126 | 127 | 128 | 129 | ''' 130 | Getting information about playlist or videos in it using link. 131 | 132 | `Playlist.get` method will give both information & videos in the playlist 133 | `Playlist.getInfo` method will give only information about the playlist. 134 | `Playlist.getFormats` method will give only formats of the playlist. 135 | 136 | ''' 137 | playlist = await Playlist.get('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 138 | print(playlist) 139 | playlistInfo = await Playlist.getInfo('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 140 | print(playlistInfo) 141 | playlistVideos = await Playlist.getVideos('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 142 | print(playlistVideos) 143 | 144 | ''' 145 | More tests to buggy Playlist class 146 | ''' 147 | playlist = await Playlist.get('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 148 | print(playlist) 149 | playlist = await Playlist.get('https://www.youtube.com/watch?v=bplUXwTTgbI&list=PL6edxAMqu2xfxgbf7Q09hSg1qCMfDI7IZ') 150 | print(playlist) 151 | 152 | 153 | 154 | ''' 155 | Getting search suggestions from YouTube. 156 | You may show search suggestions to users before making any search. 157 | ''' 158 | suggestions = await Suggestions.get('NoCopyrightSounds', language = 'en', region = 'US') 159 | print(suggestions) 160 | 161 | 162 | 163 | 164 | ''' 165 | Getting videos by hashtag. 166 | ''' 167 | hashtag = Hashtag('ncs', limit = 1) 168 | result = await hashtag.next() 169 | print(result) 170 | 171 | 172 | 173 | 174 | ''' 175 | Getting direct stream URL for a video. 176 | You may show search suggestions to users before making any search. 177 | 178 | To use this, you must have PyTube installed. 179 | StreamURLFetcher can fetch direct video URLs without any additional network requests (that's really fast). 180 | Call `get` or `getAll` method of StreamURLFetcher & pass response returned by `Video.get` or `Video.getFormats` as parameter to fetch direct URLs. 181 | Getting URLs or downloading streams using youtube-dl or PyTube is can be a slow, because of the fact that they make requests to fetch the same content, which one might have already recieved at the time of showing it to the user etc. 182 | StreamURLFetcher makes use of PyTube (if installed) & makes some slight improvements to functioning of PyTube. 183 | Avoid instantiating StreamURLFetcher more than once, it will be slow (making global object of the class will be a recommended solution). 184 | 185 | `get` method can be handy for getting URL of a particular kind. `getAll` returns all stream URLs in a dictionary. 186 | ''' 187 | 188 | fetcher = StreamURLFetcher() 189 | ''' 190 | Call this method after instanciating StreamURLFetcher & avoid calling more than once. 191 | ''' 192 | await fetcher.getJavaScript() 193 | ''' 194 | Get video information. 195 | ''' 196 | videoA = await Video.get("https://www.youtube.com/watch?v=aqz-KE-bpKQ") 197 | videoB = await Video.get("https://www.youtube.com/watch?v=ZwNxYJfW-eU") 198 | 199 | ''' 200 | Get direct stream URLs without any web requests. 201 | ''' 202 | singleUrlA = await fetcher.get(videoA, 22) 203 | allUrlsB = await fetcher.getAll(videoB) 204 | print(singleUrlA) 205 | print(allUrlsB) 206 | 207 | 208 | 209 | 210 | comments = Comments("_ZdsmLgCVdU") 211 | 212 | await comments.getNextComments() 213 | print(len(comments.comments["result"])) 214 | 215 | while len(comments.comments["result"]) < 100: 216 | await comments.getNextComments() 217 | print(len(comments.comments["result"])) 218 | print("Found all comments") 219 | 220 | 221 | 222 | print(await Transcript.get("https://www.youtube.com/watch?v=L7kF4MXXCoA")) 223 | 224 | 225 | url = "https://www.youtube.com/watch?v=-1xu0IP35FI" 226 | 227 | transcript_en = await Transcript.get(url) 228 | # you actually don't have to pass a valid URL in following Transcript call. You can input an empty string, but I do recommend still inputing a valid URL. 229 | transcript_2 = await Transcript.get(url, transcript_en["languages"][-1]["params"]) # in my case, it'd output Spanish. 230 | print(transcript_2) 231 | 232 | 233 | print(await Channel.get("UC_aEa8K-EOJ3D6gOs7HcyNg")) 234 | 235 | 236 | # Retrieve playlists of a channel 237 | channel = Channel("UC_aEa8K-EOJ3D6gOs7HcyNg") 238 | await channel.init() 239 | print(len(channel.result["playlists"])) 240 | while channel.has_more_playlists(): 241 | await channel.next() 242 | print(len(channel.result["playlists"])) 243 | 244 | 245 | 246 | 247 | ''' 248 | You may add/omit the optional parameters according to your requirement & use case. 249 | ''' 250 | 251 | 252 | ''' 253 | Thanks for your support & love! 254 | 255 | - github.com/alexmercerind 256 | ''' 257 | 258 | if __name__ == '__main__': 259 | asyncio.run(main()) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="youtube-search-python", 8 | version="1.6.6", 9 | author="Hitesh Kumar Saini", 10 | license='MIT', 11 | author_email="saini123hitesh@gmail.com", 12 | description="Search for YouTube videos, channels & playlists & get video information using link WITHOUT YouTube Data API v3", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/alexmercerind/youtube-search-python", 16 | packages=setuptools.find_packages(), 17 | include_package_data=True, 18 | zip_safe=False, 19 | install_requires=[ 20 | 'httpx>=0.14.2' 21 | ], 22 | classifiers=[ 23 | "Programming Language :: Python :: 3", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independent", 26 | ], 27 | python_requires='>=3.6', 28 | ) 29 | -------------------------------------------------------------------------------- /syncExample.py: -------------------------------------------------------------------------------- 1 | from youtubesearchpython import * 2 | 3 | 4 | 5 | 6 | ''' 7 | Searches for all types of results like videos, channels & playlists in YouTube. 8 | 'type' key in the JSON/Dictionary may be used to differentiate between the types of result. 9 | ''' 10 | allSearch = Search('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 11 | print(allSearch.result()) 12 | 13 | 14 | 15 | 16 | ''' 17 | Searches only for videos in YouTube. 18 | ''' 19 | videosSearch = VideosSearch('NoCopyrightSounds', limit = 10, language = 'en', region = 'US') 20 | 21 | print(videosSearch.result(mode = ResultMode.json)) 22 | 23 | 24 | 25 | 26 | ''' 27 | Searches only for channels in YouTube. 28 | ''' 29 | channelsSearch = ChannelsSearch('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 30 | ''' 31 | Setting mode = ResultMode.dict for getting dictionary result instead of JSON. Default is ResultMode.json. 32 | ''' 33 | print(channelsSearch.result(mode = ResultMode.json)) 34 | 35 | 36 | 37 | 38 | ''' 39 | Searches only for playlists in YouTube. 40 | ''' 41 | playlistsSearch = PlaylistsSearch('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 42 | print(playlistsSearch.result()) 43 | 44 | 45 | 46 | 47 | ''' 48 | Can be used to get search results with custom defined filters. 49 | 50 | Setting second parameter as VideoSortOrder.uploadDate, to get video results sorted according to upload date. 51 | 52 | Few of the predefined filters for you to use are: 53 | SearchMode.videos 54 | VideoUploadDateFilter.lastHour 55 | VideoDurationFilter.long 56 | VideoSortOrder.viewCount 57 | There are many other for you to check out. 58 | 59 | If this much control isn't enough then, you may pass custom string yourself by seeing the YouTube query in any web browser e.g. 60 | "EgQIBRAB" from "https://www.youtube.com/results?search_query=NoCopyrightSounds&sp=EgQIBRAB" may be passed as second parameter to get only videos, which are uploaded this year. 61 | ''' 62 | customSearch = CustomSearch('NoCopyrightSounds', VideoSortOrder.uploadDate, language = 'en', region = 'US') 63 | print(customSearch.result()) 64 | 65 | 66 | 67 | 68 | ''' 69 | Getting search results from the next pages on YouTube. 70 | Generally you'll get maximum of 20 videos in one search, for getting subsequent results, you may call `next` method. 71 | ''' 72 | search = VideosSearch('NoCopyrightSounds') 73 | index = 0 74 | for video in search.result()['result']: 75 | print(str(index) + ' - ' + video['title']) 76 | index += 1 77 | '''Getting result on 2nd page.''' 78 | search.next() 79 | for video in search.result()['result']: 80 | print(str(index) + ' - ' + video['title']) 81 | index += 1 82 | '''Getting result on 3rd page.''' 83 | search.next() 84 | for video in search.result()['result']: 85 | print(str(index) + ' - ' + video['title']) 86 | index += 1 87 | 88 | 89 | 90 | 91 | ''' 92 | Getting information about playlist or videos in it using its link. 93 | 94 | `Playlist.get` method will give both information & formats of the playlist 95 | `Playlist.getInfo` method will give only information about the playlist. 96 | `Playlist.getVideos` method will give only videos in the playlist. 97 | 98 | ''' 99 | playlist = Playlist.get('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK', mode = ResultMode.json) 100 | print(playlist) 101 | playlistInfo = Playlist.getInfo('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK', mode = ResultMode.json) 102 | print(playlistInfo) 103 | playlistVideos = Playlist.getVideos('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 104 | print(playlistVideos) 105 | 106 | 107 | ''' 108 | More tests to buggy Playlist class 109 | ''' 110 | playlist = Playlist.get('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK', mode = ResultMode.json) 111 | print(playlist) 112 | playlist = Playlist.get('https://www.youtube.com/watch?v=bplUXwTTgbI&list=PL6edxAMqu2xfxgbf7Q09hSg1qCMfDI7IZ', mode = ResultMode.json) 113 | print(playlist) 114 | 115 | 116 | playlist = Playlist('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 117 | 118 | print(f'Videos Retrieved: {len(playlist.videos)}') 119 | 120 | while playlist.hasMoreVideos: 121 | print('Getting more videos...') 122 | playlist.getNextVideos() 123 | print(f'Videos Retrieved: {len(playlist.videos)}') 124 | 125 | print('Found all the videos.') 126 | 127 | 128 | 129 | 130 | ''' 131 | Getting information about video or its formats using video link or video ID. 132 | 133 | `Video.get` method will give both information & formats of the video 134 | `Video.getInfo` method will give only information about the video. 135 | `Video.getFormats` method will give only formats of the video. 136 | 137 | You may either pass link or ID, method will take care itself. 138 | 139 | YouTube doesn't provide uploadDate and publishDate in its InnerTube API, thus we have to use HTML requests to get it. 140 | This is disabled by default as it is very inefficient, but if you really need it, you can explicitly set parameter to Video class: enableHTML=True 141 | ''' 142 | video = Video.get('https://www.youtube.com/watch?v=z0GKGpObgPY', mode = ResultMode.json, get_upload_date=True) 143 | print(video) 144 | videoInfo = Video.getInfo('https://youtu.be/z0GKGpObgPY', mode = ResultMode.json) 145 | print(videoInfo) 146 | videoFormats = Video.getFormats('z0GKGpObgPY') 147 | print(videoFormats) 148 | 149 | 150 | 151 | 152 | ''' 153 | Getting search suggestions from YouTube. 154 | You may show search suggestions to users before making any search. 155 | ''' 156 | suggestions = Suggestions(language = 'en', region = 'US') 157 | print(suggestions.get('NoCopyrightSounds', mode = ResultMode.json)) 158 | 159 | 160 | 161 | 162 | ''' 163 | Getting videos by hashtag. 164 | ''' 165 | hashtag = Hashtag('ncs', limit = 1) 166 | print(hashtag.result()) 167 | 168 | 169 | 170 | 171 | channel = ChannelSearch("Watermelon Sugar", "UCZFWPqqPkFlNwIxcpsLOwew") 172 | 173 | print(channel.result(mode=ResultMode.json)) 174 | 175 | 176 | 177 | 178 | ''' 179 | Getting direct stream URL for a video. 180 | You may show search suggestions to users before making any search. 181 | 182 | To use this, you must have PyTube installed. 183 | StreamURLFetcher can fetch direct video URLs without any additional network requests (that's really fast). 184 | Call `get` or `getAll` method of StreamURLFetcher & pass response returned by `Video.get` or `Video.getFormats` as parameter to fetch direct URLs. 185 | Getting URLs or downloading streams using youtube-dl or PyTube is can be a slow, because of the fact that they make requests to fetch the same content, which one might have already recieved at the time of showing it to the user etc. 186 | StreamURLFetcher makes use of PyTube (if installed) & makes some slight improvements to functioning of PyTube. 187 | Avoid instantiating StreamURLFetcher more than once, it will be slow (making global object of the class will be a recommended solution). 188 | 189 | `get` method can be handy for getting URL of a particular kind. `getAll` returns all stream URLs in a dictionary. 190 | ''' 191 | 192 | ''' 193 | Instantiate the class (do it only once). 194 | ''' 195 | fetcher = StreamURLFetcher() 196 | 197 | ''' 198 | Get video information. 199 | ''' 200 | videoA = Video.get("https://www.youtube.com/watch?v=aqz-KE-bpKQ") 201 | videoB = Video.get("https://www.youtube.com/watch?v=ZwNxYJfW-eU") 202 | 203 | ''' 204 | Get direct stream URLs without any web requests. 205 | ''' 206 | singleUrlA = fetcher.get(videoA, 22) 207 | allUrlsB = fetcher.getAll(videoB) 208 | print(singleUrlA) 209 | print(allUrlsB) 210 | 211 | 212 | 213 | comments = Comments("_ZdsmLgCVdU") 214 | 215 | print(len(comments.comments["result"])) 216 | 217 | while len(comments.comments["result"]) < 100: 218 | comments.getNextComments() 219 | print(len(comments.comments["result"])) 220 | print("Found all comments") 221 | 222 | 223 | 224 | print(Transcript.get("https://www.youtube.com/watch?v=L7kF4MXXCoA")) 225 | 226 | 227 | url = "https://www.youtube.com/watch?v=-1xu0IP35FI" 228 | 229 | transcript_en = Transcript.get(url) 230 | # you actually don't have to pass a valid URL in following Transcript call. You can input an empty string, but I do recommend still inputing a valid URL. 231 | transcript_2 = Transcript.get(url, transcript_en["languages"][-1]["params"]) # in my case, it'd output Spanish. 232 | print(transcript_2) 233 | 234 | 235 | print(Channel.get("UC_aEa8K-EOJ3D6gOs7HcyNg")) 236 | 237 | 238 | # Retrieve playlists of a channel 239 | channel = Channel("UC_aEa8K-EOJ3D6gOs7HcyNg") 240 | print(len(channel.result["playlists"])) 241 | while channel.has_more_playlists(): 242 | channel.next() 243 | print(len(channel.result["playlists"])) 244 | 245 | 246 | 247 | ''' 248 | You may add/omit the optional parameters according to your requirement & use case. 249 | ''' 250 | 251 | 252 | ''' 253 | Thanks for your support & love! 254 | 255 | - github.com/alexmercerind 256 | ''' -------------------------------------------------------------------------------- /tests/async/extras.py: -------------------------------------------------------------------------------- 1 | from youtubesearchpython.__future__ import * 2 | import asyncio 3 | 4 | async def main(): 5 | video = await Video.get('https://www.youtube.com/watch?v=z0GKGpObgPY', get_upload_date=True) 6 | print(video) 7 | videoInfo = await Video.getInfo('https://youtu.be/z0GKGpObgPY') 8 | print(videoInfo) 9 | videoFormats = await Video.getFormats('z0GKGpObgPY') 10 | print(videoFormats) 11 | 12 | 13 | suggestions = await Suggestions.get('NoCopyrightSounds', language = 'en', region = 'US') 14 | print(suggestions) 15 | 16 | 17 | hashtag = Hashtag('ncs', limit = 1) 18 | result = await hashtag.next() 19 | print(result) 20 | 21 | 22 | fetcher = StreamURLFetcher() 23 | await fetcher.getJavaScript() 24 | videoA = await Video.get("https://www.youtube.com/watch?v=aqz-KE-bpKQ") 25 | videoB = await Video.get("https://www.youtube.com/watch?v=ZwNxYJfW-eU") 26 | singleUrlA = await fetcher.get(videoA, 22) 27 | allUrlsB = await fetcher.getAll(videoB) 28 | print(singleUrlA) 29 | print(allUrlsB) 30 | 31 | 32 | comments = Comments("_ZdsmLgCVdU") 33 | await comments.getNextComments() 34 | while len(comments.comments["result"]) < 100: 35 | print(len(comments.comments["result"])) 36 | await comments.getNextComments() 37 | print("Found all comments") 38 | 39 | 40 | print(await Transcript.get("https://www.youtube.com/watch?v=L7kF4MXXCoA")) 41 | 42 | 43 | url = "https://www.youtube.com/watch?v=-1xu0IP35FI" 44 | 45 | transcript_en = await Transcript.get(url) 46 | transcript_2 = await Transcript.get(url, transcript_en["languages"][-1]["params"]) # in my case, it'd output Spanish. 47 | print(transcript_2) 48 | 49 | 50 | print(await Channel.get("UC_aEa8K-EOJ3D6gOs7HcyNg")) 51 | 52 | # Retrieve playlists of a channel 53 | channel = Channel("UC_aEa8K-EOJ3D6gOs7HcyNg") 54 | await channel.init() 55 | print(len(channel.result["playlists"])) 56 | while channel.has_more_playlists(): 57 | await channel.next() 58 | print(len(channel.result["playlists"])) 59 | 60 | 61 | asyncio.run(main()) 62 | -------------------------------------------------------------------------------- /tests/async/playlists.py: -------------------------------------------------------------------------------- 1 | from youtubesearchpython.__future__ import * 2 | import asyncio 3 | 4 | async def main(): 5 | playlist = await Playlist.get('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 6 | print(playlist) 7 | playlistInfo = await Playlist.getInfo('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 8 | print(playlistInfo) 9 | playlistVideos = await Playlist.getVideos('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 10 | print(playlistVideos) 11 | 12 | 13 | playlist = await Playlist.get('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 14 | print(playlist) 15 | playlist = await Playlist.get('https://www.youtube.com/watch?v=bplUXwTTgbI&list=PL6edxAMqu2xfxgbf7Q09hSg1qCMfDI7IZ') 16 | print(playlist) 17 | 18 | 19 | asyncio.run(main()) 20 | -------------------------------------------------------------------------------- /tests/async/search.py: -------------------------------------------------------------------------------- 1 | from youtubesearchpython.__future__ import * 2 | import asyncio 3 | 4 | async def main(): 5 | search = Search('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 6 | result = await search.next() 7 | print(result) 8 | 9 | 10 | videosSearch = VideosSearch('NoCopyrightSounds', limit = 10, language = 'en', region = 'US') 11 | videosResult = await videosSearch.next() 12 | print(videosResult) 13 | 14 | 15 | channelsSearch = ChannelsSearch('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 16 | channelsResult = await channelsSearch.next() 17 | print(channelsResult) 18 | 19 | 20 | playlistsSearch = PlaylistsSearch('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 21 | playlistsResult = await playlistsSearch.next() 22 | print(playlistsResult) 23 | 24 | 25 | customSearch = CustomSearch('NoCopyrightSounds', VideoSortOrder.uploadDate, language = 'en', region = 'US') 26 | customResult = await customSearch.next() 27 | print(customResult) 28 | 29 | 30 | search = ChannelSearch('Watermelon Sugar', "UCZFWPqqPkFlNwIxcpsLOwew") 31 | result = await search.next() 32 | print(result) 33 | 34 | channel = ChannelSearch('The Beatles - Topic', 'UC2XdaAVUannpujzv32jcouQ') 35 | result = await channel.next() 36 | print(result) 37 | 38 | """ 39 | channel = ChannelPlaylistSearch('PewDiePie', 'UC-lHJZR3Gqxm24_Vd_AJ5Yw') 40 | result = await channel.next() 41 | print(result) 42 | 43 | channel = ChannelPlaylistSearch('The Beatles - Topic', 'UC2XdaAVUannpujzv32jcouQ') 44 | result = await channel.next() 45 | print(result) 46 | """ 47 | 48 | 49 | search = VideosSearch('NoCopyrightSounds') 50 | index = 0 51 | result = await search.next() 52 | for video in result['result']: 53 | index += 1 54 | print(f'{index} - {video["title"]}') 55 | result = await search.next() 56 | for video in result['result']: 57 | index += 1 58 | print(f'{index} - {video["title"]}') 59 | result = await search.next() 60 | for video in result['result']: 61 | index += 1 62 | print(f'{index} - {video["title"]}') 63 | 64 | 65 | asyncio.run(main()) 66 | -------------------------------------------------------------------------------- /tests/sync/extras.py: -------------------------------------------------------------------------------- 1 | from youtubesearchpython import * 2 | 3 | 4 | video = Video.get('https://www.youtube.com/watch?v=z0GKGpObgPY', mode = ResultMode.json, get_upload_date=True) 5 | print(video) 6 | videoInfo = Video.getInfo('https://youtu.be/z0GKGpObgPY', mode = ResultMode.json) 7 | print(videoInfo) 8 | videoFormats = Video.getFormats('z0GKGpObgPY') 9 | print(videoFormats) 10 | 11 | 12 | suggestions = Suggestions(language = 'en', region = 'US') 13 | print(suggestions.get('NoCopyrightSounds', mode = ResultMode.json)) 14 | 15 | 16 | hashtag = Hashtag('ncs', limit = 1) 17 | print(hashtag.result()) 18 | 19 | 20 | fetcher = StreamURLFetcher() 21 | videoA = Video.get("https://www.youtube.com/watch?v=aqz-KE-bpKQ") 22 | videoB = Video.get("https://www.youtube.com/watch?v=ZwNxYJfW-eU") 23 | 24 | singleUrlA = fetcher.get(videoA, 22) 25 | allUrlsB = fetcher.getAll(videoB) 26 | print(singleUrlA) 27 | print(allUrlsB) 28 | 29 | 30 | comments = Comments("_ZdsmLgCVdU") 31 | 32 | print(len(comments.comments["result"])) 33 | 34 | while len(comments.comments["result"]) < 100: 35 | comments.getNextComments() 36 | print(len(comments.comments["result"])) 37 | print("Found all comments") 38 | 39 | 40 | print(Transcript.get("https://www.youtube.com/watch?v=L7kF4MXXCoA")) 41 | 42 | 43 | url = "https://www.youtube.com/watch?v=-1xu0IP35FI" 44 | 45 | transcript_en = Transcript.get(url) 46 | transcript_2 = Transcript.get(url, transcript_en["languages"][-1]["params"]) # in my case, it'd output Spanish. 47 | print(transcript_2) 48 | 49 | 50 | print(Channel.get("UC_aEa8K-EOJ3D6gOs7HcyNg")) 51 | 52 | 53 | # Retrieve playlists of a channel 54 | channel = Channel("UC_aEa8K-EOJ3D6gOs7HcyNg") 55 | print(len(channel.result["playlists"])) 56 | while channel.has_more_playlists(): 57 | channel.next() 58 | print(len(channel.result["playlists"])) 59 | -------------------------------------------------------------------------------- /tests/sync/playlists.py: -------------------------------------------------------------------------------- 1 | from youtubesearchpython import * 2 | 3 | playlist = Playlist.get('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK', mode = ResultMode.json) 4 | print(playlist) 5 | playlistInfo = Playlist.getInfo('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK', mode = ResultMode.json) 6 | print(playlistInfo) 7 | playlistVideos = Playlist.getVideos('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 8 | print(playlistVideos) 9 | 10 | 11 | 12 | 13 | playlist = Playlist.get('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK', mode = ResultMode.json) 14 | print(playlist) 15 | playlist = Playlist.get('https://www.youtube.com/watch?v=bplUXwTTgbI&list=PL6edxAMqu2xfxgbf7Q09hSg1qCMfDI7IZ', mode = ResultMode.json) 16 | print(playlist) 17 | 18 | 19 | 20 | playlist = Playlist('https://www.youtube.com/playlist?list=PLRBp0Fe2GpgmsW46rJyudVFlY6IYjFBIK') 21 | print(f'Videos Retrieved: {len(playlist.videos)}') 22 | while playlist.hasMoreVideos: 23 | print('Getting more videos...') 24 | playlist.getNextVideos() 25 | print(f'Videos Retrieved: {len(playlist.videos)}') 26 | 27 | print('Found all the videos.') 28 | 29 | -------------------------------------------------------------------------------- /tests/sync/search.py: -------------------------------------------------------------------------------- 1 | from youtubesearchpython import * 2 | 3 | 4 | allSearch = Search('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 5 | print(allSearch.result()) 6 | 7 | 8 | videosSearch = VideosSearch('NoCopyrightSounds', limit = 10, language = 'en', region = 'US') 9 | print(videosSearch.result(mode = ResultMode.json)) 10 | 11 | 12 | channelsSearch = ChannelsSearch('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 13 | print(channelsSearch.result(mode = ResultMode.json)) 14 | 15 | 16 | playlistsSearch = PlaylistsSearch('NoCopyrightSounds', limit = 1, language = 'en', region = 'US') 17 | print(playlistsSearch.result()) 18 | 19 | 20 | customSearch = CustomSearch('NoCopyrightSounds', VideoSortOrder.uploadDate, language = 'en', region = 'US') 21 | print(customSearch.result()) 22 | 23 | 24 | search = VideosSearch('NoCopyrightSounds') 25 | index = 0 26 | for video in search.result()['result']: 27 | print(str(index) + ' - ' + video['title']) 28 | index += 1 29 | search.next() 30 | for video in search.result()['result']: 31 | print(str(index) + ' - ' + video['title']) 32 | index += 1 33 | search.next() 34 | for video in search.result()['result']: 35 | print(str(index) + ' - ' + video['title']) 36 | index += 1 37 | 38 | 39 | 40 | channel = ChannelSearch("Watermelon Sugar", "UCZFWPqqPkFlNwIxcpsLOwew") 41 | print(channel.result(mode=ResultMode.json)) 42 | 43 | channel = ChannelSearch('The Beatles - Topic', 'UC2XdaAVUannpujzv32jcouQ') 44 | print(channel.result(mode=ResultMode.json)) 45 | 46 | #channel = ChannelPlaylistSearch('PewDiePie', 'UC-lHJZR3Gqxm24_Vd_AJ5Yw') 47 | #print(channel.result()) 48 | 49 | #channel = ChannelPlaylistSearch('The Beatles - Topic', 'UC2XdaAVUannpujzv32jcouQ') 50 | #print(channel.result()) 51 | -------------------------------------------------------------------------------- /youtubesearchpython/__future__/__init__.py: -------------------------------------------------------------------------------- 1 | from youtubesearchpython.__future__.search import Search, VideosSearch, ChannelsSearch, PlaylistsSearch, CustomSearch, ChannelSearch 2 | from youtubesearchpython.__future__.extras import Video, Playlist, Suggestions, Hashtag, Comments, Transcript, Channel 3 | from youtubesearchpython.__future__.streamurlfetcher import StreamURLFetcher 4 | from youtubesearchpython.core.utils import * 5 | from youtubesearchpython.core.constants import * 6 | 7 | 8 | __title__ = 'youtube-search-python' 9 | __version__ = '1.6.2' 10 | __author__ = 'alexmercerind' 11 | __license__ = 'MIT' 12 | -------------------------------------------------------------------------------- /youtubesearchpython/__future__/search.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional 2 | 3 | from youtubesearchpython.core.channelsearch import ChannelSearchCore 4 | from youtubesearchpython.core.constants import * 5 | from youtubesearchpython.core.search import SearchCore 6 | 7 | 8 | class Search(SearchCore): 9 | '''Searches for videos, channels & playlists in YouTube. 10 | 11 | Args: 12 | query (str): Sets the search query. 13 | limit (int, optional): Sets limit to the number of results. Defaults to 20. 14 | language (str, optional): Sets the result language. Defaults to 'en'. 15 | region (str, optional): Sets the result region. Defaults to 'US'. 16 | 17 | Examples: 18 | Calling `result` method gives the search result. 19 | 20 | >>> search = Search('Watermelon Sugar', limit = 1) 21 | >>> result = await search.next() 22 | >>> print(result) 23 | { 24 | "result": [ 25 | { 26 | "type": "video", 27 | "id": "E07s5ZYygMg", 28 | "title": "Harry Styles - Watermelon Sugar (Official Video)", 29 | "publishedTime": "6 months ago", 30 | "duration": "3:09", 31 | "viewCount": { 32 | "text": "162,235,006 views", 33 | "short": "162M views" 34 | }, 35 | "thumbnails": [ 36 | { 37 | "url": "https://i.ytimg.com/vi/E07s5ZYygMg/hq720.jpg?sqp=-oaymwEjCOgCEMoBSFryq4qpAxUIARUAAAAAGAElAADIQj0AgKJDeAE=&rs=AOn4CLAOWBTE1SDrtrDQ1aWNzpDZ7YiMIw", 38 | "width": 360, 39 | "height": 202 40 | }, 41 | { 42 | "url": "https://i.ytimg.com/vi/E07s5ZYygMg/hq720.jpg?sqp=-oaymwEXCNAFEJQDSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLD7U54pGZLPKTuMP-J3kpm4LIDPVg", 43 | "width": 720, 44 | "height": 404 45 | } 46 | ], 47 | "descriptionSnippet": [ 48 | { 49 | "text": "This video is dedicated to touching. Listen to Harry Styles' new album 'Fine Line' now: https://HStyles.lnk.to/FineLineAY Follow\u00a0..." 50 | } 51 | ], 52 | "channel": { 53 | "name": "Harry Styles", 54 | "id": "UCZFWPqqPkFlNwIxcpsLOwew", 55 | "thumbnails": [ 56 | { 57 | "url": "https://yt3.ggpht.com/a-/AOh14GgNUvHxwlnz4RpHamcGnZF1px13VHj01TPksw=s68-c-k-c0x00ffffff-no-rj-mo", 58 | "width": 68, 59 | "height": 68 60 | } 61 | ], 62 | "link": "https://www.youtube.com/channel/UCZFWPqqPkFlNwIxcpsLOwew" 63 | }, 64 | "accessibility": { 65 | "title": "Harry Styles - Watermelon Sugar (Official Video) by Harry Styles 6 months ago 3 minutes, 9 seconds 162,235,006 views", 66 | "duration": "3 minutes, 9 seconds" 67 | }, 68 | "link": "https://www.youtube.com/watch?v=E07s5ZYygMg", 69 | "shelfTitle": null 70 | } 71 | ] 72 | } 73 | ''' 74 | def __init__(self, query: str, limit: int = 20, language: str = 'en', region: str = 'US', timeout: Optional[int] = None): 75 | self.searchMode = (True, True, True) 76 | super().__init__(query, limit, language, region, None, timeout) # type: ignore 77 | 78 | async def next(self) -> Dict[str, Any]: 79 | return await self._nextAsync() # type: ignore 80 | 81 | 82 | class VideosSearch(SearchCore): 83 | '''Searches for videos in YouTube. 84 | 85 | Args: 86 | query (str): Sets the search query. 87 | limit (int, optional): Sets limit to the number of results. Defaults to 20. 88 | language (str, optional): Sets the result language. Defaults to 'en'. 89 | region (str, optional): Sets the result region. Defaults to 'US'. 90 | 91 | Examples: 92 | Calling `result` method gives the search result. 93 | 94 | >>> search = VideosSearch('Watermelon Sugar', limit = 1) 95 | >>> result = await search.next() 96 | >>> print(result) 97 | { 98 | "result": [ 99 | { 100 | "type": "video", 101 | "id": "E07s5ZYygMg", 102 | "title": "Harry Styles - Watermelon Sugar (Official Video)", 103 | "publishedTime": "6 months ago", 104 | "duration": "3:09", 105 | "viewCount": { 106 | "text": "162,235,006 views", 107 | "short": "162M views" 108 | }, 109 | "thumbnails": [ 110 | { 111 | "url": "https://i.ytimg.com/vi/E07s5ZYygMg/hq720.jpg?sqp=-oaymwEjCOgCEMoBSFryq4qpAxUIARUAAAAAGAElAADIQj0AgKJDeAE=&rs=AOn4CLAOWBTE1SDrtrDQ1aWNzpDZ7YiMIw", 112 | "width": 360, 113 | "height": 202 114 | }, 115 | { 116 | "url": "https://i.ytimg.com/vi/E07s5ZYygMg/hq720.jpg?sqp=-oaymwEXCNAFEJQDSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLD7U54pGZLPKTuMP-J3kpm4LIDPVg", 117 | "width": 720, 118 | "height": 404 119 | } 120 | ], 121 | "descriptionSnippet": [ 122 | { 123 | "text": "This video is dedicated to touching. Listen to Harry Styles' new album 'Fine Line' now: https://HStyles.lnk.to/FineLineAY Follow\u00a0..." 124 | } 125 | ], 126 | "channel": { 127 | "name": "Harry Styles", 128 | "id": "UCZFWPqqPkFlNwIxcpsLOwew", 129 | "thumbnails": [ 130 | { 131 | "url": "https://yt3.ggpht.com/a-/AOh14GgNUvHxwlnz4RpHamcGnZF1px13VHj01TPksw=s68-c-k-c0x00ffffff-no-rj-mo", 132 | "width": 68, 133 | "height": 68 134 | } 135 | ], 136 | "link": "https://www.youtube.com/channel/UCZFWPqqPkFlNwIxcpsLOwew" 137 | }, 138 | "accessibility": { 139 | "title": "Harry Styles - Watermelon Sugar (Official Video) by Harry Styles 6 months ago 3 minutes, 9 seconds 162,235,006 views", 140 | "duration": "3 minutes, 9 seconds" 141 | }, 142 | "link": "https://www.youtube.com/watch?v=E07s5ZYygMg", 143 | "shelfTitle": null 144 | } 145 | ] 146 | } 147 | ''' 148 | def __init__(self, query: str, limit: int = 20, language: str = 'en', region: str = 'US', timeout: Optional[int] = None): 149 | self.searchMode = (True, False, False) 150 | super().__init__(query, limit, language, region, SearchMode.videos, timeout) # type: ignore 151 | 152 | async def next(self) -> Dict[str, Any]: 153 | return await self._nextAsync() # type: ignore 154 | 155 | 156 | class ChannelsSearch(SearchCore): 157 | '''Searches for channels in YouTube. 158 | 159 | Args: 160 | query (str): Sets the search query. 161 | limit (int, optional): Sets limit to the number of results. Defaults to 20. 162 | language (str, optional): Sets the result language. Defaults to 'en'. 163 | region (str, optional): Sets the result region. Defaults to 'US'. 164 | 165 | Examples: 166 | Calling `result` method gives the search result. 167 | 168 | >>> search = ChannelsSearch('Harry Styles', limit = 1) 169 | >>> result = await search.next() 170 | >>> print(result) 171 | { 172 | "result": [ 173 | { 174 | "type": "channel", 175 | "id": "UCZFWPqqPkFlNwIxcpsLOwew", 176 | "title": "Harry Styles", 177 | "thumbnails": [ 178 | { 179 | "url": "https://yt3.ggpht.com/ytc/AAUvwnhR81ocC_KalYEk5ItnJcfMBqaiIpuM1B0lJyg4Rw=s88-c-k-c0x00ffffff-no-rj-mo", 180 | "width": 88, 181 | "height": 88 182 | }, 183 | { 184 | "url": "https://yt3.ggpht.com/ytc/AAUvwnhR81ocC_KalYEk5ItnJcfMBqaiIpuM1B0lJyg4Rw=s176-c-k-c0x00ffffff-no-rj-mo", 185 | "width": 176, 186 | "height": 176 187 | } 188 | ], 189 | "videoCount": "7", 190 | "descriptionSnippet": null, 191 | "subscribers": "9.25M subscribers", 192 | "link": "https://www.youtube.com/channel/UCZFWPqqPkFlNwIxcpsLOwew" 193 | } 194 | ] 195 | } 196 | ''' 197 | def __init__(self, query: str, limit: int = 20, language: str = 'en', region: str = 'US', timeout: Optional[int] = None): 198 | self.searchMode = (False, True, False) 199 | super().__init__(query, limit, language, region, SearchMode.channels, timeout) # type: ignore 200 | 201 | async def next(self) -> Dict[str, Any]: 202 | return await self._nextAsync() # type: ignore 203 | 204 | 205 | class PlaylistsSearch(SearchCore): 206 | '''Searches for playlists in YouTube. 207 | 208 | Args: 209 | query (str): Sets the search query. 210 | limit (int, optional): Sets limit to the number of results. Defaults to 20. 211 | language (str, optional): Sets the result language. Defaults to 'en'. 212 | region (str, optional): Sets the result region. Defaults to 'US'. 213 | 214 | Examples: 215 | Calling `result` method gives the search result. 216 | 217 | >>> search = PlaylistsSearch('Harry Styles', limit = 1) 218 | >>> result = await search.next() 219 | >>> print(result) 220 | { 221 | "result": [ 222 | { 223 | "type": "playlist", 224 | "id": "PL-Rt4gIwHnyvxpEl-9Le0ePztR7WxGDGV", 225 | "title": "fine line harry styles full album lyrics", 226 | "videoCount": "12", 227 | "channel": { 228 | "name": "ourmemoriestonight", 229 | "id": "UCZCmb5a8LE9LMxW9I3-BFjA", 230 | "link": "https://www.youtube.com/channel/UCZCmb5a8LE9LMxW9I3-BFjA" 231 | }, 232 | "thumbnails": [ 233 | { 234 | "url": "https://i.ytimg.com/vi/raTh8Mu5oyM/hqdefault.jpg?sqp=-oaymwEWCKgBEF5IWvKriqkDCQgBFQAAiEIYAQ==&rs=AOn4CLCdCfOQYMrPImHMObdrMcNimKi1PA", 235 | "width": 168, 236 | "height": 94 237 | }, 238 | { 239 | "url": "https://i.ytimg.com/vi/raTh8Mu5oyM/hqdefault.jpg?sqp=-oaymwEWCMQBEG5IWvKriqkDCQgBFQAAiEIYAQ==&rs=AOn4CLDsKmyGH8bkmt9MzZqIoXI4UaduBw", 240 | "width": 196, 241 | "height": 110 242 | }, 243 | { 244 | "url": "https://i.ytimg.com/vi/raTh8Mu5oyM/hqdefault.jpg?sqp=-oaymwEXCPYBEIoBSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLD9v7S0KeHLBLr0bF-LrRjYVycUFA", 245 | "width": 246, 246 | "height": 138 247 | }, 248 | { 249 | "url": "https://i.ytimg.com/vi/raTh8Mu5oyM/hqdefault.jpg?sqp=-oaymwEXCNACELwBSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAIzQIVxZsC0PfvLOt-v9UWJ-109Q", 250 | "width": 336, 251 | "height": 188 252 | } 253 | ], 254 | "link": "https://www.youtube.com/playlist?list=PL-Rt4gIwHnyvxpEl-9Le0ePztR7WxGDGV" 255 | } 256 | ] 257 | } 258 | ''' 259 | def __init__(self, query: str, limit: int = 20, language: str = 'en', region: str = 'US', timeout: Optional[int] = None): 260 | self.searchMode = (False, False, True) 261 | super().__init__(query, limit, language, region, SearchMode.playlists, timeout) # type: ignore 262 | 263 | async def next(self) -> Dict[str, Any]: 264 | return await self._nextAsync() # type: ignore 265 | 266 | class CustomSearch(SearchCore): 267 | '''Performs custom search in YouTube with search filters or sorting orders. 268 | Few of the predefined filters and sorting orders are: 269 | 270 | 1 - SearchMode.videos 271 | 2 - VideoUploadDateFilter.lastHour 272 | 3 - VideoDurationFilter.long 273 | 4 - VideoSortOrder.viewCount 274 | 275 | There are many other to use. 276 | The value of `sp` parameter in the YouTube search query can be used as a search filter e.g. 277 | `EgQIBRAB` from https://www.youtube.com/results?search_query=NoCopyrightSounds&sp=EgQIBRAB can be passed as `searchPreferences`, to get videos, which are uploaded this year. 278 | 279 | Args: 280 | query (str): Sets the search query. 281 | searchPreferences (str): Sets the `sp` query parameter in the YouTube search request. 282 | limit (int, optional): Sets limit to the number of results. Defaults to 20. 283 | language (str, optional): Sets the result language. Defaults to 'en'. 284 | region (str, optional): Sets the result region. Defaults to 'US'. 285 | 286 | Examples: 287 | Calling `result` method gives the search result. 288 | 289 | >>> search = CustomSearch('Harry Styles', VideoSortOrder.viewCount, limit = 1) 290 | >>> result = await search.next() 291 | >>> print(result) 292 | { 293 | "result": [ 294 | { 295 | "type": "video", 296 | "id": "QJO3ROT-A4E", 297 | "title": "One Direction - What Makes You Beautiful (Official Video)", 298 | "publishedTime": "9 years ago", 299 | "duration": "3:27", 300 | "viewCount": { 301 | "text": "1,212,146,802 views", 302 | "short": "1.2B views" 303 | }, 304 | "thumbnails": [ 305 | { 306 | "url": "https://i.ytimg.com/vi/QJO3ROT-A4E/hq720.jpg?sqp=-oaymwEjCOgCEMoBSFryq4qpAxUIARUAAAAAGAElAADIQj0AgKJDeAE=&rs=AOn4CLDeFKrH99gmpnvKyG4czdd__YRDkw", 307 | "width": 360, 308 | "height": 202 309 | }, 310 | { 311 | "url": "https://i.ytimg.com/vi/QJO3ROT-A4E/hq720.jpg?sqp=-oaymwEXCNAFEJQDSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBJ_wUjsRFXGsbvRpwYpSLlsGmbkw", 312 | "width": 720, 313 | "height": 404 314 | } 315 | ], 316 | "descriptionSnippet": [ 317 | { 318 | "text": "One Direction \u2013 What Makes You Beautiful (Official Video) Follow on Spotify - https://1D.lnk.to/Spotify Listen on Apple Music\u00a0..." 319 | } 320 | ], 321 | "channel": { 322 | "name": "One Direction", 323 | "id": "UCb2HGwORFBo94DmRx4oLzow", 324 | "thumbnails": [ 325 | { 326 | "url": "https://yt3.ggpht.com/a-/AOh14Gj3SMvtIAvVNUrHWFTJFubPN7qozzPl5gFkoA=s68-c-k-c0x00ffffff-no-rj-mo", 327 | "width": 68, 328 | "height": 68 329 | } 330 | ], 331 | "link": "https://www.youtube.com/channel/UCb2HGwORFBo94DmRx4oLzow" 332 | }, 333 | "accessibility": { 334 | "title": "One Direction - What Makes You Beautiful (Official Video) by One Direction 9 years ago 3 minutes, 27 seconds 1,212,146,802 views", 335 | "duration": "3 minutes, 27 seconds" 336 | }, 337 | "link": "https://www.youtube.com/watch?v=QJO3ROT-A4E", 338 | "shelfTitle": null 339 | } 340 | ] 341 | } 342 | ''' 343 | def __init__(self, query: str, searchPreferences: str, limit: int = 20, language: str = 'en', region: str = 'US', timeout: Optional[int] = None): 344 | self.searchMode = (True, True, True) 345 | super().__init__(query, limit, language, region, searchPreferences, timeout) # type: ignore 346 | 347 | async def next(self) -> Dict[str, Any]: 348 | return await self._nextAsync() # type: ignore 349 | 350 | class ChannelSearch(ChannelSearchCore): 351 | '''Searches for videos in specific channel in YouTube. 352 | 353 | Args: 354 | query (str): Sets the search query. 355 | browseId (str): Channel ID 356 | language (str, optional): Sets the result language. Defaults to 'en'. 357 | region (str, optional): Sets the result region. Defaults to 'US'. 358 | 359 | Examples: 360 | Calling `result` method gives the search result. 361 | 362 | >>> search = ChannelSearch('Watermelon Sugar', "UCZFWPqqPkFlNwIxcpsLOwew") 363 | >>> result = await search.next() 364 | >>> print(result) 365 | { 366 | "result": [ 367 | { 368 | "id": "WMcIfZuRuU8", 369 | "thumbnails": { 370 | "normal": [ 371 | { 372 | "url": "https://i.ytimg.com/vi/WMcIfZuRuU8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLClFg6C1r5NfTQy7TYUq6X5qHUmPA", 373 | "width": 168, 374 | "height": 94 375 | }, 376 | { 377 | "url": "https://i.ytimg.com/vi/WMcIfZuRuU8/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAoOyftwY0jLV4geWb5hejULYp3Zw", 378 | "width": 196, 379 | "height": 110 380 | }, 381 | { 382 | "url": "https://i.ytimg.com/vi/WMcIfZuRuU8/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCdqkhn7JDwLvRtTNx3jq-olz7k-Q", 383 | "width": 246, 384 | "height": 138 385 | }, 386 | { 387 | "url": "https://i.ytimg.com/vi/WMcIfZuRuU8/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAhYedsqBFKI0Ra2qzIv9cVoZhfKQ", 388 | "width": 336, 389 | "height": 188 390 | } 391 | ], 392 | "rich": null 393 | }, 394 | "title": "Harry Styles \u2013 Watermelon Sugar (Lost Tour Visual)", 395 | "descriptionSnippet": "This video is dedicated to touching.\nListen to Harry Styles\u2019 new album \u2018Fine Line\u2019 now: https://HStyles.lnk.to/FineLineAY \n\nFollow Harry Styles:\nFacebook: https://HarryStyles.lnk.to/followFI...", 396 | "uri": "/watch?v=WMcIfZuRuU8", 397 | "views": { 398 | "precise": "3,888,287 views", 399 | "simple": "3.8M views", 400 | "approximate": "3.8 million views" 401 | }, 402 | "duration": { 403 | "simpleText": "2:55", 404 | "text": "2 minutes, 55 seconds" 405 | }, 406 | "published": "10 months ago", 407 | "channel": { 408 | "name": "Harry Styles", 409 | "thumbnails": [ 410 | { 411 | "url": "https://yt3.ggpht.com/ytc/AAUvwnhR81ocC_KalYEk5ItnJcfMBqaiIpuM1B0lJyg4Rw=s88-c-k-c0x00ffffff-no-rj", 412 | "width": 68, 413 | "height": 68 414 | } 415 | ] 416 | }, 417 | "type": "video" 418 | }, 419 | ] 420 | } 421 | ''' 422 | 423 | def __init__(self, query: str, browseId: str, language: str = 'en', region: str = 'US', searchPreferences: str = "EgZzZWFyY2g%3D", timeout: Optional[int] = None): 424 | super().__init__(query, language, region, searchPreferences, browseId, timeout) # type: ignore 425 | -------------------------------------------------------------------------------- /youtubesearchpython/__future__/streamurlfetcher.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from youtubesearchpython.core.streamurlfetcher import StreamURLFetcherCore 3 | 4 | 5 | class StreamURLFetcher(StreamURLFetcherCore): 6 | '''Gets direct stream URLs for a YouTube video fetched using `Video.get` or `Video.getFormats`. 7 | 8 | This class can fetch direct video URLs without any additional network requests (that's really fast). 9 | 10 | Call `get` or `getAll` method of this class & pass response returned by `Video.get` or `Video.getFormats` as parameter to fetch direct URLs. 11 | Getting URLs or downloading streams using youtube-dl or PyTube is can be a slow, because of the fact that they make requests to fetch the same content, which one might have already recieved at the time of showing it to the user etc. 12 | This class makes use of PyTube (if installed) & makes some slight improvements to functioning of PyTube. 13 | 14 | Call `self.getJavaScript` method before any other method from this class. 15 | Do not call this method more than once & avoid reinstaciating the class. 16 | 17 | Raises: 18 | Exception: "ERROR: PyTube is not installed. To use this functionality of youtube-search-python, PyTube must be installed." 19 | 20 | Examples: 21 | Returns direct stream URL. 22 | 23 | >>> from youtubesearchpython import * 24 | >>> fetcher = StreamURLFetcher() 25 | >>> fetcher.getJavaScript() 26 | >>> video = Video.get("https://www.youtube.com/watch?v=aqz-KE-bpKQ") 27 | >>> url = fetcher.get(video, 251) 28 | >>> print(url) 29 | "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=251&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=audio%2Fwebm&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=10210834&dur=634.601&lmt=1544629945422176&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5511222&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgKKIEiwQTgXsdKPEyOckgVPs_LMH6KJoeaYmZic_lelECIHXHs1ZnSP5mgtpffNlIMJM3DhxcvDbA-4udFFE6AmVP&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D" 30 | ''' 31 | def __init__(self): 32 | super().__init__() 33 | 34 | async def get(self, videoFormats: dict, itag: int) -> Union[str, None]: 35 | '''Gets direct stream URL for a YouTube video fetched using `Video.get` or `Video.getFormats`. 36 | 37 | Args: 38 | videoFormats (dict): Dictionary returned by `Video.get` or `Video.getFormats`. 39 | itag (int): Itag of the required stream. 40 | 41 | Returns: 42 | Union[str, None]: Returns stream URL as string. None, if no stream is present for that itag. 43 | 44 | Examples: 45 | Returns direct stream URL. 46 | 47 | >>> from youtubesearchpython import * 48 | >>> fetcher = StreamURLFetcher() 49 | >>> await fetcher.getJavaScript() 50 | >>> video = await Video.get("https://www.youtube.com/watch?v=aqz-KE-bpKQ") 51 | >>> url = await fetcher.get(video, 251) 52 | >>> print(url) 53 | "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=251&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=audio%2Fwebm&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=10210834&dur=634.601&lmt=1544629945422176&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5511222&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgKKIEiwQTgXsdKPEyOckgVPs_LMH6KJoeaYmZic_lelECIHXHs1ZnSP5mgtpffNlIMJM3DhxcvDbA-4udFFE6AmVP&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D" 54 | ''' 55 | self._getDecipheredURLs(videoFormats, itag) 56 | if len(self._streams) == 1: 57 | return self._streams[0]["url"] 58 | return None 59 | 60 | async def getAll(self, videoFormats: dict) -> dict: 61 | '''Gets all stream URLs for a YouTube video fetched using `Video.get` or `Video.getFormats`. 62 | 63 | Args: 64 | videoFormats (dict): Dictionary returned by `Video.get` or `Video.getFormats`. 65 | 66 | Returns: 67 | Union[dict, None]: Returns stream URLs in a dictionary. 68 | 69 | Examples: 70 | Returns direct stream URLs in a dictionary. 71 | 72 | >>> from youtubesearchpython import * 73 | >>> fetcher = StreamURLFetcher() 74 | >>> await fetcher.getJavaScript() 75 | >>> video = await Video.get("https://www.youtube.com/watch?v=aqz-KE-bpKQ") 76 | >>> allURL = await fetcher.getAll(video) 77 | >>> print(allURL) 78 | { 79 | "streams": [ 80 | { 81 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=18&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=video%2Fmp4&ns=AAHB1CvhVqlATtzQj67WHI8F&gir=yes&clen=47526444&ratebypass=yes&dur=634.624&lmt=1544610273905877&mt=1610776131&fvip=6&c=WEB&txp=5531432&n=Laycu1cJ2fCN_K&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRQIgdjTwmtEc3MpmRxH27ZvTgktL-d2by5HXXGFwo3EGR4MCIQDi0oiI8mshGssiOFu1XzQCqljZuNLhA6z19S8Ig0CRTQ%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 82 | "type": "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", 83 | "quality": "medium", 84 | "itag": 18, 85 | "bitrate": 599167, 86 | "is_otf": false 87 | }, 88 | { 89 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=22&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=video%2Fmp4&ns=AAHB1CvhVqlATtzQj67WHI8F&ratebypass=yes&dur=634.624&lmt=1544610886483826&mt=1610776131&fvip=6&c=WEB&txp=5532432&n=Laycu1cJ2fCN_K&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRQIhALaSHkcx0m9rfqJKoiJT1dY7spIKf-zDfq12SOdN7Ej5AiBCgvcUvLUGqGoMBnc0NIQtDeNM8ETJD2lTt9Bi7T186g%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 90 | "type": "video/mp4; codecs=\"avc1.64001F, mp4a.40.2\"", 91 | "quality": "hd720", 92 | "itag": 22, 93 | "bitrate": 1340380, 94 | "is_otf": false 95 | }, 96 | { 97 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=315&aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C394%2C395%2C396%2C397%2C398%2C399&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=video%2Fwebm&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=1648069666&dur=634.566&lmt=1544611995945231&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5532432&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgGaJmx70EkBCsfAYOI1lI695hXnFSEn-ZAfRiqWrnt9ACIQClBT5YZlou5ttgFzKnLZkUKxjZznxMJGPTNvtXCAlebw%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 98 | "type": "video/webm; codecs=\"vp9\"", 99 | "quality": "hd2160", 100 | "itag": 315, 101 | "bitrate": 26416339, 102 | "is_otf": false 103 | }, 104 | { 105 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=308&aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C394%2C395%2C396%2C397%2C398%2C399&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=video%2Fwebm&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=627075264&dur=634.566&lmt=1544611159960793&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5532432&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhALl1_ksmnpBhD49Hgjdg-z-Y4H2AL8hBx63ephvsvhbCAiAFrqyy65MimA4mCXYQBopP67G9dtwH9xyjHS_0hZ-rJA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 106 | "type": "video/webm; codecs=\"vp9\"", 107 | "quality": "hd1440", 108 | "itag": 308, 109 | "bitrate": 13381315, 110 | "is_otf": false 111 | }, 112 | { 113 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=134&aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C394%2C395%2C396%2C397%2C398%2C399&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=video%2Fmp4&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=26072934&dur=634.566&lmt=1544609325917976&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5532432&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAKT9N5EmUz3OQOc9IA8P1CuYgzPStz4ulJvCkA8Y1Cf4AiEAwwC2mCjOFWD5jFhAu8g0O6EF5fYJ7HmwskN1sjqTHlA%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 114 | "type": "video/mp4; codecs=\"avc1.4d401e\"", 115 | "quality": "medium", 116 | "itag": 134, 117 | "bitrate": 723888, 118 | "is_otf": false 119 | }, 120 | { 121 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=249&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=audio%2Fwebm&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=3936299&dur=634.601&lmt=1544629945028066&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5511222&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAJ_UffgeslE26GFwlMZHBsW-zYLcnanMqrvESdjWoupYAiAH7KlvQlYsokTVCCcD7jflD21Fjiim28qNzhOKZ88D3Q%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 122 | "type": "audio/webm; codecs=\"opus\"", 123 | "quality": "tiny", 124 | "itag": 249, 125 | "bitrate": 57976, 126 | "is_otf": false 127 | }, 128 | { 129 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=258&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=audio%2Fmp4&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=30769612&dur=634.666&lmt=1544629837561969&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5511222&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAP6XrnFm3AHxyk8xjU6mJLdVN-uWLl1ItHk5_ONUiRuPAiEAlEYQBsOoEraFemkJIL7OMyHL9aszxW4CbDlxro-AY3Q%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 130 | "type": "audio/mp4; codecs=\"mp4a.40.2\"", 131 | "quality": "tiny", 132 | "itag": 258, 133 | "bitrate": 390017, 134 | "is_otf": false 135 | } 136 | ] 137 | } 138 | ''' 139 | self._getDecipheredURLs(videoFormats) 140 | return {"streams": self._streams} 141 | -------------------------------------------------------------------------------- /youtubesearchpython/__init__.py: -------------------------------------------------------------------------------- 1 | from youtubesearchpython.search import Search, VideosSearch, ChannelsSearch, PlaylistsSearch, CustomSearch, ChannelSearch 2 | from youtubesearchpython.extras import Video, Playlist, Suggestions, Hashtag, Comments, Transcript, Channel 3 | from youtubesearchpython.streamurlfetcher import StreamURLFetcher 4 | from youtubesearchpython.core.constants import * 5 | from youtubesearchpython.core.utils import * 6 | 7 | 8 | __title__ = 'youtube-search-python' 9 | __version__ = '1.6.2' 10 | __author__ = 'alexmercerind' 11 | __license__ = 'MIT' 12 | 13 | 14 | ''' Deprecated. Present for legacy support. ''' 15 | from youtubesearchpython.legacy import SearchVideos, SearchPlaylists 16 | from youtubesearchpython.legacy import SearchVideos as searchYoutube 17 | -------------------------------------------------------------------------------- /youtubesearchpython/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .video import VideoCore 2 | from .constants import * 3 | -------------------------------------------------------------------------------- /youtubesearchpython/core/channel.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | from typing import Union, List 4 | from urllib.parse import urlencode 5 | 6 | from youtubesearchpython.core.constants import * 7 | from youtubesearchpython.core.requests import RequestCore 8 | from youtubesearchpython.core.componenthandler import getValue, getVideoId 9 | 10 | 11 | class ChannelCore(RequestCore): 12 | def __init__(self, channel_id: str, request_params: str): 13 | super().__init__() 14 | self.browseId = channel_id 15 | self.params = request_params 16 | self.result = {} 17 | self.continuation = None 18 | 19 | def prepare_request(self): 20 | self.url = 'https://www.youtube.com/youtubei/v1/browse' + "?" + urlencode({ 21 | 'key': searchKey, 22 | "prettyPrint": "false" 23 | }) 24 | self.data = copy.deepcopy(requestPayload) 25 | if not self.continuation: 26 | self.data["params"] = self.params 27 | self.data["browseId"] = self.browseId 28 | else: 29 | self.data["continuation"] = self.continuation 30 | 31 | def playlist_parse(self, i) -> dict: 32 | return { 33 | "id": getValue(i, ["playlistId"]), 34 | "thumbnails": getValue(i, ["thumbnail", "thumbnails"]), 35 | "title": getValue(i, ["title", "runs", 0, "text"]), 36 | "videoCount": getValue(i, ["videoCountShortText", "simpleText"]), 37 | "lastEdited": getValue(i, ["publishedTimeText", "simpleText"]), 38 | } 39 | 40 | def parse_response(self): 41 | response = self.data.json() 42 | 43 | thumbnails = [] 44 | try: 45 | thumbnails.extend(getValue(response, ["header", "c4TabbedHeaderRenderer", "avatar", "thumbnails"])) 46 | except: 47 | pass 48 | try: 49 | thumbnails.extend(getValue(response, ["metadata", "channelMetadataRenderer", "avatar", "thumbnails"])) 50 | except: 51 | pass 52 | try: 53 | thumbnails.extend(getValue(response, ["microformat", "microformatDataRenderer", "thumbnail", "thumbnails"])) 54 | except: 55 | pass 56 | 57 | tabData: dict = {} 58 | playlists: list = [] 59 | 60 | for tab in getValue(response, ["contents", "twoColumnBrowseResultsRenderer", "tabs"]): 61 | tab: dict 62 | title = getValue(tab, ["tabRenderer", "title"]) 63 | if title == "Playlists": 64 | playlist = getValue(tab, 65 | ["tabRenderer", "content", "sectionListRenderer", "contents", 0, "itemSectionRenderer", 66 | "contents", 0, "gridRenderer", "items"]) 67 | if playlist is not None and getValue(playlist, [0, "gridPlaylistRenderer"]): 68 | for i in playlist: 69 | if getValue(i, ["continuationItemRenderer"]): 70 | self.continuation = getValue(i, ["continuationItemRenderer", "continuationEndpoint", 71 | "continuationCommand", "token"]) 72 | break 73 | i: dict = i["gridPlaylistRenderer"] 74 | playlists.append(self.playlist_parse(i)) 75 | elif title == "About": 76 | tabData = tab["tabRenderer"] 77 | 78 | metadata = getValue(tabData, 79 | ["content", "sectionListRenderer", "contents", 0, "itemSectionRenderer", "contents", 0, 80 | "channelAboutFullMetadataRenderer"]) 81 | 82 | self.result = { 83 | "id": getValue(response, ["metadata", "channelMetadataRenderer", "externalId"]), 84 | "url": getValue(response, ["metadata", "channelMetadataRenderer", "channelUrl"]), 85 | "description": getValue(response, ["metadata", "channelMetadataRenderer", "description"]), 86 | "title": getValue(response, ["metadata", "channelMetadataRenderer", "title"]), 87 | "banners": getValue(response, ["header", "c4TabbedHeaderRenderer", "banner", "thumbnails"]), 88 | "subscribers": { 89 | "simpleText": getValue(response, 90 | ["header", "c4TabbedHeaderRenderer", "subscriberCountText", "simpleText"]), 91 | "label": getValue(response, ["header", "c4TabbedHeaderRenderer", "subscriberCountText", "accessibility", 92 | "accessibilityData", "label"]) 93 | }, 94 | "thumbnails": thumbnails, 95 | "availableCountryCodes": getValue(response, 96 | ["metadata", "channelMetadataRenderer", "availableCountryCodes"]), 97 | "isFamilySafe": getValue(response, ["metadata", "channelMetadataRenderer", "isFamilySafe"]), 98 | "keywords": getValue(response, ["metadata", "channelMetadataRenderer", "keywords"]), 99 | "tags": getValue(response, ["microformat", "microformatDataRenderer", "tags"]), 100 | "views": getValue(metadata, ["viewCountText", "simpleText"]) if metadata else None, 101 | "joinedDate": getValue(metadata, ["joinedDateText", "runs", -1, "text"]) if metadata else None, 102 | "country": getValue(metadata, ["country", "simpleText"]) if metadata else None, 103 | "playlists": playlists, 104 | } 105 | 106 | def parse_next_response(self): 107 | response = self.data.json() 108 | 109 | self.continuation = None 110 | 111 | response = getValue(response, ["onResponseReceivedActions", 0, "appendContinuationItemsAction", "continuationItems"]) 112 | for i in response: 113 | if getValue(i, ["continuationItemRenderer"]): 114 | self.continuation = getValue(i, ["continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token"]) 115 | break 116 | elif getValue(i, ['gridPlaylistRenderer']): 117 | self.result["playlists"].append(self.playlist_parse(getValue(i, ['gridPlaylistRenderer']))) 118 | # TODO: Handle other types like gridShowRenderer 119 | 120 | async def async_next(self): 121 | if not self.continuation: 122 | return 123 | self.prepare_request() 124 | self.data = await self.asyncPostRequest() 125 | self.parse_next_response() 126 | 127 | def sync_next(self): 128 | if not self.continuation: 129 | return 130 | self.prepare_request() 131 | self.data = self.syncPostRequest() 132 | self.parse_next_response() 133 | 134 | def has_more_playlists(self): 135 | return self.continuation is not None 136 | 137 | async def async_create(self): 138 | self.prepare_request() 139 | self.data = await self.asyncPostRequest() 140 | self.parse_response() 141 | 142 | def sync_create(self): 143 | self.prepare_request() 144 | self.data = self.syncPostRequest() 145 | self.parse_response() 146 | -------------------------------------------------------------------------------- /youtubesearchpython/core/channelsearch.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from typing import Union 3 | import json 4 | from urllib.parse import urlencode 5 | 6 | from youtubesearchpython.core.requests import RequestCore 7 | from youtubesearchpython.handlers.componenthandler import ComponentHandler 8 | from youtubesearchpython.core.constants import * 9 | 10 | 11 | class ChannelSearchCore(RequestCore, ComponentHandler): 12 | response = None 13 | responseSource = None 14 | resultComponents = [] 15 | 16 | def __init__(self, query: str, language: str, region: str, searchPreferences: str, browseId: str, timeout: int): 17 | super().__init__() 18 | self.query = query 19 | self.language = language 20 | self.region = region 21 | self.browseId = browseId 22 | self.searchPreferences = searchPreferences 23 | self.continuationKey = None 24 | self.timeout = timeout 25 | 26 | def sync_create(self): 27 | self._syncRequest() 28 | self._parseChannelSearchSource() 29 | self.response = self._getChannelSearchComponent(self.response) 30 | 31 | async def next(self): 32 | await self._asyncRequest() 33 | self._parseChannelSearchSource() 34 | self.response = self._getChannelSearchComponent(self.response) 35 | return self.response 36 | 37 | def _parseChannelSearchSource(self) -> None: 38 | try: 39 | last_tab = self.response["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][-1] 40 | if 'expandableTabRenderer' in last_tab: 41 | self.response = last_tab["expandableTabRenderer"]["content"]["sectionListRenderer"]["contents"] 42 | else: 43 | tab_renderer = last_tab["tabRenderer"] 44 | if 'content' in tab_renderer: 45 | self.response = tab_renderer["content"]["sectionListRenderer"]["contents"] 46 | else: 47 | self.response = [] 48 | except: 49 | raise Exception('ERROR: Could not parse YouTube response.') 50 | 51 | def _getRequestBody(self): 52 | ''' Fixes #47 ''' 53 | requestBody = copy.deepcopy(requestPayload) 54 | requestBody['query'] = self.query 55 | requestBody['client'] = { 56 | 'hl': self.language, 57 | 'gl': self.region, 58 | } 59 | requestBody['params'] = self.searchPreferences 60 | requestBody['browseId'] = self.browseId 61 | self.url = 'https://www.youtube.com/youtubei/v1/browse' + '?' + urlencode({ 62 | 'key': searchKey, 63 | }) 64 | self.data = requestBody 65 | 66 | def _syncRequest(self) -> None: 67 | ''' Fixes #47 ''' 68 | self._getRequestBody() 69 | 70 | request = self.syncPostRequest() 71 | try: 72 | self.response = request.json() 73 | except: 74 | raise Exception('ERROR: Could not make request.') 75 | 76 | async def _asyncRequest(self) -> None: 77 | ''' Fixes #47 ''' 78 | self._getRequestBody() 79 | 80 | request = await self.asyncPostRequest() 81 | try: 82 | self.response = request.json() 83 | except: 84 | raise Exception('ERROR: Could not make request.') 85 | 86 | def result(self, mode: int = ResultMode.dict) -> Union[str, dict]: 87 | '''Returns the search result. 88 | Args: 89 | mode (int, optional): Sets the type of result. Defaults to ResultMode.dict. 90 | Returns: 91 | Union[str, dict]: Returns JSON or dictionary. 92 | ''' 93 | if mode == ResultMode.json: 94 | return json.dumps({'result': self.response}, indent=4) 95 | elif mode == ResultMode.dict: 96 | return {'result': self.response} 97 | 98 | -------------------------------------------------------------------------------- /youtubesearchpython/core/comments.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import copy 3 | import itertools 4 | import json 5 | from typing import Iterable, Mapping, Tuple, TypeVar, Union, List 6 | from urllib.parse import urlencode 7 | from urllib.request import Request, urlopen 8 | 9 | from youtubesearchpython.core.componenthandler import getVideoId, getValue 10 | from youtubesearchpython.core.constants import * 11 | from youtubesearchpython.core.requests import RequestCore 12 | 13 | K = TypeVar("K") 14 | T = TypeVar("T") 15 | 16 | 17 | class CommentsCore(RequestCore): 18 | result = None 19 | continuationKey = None 20 | isNextRequest = False 21 | response = None 22 | 23 | def __init__(self, videoLink: str): 24 | super().__init__() 25 | self.commentsComponent = {"result": []} 26 | self.responseSource = None 27 | self.videoLink = videoLink 28 | 29 | def prepare_continuation_request(self): 30 | self.data = { 31 | "context": {"client": {"clientName": "WEB", "clientVersion": "2.20210820.01.00"}}, 32 | "videoId": getVideoId(self.videoLink) 33 | } 34 | self.url = f"https://www.youtube.com/youtubei/v1/next?key={searchKey}" 35 | 36 | def prepare_comments_request(self): 37 | self.data = { 38 | "context": {"client": {"clientName": "WEB", "clientVersion": "2.20210820.01.00"}}, 39 | "continuation": self.continuationKey 40 | } 41 | 42 | def parse_source(self): 43 | self.responseSource = getValue(self.response.json(), [ 44 | "onResponseReceivedEndpoints", 45 | 0 if self.isNextRequest else 1, 46 | "appendContinuationItemsAction" if self.isNextRequest else "reloadContinuationItemsCommand", 47 | "continuationItems", 48 | ]) 49 | 50 | def parse_continuation_source(self): 51 | self.continuationKey = getValue( 52 | self.response.json(), 53 | [ 54 | "contents", 55 | "twoColumnWatchNextResults", 56 | "results", 57 | "results", 58 | "contents", 59 | -1, 60 | "itemSectionRenderer", 61 | "contents", 62 | 0, 63 | "continuationItemRenderer", 64 | "continuationEndpoint", 65 | "continuationCommand", 66 | "token", 67 | ] 68 | ) 69 | 70 | def sync_make_comment_request(self): 71 | self.prepare_comments_request() 72 | self.response = self.syncPostRequest() 73 | if self.response.status_code == 200: 74 | self.parse_source() 75 | 76 | def sync_make_continuation_request(self): 77 | self.prepare_continuation_request() 78 | self.response = self.syncPostRequest() 79 | if self.response.status_code == 200: 80 | self.parse_continuation_source() 81 | if not self.continuationKey: 82 | raise Exception("Could not retrieve continuation token") 83 | else: 84 | raise Exception("Status code is not 200") 85 | 86 | async def async_make_comment_request(self): 87 | self.prepare_comments_request() 88 | self.response = await self.asyncPostRequest() 89 | if self.response.status_code == 200: 90 | self.parse_source() 91 | 92 | async def async_make_continuation_request(self): 93 | self.prepare_continuation_request() 94 | self.response = await self.asyncPostRequest() 95 | if self.response.status_code == 200: 96 | self.parse_continuation_source() 97 | if not self.continuationKey: 98 | raise Exception("Could not retrieve continuation token") 99 | else: 100 | raise Exception("Status code is not 200") 101 | 102 | def sync_create(self): 103 | self.sync_make_continuation_request() 104 | self.sync_make_comment_request() 105 | self.__getComponents() 106 | 107 | def sync_create_next(self): 108 | self.isNextRequest = True 109 | self.sync_make_comment_request() 110 | self.__getComponents() 111 | 112 | async def async_create(self): 113 | await self.async_make_continuation_request() 114 | await self.async_make_comment_request() 115 | self.__getComponents() 116 | 117 | async def async_create_next(self): 118 | self.isNextRequest = True 119 | await self.async_make_comment_request() 120 | self.__getComponents() 121 | 122 | def __getComponents(self) -> None: 123 | comments = [] 124 | for comment in self.responseSource: 125 | comment = getValue(comment, ["commentThreadRenderer", "comment", "commentRenderer"]) 126 | #print(json.dumps(comment, indent=4)) 127 | try: 128 | j = { 129 | "id": self.__getValue(comment, ["commentId"]), 130 | "author": { 131 | "id": self.__getValue(comment, ["authorEndpoint", "browseEndpoint", "browseId"]), 132 | "name": self.__getValue(comment, ["authorText", "simpleText"]), 133 | "thumbnails": self.__getValue(comment, ["authorThumbnail", "thumbnails"]) 134 | }, 135 | "content": self.__getValue(comment, ["contentText", "runs", 0, "text"]), 136 | "published": self.__getValue(comment, ["publishedTimeText", "runs", 0, "text"]), 137 | "isLiked": self.__getValue(comment, ["isLiked"]), 138 | "authorIsChannelOwner": self.__getValue(comment, ["authorIsChannelOwner"]), 139 | "voteStatus": self.__getValue(comment, ["voteStatus"]), 140 | "votes": { 141 | "simpleText": self.__getValue(comment, ["voteCount", "simpleText"]), 142 | "label": self.__getValue(comment, ["voteCount", "accessibility", "accessibilityData", "label"]) 143 | }, 144 | "replyCount": self.__getValue(comment, ["replyCount"]), 145 | } 146 | comments.append(j) 147 | except: 148 | pass 149 | 150 | self.commentsComponent["result"].extend(comments) 151 | self.continuationKey = self.__getValue(self.responseSource, [-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token"]) 152 | 153 | def __result(self, mode: int) -> Union[dict, str]: 154 | if mode == ResultMode.dict: 155 | return self.commentsComponent 156 | elif mode == ResultMode.json: 157 | return json.dumps(self.commentsComponent, indent=4) 158 | 159 | def __getValue(self, source: dict, path: Iterable[str]) -> Union[str, int, dict, None]: 160 | value = source 161 | for key in path: 162 | if type(key) is str: 163 | if key in value.keys(): 164 | value = value[key] 165 | else: 166 | value = None 167 | break 168 | elif type(key) is int: 169 | if len(value) != 0: 170 | value = value[key] 171 | else: 172 | value = None 173 | break 174 | return value 175 | 176 | def __getAllWithKey(self, source: Iterable[Mapping[K, T]], key: K) -> Iterable[T]: 177 | for item in source: 178 | if key in item: 179 | yield item[key] 180 | 181 | def __getValueEx(self, source: dict, path: List[str]) -> Iterable[Union[str, int, dict, None]]: 182 | if len(path) <= 0: 183 | yield source 184 | return 185 | key = path[0] 186 | upcoming = path[1:] 187 | if key is None: 188 | following_key = upcoming[0] 189 | upcoming = upcoming[1:] 190 | if following_key is None: 191 | raise Exception("Cannot search for a key twice consecutive or at the end with no key given") 192 | values = self.__getAllWithKey(source, following_key) 193 | for val in values: 194 | yield from self.__getValueEx(val, path=upcoming) 195 | else: 196 | val = self.__getValue(source, path=[key]) 197 | yield from self.__getValueEx(val, path=upcoming) 198 | 199 | def __getFirstValue(self, source: dict, path: Iterable[str]) -> Union[str, int, dict, None]: 200 | values = self.__getValueEx(source, list(path)) 201 | for val in values: 202 | if val is not None: 203 | return val 204 | return None 205 | -------------------------------------------------------------------------------- /youtubesearchpython/core/componenthandler.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List 2 | 3 | 4 | def getValue(source: dict, path: List[str]) -> Union[str, int, dict, None]: 5 | value = source 6 | for key in path: 7 | if type(key) is str: 8 | if key in value.keys(): 9 | value = value[key] 10 | else: 11 | value = None 12 | break 13 | elif type(key) is int: 14 | if len(value) != 0: 15 | value = value[key] 16 | else: 17 | value = None 18 | break 19 | return value 20 | 21 | 22 | def getVideoId(videoLink: str) -> str: 23 | if 'youtu.be' in videoLink: 24 | if videoLink[-1] == '/': 25 | return videoLink.split('/')[-2] 26 | return videoLink.split('/')[-1] 27 | elif 'youtube.com' in videoLink: 28 | if '&' not in videoLink: 29 | return videoLink[videoLink.index('v=') + 2:] 30 | return videoLink[videoLink.index('v=') + 2: videoLink.index('&')] 31 | else: 32 | return videoLink 33 | 34 | -------------------------------------------------------------------------------- /youtubesearchpython/core/constants.py: -------------------------------------------------------------------------------- 1 | requestPayload = { 2 | "context": { 3 | "client": { 4 | "clientName": "WEB", 5 | "clientVersion": "2.20210224.06.00", 6 | "newVisitorCookie": True, 7 | }, 8 | "user": { 9 | "lockedSafetyMode": False, 10 | } 11 | } 12 | } 13 | userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36' 14 | 15 | 16 | videoElementKey = 'videoRenderer' 17 | channelElementKey = 'channelRenderer' 18 | playlistElementKey = 'playlistRenderer' 19 | shelfElementKey = 'shelfRenderer' 20 | itemSectionKey = 'itemSectionRenderer' 21 | continuationItemKey = 'continuationItemRenderer' 22 | playerResponseKey = 'playerResponse' 23 | richItemKey = 'richItemRenderer' 24 | hashtagElementKey = 'hashtagTileRenderer' 25 | hashtagBrowseKey = 'FEhashtag' 26 | hashtagVideosPath = ['contents', 'twoColumnBrowseResultsRenderer', 'tabs', 0, 'tabRenderer', 'content', 'richGridRenderer', 'contents'] 27 | hashtagContinuationVideosPath = ['onResponseReceivedActions', 0, 'appendContinuationItemsAction', 'continuationItems'] 28 | searchKey = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8' 29 | contentPath = ['contents', 'twoColumnSearchResultsRenderer', 'primaryContents', 'sectionListRenderer', 'contents'] 30 | fallbackContentPath = ['contents', 'twoColumnSearchResultsRenderer', 'primaryContents', 'richGridRenderer', 'contents'] 31 | continuationContentPath = ['onResponseReceivedCommands', 0, 'appendContinuationItemsAction', 'continuationItems'] 32 | continuationKeyPath = ['continuationItemRenderer', 'continuationEndpoint', 'continuationCommand', 'token'] 33 | playlistInfoPath = ['response', 'sidebar', 'playlistSidebarRenderer', 'items'] 34 | playlistVideosPath = ['response', 'contents', 'twoColumnBrowseResultsRenderer', 'tabs', 0, 'tabRenderer', 'content', 'sectionListRenderer', 'contents', 0, 'itemSectionRenderer', 'contents', 0, 'playlistVideoListRenderer', 'contents'] 35 | playlistPrimaryInfoKey = 'playlistSidebarPrimaryInfoRenderer' 36 | playlistSecondaryInfoKey = 'playlistSidebarSecondaryInfoRenderer' 37 | playlistVideoKey = 'playlistVideoRenderer' 38 | 39 | 40 | class ResultMode: 41 | json = 0 42 | dict = 1 43 | 44 | 45 | class SearchMode: 46 | videos = 'EgIQAQ%3D%3D' 47 | channels = 'EgIQAg%3D%3D' 48 | playlists = 'EgIQAw%3D%3D' 49 | livestreams = 'EgJAAQ%3D%3D' 50 | 51 | 52 | class VideoUploadDateFilter: 53 | lastHour = 'EgQIARAB' 54 | today = 'EgQIAhAB' 55 | thisWeek = 'EgQIAxAB' 56 | thisMonth = 'EgQIBBAB' 57 | thisYear = 'EgQIBRAB' 58 | 59 | 60 | class VideoDurationFilter: 61 | short = 'EgQQARgB' 62 | long = 'EgQQARgC' 63 | 64 | 65 | class VideoSortOrder: 66 | relevance = 'CAASAhAB' 67 | uploadDate = 'CAISAhAB' 68 | viewCount = 'CAMSAhAB' 69 | rating = 'CAESAhAB' 70 | 71 | 72 | class ChannelRequestType: 73 | info = "EgVhYm91dA%3D%3D" 74 | playlists = "EglwbGF5bGlzdHMYAyABcAA%3D" 75 | -------------------------------------------------------------------------------- /youtubesearchpython/core/hashtag.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | from typing import Union 4 | from urllib.parse import urlencode 5 | from urllib.request import Request, urlopen 6 | 7 | import httpx 8 | 9 | from youtubesearchpython.core.constants import * 10 | from youtubesearchpython.handlers.componenthandler import ComponentHandler 11 | 12 | 13 | class HashtagCore(ComponentHandler): 14 | response = None 15 | resultComponents = [] 16 | 17 | def __init__(self, hashtag: str, limit: int, language: str, region: str, timeout: int): 18 | self.hashtag = hashtag 19 | self.limit = limit 20 | self.language = language 21 | self.region = region 22 | self.timeout = timeout 23 | self.continuationKey = None 24 | self.params = None 25 | 26 | def sync_create(self): 27 | self._getParams() 28 | self._makeRequest() 29 | self._getComponents() 30 | 31 | def result(self, mode: int = ResultMode.dict) -> Union[str, dict]: 32 | '''Returns the hashtag videos. 33 | Args: 34 | mode (int, optional): Sets the type of result. Defaults to ResultMode.dict. 35 | Returns: 36 | Union[str, dict]: Returns JSON or dictionary. 37 | ''' 38 | if mode == ResultMode.json: 39 | return json.dumps({'result': self.resultComponents}, indent = 4) 40 | elif mode == ResultMode.dict: 41 | return {'result': self.resultComponents} 42 | 43 | def next(self) -> bool: 44 | '''Gets the videos from the next page. Call result 45 | Returns: 46 | bool: Returns True if getting more results was successful. 47 | ''' 48 | self.response = None 49 | self.resultComponents = [] 50 | if self.continuationKey: 51 | self._makeRequest() 52 | self._getComponents() 53 | if self.resultComponents: 54 | return True 55 | return False 56 | 57 | def _getParams(self) -> None: 58 | requestBody = copy.deepcopy(requestPayload) 59 | requestBody['query'] = "#" + self.hashtag 60 | requestBody['client'] = { 61 | 'hl': self.language, 62 | 'gl': self.region, 63 | } 64 | requestBodyBytes = json.dumps(requestBody).encode('utf_8') 65 | request = Request( 66 | 'https://www.youtube.com/youtubei/v1/search' + '?' + urlencode({ 67 | 'key': searchKey, 68 | }), 69 | data = requestBodyBytes, 70 | headers = { 71 | 'Content-Type': 'application/json; charset=utf-8', 72 | 'Content-Length': len(requestBodyBytes), 73 | 'User-Agent': userAgent, 74 | } 75 | ) 76 | try: 77 | response = urlopen(request, timeout=self.timeout).read().decode('utf_8') 78 | except: 79 | raise Exception('ERROR: Could not make request.') 80 | content = self._getValue(json.loads(response), contentPath) 81 | for item in self._getValue(content, [0, 'itemSectionRenderer', 'contents']): 82 | if hashtagElementKey in item.keys(): 83 | self.params = self._getValue(item[hashtagElementKey], ['onTapCommand', 'browseEndpoint', 'params']) 84 | return 85 | 86 | async def _asyncGetParams(self) -> None: 87 | requestBody = copy.deepcopy(requestPayload) 88 | requestBody['query'] = "#" + self.hashtag 89 | requestBody['client'] = { 90 | 'hl': self.language, 91 | 'gl': self.region, 92 | } 93 | try: 94 | async with httpx.AsyncClient() as client: 95 | response = await client.post( 96 | 'https://www.youtube.com/youtubei/v1/search', 97 | params = { 98 | 'key': searchKey, 99 | }, 100 | headers = { 101 | 'User-Agent': userAgent, 102 | }, 103 | json = requestBody, 104 | timeout = self.timeout 105 | ) 106 | response = response.json() 107 | except: 108 | raise Exception('ERROR: Could not make request.') 109 | content = self._getValue(response, contentPath) 110 | for item in self._getValue(content, [0, 'itemSectionRenderer', 'contents']): 111 | if hashtagElementKey in item.keys(): 112 | self.params = self._getValue(item[hashtagElementKey], ['onTapCommand', 'browseEndpoint', 'params']) 113 | return 114 | 115 | def _makeRequest(self) -> None: 116 | if self.params == None: 117 | return 118 | requestBody = copy.deepcopy(requestPayload) 119 | requestBody['browseId'] = hashtagBrowseKey 120 | requestBody['params'] = self.params 121 | requestBody['client'] = { 122 | 'hl': self.language, 123 | 'gl': self.region, 124 | } 125 | if self.continuationKey: 126 | requestBody['continuation'] = self.continuationKey 127 | requestBodyBytes = json.dumps(requestBody).encode('utf_8') 128 | request = Request( 129 | 'https://www.youtube.com/youtubei/v1/browse' + '?' + urlencode({ 130 | 'key': searchKey, 131 | }), 132 | data = requestBodyBytes, 133 | headers = { 134 | 'Content-Type': 'application/json; charset=utf-8', 135 | 'Content-Length': len(requestBodyBytes), 136 | 'User-Agent': userAgent, 137 | } 138 | ) 139 | try: 140 | self.response = urlopen(request, timeout=self.timeout).read().decode('utf_8') 141 | except: 142 | raise Exception('ERROR: Could not make request.') 143 | 144 | async def _asyncMakeRequest(self) -> None: 145 | if self.params == None: 146 | return 147 | requestBody = copy.deepcopy(requestPayload) 148 | requestBody['browseId'] = hashtagBrowseKey 149 | requestBody['params'] = self.params 150 | requestBody['client'] = { 151 | 'hl': self.language, 152 | 'gl': self.region, 153 | } 154 | if self.continuationKey: 155 | requestBody['continuation'] = self.continuationKey 156 | try: 157 | async with httpx.AsyncClient() as client: 158 | response = await client.post( 159 | 'https://www.youtube.com/youtubei/v1/browse', 160 | params = { 161 | 'key': searchKey, 162 | }, 163 | headers = { 164 | 'User-Agent': userAgent, 165 | }, 166 | json = requestBody, 167 | timeout = self.timeout 168 | ) 169 | self.response = response.content 170 | except: 171 | raise Exception('ERROR: Could not make request.') 172 | 173 | def _getComponents(self) -> None: 174 | if self.response == None: 175 | return 176 | self.resultComponents = [] 177 | try: 178 | if not self.continuationKey: 179 | responseSource = self._getValue(json.loads(self.response), hashtagVideosPath) 180 | else: 181 | responseSource = self._getValue(json.loads(self.response), hashtagContinuationVideosPath) 182 | if responseSource: 183 | for element in responseSource: 184 | if richItemKey in element.keys(): 185 | richItemElement = self._getValue(element, [richItemKey, 'content']) 186 | if videoElementKey in richItemElement.keys(): 187 | videoComponent = self._getVideoComponent(richItemElement) 188 | self.resultComponents.append(videoComponent) 189 | if len(self.resultComponents) >= self.limit: 190 | break 191 | self.continuationKey = self._getValue(responseSource[-1], continuationKeyPath) 192 | except: 193 | raise Exception('ERROR: Could not parse YouTube response.') -------------------------------------------------------------------------------- /youtubesearchpython/core/playlist.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import copy 3 | import itertools 4 | import json 5 | import re 6 | from typing import Iterable, Mapping, Tuple, TypeVar, Union, List 7 | from urllib.parse import urlencode 8 | from urllib.request import Request, urlopen 9 | 10 | from youtubesearchpython.core.constants import * 11 | from youtubesearchpython.core.requests import RequestCore 12 | 13 | 14 | K = TypeVar("K") 15 | T = TypeVar("T") 16 | 17 | 18 | class PlaylistCore(RequestCore): 19 | playlistComponent = None 20 | result = None 21 | continuationKey = None 22 | 23 | def __init__(self, playlistLink: str, componentMode: str, resultMode: int, timeout: int): 24 | super().__init__() 25 | self.componentMode = componentMode 26 | self.resultMode = resultMode 27 | self.timeout = timeout 28 | self.url = playlistLink 29 | 30 | def post_processing(self): 31 | self.__parseSource() 32 | self.__getComponents() 33 | if self.resultMode == ResultMode.json: 34 | self.result = json.dumps(self.playlistComponent, indent=4) 35 | else: 36 | self.result = self.playlistComponent 37 | 38 | def sync_create(self): 39 | statusCode = self.__makeRequest() 40 | if statusCode == 200: 41 | self.post_processing() 42 | else: 43 | raise Exception('ERROR: Invalid status code.') 44 | 45 | async def async_create(self): 46 | # Why do I use sync request in a async function, you might ask 47 | # Well, there were some problems with httpx. 48 | # Until I solve those problems, it is going to stay this way. 49 | statusCode = await self.__makeAsyncRequest() 50 | if statusCode == 200: 51 | self.post_processing() 52 | else: 53 | raise Exception('ERROR: Invalid status code.') 54 | 55 | def next_post_processing(self): 56 | self.__parseSource() 57 | self.__getNextComponents() 58 | if self.resultMode == ResultMode.json: 59 | self.result = json.dumps(self.playlistComponent, indent=4) 60 | else: 61 | self.result = self.playlistComponent 62 | 63 | def _next(self): 64 | self.prepare_next_request() 65 | if self.continuationKey: 66 | statusCode = self.syncPostRequest() 67 | self.response = statusCode.text 68 | if statusCode.status_code == 200: 69 | self.next_post_processing() 70 | else: 71 | raise Exception('ERROR: Invalid status code.') 72 | 73 | async def _async_next(self): 74 | if self.continuationKey: 75 | self.prepare_next_request() 76 | statusCode = await self.asyncPostRequest() 77 | self.response = statusCode.text 78 | if statusCode.status_code == 200: 79 | self.next_post_processing() 80 | else: 81 | raise Exception('ERROR: Invalid status code.') 82 | else: 83 | await self.async_create() 84 | 85 | def prepare_first_request(self): 86 | self.url.strip('/') 87 | 88 | id = re.search(r"(?<=list=)([a-zA-Z0-9+/=_-]+)", self.url).group() 89 | browseId = "VL" + id if not id.startswith("VL") else id 90 | 91 | self.url = 'https://www.youtube.com/youtubei/v1/browse' + '?' + urlencode({ 92 | 'key': searchKey, 93 | }) 94 | self.data = { 95 | "browseId": browseId, 96 | } 97 | self.data.update(copy.deepcopy(requestPayload)) 98 | 99 | def __makeRequest(self) -> int: 100 | self.prepare_first_request() 101 | request = self.syncPostRequest() 102 | self.response = request.text 103 | return request.status_code 104 | 105 | async def __makeAsyncRequest(self) -> int: 106 | self.prepare_first_request() 107 | request = await self.asyncPostRequest() 108 | self.response = request.text 109 | return request.status_code 110 | 111 | def prepare_next_request(self): 112 | requestBody = copy.deepcopy(requestPayload) 113 | requestBody['continuation'] = self.continuationKey 114 | self.data = requestBody 115 | self.url = 'https://www.youtube.com/youtubei/v1/browse' + '?' + urlencode({ 116 | 'key': searchKey, 117 | }) 118 | 119 | def __makeNextRequest(self) -> int: 120 | response = self.syncPostRequest() 121 | try: 122 | self.response = response.text 123 | return response.status_code 124 | except: 125 | raise Exception('ERROR: Could not make request.') 126 | 127 | def __parseSource(self) -> None: 128 | try: 129 | self.responseSource = json.loads(self.response) 130 | except: 131 | raise Exception('ERROR: Could not parse YouTube response.') 132 | 133 | def __getComponents(self) -> None: 134 | #print(self.responseSource) 135 | sidebar = self.responseSource["sidebar"]["playlistSidebarRenderer"]["items"] 136 | inforenderer = sidebar[0]["playlistSidebarPrimaryInfoRenderer"] 137 | channel_details_available = len(sidebar) != 1 138 | channelrenderer = sidebar[1]["playlistSidebarSecondaryInfoRenderer"]["videoOwner"]["videoOwnerRenderer"] if channel_details_available else None 139 | videorenderer: list = self.__getFirstValue(self.responseSource, ["contents", "twoColumnBrowseResultsRenderer", "tabs", None, "tabRenderer", "content", "sectionListRenderer", "contents", None, "itemSectionRenderer", "contents", None, "playlistVideoListRenderer", "contents"]) 140 | videos = [] 141 | for video in videorenderer: 142 | try: 143 | video = video["playlistVideoRenderer"] 144 | j = { 145 | "id": self.__getValue(video, ["videoId"]), 146 | "thumbnails": self.__getValue(video, ["thumbnail", "thumbnails"]), 147 | "title": self.__getValue(video, ["title", "runs", 0, "text"]), 148 | "channel": { 149 | "name": self.__getValue(video, ["shortBylineText", "runs", 0, "text"]), 150 | "id": self.__getValue(video, ["shortBylineText", "runs", 0, "navigationEndpoint", "browseEndpoint", "browseId"]), 151 | "link": self.__getValue(video, ["shortBylineText", "runs", 0, "navigationEndpoint", "browseEndpoint", "canonicalBaseUrl"]), 152 | }, 153 | "duration": self.__getValue(video, ["lengthText", "simpleText"]), 154 | "accessibility": { 155 | "title": self.__getValue(video, ["title", "accessibility", "accessibilityData", "label"]), 156 | "duration": self.__getValue(video, ["lengthText", "accessibility", "accessibilityData", "label"]), 157 | }, 158 | "link": "https://www.youtube.com" + self.__getValue(video, ["navigationEndpoint", "commandMetadata", "webCommandMetadata", "url"]), 159 | "isPlayable": self.__getValue(video, ["isPlayable"]), 160 | } 161 | videos.append(j) 162 | except: 163 | pass 164 | 165 | playlistElement = { 166 | 'info': { 167 | "id": self.__getValue(inforenderer, ["title", "runs", 0, "navigationEndpoint", "watchEndpoint", "playlistId"]), 168 | "thumbnails": self.__getValue(inforenderer, ["thumbnailRenderer", "playlistVideoThumbnailRenderer", "thumbnail", "thumbnails"]), 169 | "title": self.__getValue(inforenderer, ["title", "runs", 0, "text"]), 170 | "videoCount": self.__getValue(inforenderer, ["stats", 0, "runs", 0, "text"]), 171 | "viewCount": self.__getValue(inforenderer, ["stats", 1, "simpleText"]), 172 | "link": self.__getValue(self.responseSource, ["microformat", "microformatDataRenderer", "urlCanonical"]), 173 | "channel": { 174 | "id": self.__getValue(channelrenderer, ["title", "runs", 0, "navigationEndpoint", "browseEndpoint", "browseId"]) if channel_details_available else None, 175 | "name": self.__getValue(channelrenderer, ["title", "runs", 0, "text"]) if channel_details_available else None, 176 | "detailsAvailable": channel_details_available, 177 | "link": "https://www.youtube.com" + self.__getValue(channelrenderer, ["title", "runs", 0, "navigationEndpoint", "browseEndpoint", "canonicalBaseUrl"]) if channel_details_available else None, 178 | "thumbnails": self.__getValue(channelrenderer, ["thumbnail", "thumbnails"]) if channel_details_available else None, 179 | } 180 | }, 181 | 'videos': videos, 182 | } 183 | if self.componentMode == "getInfo": 184 | self.playlistComponent = playlistElement["info"] 185 | elif self.componentMode == "getVideos": 186 | self.playlistComponent = {"videos": videos} 187 | else: 188 | self.playlistComponent = playlistElement 189 | self.continuationKey = self.__getValue(videorenderer, [-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token"]) 190 | 191 | def __getNextComponents(self) -> None: 192 | self.continuationKey = None 193 | playlistComponent = { 194 | 'videos': [], 195 | } 196 | continuationElements = self.__getValue(self.responseSource, 197 | ['onResponseReceivedActions', 0, 'appendContinuationItemsAction', 198 | 'continuationItems']) 199 | if continuationElements is None: 200 | # YouTube Backend issue - See https://github.com/alexmercerind/youtube-search-python/issues/157 201 | return 202 | for videoElement in continuationElements: 203 | if playlistVideoKey in videoElement.keys(): 204 | videoComponent = { 205 | 'id': self.__getValue(videoElement, [playlistVideoKey, 'videoId']), 206 | 'title': self.__getValue(videoElement, [playlistVideoKey, 'title', 'runs', 0, 'text']), 207 | 'thumbnails': self.__getValue(videoElement, [playlistVideoKey, 'thumbnail', 'thumbnails']), 208 | 'link': "https://www.youtube.com" + self.__getValue(videoElement, [playlistVideoKey, "navigationEndpoint", "commandMetadata", "webCommandMetadata", "url"]), 209 | 'channel': { 210 | 'name': self.__getValue(videoElement, [playlistVideoKey, 'shortBylineText', 'runs', 0, 'text']), 211 | 'id': self.__getValue(videoElement, 212 | [playlistVideoKey, 'shortBylineText', 'runs', 0, 'navigationEndpoint', 213 | 'browseEndpoint', 'browseId']), 214 | "link": "https://www.youtube.com" + self.__getValue(videoElement, [playlistVideoKey, "shortBylineText", "runs", 0, "navigationEndpoint", "browseEndpoint", "canonicalBaseUrl"]) 215 | }, 216 | 'duration': self.__getValue(videoElement, [playlistVideoKey, 'lengthText', 'simpleText']), 217 | 'accessibility': { 218 | 'title': self.__getValue(videoElement, 219 | [playlistVideoKey, 'title', 'accessibility', 'accessibilityData', 220 | 'label']), 221 | 'duration': self.__getValue(videoElement, [playlistVideoKey, 'lengthText', 'accessibility', 222 | 'accessibilityData', 'label']), 223 | }, 224 | } 225 | playlistComponent['videos'].append( 226 | videoComponent 227 | ) 228 | self.continuationKey = self.__getValue(videoElement, continuationKeyPath) 229 | self.playlistComponent["videos"].extend(playlistComponent['videos']) 230 | 231 | def __getPlaylistComponent(self, element: dict, mode: str) -> dict: 232 | playlistComponent = {} 233 | if mode in ['getInfo', None]: 234 | for infoElement in element['info']: 235 | if playlistPrimaryInfoKey in infoElement.keys(): 236 | component = { 237 | 'id': self.__getValue(infoElement, 238 | [playlistPrimaryInfoKey, 'title', 'runs', 0, 'navigationEndpoint', 239 | 'watchEndpoint', 'playlistId']), 240 | 'title': self.__getValue(infoElement, [playlistPrimaryInfoKey, 'title', 'runs', 0, 'text']), 241 | 'videoCount': self.__getValue(infoElement, 242 | [playlistPrimaryInfoKey, 'stats', 0, 'runs', 0, 'text']), 243 | 'viewCount': self.__getValue(infoElement, [playlistPrimaryInfoKey, 'stats', 1, 'simpleText']), 244 | 'thumbnails': self.__getValue(infoElement, [playlistPrimaryInfoKey, 'thumbnailRenderer', 245 | 'playlistVideoThumbnailRenderer', 'thumbnail']), 246 | } 247 | if not component['thumbnails']: 248 | component['thumbnails'] = self.__getValue(infoElement, 249 | [playlistPrimaryInfoKey, 'thumbnailRenderer', 250 | 'playlistCustomThumbnailRenderer', 'thumbnail', 251 | 'thumbnails']), 252 | component['link'] = 'https://www.youtube.com/playlist?list=' + component['id'] 253 | playlistComponent.update(component) 254 | if playlistSecondaryInfoKey in infoElement.keys(): 255 | component = { 256 | 'channel': { 257 | 'name': self.__getValue(infoElement, 258 | [playlistSecondaryInfoKey, 'videoOwner', 'videoOwnerRenderer', 259 | 'title', 'runs', 0, 'text']), 260 | 'id': self.__getValue(infoElement, 261 | [playlistSecondaryInfoKey, 'videoOwner', 'videoOwnerRenderer', 262 | 'title', 'runs', 0, 'navigationEndpoint', 'browseEndpoint', 263 | 'browseId']), 264 | 'thumbnails': self.__getValue(infoElement, 265 | [playlistSecondaryInfoKey, 'videoOwner', 'videoOwnerRenderer', 266 | 'thumbnail', 'thumbnails']), 267 | }, 268 | } 269 | component['channel']['link'] = 'https://www.youtube.com/channel/' + component['channel']['id'] 270 | playlistComponent.update(component) 271 | if mode in ['getVideos', None]: 272 | self.continuationKey = None 273 | playlistComponent['videos'] = [] 274 | for videoElement in element['videos']: 275 | if playlistVideoKey in videoElement.keys(): 276 | videoComponent = { 277 | 'id': self.__getValue(videoElement, [playlistVideoKey, 'videoId']), 278 | 'title': self.__getValue(videoElement, [playlistVideoKey, 'title', 'runs', 0, 'text']), 279 | 'thumbnails': self.__getValue(videoElement, [playlistVideoKey, 'thumbnail', 'thumbnails']), 280 | 'channel': { 281 | 'name': self.__getValue(videoElement, 282 | [playlistVideoKey, 'shortBylineText', 'runs', 0, 'text']), 283 | 'id': self.__getValue(videoElement, 284 | [playlistVideoKey, 'shortBylineText', 'runs', 0, 'navigationEndpoint', 285 | 'browseEndpoint', 'browseId']), 286 | }, 287 | 'duration': self.__getValue(videoElement, [playlistVideoKey, 'lengthText', 'simpleText']), 288 | 'accessibility': { 289 | 'title': self.__getValue(videoElement, 290 | [playlistVideoKey, 'title', 'accessibility', 'accessibilityData', 291 | 'label']), 292 | 'duration': self.__getValue(videoElement, [playlistVideoKey, 'lengthText', 'accessibility', 293 | 'accessibilityData', 'label']), 294 | }, 295 | } 296 | videoComponent['link'] = 'https://www.youtube.com/watch?v=' + videoComponent['id'] 297 | videoComponent['channel']['link'] = 'https://www.youtube.com/channel/' + videoComponent['channel'][ 298 | 'id'] 299 | playlistComponent['videos'].append( 300 | videoComponent 301 | ) 302 | if continuationItemKey in videoElement.keys(): 303 | self.continuationKey = self.__getValue(videoElement, continuationKeyPath) 304 | return playlistComponent 305 | 306 | def __result(self, mode: int) -> Union[dict, str]: 307 | if mode == ResultMode.dict: 308 | return self.playlistComponent 309 | elif mode == ResultMode.json: 310 | return json.dumps(self.playlistComponent, indent=4) 311 | 312 | def __getValue(self, source: dict, path: Iterable[str]) -> Union[str, int, dict, None]: 313 | value = source 314 | for key in path: 315 | if type(key) is str: 316 | if key in value.keys(): 317 | value = value[key] 318 | else: 319 | value = None 320 | break 321 | elif type(key) is int: 322 | if len(value) != 0: 323 | value = value[key] 324 | else: 325 | value = None 326 | break 327 | return value 328 | 329 | def __getAllWithKey(self, source: Iterable[Mapping[K, T]], key: K) -> Iterable[T]: 330 | for item in source: 331 | if key in item: 332 | yield item[key] 333 | 334 | def __getValueEx(self, source: dict, path: List[str]) -> Iterable[Union[str, int, dict, None]]: 335 | if len(path) <= 0: 336 | yield source 337 | return 338 | key = path[0] 339 | upcoming = path[1:] 340 | if key is None: 341 | following_key = upcoming[0] 342 | upcoming = upcoming[1:] 343 | if following_key is None: 344 | raise Exception("Cannot search for a key twice consecutive or at the end with no key given") 345 | values = self.__getAllWithKey(source, following_key) 346 | for val in values: 347 | yield from self.__getValueEx(val, path=upcoming) 348 | else: 349 | val = self.__getValue(source, path=[key]) 350 | yield from self.__getValueEx(val, path=upcoming) 351 | 352 | def __getFirstValue(self, source: dict, path: Iterable[str]) -> Union[str, int, dict, list, None]: 353 | values = self.__getValueEx(source, list(path)) 354 | for val in values: 355 | if val is not None: 356 | return val 357 | return None 358 | -------------------------------------------------------------------------------- /youtubesearchpython/core/requests.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import os 3 | 4 | from youtubesearchpython.core.constants import userAgent 5 | 6 | class RequestCore: 7 | def __init__(self): 8 | self.url = None 9 | self.data = None 10 | self.timeout = 2 11 | self.proxy = {} 12 | http_proxy = os.environ.get("HTTP_PROXY") 13 | if http_proxy: 14 | self.proxy["http://"] = http_proxy 15 | https_proxy = os.environ.get("HTTPS_PROXY") 16 | if https_proxy: 17 | self.proxy["https://"] = https_proxy 18 | 19 | def syncPostRequest(self) -> httpx.Response: 20 | return httpx.post( 21 | self.url, 22 | headers={"User-Agent": userAgent}, 23 | json=self.data, 24 | timeout=self.timeout, 25 | proxies=self.proxy 26 | ) 27 | 28 | async def asyncPostRequest(self) -> httpx.Response: 29 | async with httpx.AsyncClient(proxies=self.proxy) as client: 30 | r = await client.post(self.url, headers={"User-Agent": userAgent}, json=self.data, timeout=self.timeout) 31 | return r 32 | 33 | def syncGetRequest(self) -> httpx.Response: 34 | return httpx.get(self.url, headers={"User-Agent": userAgent}, timeout=self.timeout, cookies={'CONSENT': 'YES+1'}, proxies=self.proxy) 35 | 36 | async def asyncGetRequest(self) -> httpx.Response: 37 | async with httpx.AsyncClient(proxies=self.proxy) as client: 38 | r = await client.get(self.url, headers={"User-Agent": userAgent}, timeout=self.timeout, cookies={'CONSENT': 'YES+1'}) 39 | return r 40 | -------------------------------------------------------------------------------- /youtubesearchpython/core/search.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from typing import Union 3 | from urllib.parse import urlencode 4 | 5 | from youtubesearchpython.core.requests import RequestCore 6 | from youtubesearchpython.handlers.componenthandler import ComponentHandler 7 | from youtubesearchpython.handlers.requesthandler import RequestHandler 8 | from youtubesearchpython.core.constants import * 9 | 10 | import json 11 | 12 | 13 | class SearchCore(RequestCore, RequestHandler, ComponentHandler): 14 | response = None 15 | responseSource = None 16 | resultComponents = [] 17 | 18 | def __init__(self, query: str, limit: int, language: str, region: str, searchPreferences: str, timeout: int): 19 | super().__init__() 20 | self.query = query 21 | self.limit = limit 22 | self.language = language 23 | self.region = region 24 | self.searchPreferences = searchPreferences 25 | self.timeout = timeout 26 | self.continuationKey = None 27 | 28 | def sync_create(self): 29 | self._makeRequest() 30 | self._parseSource() 31 | 32 | def _getRequestBody(self): 33 | ''' Fixes #47 ''' 34 | requestBody = copy.deepcopy(requestPayload) 35 | requestBody['query'] = self.query 36 | requestBody['client'] = { 37 | 'hl': self.language, 38 | 'gl': self.region, 39 | } 40 | if self.searchPreferences: 41 | requestBody['params'] = self.searchPreferences 42 | if self.continuationKey: 43 | requestBody['continuation'] = self.continuationKey 44 | self.url = 'https://www.youtube.com/youtubei/v1/search' + '?' + urlencode({ 45 | 'key': searchKey, 46 | }) 47 | self.data = requestBody 48 | 49 | def _makeRequest(self) -> None: 50 | self._getRequestBody() 51 | request = self.syncPostRequest() 52 | try: 53 | self.response = request.text 54 | except: 55 | raise Exception('ERROR: Could not make request.') 56 | 57 | async def _makeAsyncRequest(self) -> None: 58 | self._getRequestBody() 59 | request = await self.asyncPostRequest() 60 | try: 61 | self.response = request.text 62 | except: 63 | raise Exception('ERROR: Could not make request.') 64 | 65 | def result(self, mode: int = ResultMode.dict) -> Union[str, dict]: 66 | '''Returns the search result. 67 | 68 | Args: 69 | mode (int, optional): Sets the type of result. Defaults to ResultMode.dict. 70 | 71 | Returns: 72 | Union[str, dict]: Returns JSON or dictionary. 73 | ''' 74 | if mode == ResultMode.json: 75 | return json.dumps({'result': self.resultComponents}, indent=4) 76 | elif mode == ResultMode.dict: 77 | return {'result': self.resultComponents} 78 | 79 | def _next(self) -> bool: 80 | '''Gets the subsequent search result. Call result 81 | 82 | Args: 83 | mode (int, optional): Sets the type of result. Defaults to ResultMode.dict. 84 | 85 | Returns: 86 | Union[str, dict]: Returns True if getting more results was successful. 87 | ''' 88 | if self.continuationKey: 89 | self.response = None 90 | self.responseSource = None 91 | self.resultComponents = [] 92 | self._makeRequest() 93 | self._parseSource() 94 | self._getComponents(*self.searchMode) 95 | return True 96 | else: 97 | return False 98 | 99 | async def _nextAsync(self) -> dict: 100 | self.response = None 101 | self.responseSource = None 102 | self.resultComponents = [] 103 | await self._makeAsyncRequest() 104 | self._parseSource() 105 | self._getComponents(*self.searchMode) 106 | return { 107 | 'result': self.resultComponents, 108 | } 109 | 110 | def _getComponents(self, findVideos: bool, findChannels: bool, findPlaylists: bool) -> None: 111 | self.resultComponents = [] 112 | for element in self.responseSource: 113 | if videoElementKey in element.keys() and findVideos: 114 | self.resultComponents.append(self._getVideoComponent(element)) 115 | if channelElementKey in element.keys() and findChannels: 116 | self.resultComponents.append(self._getChannelComponent(element)) 117 | if playlistElementKey in element.keys() and findPlaylists: 118 | self.resultComponents.append(self._getPlaylistComponent(element)) 119 | if shelfElementKey in element.keys() and findVideos: 120 | for shelfElement in self._getShelfComponent(element)['elements']: 121 | self.resultComponents.append( 122 | self._getVideoComponent(shelfElement, shelfTitle=self._getShelfComponent(element)['title'])) 123 | if richItemKey in element.keys() and findVideos: 124 | richItemElement = self._getValue(element, [richItemKey, 'content']) 125 | ''' Initial fallback handling for VideosSearch ''' 126 | if videoElementKey in richItemElement.keys(): 127 | videoComponent = self._getVideoComponent(richItemElement) 128 | self.resultComponents.append(videoComponent) 129 | if len(self.resultComponents) >= self.limit: 130 | break -------------------------------------------------------------------------------- /youtubesearchpython/core/streamurlfetcher.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import urllib.request 3 | import urllib.parse 4 | 5 | import re 6 | 7 | from youtubesearchpython.core.constants import ResultMode 8 | from youtubesearchpython.core.video import VideoCore 9 | from youtubesearchpython.core.componenthandler import getValue 10 | from youtubesearchpython.core.requests import RequestCore 11 | 12 | isYtDLPinstalled = False 13 | 14 | try: 15 | from yt_dlp.extractor.youtube import YoutubeBaseInfoExtractor, YoutubeIE 16 | from yt_dlp import YoutubeDL 17 | from yt_dlp.utils import url_or_none, try_get, update_url_query, ExtractorError 18 | 19 | isYtDLPinstalled = True 20 | except: 21 | pass 22 | 23 | 24 | class StreamURLFetcherCore(RequestCore): 25 | ''' 26 | Overrided parent's constructor. 27 | ''' 28 | def __init__(self): 29 | if isYtDLPinstalled: 30 | super().__init__() 31 | self._js_url = None 32 | self._js = None 33 | #self.ytdlp = YoutubeBaseInfoExtractor() 34 | self.ytie = YoutubeIE() 35 | self.ytie.set_downloader(YoutubeDL()) 36 | self._streams = [] 37 | else: 38 | raise Exception('ERROR: yt-dlp is not installed. To use this functionality of youtube-search-python, yt-dlp must be installed.') 39 | 40 | ''' 41 | Saving videoFormats inside a dictionary with key "player_response" for apply_descrambler & apply_signature methods. 42 | ''' 43 | def _getDecipheredURLs(self, videoFormats: dict, formatId: int = None) -> None: 44 | # We reset our stream list 45 | # See https://github.com/alexmercerind/youtube-search-python/pull/155#discussion_r790165920 46 | # If we don't reset it, then it's going to cache older URLs and as we are using length comparison in upper class 47 | # it would return None, because length is not 1 48 | self._streams = [] 49 | 50 | self.video_id = videoFormats["id"] 51 | if not videoFormats["streamingData"]: 52 | # Video is age-restricted. Try to retrieve it using ANDROID_EMBED client and override old response. 53 | # This works most time. 54 | vc = VideoCore(self.video_id, None, ResultMode.dict, None, False, overridedClient="TV_EMBED") 55 | vc.sync_create() 56 | videoFormats = vc.result 57 | if not videoFormats["streamingData"]: 58 | # Video is: 59 | # 1. Either age-restricted on so called level 3 60 | # 2. Needs payment (is only for users that use so called "Join feature") 61 | raise Exception("streamingData is not present in Video.get. This is most likely a age-restricted video") 62 | # We deepcopy a list, otherwise it would duplicate 63 | # See https://github.com/alexmercerind/youtube-search-python/pull/155#discussion_r790165920 64 | self._player_response = copy.deepcopy(videoFormats["streamingData"]["formats"]) 65 | self._player_response.extend(videoFormats["streamingData"]["adaptiveFormats"]) 66 | self.format_id = formatId 67 | self._decipher() 68 | 69 | def extract_js_url(self, res: str): 70 | if res: 71 | # My modified RegEx derived from yt-dlp, that retrieves JavaScript version 72 | # Source: https://github.com/yt-dlp/yt-dlp/blob/e600a5c90817f4caac221679f6639211bba1f3a2/yt_dlp/extractor/youtube.py#L2258 73 | player_version = re.search( 74 | r'([0-9a-fA-F]{8})\\?', res) 75 | player_version = player_version.group().replace("\\", "") 76 | self._js_url = f'https://www.youtube.com/s/player/{player_version}/player_ias.vflset/en_US/base.js' 77 | else: 78 | raise Exception("Failed to retrieve JavaScript for this video") 79 | 80 | def _getJS(self) -> None: 81 | # Here we get a JavaScript that links to specific Player JavaScript 82 | self.url = 'https://www.youtube.com/iframe_api' 83 | res = self.syncGetRequest() 84 | self.extract_js_url(res.text) 85 | 86 | async def getJavaScript(self): 87 | # Same as in _getJS(), except it's asynchronous 88 | self.url = 'https://www.youtube.com/iframe_api' 89 | res = await self.asyncGetRequest() 90 | self.extract_js_url(res.text) 91 | 92 | def _decipher(self, retry: bool = False): 93 | if not self._js_url or retry: 94 | self._js_url = None 95 | self._js = None 96 | self._getJS() 97 | try: 98 | # We need to decipher one URL at time. 99 | for yt_format in self._player_response: 100 | # If format_id is specified, then it means that we requested only for one URL (ITAG), thus we can skip 101 | # all other ITAGs, which would take up our precious system resources and our valuable time 102 | if self.format_id == yt_format["itag"] or self.format_id is None: 103 | # If "url" is specified in JSON, it is definitely an unciphered URL. 104 | # Thus we can skip deciphering completely. 105 | if getValue(yt_format, ["url"]): 106 | # This is a non-ciphered URL 107 | yt_format["throttled"] = False 108 | self._streams.append(yt_format) 109 | continue 110 | else: 111 | cipher = yt_format["signatureCipher"] 112 | # Some deciphering magic from yt-dlp 113 | # Source: https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/extractor/youtube.py#L2972-L2981 114 | sc = urllib.parse.parse_qs(cipher) 115 | fmt_url = url_or_none(try_get(sc, lambda x: x['url'][0])) 116 | encrypted_sig = try_get(sc, lambda x: x['s'][0]) 117 | if not (sc and fmt_url and encrypted_sig): 118 | # It's not ciphered 119 | yt_format["throttled"] = False 120 | self._streams.append(yt_format) 121 | continue 122 | if not cipher: 123 | continue 124 | signature = self.ytie._decrypt_signature(sc['s'][0], self.video_id, self._js_url) 125 | sp = try_get(sc, lambda x: x['sp'][0]) or 'signature' 126 | fmt_url += '&' + sp + '=' + signature 127 | 128 | # Some magic to unthrottle streams 129 | # Source: https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/extractor/youtube.py#L2983-L2993 130 | query = urllib.parse.parse_qs(fmt_url) 131 | throttled = False 132 | if query.get('n'): 133 | try: 134 | fmt_url = update_url_query(fmt_url, { 135 | 'n': self.ytie._decrypt_nsig(query['n'][0], self.video_id, self._js_url)}) 136 | except ExtractorError as e: 137 | throttled = True 138 | yt_format["url"] = fmt_url 139 | yt_format["throttled"] = throttled 140 | self._streams.append(yt_format) 141 | except Exception as e: 142 | if retry: 143 | raise e 144 | ''' 145 | Fetch updated player JavaScript to get new cipher algorithm. 146 | ''' 147 | self._decipher(retry=True) 148 | -------------------------------------------------------------------------------- /youtubesearchpython/core/suggestions.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Union 3 | from urllib.parse import urlencode 4 | from urllib.request import Request, urlopen 5 | 6 | import httpx 7 | 8 | from youtubesearchpython.core.constants import ResultMode, userAgent 9 | from youtubesearchpython.core.requests import RequestCore 10 | 11 | 12 | class SuggestionsCore(RequestCore): 13 | '''Gets search suggestions for the given query. 14 | 15 | Args: 16 | language (str, optional): Sets the suggestion language. Defaults to 'en'. 17 | region (str, optional): Sets the suggestion region. Defaults to 'US'. 18 | 19 | Examples: 20 | Calling `result` method gives the search result. 21 | 22 | >>> suggestions = Suggestions(language = 'en', region = 'US').get('Harry Styles', mode = ResultMode.json) 23 | >>> print(suggestions) 24 | { 25 | 'result': [ 26 | 'harry styles', 27 | 'harry styles treat people with kindness', 28 | 'harry styles golden music video', 29 | 'harry styles interview', 30 | 'harry styles adore you', 31 | 'harry styles watermelon sugar', 32 | 'harry styles snl', 33 | 'harry styles falling', 34 | 'harry styles tpwk', 35 | 'harry styles sign of the times', 36 | 'harry styles jingle ball 2020', 37 | 'harry styles christmas', 38 | 'harry styles live', 39 | 'harry styles juice' 40 | ] 41 | } 42 | ''' 43 | 44 | def __init__(self, language: str = 'en', region: str = 'US', timeout: int = None): 45 | super().__init__() 46 | self.language = language 47 | self.region = region 48 | self.timeout = timeout 49 | 50 | def _post_request_processing(self, mode): 51 | searchSuggestions = [] 52 | 53 | self.__parseSource() 54 | for element in self.responseSource: 55 | if type(element) is list: 56 | for searchSuggestionElement in element: 57 | searchSuggestions.append(searchSuggestionElement[0]) 58 | break 59 | if mode == ResultMode.dict: 60 | return {'result': searchSuggestions} 61 | elif mode == ResultMode.json: 62 | return json.dumps({'result': searchSuggestions}, indent=4) 63 | 64 | def _get(self, query: str, mode: int = ResultMode.dict) -> Union[dict, str]: 65 | self.url = 'https://clients1.google.com/complete/search' + '?' + urlencode({ 66 | 'hl': self.language, 67 | 'gl': self.region, 68 | 'q': query, 69 | 'client': 'youtube', 70 | 'gs_ri': 'youtube', 71 | 'ds': 'yt', 72 | }) 73 | 74 | self.__makeRequest() 75 | return self._post_request_processing(mode) 76 | 77 | async def _getAsync(self, query: str, mode: int = ResultMode.dict) -> Union[dict, str]: 78 | self.url = 'https://clients1.google.com/complete/search' + '?' + urlencode({ 79 | 'hl': self.language, 80 | 'gl': self.region, 81 | 'q': query, 82 | 'client': 'youtube', 83 | 'gs_ri': 'youtube', 84 | 'ds': 'yt', 85 | }) 86 | 87 | await self.__makeAsyncRequest() 88 | return self._post_request_processing(mode) 89 | 90 | def __parseSource(self) -> None: 91 | try: 92 | self.responseSource = json.loads(self.response[self.response.index('(') + 1: self.response.index(')')]) 93 | except: 94 | raise Exception('ERROR: Could not parse YouTube response.') 95 | 96 | def __makeRequest(self) -> None: 97 | request = self.syncGetRequest() 98 | self.response = request.text 99 | 100 | async def __makeAsyncRequest(self) -> None: 101 | request = await self.asyncGetRequest() 102 | self.response = request.text 103 | -------------------------------------------------------------------------------- /youtubesearchpython/core/transcript.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | from typing import Union, List 4 | from urllib.parse import urlencode 5 | 6 | from youtubesearchpython.core.constants import * 7 | from youtubesearchpython.core.requests import RequestCore 8 | from youtubesearchpython.core.componenthandler import getValue, getVideoId 9 | 10 | 11 | 12 | class TranscriptCore(RequestCore): 13 | def __init__(self, videoLink: str, key: str): 14 | super().__init__() 15 | self.videoLink = videoLink 16 | self.key = key 17 | 18 | def prepare_params_request(self): 19 | self.url = 'https://www.youtube.com/youtubei/v1/next' + "?" + urlencode({ 20 | 'key': searchKey, 21 | "prettyPrint": "false" 22 | }) 23 | self.data = copy.deepcopy(requestPayload) 24 | self.data["videoId"] = getVideoId(self.videoLink) 25 | 26 | def extract_continuation_key(self, r): 27 | j = r.json() 28 | panels = getValue(j, ["engagementPanels"]) 29 | if not panels: 30 | raise Exception("Failed to create first request - No engagementPanels is present.") 31 | key = "" 32 | for panel in panels: 33 | panel = panel["engagementPanelSectionListRenderer"] 34 | if getValue(panel, ["targetId"]) == "engagement-panel-searchable-transcript": 35 | key = getValue(panel, ["content", "continuationItemRenderer", "continuationEndpoint", "getTranscriptEndpoint", "params"]) 36 | if key == "" or not key: 37 | self.result = {"segments": [], "languages": []} 38 | return True 39 | self.key = key 40 | return False 41 | 42 | def prepare_transcript_request(self): 43 | self.url = 'https://www.youtube.com/youtubei/v1/get_transcript' + "?" + urlencode({ 44 | 'key': searchKey, 45 | "prettyPrint": "false" 46 | }) 47 | # clientVersion must be newer than in requestPayload 48 | self.data = { 49 | "context": { 50 | "client": { 51 | "clientName": "WEB", 52 | "clientVersion": "2.20220318.00.00", 53 | "newVisitorCookie": True, 54 | }, 55 | "user": { 56 | "lockedSafetyMode": False, 57 | } 58 | }, 59 | "params": self.key 60 | } 61 | 62 | def extract_transcript(self): 63 | response = self.data.json() 64 | transcripts = getValue(response, ["actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", "initialSegments"]) 65 | segments = [] 66 | languages = [] 67 | for segment in transcripts: 68 | segment = getValue(segment, ["transcriptSegmentRenderer"]) 69 | j = { 70 | "startMs": getValue(segment, ["startMs"]), 71 | "endMs": getValue(segment, ["endMs"]), 72 | "text": getValue(segment, ["snippet", "runs", 0, "text"]), 73 | "startTime": getValue(segment, ["startTimeText", "simpleText"]) 74 | } 75 | segments.append(j) 76 | langs = getValue(response, ["actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", "content", "transcriptSearchPanelRenderer", "footer", "transcriptFooterRenderer", "languageMenu", "sortFilterSubMenuRenderer", "subMenuItems"]) 77 | if langs: 78 | for language in langs: 79 | j = { 80 | "params": getValue(language, ["continuation", "reloadContinuationData", "continuation"]), 81 | "selected": getValue(language, ["selected"]), 82 | "title": getValue(language, ["title"]) 83 | } 84 | languages.append(j) 85 | self.result = { 86 | "segments": segments, 87 | "languages": languages 88 | } 89 | 90 | async def async_create(self): 91 | if not self.key: 92 | self.prepare_params_request() 93 | r = await self.asyncPostRequest() 94 | end = self.extract_continuation_key(r) 95 | if end: 96 | return 97 | self.prepare_transcript_request() 98 | self.data = await self.asyncPostRequest() 99 | self.extract_transcript() 100 | 101 | def sync_create(self): 102 | if not self.key: 103 | self.prepare_params_request() 104 | r = self.syncPostRequest() 105 | end = self.extract_continuation_key(r) 106 | if end: 107 | return 108 | self.prepare_transcript_request() 109 | self.data = self.syncPostRequest() 110 | self.extract_transcript() 111 | 112 | 113 | -------------------------------------------------------------------------------- /youtubesearchpython/core/utils.py: -------------------------------------------------------------------------------- 1 | def playlist_from_channel_id(channel_id: str) -> str: 2 | list_id = "UU" + channel_id[2:] 3 | return f"https://www.youtube.com/playlist?list={list_id}" 4 | -------------------------------------------------------------------------------- /youtubesearchpython/core/video.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | from typing import Union, List 4 | from urllib.parse import urlencode 5 | 6 | from youtubesearchpython.core.constants import * 7 | from youtubesearchpython.core.requests import RequestCore 8 | from youtubesearchpython.core.componenthandler import getValue, getVideoId 9 | 10 | 11 | CLIENTS = { 12 | "MWEB": { 13 | 'context': { 14 | 'client': { 15 | 'clientName': 'MWEB', 16 | 'clientVersion': '2.20211109.01.00' 17 | } 18 | }, 19 | 'api_key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8' 20 | }, 21 | "ANDROID": { 22 | 'context': { 23 | 'client': { 24 | 'clientName': 'ANDROID', 25 | 'clientVersion': '16.20' 26 | } 27 | }, 28 | 'api_key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8' 29 | }, 30 | "ANDROID_EMBED": { 31 | 'context': { 32 | 'client': { 33 | 'clientName': 'ANDROID', 34 | 'clientVersion': '16.20', 35 | 'clientScreen': 'EMBED' 36 | } 37 | }, 38 | 'api_key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8' 39 | }, 40 | "TV_EMBED": { 41 | "context": { 42 | "client": { 43 | "clientName": "TVHTML5_SIMPLY_EMBEDDED_PLAYER", 44 | "clientVersion": "2.0" 45 | }, 46 | "thirdParty": { 47 | "embedUrl": "https://www.youtube.com/", 48 | } 49 | }, 50 | 'api_key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8' 51 | } 52 | } 53 | 54 | 55 | class VideoCore(RequestCore): 56 | def __init__(self, videoLink: str, componentMode: str, resultMode: int, timeout: int, enableHTML: bool, overridedClient: str = "ANDROID"): 57 | super().__init__() 58 | self.timeout = timeout 59 | self.resultMode = resultMode 60 | self.componentMode = componentMode 61 | self.videoLink = videoLink 62 | self.enableHTML = enableHTML 63 | self.overridedClient = overridedClient 64 | 65 | # We call this when we use only HTML 66 | def post_request_only_html_processing(self): 67 | self.__getVideoComponent(self.componentMode) 68 | self.result = self.__videoComponent 69 | 70 | def post_request_processing(self): 71 | self.__parseSource() 72 | self.__getVideoComponent(self.componentMode) 73 | self.result = self.__videoComponent 74 | 75 | def prepare_innertube_request(self): 76 | self.url = 'https://www.youtube.com/youtubei/v1/player' + "?" + urlencode({ 77 | 'key': searchKey, 78 | 'contentCheckOk': True, 79 | 'racyCheckOk': True, 80 | "videoId": getVideoId(self.videoLink) 81 | }) 82 | self.data = copy.deepcopy(CLIENTS[self.overridedClient]) 83 | 84 | async def async_create(self): 85 | self.prepare_innertube_request() 86 | response = await self.asyncPostRequest() 87 | self.response = response.text 88 | if response.status_code == 200: 89 | self.post_request_processing() 90 | else: 91 | raise Exception('ERROR: Invalid status code.') 92 | 93 | def sync_create(self): 94 | self.prepare_innertube_request() 95 | response = self.syncPostRequest() 96 | self.response = response.text 97 | if response.status_code == 200: 98 | self.post_request_processing() 99 | else: 100 | raise Exception('ERROR: Invalid status code.') 101 | 102 | def prepare_html_request(self): 103 | self.url = 'https://www.youtube.com/youtubei/v1/player' + "?" + urlencode({ 104 | 'key': searchKey, 105 | 'contentCheckOk': True, 106 | 'racyCheckOk': True, 107 | "videoId": getVideoId(self.videoLink) 108 | }) 109 | self.data = CLIENTS["MWEB"] 110 | 111 | def sync_html_create(self): 112 | self.prepare_html_request() 113 | response = self.syncPostRequest() 114 | self.HTMLresponseSource = response.json() 115 | 116 | async def async_html_create(self): 117 | self.prepare_html_request() 118 | response = await self.asyncPostRequest() 119 | self.HTMLresponseSource = response.json() 120 | 121 | def __parseSource(self) -> None: 122 | try: 123 | self.responseSource = json.loads(self.response) 124 | except Exception as e: 125 | raise Exception('ERROR: Could not parse YouTube response.') 126 | 127 | def __result(self, mode: int) -> Union[dict, str]: 128 | if mode == ResultMode.dict: 129 | return self.__videoComponent 130 | elif mode == ResultMode.json: 131 | return json.dumps(self.__videoComponent, indent=4) 132 | 133 | def __getVideoComponent(self, mode: str) -> None: 134 | videoComponent = {} 135 | if mode in ['getInfo', None]: 136 | try: 137 | responseSource = self.responseSource 138 | except: 139 | responseSource = None 140 | if self.enableHTML: 141 | responseSource = self.HTMLresponseSource 142 | component = { 143 | 'id': getValue(responseSource, ['videoDetails', 'videoId']), 144 | 'title': getValue(responseSource, ['videoDetails', 'title']), 145 | 'duration': { 146 | 'secondsText': getValue(responseSource, ['videoDetails', 'lengthSeconds']), 147 | }, 148 | 'viewCount': { 149 | 'text': getValue(responseSource, ['videoDetails', 'viewCount']) 150 | }, 151 | 'thumbnails': getValue(responseSource, ['videoDetails', 'thumbnail', 'thumbnails']), 152 | 'description': getValue(responseSource, ['videoDetails', 'shortDescription']), 153 | 'channel': { 154 | 'name': getValue(responseSource, ['videoDetails', 'author']), 155 | 'id': getValue(responseSource, ['videoDetails', 'channelId']), 156 | }, 157 | 'allowRatings': getValue(responseSource, ['videoDetails', 'allowRatings']), 158 | 'averageRating': getValue(responseSource, ['videoDetails', 'averageRating']), 159 | 'keywords': getValue(responseSource, ['videoDetails', 'keywords']), 160 | 'isLiveContent': getValue(responseSource, ['videoDetails', 'isLiveContent']), 161 | 'publishDate': getValue(responseSource, ['microformat', 'playerMicroformatRenderer', 'publishDate']), 162 | 'uploadDate': getValue(responseSource, ['microformat', 'playerMicroformatRenderer', 'uploadDate']), 163 | 'isFamilySafe': getValue(responseSource, ['microformat', 'playerMicroformatRenderer', 'isFamilySafe']), 164 | 'category': getValue(responseSource, ['microformat', 'playerMicroformatRenderer', 'category']), 165 | } 166 | component['isLiveNow'] = component['isLiveContent'] and component['duration']['secondsText'] == "0" 167 | component['link'] = 'https://www.youtube.com/watch?v=' + component['id'] 168 | component['channel']['link'] = 'https://www.youtube.com/channel/' + component['channel']['id'] 169 | videoComponent.update(component) 170 | if mode in ['getFormats', None]: 171 | videoComponent.update( 172 | { 173 | "streamingData": getValue(self.responseSource, ["streamingData"]) 174 | } 175 | ) 176 | if self.enableHTML: 177 | videoComponent["publishDate"] = getValue(self.HTMLresponseSource, ['microformat', 'playerMicroformatRenderer', 'publishDate']) 178 | videoComponent["uploadDate"] = getValue(self.HTMLresponseSource, ['microformat', 'playerMicroformatRenderer', 'uploadDate']) 179 | self.__videoComponent = videoComponent 180 | -------------------------------------------------------------------------------- /youtubesearchpython/handlers/componenthandler.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | from youtubesearchpython.core.constants import * 3 | 4 | 5 | class ComponentHandler: 6 | def _getVideoComponent(self, element: dict, shelfTitle: str = None) -> dict: 7 | video = element[videoElementKey] 8 | component = { 9 | 'type': 'video', 10 | 'id': self._getValue(video, ['videoId']), 11 | 'title': self._getValue(video, ['title', 'runs', 0, 'text']), 12 | 'publishedTime': self._getValue(video, ['publishedTimeText', 'simpleText']), 13 | 'duration': self._getValue(video, ['lengthText', 'simpleText']), 14 | 'viewCount': { 15 | 'text': self._getValue(video, ['viewCountText', 'simpleText']), 16 | 'short': self._getValue(video, ['shortViewCountText', 'simpleText']), 17 | }, 18 | 'thumbnails': self._getValue(video, ['thumbnail', 'thumbnails']), 19 | 'richThumbnail': self._getValue(video, ['richThumbnail', 'movingThumbnailRenderer', 'movingThumbnailDetails', 'thumbnails', 0]), 20 | 'descriptionSnippet': self._getValue(video, ['detailedMetadataSnippets', 0, 'snippetText', 'runs']), 21 | 'channel': { 22 | 'name': self._getValue(video, ['ownerText', 'runs', 0, 'text']), 23 | 'id': self._getValue(video, ['ownerText', 'runs', 0, 'navigationEndpoint', 'browseEndpoint', 'browseId']), 24 | 'thumbnails': self._getValue(video, ['channelThumbnailSupportedRenderers', 'channelThumbnailWithLinkRenderer', 'thumbnail', 'thumbnails']), 25 | }, 26 | 'accessibility': { 27 | 'title': self._getValue(video, ['title', 'accessibility', 'accessibilityData', 'label']), 28 | 'duration': self._getValue(video, ['lengthText', 'accessibility', 'accessibilityData', 'label']), 29 | }, 30 | } 31 | component['link'] = 'https://www.youtube.com/watch?v=' + component['id'] 32 | component['channel']['link'] = 'https://www.youtube.com/channel/' + component['channel']['id'] 33 | component['shelfTitle'] = shelfTitle 34 | return component 35 | 36 | def _getChannelComponent(self, element: dict) -> dict: 37 | channel = element[channelElementKey] 38 | component = { 39 | 'type': 'channel', 40 | 'id': self._getValue(channel, ['channelId']), 41 | 'title': self._getValue(channel, ['title', 'simpleText']), 42 | 'thumbnails': self._getValue(channel, ['thumbnail', 'thumbnails']), 43 | 'videoCount': self._getValue(channel, ['videoCountText', 'runs', 0, 'text']), 44 | 'descriptionSnippet': self._getValue(channel, ['descriptionSnippet', 'runs']), 45 | 'subscribers': self._getValue(channel, ['subscriberCountText', 'simpleText']), 46 | } 47 | component['link'] = 'https://www.youtube.com/channel/' + component['id'] 48 | return component 49 | 50 | def _getPlaylistComponent(self, element: dict) -> dict: 51 | playlist = element[playlistElementKey] 52 | component = { 53 | 'type': 'playlist', 54 | 'id': self._getValue(playlist, ['playlistId']), 55 | 'title': self._getValue(playlist, ['title', 'simpleText']), 56 | 'videoCount': self._getValue(playlist, ['videoCount']), 57 | 'channel': { 58 | 'name': self._getValue(playlist, ['shortBylineText', 'runs', 0, 'text']), 59 | 'id': self._getValue(playlist, ['shortBylineText', 'runs', 0, 'navigationEndpoint', 'browseEndpoint', 'browseId']), 60 | }, 61 | 'thumbnails': self._getValue(playlist, ['thumbnailRenderer', 'playlistVideoThumbnailRenderer', 'thumbnail', 'thumbnails']), 62 | } 63 | component['link'] = 'https://www.youtube.com/playlist?list=' + component['id'] 64 | component['channel']['link'] = 'https://www.youtube.com/channel/' + component['channel']['id'] 65 | return component 66 | 67 | def _getVideoFromChannelSearch(self, elements: list) -> list: 68 | channelsearch = [] 69 | for element in elements: 70 | element = self._getValue(element, ["childVideoRenderer"]) 71 | json = { 72 | "id": self._getValue(element, ["videoId"]), 73 | "title": self._getValue(element, ["title", "simpleText"]), 74 | "uri": self._getValue(element, ["navigationEndpoint", "commandMetadata", "webCommandMetadata", "url"]), 75 | "duration": { 76 | "simpleText": self._getValue(element, ["lengthText", "simpleText"]), 77 | "text": self._getValue(element, ["lengthText", "accessibility", "accessibilityData", "label"]) 78 | } 79 | } 80 | channelsearch.append(json) 81 | return channelsearch 82 | 83 | def _getChannelSearchComponent(self, elements: list) -> list: 84 | channelsearch = [] 85 | for element in elements: 86 | responsetype = None 87 | 88 | if 'gridPlaylistRenderer' in element: 89 | element = element['gridPlaylistRenderer'] 90 | responsetype = 'gridplaylist' 91 | elif 'itemSectionRenderer' in element: 92 | first_content = element["itemSectionRenderer"]["contents"][0] 93 | if 'videoRenderer' in first_content: 94 | element = first_content['videoRenderer'] 95 | responsetype = "video" 96 | elif 'playlistRenderer' in first_content: 97 | element = first_content["playlistRenderer"] 98 | responsetype = "playlist" 99 | else: 100 | raise Exception(f'Unexpected first_content {first_content}') 101 | elif 'continuationItemRenderer' in element: 102 | # for endless scrolling, not needed here 103 | # TODO: Implement endless scrolling 104 | continue 105 | else: 106 | raise Exception(f'Unexpected element {element}') 107 | 108 | if responsetype == "video": 109 | json = { 110 | "id": self._getValue(element, ["videoId"]), 111 | "thumbnails": { 112 | "normal": self._getValue(element, ["thumbnail", "thumbnails"]), 113 | "rich": self._getValue(element, ["richThumbnail", "movingThumbnailRenderer", "movingThumbnailDetails", "thumbnails"]) 114 | }, 115 | "title": self._getValue(element, ["title", "runs", 0, "text"]), 116 | "descriptionSnippet": self._getValue(element, ["descriptionSnippet", "runs", 0, "text"]), 117 | "uri": self._getValue(element, ["navigationEndpoint", "commandMetadata", "webCommandMetadata", "url"]), 118 | "views": { 119 | "precise": self._getValue(element, ["viewCountText", "simpleText"]), 120 | "simple": self._getValue(element, ["shortViewCountText", "simpleText"]), 121 | "approximate": self._getValue(element, ["shortViewCountText", "accessibility", "accessibilityData", "label"]) 122 | }, 123 | "duration": { 124 | "simpleText": self._getValue(element, ["lengthText", "simpleText"]), 125 | "text": self._getValue(element, ["lengthText", "accessibility", "accessibilityData", "label"]) 126 | }, 127 | "published": self._getValue(element, ["publishedTimeText", "simpleText"]), 128 | "channel": { 129 | "name": self._getValue(element, ["ownerText", "runs", 0, "text"]), 130 | "thumbnails": self._getValue(element, ["channelThumbnailSupportedRenderers", "channelThumbnailWithLinkRenderer", "thumbnail", "thumbnails"]) 131 | }, 132 | "type": responsetype 133 | } 134 | elif responsetype == 'playlist': 135 | json = { 136 | "id": self._getValue(element, ["playlistId"]), 137 | "videos": self._getVideoFromChannelSearch(self._getValue(element, ["videos"])), 138 | "thumbnails": { 139 | "normal": self._getValue(element, ["thumbnails"]), 140 | }, 141 | "title": self._getValue(element, ["title", "simpleText"]), 142 | "uri": self._getValue(element, ["navigationEndpoint", "commandMetadata", "webCommandMetadata", "url"]), 143 | "channel": { 144 | "name": self._getValue(element, ["longBylineText", "runs", 0, "text"]), 145 | }, 146 | "type": responsetype 147 | } 148 | else: 149 | json = { 150 | "id": self._getValue(element, ["playlistId"]), 151 | "thumbnails": { 152 | "normal": self._getValue(element, ["thumbnail", "thumbnails", 0]), 153 | }, 154 | "title": self._getValue(element, ["title", "runs", 0, "text"]), 155 | "uri": self._getValue(element, ["navigationEndpoint", "commandMetadata", "webCommandMetadata", "url"]), 156 | "type": 'playlist' 157 | } 158 | channelsearch.append(json) 159 | return channelsearch 160 | 161 | def _getShelfComponent(self, element: dict) -> dict: 162 | shelf = element[shelfElementKey] 163 | return { 164 | 'title': self._getValue(shelf, ['title', 'simpleText']), 165 | 'elements': self._getValue(shelf, ['content', 'verticalListRenderer', 'items']), 166 | } 167 | 168 | def _getValue(self, source: dict, path: List[str]) -> Union[str, int, dict, None]: 169 | value = source 170 | for key in path: 171 | if type(key) is str: 172 | if key in value.keys(): 173 | value = value[key] 174 | else: 175 | value = None 176 | break 177 | elif type(key) is int: 178 | if len(value) != 0: 179 | value = value[key] 180 | else: 181 | value = None 182 | break 183 | return value 184 | -------------------------------------------------------------------------------- /youtubesearchpython/handlers/requesthandler.py: -------------------------------------------------------------------------------- 1 | from urllib.request import Request, urlopen 2 | from urllib.parse import urlencode 3 | import json 4 | import copy 5 | from youtubesearchpython.handlers.componenthandler import ComponentHandler 6 | from youtubesearchpython.core.constants import * 7 | 8 | 9 | class RequestHandler(ComponentHandler): 10 | def _makeRequest(self) -> None: 11 | ''' Fixes #47 ''' 12 | requestBody = copy.deepcopy(requestPayload) 13 | requestBody['query'] = self.query 14 | requestBody['client'] = { 15 | 'hl': self.language, 16 | 'gl': self.region, 17 | } 18 | if self.searchPreferences: 19 | requestBody['params'] = self.searchPreferences 20 | if self.continuationKey: 21 | requestBody['continuation'] = self.continuationKey 22 | requestBodyBytes = json.dumps(requestBody).encode('utf_8') 23 | request = Request( 24 | 'https://www.youtube.com/youtubei/v1/search' + '?' + urlencode({ 25 | 'key': searchKey, 26 | }), 27 | data = requestBodyBytes, 28 | headers = { 29 | 'Content-Type': 'application/json; charset=utf-8', 30 | 'Content-Length': len(requestBodyBytes), 31 | 'User-Agent': userAgent, 32 | } 33 | ) 34 | try: 35 | self.response = urlopen(request, timeout=self.timeout).read().decode('utf_8') 36 | except: 37 | raise Exception('ERROR: Could not make request.') 38 | 39 | def _parseSource(self) -> None: 40 | try: 41 | if not self.continuationKey: 42 | responseContent = self._getValue(json.loads(self.response), contentPath) 43 | else: 44 | responseContent = self._getValue(json.loads(self.response), continuationContentPath) 45 | if responseContent: 46 | for element in responseContent: 47 | if itemSectionKey in element.keys(): 48 | self.responseSource = self._getValue(element, [itemSectionKey, 'contents']) 49 | if continuationItemKey in element.keys(): 50 | self.continuationKey = self._getValue(element, continuationKeyPath) 51 | else: 52 | self.responseSource = self._getValue(json.loads(self.response), fallbackContentPath) 53 | self.continuationKey = self._getValue(self.responseSource[-1], continuationKeyPath) 54 | except: 55 | raise Exception('ERROR: Could not parse YouTube response.') 56 | -------------------------------------------------------------------------------- /youtubesearchpython/legacy/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | import json 3 | from youtubesearchpython.handlers.componenthandler import ComponentHandler 4 | from youtubesearchpython.handlers.requesthandler import RequestHandler 5 | from youtubesearchpython.core.constants import * 6 | 7 | 8 | def overrides(interface_class): 9 | def overrider(method): 10 | assert(method.__name__ in dir(interface_class)) 11 | return method 12 | return overrider 13 | 14 | 15 | class LegacyComponentHandler(RequestHandler, ComponentHandler): 16 | index = 0 17 | 18 | @overrides(ComponentHandler) 19 | def _getVideoComponent(self, element: dict, shelfTitle: str = None) -> dict: 20 | video = element[videoElementKey] 21 | videoId = self.__getValue(video, ['videoId']) 22 | viewCount = 0 23 | thumbnails = [] 24 | for character in self.__getValue(video, ['viewCountText', 'simpleText']): 25 | if character.isnumeric(): 26 | viewCount = viewCount * 10 + int(character) 27 | modes = ['default', 'hqdefault', 'mqdefault', 'sddefault', 'maxresdefault'] 28 | for mode in modes: 29 | thumbnails.append('https://img.youtube.com/vi/' + videoId + '/' + mode + '.jpg') 30 | component = { 31 | 'index': self.index, 32 | 'id': videoId, 33 | 'link': 'https://www.youtube.com/watch?v=' + videoId, 34 | 'title': self.__getValue(video, ['title', 'runs', 0, 'text']), 35 | 'channel': self.__getValue(video, ['ownerText', 'runs', 0, 'text']), 36 | 'duration': self.__getValue(video, ['lengthText', 'simpleText']), 37 | 'views': viewCount, 38 | 'thumbnails': thumbnails, 39 | 'channeId': self.__getValue(video, ['ownerText', 'runs', 0, 'navigationEndpoint', 'browseEndpoint', 'browseId']), 40 | 'publishTime': self.__getValue(video, ['publishedTimeText', 'simpleText']), 41 | } 42 | self.index += 1 43 | return component 44 | 45 | @overrides(ComponentHandler) 46 | def _getPlaylistComponent(self, element: dict) -> dict: 47 | playlist = element[playlistElementKey] 48 | playlistId = self.__getValue(playlist, ['playlistId']) 49 | thumbnailVideoId = self.__getValue(playlist, ['navigationEndpoint', 'watchEndpoint', 'videoId']) 50 | thumbnails = [] 51 | modes = ['default', 'hqdefault', 'mqdefault', 'sddefault', 'maxresdefault'] 52 | for mode in modes: 53 | thumbnails.append('https://img.youtube.com/vi/' + thumbnailVideoId + '/' + mode + '.jpg') 54 | component = { 55 | 'index': self.index, 56 | 'id': playlistId, 57 | 'link': 'https://www.youtube.com/playlist?list=' + playlistId, 58 | 'title': self.__getValue(playlist, ['title', 'simpleText']), 59 | 'thumbnails': thumbnails, 60 | 'count': self.__getValue(playlist, ['videoCount']), 61 | 'channel': self.__getValue(playlist, ['shortBylineText', 'runs', 0, 'text']), 62 | } 63 | self.index += 1 64 | return component 65 | 66 | @overrides(ComponentHandler) 67 | def _getShelfComponent(self, element: dict) -> dict: 68 | shelf = element[shelfElementKey] 69 | return { 70 | 'title': self.__getValue(shelf, ['title', 'simpleText']), 71 | 'elements': self.__getValue(shelf, ['content', 'verticalListRenderer', 'items']), 72 | } 73 | 74 | def __getValue(self, component: dict, path: List[str]) -> Union[str, int, dict]: 75 | value = component 76 | for key in path: 77 | if type(key) is str: 78 | if key in value.keys(): 79 | value = value[key] 80 | else: 81 | value = 'LIVE' 82 | break 83 | elif type(key) is int: 84 | if len(value) != 0: 85 | value = value[key] 86 | else: 87 | value = 'LIVE' 88 | break 89 | return value 90 | 91 | class LegacySearchInternal(LegacyComponentHandler): 92 | exception = False 93 | resultComponents = [] 94 | responseSource = [] 95 | 96 | def __init__(self, keyword, offset, mode, max_results, language, region): 97 | self.page = offset 98 | self.query = keyword 99 | self.mode = mode 100 | self.limit = max_results 101 | self.language = language 102 | self.region = region 103 | self.continuationKey = None 104 | self.timeout = None 105 | 106 | def result(self) -> Union[str, dict, list, None]: 107 | '''Returns the search result. 108 | 109 | Returns: 110 | Union[str, dict, list, None]: Returns JSON, list or dictionary & None in case of any exception. 111 | ''' 112 | if self.exception or len(self.resultComponents) == 0: 113 | return None 114 | else: 115 | if self.mode == 'dict': 116 | return {'search_result': self.resultComponents} 117 | elif self.mode == 'json': 118 | return json.dumps({'search_result': self.resultComponents}, indent = 4) 119 | elif self.mode == 'list': 120 | result = [] 121 | for component in self.resultComponents: 122 | listComponent = [] 123 | for key in component.keys(): 124 | listComponent.append(component[key]) 125 | result.append(listComponent) 126 | return result 127 | 128 | 129 | class SearchVideos(LegacySearchInternal): 130 | ''' 131 | DEPRECATED 132 | ---------- 133 | Use `VideosSearch` instead. 134 | 135 | Searches for playlists in YouTube. 136 | 137 | Args: 138 | keyword (str): Sets the search query. 139 | offset (int, optional): Sets the search result page number. Defaults to 1. 140 | mode (str, optional): Sets the result type, can be 'json', 'dict' or 'list'. Defaults to 'json'. 141 | max_results (int, optional): Sets limit to the number of results. Defaults to 20. 142 | language (str, optional): Sets the result language. Defaults to 'en-US'. 143 | region (str, optional): Sets the result region. Defaults to 'US'. 144 | 145 | Examples: 146 | Calling `result` method gives the search result. 147 | 148 | >>> search = SearchPlaylists('Harry Styles', max_results = 1) 149 | >>> print(search.result()) 150 | { 151 | "search_result": [ 152 | { 153 | "index": 0, 154 | "id": "PLj-vAPBrjcxoBfEk3q2Jp-naXRFpekySW", 155 | "link": "https://www.youtube.com/playlist?list=PLj-vAPBrjcxoBfEk3q2Jp-naXRFpekySW", 156 | "title": "Harry Styles - Harry Styles Full Album videos with lyrics", 157 | "thumbnails": [ 158 | "https://img.youtube.com/vi/Y9yOG_dJwFg/default.jpg", 159 | "https://img.youtube.com/vi/Y9yOG_dJwFg/hqdefault.jpg", 160 | "https://img.youtube.com/vi/Y9yOG_dJwFg/mqdefault.jpg", 161 | "https://img.youtube.com/vi/Y9yOG_dJwFg/sddefault.jpg", 162 | "https://img.youtube.com/vi/Y9yOG_dJwFg/maxresdefault.jpg" 163 | ], 164 | "count": "10", 165 | "channel": "Jana Hol\u00fabkov\u00e1" 166 | } 167 | ] 168 | } 169 | ''' 170 | def __init__(self, keyword, offset = 1, mode = 'json', max_results = 20, language = 'en', region = 'US'): 171 | super().__init__(keyword, offset, mode, max_results, language, region) 172 | self.searchPreferences = 'EgIQAQ%3D%3D' 173 | self._makeRequest() 174 | self._parseSource() 175 | self.__makeComponents() 176 | 177 | def __makeComponents(self) -> None: 178 | self.resultComponents = [] 179 | for element in self.responseSource: 180 | if videoElementKey in element.keys(): 181 | self.resultComponents.append(self._getVideoComponent(element)) 182 | if shelfElementKey in element.keys(): 183 | for shelfElement in self._getShelfComponent(element)['elements']: 184 | self.resultComponents.append(self._getVideoComponent(shelfElement)) 185 | if len(self.resultComponents) >= self.limit: 186 | break 187 | 188 | class SearchPlaylists(LegacySearchInternal): 189 | ''' 190 | DEPRECATED 191 | ---------- 192 | Use `PlaylistsSearch` instead. 193 | 194 | Searches for playlists in YouTube. 195 | 196 | Args: 197 | keyword (str): Sets the search query. 198 | offset (int, optional): Sets the search result page number. Defaults to 1. 199 | mode (str, optional): Sets the result type, can be 'json', 'dict' or 'list'. Defaults to 'json'. 200 | max_results (int, optional): Sets limit to the number of results. Defaults to 20. 201 | language (str, optional): Sets the result language. Defaults to 'en-US'. 202 | region (str, optional): Sets the result region. Defaults to 'US'. 203 | 204 | Examples: 205 | Calling `result` method gives the search result. 206 | 207 | >>> search = SearchVideos('Watermelon Sugar', max_results = 1) 208 | >>> print(search.result()) 209 | { 210 | "search_result": [ 211 | { 212 | "index": 0, 213 | "id": "E07s5ZYygMg", 214 | "link": "https://www.youtube.com/watch?v=E07s5ZYygMg", 215 | "title": "Harry Styles - Watermelon Sugar (Official Video)", 216 | "channel": "Harry Styles", 217 | "duration": "3:09", 218 | "views": 162235006, 219 | "thumbnails": [ 220 | "https://img.youtube.com/vi/E07s5ZYygMg/default.jpg", 221 | "https://img.youtube.com/vi/E07s5ZYygMg/hqdefault.jpg", 222 | "https://img.youtube.com/vi/E07s5ZYygMg/mqdefault.jpg", 223 | "https://img.youtube.com/vi/E07s5ZYygMg/sddefault.jpg", 224 | "https://img.youtube.com/vi/E07s5ZYygMg/maxresdefault.jpg" 225 | ], 226 | "channeId": "UCZFWPqqPkFlNwIxcpsLOwew", 227 | "publishTime": "6 months ago" 228 | } 229 | ] 230 | } 231 | ''' 232 | def __init__(self, keyword, offset = 1, mode = 'json', max_results = 20, language = 'en', region = 'US'): 233 | super().__init__(keyword, offset, mode, max_results, language, region) 234 | self.searchPreferences = 'EgIQAw%3D%3D' 235 | self._makeRequest() 236 | self._parseSource() 237 | self.__makeComponents() 238 | 239 | def __makeComponents(self) -> None: 240 | self.resultComponents = [] 241 | for element in self.responseSource: 242 | if playlistElementKey in element.keys(): 243 | self.resultComponents.append(self._getPlaylistComponent(element)) 244 | if len(self.resultComponents) >= self.limit: 245 | break 246 | -------------------------------------------------------------------------------- /youtubesearchpython/search.py: -------------------------------------------------------------------------------- 1 | from youtubesearchpython.core.constants import * 2 | from youtubesearchpython.core.search import SearchCore 3 | from youtubesearchpython.core.channelsearch import ChannelSearchCore 4 | 5 | 6 | class Search(SearchCore): 7 | '''Searches for videos, channels & playlists in YouTube. 8 | 9 | Args: 10 | query (str): Sets the search query. 11 | limit (int, optional): Sets limit to the number of results. Defaults to 20. 12 | language (str, optional): Sets the result language. Defaults to 'en'. 13 | region (str, optional): Sets the result region. Defaults to 'US'. 14 | 15 | Examples: 16 | Calling `result` method gives the search result. 17 | 18 | >>> search = Search('Watermelon Sugar', limit = 1) 19 | >>> print(search.result()) 20 | { 21 | "result": [ 22 | { 23 | "type": "video", 24 | "id": "E07s5ZYygMg", 25 | "title": "Harry Styles - Watermelon Sugar (Official Video)", 26 | "publishedTime": "6 months ago", 27 | "duration": "3:09", 28 | "viewCount": { 29 | "text": "162,235,006 views", 30 | "short": "162M views" 31 | }, 32 | "thumbnails": [ 33 | { 34 | "url": "https://i.ytimg.com/vi/E07s5ZYygMg/hq720.jpg?sqp=-oaymwEjCOgCEMoBSFryq4qpAxUIARUAAAAAGAElAADIQj0AgKJDeAE=&rs=AOn4CLAOWBTE1SDrtrDQ1aWNzpDZ7YiMIw", 35 | "width": 360, 36 | "height": 202 37 | }, 38 | { 39 | "url": "https://i.ytimg.com/vi/E07s5ZYygMg/hq720.jpg?sqp=-oaymwEXCNAFEJQDSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLD7U54pGZLPKTuMP-J3kpm4LIDPVg", 40 | "width": 720, 41 | "height": 404 42 | } 43 | ], 44 | "descriptionSnippet": [ 45 | { 46 | "text": "This video is dedicated to touching. Listen to Harry Styles' new album 'Fine Line' now: https://HStyles.lnk.to/FineLineAY Follow\u00a0..." 47 | } 48 | ], 49 | "channel": { 50 | "name": "Harry Styles", 51 | "id": "UCZFWPqqPkFlNwIxcpsLOwew", 52 | "thumbnails": [ 53 | { 54 | "url": "https://yt3.ggpht.com/a-/AOh14GgNUvHxwlnz4RpHamcGnZF1px13VHj01TPksw=s68-c-k-c0x00ffffff-no-rj-mo", 55 | "width": 68, 56 | "height": 68 57 | } 58 | ], 59 | "link": "https://www.youtube.com/channel/UCZFWPqqPkFlNwIxcpsLOwew" 60 | }, 61 | "accessibility": { 62 | "title": "Harry Styles - Watermelon Sugar (Official Video) by Harry Styles 6 months ago 3 minutes, 9 seconds 162,235,006 views", 63 | "duration": "3 minutes, 9 seconds" 64 | }, 65 | "link": "https://www.youtube.com/watch?v=E07s5ZYygMg", 66 | "shelfTitle": null 67 | } 68 | ] 69 | } 70 | ''' 71 | def __init__(self, query: str, limit: int = 20, language: str = 'en', region: str = 'US', timeout: int = None): 72 | self.searchMode = (True, True, True) 73 | super().__init__(query, limit, language, region, None, timeout) 74 | self.sync_create() 75 | self._getComponents(*self.searchMode) 76 | 77 | def next(self) -> bool: 78 | return self._next() 79 | 80 | class VideosSearch(SearchCore): 81 | '''Searches for videos in YouTube. 82 | 83 | Args: 84 | query (str): Sets the search query. 85 | limit (int, optional): Sets limit to the number of results. Defaults to 20. 86 | language (str, optional): Sets the result language. Defaults to 'en'. 87 | region (str, optional): Sets the result region. Defaults to 'US'. 88 | 89 | Examples: 90 | Calling `result` method gives the search result. 91 | 92 | >>> search = VideosSearch('Watermelon Sugar', limit = 1) 93 | >>> print(search.result()) 94 | { 95 | "result": [ 96 | { 97 | "type": "video", 98 | "id": "E07s5ZYygMg", 99 | "title": "Harry Styles - Watermelon Sugar (Official Video)", 100 | "publishedTime": "6 months ago", 101 | "duration": "3:09", 102 | "viewCount": { 103 | "text": "162,235,006 views", 104 | "short": "162M views" 105 | }, 106 | "thumbnails": [ 107 | { 108 | "url": "https://i.ytimg.com/vi/E07s5ZYygMg/hq720.jpg?sqp=-oaymwEjCOgCEMoBSFryq4qpAxUIARUAAAAAGAElAADIQj0AgKJDeAE=&rs=AOn4CLAOWBTE1SDrtrDQ1aWNzpDZ7YiMIw", 109 | "width": 360, 110 | "height": 202 111 | }, 112 | { 113 | "url": "https://i.ytimg.com/vi/E07s5ZYygMg/hq720.jpg?sqp=-oaymwEXCNAFEJQDSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLD7U54pGZLPKTuMP-J3kpm4LIDPVg", 114 | "width": 720, 115 | "height": 404 116 | } 117 | ], 118 | "descriptionSnippet": [ 119 | { 120 | "text": "This video is dedicated to touching. Listen to Harry Styles' new album 'Fine Line' now: https://HStyles.lnk.to/FineLineAY Follow\u00a0..." 121 | } 122 | ], 123 | "channel": { 124 | "name": "Harry Styles", 125 | "id": "UCZFWPqqPkFlNwIxcpsLOwew", 126 | "thumbnails": [ 127 | { 128 | "url": "https://yt3.ggpht.com/a-/AOh14GgNUvHxwlnz4RpHamcGnZF1px13VHj01TPksw=s68-c-k-c0x00ffffff-no-rj-mo", 129 | "width": 68, 130 | "height": 68 131 | } 132 | ], 133 | "link": "https://www.youtube.com/channel/UCZFWPqqPkFlNwIxcpsLOwew" 134 | }, 135 | "accessibility": { 136 | "title": "Harry Styles - Watermelon Sugar (Official Video) by Harry Styles 6 months ago 3 minutes, 9 seconds 162,235,006 views", 137 | "duration": "3 minutes, 9 seconds" 138 | }, 139 | "link": "https://www.youtube.com/watch?v=E07s5ZYygMg", 140 | "shelfTitle": null 141 | } 142 | ] 143 | } 144 | ''' 145 | def __init__(self, query: str, limit: int = 20, language: str = 'en', region: str = 'US', timeout: int = None): 146 | self.searchMode = (True, False, False) 147 | super().__init__(query, limit, language, region, SearchMode.videos, timeout) 148 | self.sync_create() 149 | self._getComponents(*self.searchMode) 150 | 151 | def next(self) -> bool: 152 | return self._next() 153 | 154 | 155 | class ChannelsSearch(SearchCore): 156 | '''Searches for channels in YouTube. 157 | 158 | Args: 159 | query (str): Sets the search query. 160 | limit (int, optional): Sets limit to the number of results. Defaults to 20. 161 | language (str, optional): Sets the result language. Defaults to 'en'. 162 | region (str, optional): Sets the result region. Defaults to 'US'. 163 | 164 | Examples: 165 | Calling `result` method gives the search result. 166 | 167 | >>> search = ChannelsSearch('Harry Styles', limit = 1) 168 | >>> print(search.result()) 169 | { 170 | "result": [ 171 | { 172 | "type": "channel", 173 | "id": "UCZFWPqqPkFlNwIxcpsLOwew", 174 | "title": "Harry Styles", 175 | "thumbnails": [ 176 | { 177 | "url": "https://yt3.ggpht.com/ytc/AAUvwnhR81ocC_KalYEk5ItnJcfMBqaiIpuM1B0lJyg4Rw=s88-c-k-c0x00ffffff-no-rj-mo", 178 | "width": 88, 179 | "height": 88 180 | }, 181 | { 182 | "url": "https://yt3.ggpht.com/ytc/AAUvwnhR81ocC_KalYEk5ItnJcfMBqaiIpuM1B0lJyg4Rw=s176-c-k-c0x00ffffff-no-rj-mo", 183 | "width": 176, 184 | "height": 176 185 | } 186 | ], 187 | "videoCount": "7", 188 | "descriptionSnippet": null, 189 | "subscribers": "9.25M subscribers", 190 | "link": "https://www.youtube.com/channel/UCZFWPqqPkFlNwIxcpsLOwew" 191 | } 192 | ] 193 | } 194 | ''' 195 | def __init__(self, query: str, limit: int = 20, language: str = 'en', region: str = 'US', timeout: int = None): 196 | self.searchMode = (False, True, False) 197 | super().__init__(query, limit, language, region, SearchMode.channels, timeout) 198 | self.sync_create() 199 | self._getComponents(*self.searchMode) 200 | 201 | def next(self) -> bool: 202 | return self._next() 203 | 204 | 205 | class PlaylistsSearch(SearchCore): 206 | '''Searches for playlists in YouTube. 207 | 208 | Args: 209 | query (str): Sets the search query. 210 | limit (int, optional): Sets limit to the number of results. Defaults to 20. 211 | language (str, optional): Sets the result language. Defaults to 'en'. 212 | region (str, optional): Sets the result region. Defaults to 'US'. 213 | 214 | Examples: 215 | Calling `result` method gives the search result. 216 | 217 | >>> search = PlaylistsSearch('Harry Styles', limit = 1) 218 | >>> print(search.result()) 219 | { 220 | "result": [ 221 | { 222 | "type": "playlist", 223 | "id": "PL-Rt4gIwHnyvxpEl-9Le0ePztR7WxGDGV", 224 | "title": "fine line harry styles full album lyrics", 225 | "videoCount": "12", 226 | "channel": { 227 | "name": "ourmemoriestonight", 228 | "id": "UCZCmb5a8LE9LMxW9I3-BFjA", 229 | "link": "https://www.youtube.com/channel/UCZCmb5a8LE9LMxW9I3-BFjA" 230 | }, 231 | "thumbnails": [ 232 | { 233 | "url": "https://i.ytimg.com/vi/raTh8Mu5oyM/hqdefault.jpg?sqp=-oaymwEWCKgBEF5IWvKriqkDCQgBFQAAiEIYAQ==&rs=AOn4CLCdCfOQYMrPImHMObdrMcNimKi1PA", 234 | "width": 168, 235 | "height": 94 236 | }, 237 | { 238 | "url": "https://i.ytimg.com/vi/raTh8Mu5oyM/hqdefault.jpg?sqp=-oaymwEWCMQBEG5IWvKriqkDCQgBFQAAiEIYAQ==&rs=AOn4CLDsKmyGH8bkmt9MzZqIoXI4UaduBw", 239 | "width": 196, 240 | "height": 110 241 | }, 242 | { 243 | "url": "https://i.ytimg.com/vi/raTh8Mu5oyM/hqdefault.jpg?sqp=-oaymwEXCPYBEIoBSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLD9v7S0KeHLBLr0bF-LrRjYVycUFA", 244 | "width": 246, 245 | "height": 138 246 | }, 247 | { 248 | "url": "https://i.ytimg.com/vi/raTh8Mu5oyM/hqdefault.jpg?sqp=-oaymwEXCNACELwBSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLAIzQIVxZsC0PfvLOt-v9UWJ-109Q", 249 | "width": 336, 250 | "height": 188 251 | } 252 | ], 253 | "link": "https://www.youtube.com/playlist?list=PL-Rt4gIwHnyvxpEl-9Le0ePztR7WxGDGV" 254 | } 255 | ] 256 | } 257 | ''' 258 | def __init__(self, query: str, limit: int = 20, language: str = 'en', region: str = 'US', timeout: int = None): 259 | self.searchMode = (False, False, True) 260 | super().__init__(query, limit, language, region, SearchMode.playlists, timeout) 261 | self.sync_create() 262 | self._getComponents(*self.searchMode) 263 | 264 | def next(self) -> bool: 265 | return self._next() 266 | 267 | 268 | class ChannelSearch(ChannelSearchCore): 269 | '''Searches for videos in specific channel in YouTube. 270 | 271 | Args: 272 | query (str): Sets the search query. 273 | browseId (str): Channel ID 274 | language (str, optional): Sets the result language. Defaults to 'en'. 275 | region (str, optional): Sets the result region. Defaults to 'US'. 276 | 277 | Examples: 278 | Calling `result` method gives the search result. 279 | 280 | >>> search = ChannelSearch('Watermelon Sugar', "UCZFWPqqPkFlNwIxcpsLOwew") 281 | >>> print(search.result()) 282 | { 283 | "result": [ 284 | { 285 | "id": "WMcIfZuRuU8", 286 | "thumbnails": { 287 | "normal": [ 288 | { 289 | "url": "https://i.ytimg.com/vi/WMcIfZuRuU8/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLClFg6C1r5NfTQy7TYUq6X5qHUmPA", 290 | "width": 168, 291 | "height": 94 292 | }, 293 | { 294 | "url": "https://i.ytimg.com/vi/WMcIfZuRuU8/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAoOyftwY0jLV4geWb5hejULYp3Zw", 295 | "width": 196, 296 | "height": 110 297 | }, 298 | { 299 | "url": "https://i.ytimg.com/vi/WMcIfZuRuU8/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCdqkhn7JDwLvRtTNx3jq-olz7k-Q", 300 | "width": 246, 301 | "height": 138 302 | }, 303 | { 304 | "url": "https://i.ytimg.com/vi/WMcIfZuRuU8/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLAhYedsqBFKI0Ra2qzIv9cVoZhfKQ", 305 | "width": 336, 306 | "height": 188 307 | } 308 | ], 309 | "rich": null 310 | }, 311 | "title": "Harry Styles \u2013 Watermelon Sugar (Lost Tour Visual)", 312 | "descriptionSnippet": "This video is dedicated to touching.\nListen to Harry Styles\u2019 new album \u2018Fine Line\u2019 now: https://HStyles.lnk.to/FineLineAY \n\nFollow Harry Styles:\nFacebook: https://HarryStyles.lnk.to/followFI...", 313 | "uri": "/watch?v=WMcIfZuRuU8", 314 | "views": { 315 | "precise": "3,888,287 views", 316 | "simple": "3.8M views", 317 | "approximate": "3.8 million views" 318 | }, 319 | "duration": { 320 | "simpleText": "2:55", 321 | "text": "2 minutes, 55 seconds" 322 | }, 323 | "published": "10 months ago", 324 | "channel": { 325 | "name": "Harry Styles", 326 | "thumbnails": [ 327 | { 328 | "url": "https://yt3.ggpht.com/ytc/AAUvwnhR81ocC_KalYEk5ItnJcfMBqaiIpuM1B0lJyg4Rw=s88-c-k-c0x00ffffff-no-rj", 329 | "width": 68, 330 | "height": 68 331 | } 332 | ] 333 | }, 334 | "type": "video" 335 | }, 336 | ] 337 | } 338 | ''' 339 | 340 | def __init__(self, query: str, browseId: str, language: str = 'en', region: str = 'US', searchPreferences: str = "EgZzZWFyY2g%3D", timeout: int = None): 341 | super().__init__(query, language, region, searchPreferences, browseId, timeout) 342 | self.sync_create() 343 | 344 | 345 | class CustomSearch(SearchCore): 346 | '''Performs custom search in YouTube with search filters or sorting orders. 347 | Few of the predefined filters and sorting orders are: 348 | 349 | 1 - SearchMode.videos 350 | 2 - VideoUploadDateFilter.lastHour 351 | 3 - VideoDurationFilter.long 352 | 4 - VideoSortOrder.viewCount 353 | 354 | There are many other to use. 355 | The value of `sp` parameter in the YouTube search query can be used as a search filter e.g. 356 | `EgQIBRAB` from https://www.youtube.com/results?search_query=NoCopyrightSounds&sp=EgQIBRAB can be passed as `searchPreferences`, to get videos, which are uploaded this year. 357 | 358 | Args: 359 | query (str): Sets the search query. 360 | searchPreferences (str): Sets the `sp` query parameter in the YouTube search request. 361 | limit (int, optional): Sets limit to the number of results. Defaults to 20. 362 | language (str, optional): Sets the result language. Defaults to 'en'. 363 | region (str, optional): Sets the result region. Defaults to 'US'. 364 | 365 | Examples: 366 | Calling `result` method gives the search result. 367 | 368 | >>> search = CustomSearch('Harry Styles', VideoSortOrder.viewCount, limit = 1) 369 | >>> print(search.result()) 370 | { 371 | "result": [ 372 | { 373 | "type": "video", 374 | "id": "QJO3ROT-A4E", 375 | "title": "One Direction - What Makes You Beautiful (Official Video)", 376 | "publishedTime": "9 years ago", 377 | "duration": "3:27", 378 | "viewCount": { 379 | "text": "1,212,146,802 views", 380 | "short": "1.2B views" 381 | }, 382 | "thumbnails": [ 383 | { 384 | "url": "https://i.ytimg.com/vi/QJO3ROT-A4E/hq720.jpg?sqp=-oaymwEjCOgCEMoBSFryq4qpAxUIARUAAAAAGAElAADIQj0AgKJDeAE=&rs=AOn4CLDeFKrH99gmpnvKyG4czdd__YRDkw", 385 | "width": 360, 386 | "height": 202 387 | }, 388 | { 389 | "url": "https://i.ytimg.com/vi/QJO3ROT-A4E/hq720.jpg?sqp=-oaymwEXCNAFEJQDSFryq4qpAwkIARUAAIhCGAE=&rs=AOn4CLBJ_wUjsRFXGsbvRpwYpSLlsGmbkw", 390 | "width": 720, 391 | "height": 404 392 | } 393 | ], 394 | "descriptionSnippet": [ 395 | { 396 | "text": "One Direction \u2013 What Makes You Beautiful (Official Video) Follow on Spotify - https://1D.lnk.to/Spotify Listen on Apple Music\u00a0..." 397 | } 398 | ], 399 | "channel": { 400 | "name": "One Direction", 401 | "id": "UCb2HGwORFBo94DmRx4oLzow", 402 | "thumbnails": [ 403 | { 404 | "url": "https://yt3.ggpht.com/a-/AOh14Gj3SMvtIAvVNUrHWFTJFubPN7qozzPl5gFkoA=s68-c-k-c0x00ffffff-no-rj-mo", 405 | "width": 68, 406 | "height": 68 407 | } 408 | ], 409 | "link": "https://www.youtube.com/channel/UCb2HGwORFBo94DmRx4oLzow" 410 | }, 411 | "accessibility": { 412 | "title": "One Direction - What Makes You Beautiful (Official Video) by One Direction 9 years ago 3 minutes, 27 seconds 1,212,146,802 views", 413 | "duration": "3 minutes, 27 seconds" 414 | }, 415 | "link": "https://www.youtube.com/watch?v=QJO3ROT-A4E", 416 | "shelfTitle": null 417 | } 418 | ] 419 | } 420 | ''' 421 | def __init__(self, query: str, searchPreferences: str, limit: int = 20, language: str = 'en', region: str = 'US', timeout: int = None): 422 | self.searchMode = (True, True, True) 423 | super().__init__(query, limit, language, region, searchPreferences, timeout) 424 | self.sync_create() 425 | self._getComponents(*self.searchMode) 426 | 427 | def next(self): 428 | self._next() 429 | -------------------------------------------------------------------------------- /youtubesearchpython/streamurlfetcher.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from youtubesearchpython.core.streamurlfetcher import StreamURLFetcherCore 3 | 4 | 5 | class StreamURLFetcher(StreamURLFetcherCore): 6 | '''Gets direct stream URLs for a YouTube video fetched using `Video.get` or `Video.getFormats`. 7 | 8 | This class can fetch direct video URLs without any additional network requests (that's really fast). 9 | 10 | Call `get` or `getAll` method of this class & pass response returned by `Video.get` or `Video.getFormats` as parameter to fetch direct URLs. 11 | Getting URLs or downloading streams using youtube-dl or PyTube is can be a slow, because of the fact that they make requests to fetch the same content, which one might have already recieved at the time of showing it to the user etc. 12 | This class makes use of PyTube (if installed) & makes some slight improvements to functioning of PyTube. 13 | Avoid instantiating this class more than once, it will be slow (making global object of the class will be a recommended solution). 14 | 15 | Raises: 16 | Exception: "ERROR: PyTube is not installed. To use this functionality of youtube-search-python, PyTube must be installed." 17 | 18 | Examples: 19 | Returns direct stream URL. 20 | 21 | >>> from youtubesearchpython import * 22 | >>> fetcher = StreamURLFetcher() 23 | >>> video = Video.get("https://www.youtube.com/watch?v=aqz-KE-bpKQ") 24 | >>> url = fetcher.get(video, 251) 25 | >>> print(url) 26 | "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=251&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=audio%2Fwebm&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=10210834&dur=634.601&lmt=1544629945422176&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5511222&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgKKIEiwQTgXsdKPEyOckgVPs_LMH6KJoeaYmZic_lelECIHXHs1ZnSP5mgtpffNlIMJM3DhxcvDbA-4udFFE6AmVP&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D" 27 | ''' 28 | def __init__(self): 29 | super().__init__() 30 | #self._getJS() 31 | 32 | def get(self, videoFormats: dict, itag: int) -> Union[str, None]: 33 | '''Gets direct stream URL for a YouTube video fetched using `Video.get` or `Video.getFormats`. 34 | 35 | Args: 36 | videoFormats (dict): Dictionary returned by `Video.get` or `Video.getFormats`. 37 | itag (int): Itag of the required stream. 38 | 39 | Returns: 40 | Union[str, None]: Returns stream URL as string. None, if no stream is present for that itag. 41 | 42 | Examples: 43 | Returns direct stream URL. 44 | 45 | >>> from youtubesearchpython import * 46 | >>> fetcher = StreamURLFetcher() 47 | >>> video = Video.get("https://www.youtube.com/watch?v=aqz-KE-bpKQ") 48 | >>> url = fetcher.get(video, 251) 49 | >>> print(url) 50 | "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=251&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=audio%2Fwebm&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=10210834&dur=634.601&lmt=1544629945422176&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5511222&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRAIgKKIEiwQTgXsdKPEyOckgVPs_LMH6KJoeaYmZic_lelECIHXHs1ZnSP5mgtpffNlIMJM3DhxcvDbA-4udFFE6AmVP&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D" 51 | ''' 52 | self._getDecipheredURLs(videoFormats, itag) 53 | if len(self._streams) == 1: 54 | return self._streams[0]["url"] 55 | return None 56 | 57 | def getAll(self, videoFormats: dict) -> Union[dict, None]: 58 | '''Gets all stream URLs for a YouTube video fetched using `Video.get` or `Video.getFormats`. 59 | 60 | Args: 61 | videoFormats (dict): Dictionary returned by `Video.get` or `Video.getFormats`. 62 | 63 | Returns: 64 | Union[dict, None]: Returns stream URLs in a dictionary. 65 | 66 | Examples: 67 | Returns direct stream URLs in a dictionary. 68 | 69 | >>> from youtubesearchpython import * 70 | >>> fetcher = StreamURLFetcher() 71 | >>> video = Video.get("https://www.youtube.com/watch?v=aqz-KE-bpKQ") 72 | >>> allUrls = fetcher.getAll(video) 73 | >>> print(allUrls) 74 | { 75 | "streams": [ 76 | { 77 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=18&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=video%2Fmp4&ns=AAHB1CvhVqlATtzQj67WHI8F&gir=yes&clen=47526444&ratebypass=yes&dur=634.624&lmt=1544610273905877&mt=1610776131&fvip=6&c=WEB&txp=5531432&n=Laycu1cJ2fCN_K&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRQIgdjTwmtEc3MpmRxH27ZvTgktL-d2by5HXXGFwo3EGR4MCIQDi0oiI8mshGssiOFu1XzQCqljZuNLhA6z19S8Ig0CRTQ%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 78 | "type": "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", 79 | "quality": "medium", 80 | "itag": 18, 81 | "bitrate": 599167, 82 | "is_otf": false 83 | }, 84 | { 85 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=22&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=video%2Fmp4&ns=AAHB1CvhVqlATtzQj67WHI8F&ratebypass=yes&dur=634.624&lmt=1544610886483826&mt=1610776131&fvip=6&c=WEB&txp=5532432&n=Laycu1cJ2fCN_K&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRQIhALaSHkcx0m9rfqJKoiJT1dY7spIKf-zDfq12SOdN7Ej5AiBCgvcUvLUGqGoMBnc0NIQtDeNM8ETJD2lTt9Bi7T186g%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 86 | "type": "video/mp4; codecs=\"avc1.64001F, mp4a.40.2\"", 87 | "quality": "hd720", 88 | "itag": 22, 89 | "bitrate": 1340380, 90 | "is_otf": false 91 | }, 92 | { 93 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=315&aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C394%2C395%2C396%2C397%2C398%2C399&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=video%2Fwebm&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=1648069666&dur=634.566&lmt=1544611995945231&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5532432&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIgGaJmx70EkBCsfAYOI1lI695hXnFSEn-ZAfRiqWrnt9ACIQClBT5YZlou5ttgFzKnLZkUKxjZznxMJGPTNvtXCAlebw%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 94 | "type": "video/webm; codecs=\"vp9\"", 95 | "quality": "hd2160", 96 | "itag": 315, 97 | "bitrate": 26416339, 98 | "is_otf": false 99 | }, 100 | { 101 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=308&aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C394%2C395%2C396%2C397%2C398%2C399&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=video%2Fwebm&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=627075264&dur=634.566&lmt=1544611159960793&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5532432&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhALl1_ksmnpBhD49Hgjdg-z-Y4H2AL8hBx63ephvsvhbCAiAFrqyy65MimA4mCXYQBopP67G9dtwH9xyjHS_0hZ-rJA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 102 | "type": "video/webm; codecs=\"vp9\"", 103 | "quality": "hd1440", 104 | "itag": 308, 105 | "bitrate": 13381315, 106 | "is_otf": false 107 | }, 108 | { 109 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=134&aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C394%2C395%2C396%2C397%2C398%2C399&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=video%2Fmp4&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=26072934&dur=634.566&lmt=1544609325917976&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5532432&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAKT9N5EmUz3OQOc9IA8P1CuYgzPStz4ulJvCkA8Y1Cf4AiEAwwC2mCjOFWD5jFhAu8g0O6EF5fYJ7HmwskN1sjqTHlA%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 110 | "type": "video/mp4; codecs=\"avc1.4d401e\"", 111 | "quality": "medium", 112 | "itag": 134, 113 | "bitrate": 723888, 114 | "is_otf": false 115 | }, 116 | { 117 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=249&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=audio%2Fwebm&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=3936299&dur=634.601&lmt=1544629945028066&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5511222&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRQIhAJ_UffgeslE26GFwlMZHBsW-zYLcnanMqrvESdjWoupYAiAH7KlvQlYsokTVCCcD7jflD21Fjiim28qNzhOKZ88D3Q%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 118 | "type": "audio/webm; codecs=\"opus\"", 119 | "quality": "tiny", 120 | "itag": 249, 121 | "bitrate": 57976, 122 | "is_otf": false 123 | }, 124 | { 125 | "url": "https://r6---sn-gwpa-5bgk.googlevideo.com/videoplayback?expire=1610798125&ei=zX8CYITXEIGKz7sP9MWL0AE&ip=2409%3A4053%3A803%3A2b22%3Adc68%3Adfb9%3Aa676%3A26a3&id=o-APBakKSE2_eMDMegtCmeWXfuhhUfAzJTmOCWj4lkEjAM&itag=258&source=youtube&requiressl=yes&mh=aP&mm=31%2C29&mn=sn-gwpa-5bgk%2Csn-gwpa-qxad&ms=au%2Crdu&mv=m&mvi=6&pl=36&initcwndbps=146250&vprv=1&mime=audio%2Fmp4&ns=ULL4mkMO31KDtEhOjkOrmpkF&gir=yes&clen=30769612&dur=634.666&lmt=1544629837561969&mt=1610776131&fvip=6&keepalive=yes&c=WEB&txp=5511222&n=uEjSqtzBZaJyVn&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&sig=AOq0QJ8wRgIhAP6XrnFm3AHxyk8xjU6mJLdVN-uWLl1ItHk5_ONUiRuPAiEAlEYQBsOoEraFemkJIL7OMyHL9aszxW4CbDlxro-AY3Q%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRQIhAPmhL745RYeL_ffgUJk_xJLC-8riXKMylLTLA_pITYWWAiB2qUIXur8ThW7cLfQ73mIVK61mMZc2ncK6FZWjUHGcUw%3D%3D", 126 | "type": "audio/mp4; codecs=\"mp4a.40.2\"", 127 | "quality": "tiny", 128 | "itag": 258, 129 | "bitrate": 390017, 130 | "is_otf": false 131 | } 132 | ] 133 | } 134 | ''' 135 | self._getDecipheredURLs(videoFormats) 136 | return {"streams": self._streams} 137 | --------------------------------------------------------------------------------