Skip to content

Feature innertube client fix - #219

Open
alive4ever wants to merge 10 commits into
user234683:masterfrom
alive4ever:feature-innertube-client-fix
Open

Feature innertube client fix#219
alive4ever wants to merge 10 commits into
user234683:masterfrom
alive4ever:feature-innertube-client-fix

Conversation

@alive4ever

@alive4ever alive4ever commented Oct 28, 2024

Copy link
Copy Markdown
Contributor

The major change is the addition of mweb innertube client, which includes some refactoring of how base_js is handled and a new trick to decrypt n signature by extracting the relevant decryption code from base_js file, using technique similar to iv-org/inv_sig_helper. Update: the signature decryption is based on yt-dlp-ejs.

This pull request also introduce three dependencies: fake-useragent to simplify user agent header creation of mobile and desktop browser, flpc to parse nsig decryption regex, and dukpy several js_runtimes1 to execute the extracted nsig decryption code.

Also add the ability to parse visitorData in the YT Api response and specifying own visitorData and poToken pair using properly formatted json in the data/po_token_cache.txt file.

Also several more fixes for android and ios client and make innertube client selectable.

Also several changes in settings.py, notably to allow reloading of tv_embedded client in case of missing player urls and showing Download placeholder via use_video_download option, which credits ~heckyel/yt-local.

This will hopefully fix #218. Update: it has been fixed for a long time.


Update: fake-useragent has been removed from the requirements.txt since the module doesn't work with python 3.7 (oldwin) and 3.8 (latest version supporting win7). No more prebuilt release for oldwin.

Also added a workaround to get latest INNERTUBE_CONTEXT for web, mweb, and tv client via ytcfg.

Prebuilt releases for Windows (both 64 bit and 32 bit oldwin2) can be found on my releases page.


Footnotes

  1. One of [ 'deno', 'bun', 'node'], using yt-dlp-ejs package. Make sure the js runtime is available in $PATH (i.e. bring your own runtime) before running the server:application, otherwise the web innertube clients won't work.

  2. No more oldwin build, because the integration of yt-dlp-ejs requires python>=3.10

@alive4ever
alive4ever force-pushed the feature-innertube-client-fix branch 2 times, most recently from 1da876d to 5276825 Compare November 5, 2024 15:12
@alive4ever

Copy link
Copy Markdown
Contributor Author

Hi, it's been a while without any feedback.

For n signature solving, dukpy is used here. I tried several python js bindings before coming to this setup.

  • First, I tried importing JSInterpreter from yt-dlp project. This add yt-dlp as dependency, which is huge (more than 20MB) and the js execution time is slow. The nice thing is that JSInterpreter can execute js functions from base.js without extracting the specified function before.
  • The second is py-mini-racer. It is slightly faster than JSInterpreter from yt-dlp but the package is rather huge (18 MB of site-package files). It requires the js function to be extracted.
  • The third is dukpy, which I consider very good with smaller site-package than py-mini-racer (9MB). It also needs the js function to be extracted.
  • The fourth is combining quickjs with jsengine, which is fastest and smallest (2MB). It can call js function directly from base.js. The downside is there is no pre-built wheel for quickjs module on aarch64 which results in source install for aarch64, so I stick to dukpy for this reason (pre-built wheels and faster aarch64 install.

@user234683

user234683 commented Nov 8, 2024 via email

Copy link
Copy Markdown
Owner

@alive4ever

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback. It gives a peace of mind for me.

No need to rush. I am open for any improvement suggestions to this pull request.

Comment thread youtube/util.py Outdated
print('Unable to access ' + player_file)

signature_timestamp = None
signature_timestamp_cache = settings.data_dir + '/sts_' + player_version + 'txt'

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend using os.path.join here. Also, .txt, not txt

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for late reply.

I've used os.path.join in place of string concatenation in this file and similar places.

Comment thread youtube/util.py Outdated
response_dict = json.loads(response)
if settings.use_visitor_data:
if not settings.use_po_token:
if response_dict['responseContext'].get('visitorData'):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't assume 'responseContext' will be present - otherwise it will raise an exception when youtube changes something.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've put this inside try ... except block, with specific KeyError exception message.

Comment thread youtube/util.py Outdated
if settings.use_visitor_data:
if not settings.use_po_token:
if response_dict['responseContext'].get('visitorData'):
if not os.path.exists(visitor_data_file):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how the visitor system works - but do we want to refresh this file ever? Maybe YouTube issues an updated token for example and marks the old one as invalid?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added os.path.getmtime check to make sure that the visitorData.txt file is less than 86400 seconds old before using its content. Otherwise, the visitor data file will be deleted and replaced with new one.

Comment thread youtube/util.py Outdated
else:
if os.path.exists(visitor_data_file):
print('Removing visitor_data file')
os.remove(visitor_data_file)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For anonymity's sake - do we want to consider refreshing the visitor data every day? Again, not really sure what constraints go into it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, as I mentioned above.

Comment thread server.py Outdated
with open(visitor_data_file, "r") as file:
visitor_data = file.read()
file.close()
except:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use except Exception, otherwise you'll catch KeyboardInterrupt and SystemExit: https://stackoverflow.com/questions/54948548/what-is-wrong-with-using-a-bare-except

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added except OSError to notify if there is a file access error which prevents access to the visitor data file.


def extract_nsig_func(base_js):
for i, member in enumerate(NSIG_FUNCTION_ARRAYS):
func_array_re = regex.compile(member.replace('$', '\\$'))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend against this; I would just put the three \ escapes you need into your regex instead of modifying it at runtime

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only tried doing what inv_sig_helper does, copying exactly the same regex pattern with runtime replacement of escaping dollar sign which only done once for as long as the extracted nsig_func_{player_version}.js file exists.

The resulting n_sig_code is cached as data/nsig_func_{player_version}.js and loaded as info['nsig_func'] = { player_version: js_nsig_decrypt_code } during runtime of the youtube-local` session.

So the n_sig_function extraction is only done once and the subsequent access to it is either loaded directly from the info['nsig_func'] dict or loaded from nsig_func_{player_version}.js file if the file is already exists.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, as requested by the project author.

func_body_re = []
for i, member in enumerate(NSIG_FUNCTION_ENDINGS):
func_body_re_item = ''
func_body_re_item += func_context.group(1)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to do a re.escape() on this before appending it to your regexes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +1111 to +1119
print('jscode len is: ' + str(len(jscode)))
dukpy_session = dukpy.JSInterpreter()
# Loading the function into dukpy session
dukpy_session.evaljs(jscode)
print('n_sig = ' + n_sig)
#n_sig_result = dukpy_session.evaljs('decrypt_nsig("' + n_sig + '")')
n_sig_result = dukpy_session.evaljs("decrypt_nsig(dukpy['n'])", n=n_sig)
print('n_sig_result = ' + n_sig_result)
return n_sig_result

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we verified that dukpy has limited execution privileges? For instance, can javascript code executed with Dukpy make network requests or open files? If so it would be a massive security hole

Also recommend removing these debugging print statmenets when you're done

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dukpy is just a python wrapper for duktape js engine.

The pypi package of dukpy has dukpy-install command which is able to download npm packages from the internet.

Unless told to do so, dukpy module doesn't access the internet for as far as I know. The nsig_func doesn't need access to the internet during runtime, which I have verified doing manual n_sig decryption using various python js bindings.

I also consider dukpy as just-work lightweight js engine for python, since it has wheels for arm64 on pypi and armhf on piwheels.org so if anyone runs this on their single board computers, they will hopefully meet no problems during runtime.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirming that this works on arm64.

Comment thread requirements.txt Outdated
cachetools>=4.0.0
stem>=1.8.0
fake-useragent>=1.5.1
flpc>=0.2.5

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you do any performance testing that suggested the need for this? Is there a noticeable speedup? Would rather avoid dependencies if possible

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With no break statement, the re module will hang for some time. flpc will not hang in finding the specified regex, even without break statement in the for loop.

I added the break statement in the for loop so the regex engine will be freed from work (i.e. testing another regex pattern) after a match is found, which mitigates hanging on the built-in re module.

I've removed flpc from the requirements to use the built-in re module as you wish, with very small or no performance degradation during my extended testing.

Comment thread youtube/util.py Outdated
@sapnolas

Copy link
Copy Markdown

Any chance you could make a version of this similar to the youtube local main branch that uses Python 3.6 or earlier so it can run on Windows 7?

@alive4ever

Copy link
Copy Markdown
Contributor Author

Any chance you could make a version of this similar to the youtube local main branch that uses Python 3.6 or earlier so it can run on Windows 7?

I actually tried several times to also build packages for oldwin (32 bit) as found on my github-actions history with no success. Tried several version of python, from oldwin to 3.8.7 with failure regarding finding _socks module.

So I reverted my github action recipe to only build for python 3.11 on Windows.

@alive4ever
alive4ever force-pushed the feature-innertube-client-fix branch from f50cae3 to c7e1cac Compare November 24, 2024 02:05
@alive4ever

Copy link
Copy Markdown
Contributor Author

Update for today:

Currently experiencing 403 errors with mweb client after ±1 minutes of video playback on formats other than integrated 360p format. Still haven't found its cause.

Full video can only be played on mweb with integrated 360p format (itag 18).

@alive4ever

alive4ever commented Dec 17, 2024

Copy link
Copy Markdown
Contributor Author

Update for today:

Currently experiencing 403 errors with mweb client after ±1 minutes of video playback on formats other than integrated 360p format. Still haven't found its cause.

Full video can only be played on mweb with integrated 360p format (itag 18).

Update for today:

It seems that the cause of 1 minute playable stream is mweb client started to require po_token. According to BgUtils author, this is case 2 of mandatory po_token use.

When to Use a PoToken

YouTube's web player checks the "sps" (StreamProtectionStatus) of each media segment request (only if using UMP or SABR; our browser example uses UMP) to determine if the stream needs a PoToken.

Status 1: The stream is either already using a PoToken or does not need one.
Status 2: The stream requires a PoToken but will allow the client to request up to 1-2 MB of data before interrupting playback.
Status 3: The stream requires a PoToken and will interrupt playback immediately.

Adding data/po_token_cache.txt and enabling settings.use_po_token solves this issue for me.

Btw, po_token_cache.txt can be extracted from browser or created using bgutils or similar tools.

Any improvement suggestion is appreciated.

@alive4ever

Copy link
Copy Markdown
Contributor Author

Currently encountering issue with player 643afba4 and newer, so client which requires js player doesn't work.

@alive4ever

Copy link
Copy Markdown
Contributor Author

Hi, for those who are interested, I have created my own simple YT web player based on Quart and yt_dlp. Check it out.

@user234683

Copy link
Copy Markdown
Owner

Haven't had bandwidth to engage with this repo in awhile, sorry. Is this PR ready to go or still needed, do you want me to rereview it? I recall the hotfix you provided awhile back fixed most videos (except age restricted and a couple copyrighted videos maybe); does this fix address anything else currently?

@alive4ever

Copy link
Copy Markdown
Contributor Author

This branch is working for my needs, i.e. a self hosted instance, and the accumulated changes has gotten too much to be reviewed.

The issue I encountered myself is there is probability of race condition when YT updated the player version when there is older cached iframe_api_{player_version}.js in the data directory.

@alive4ever
alive4ever force-pushed the feature-innertube-client-fix branch from 2f3f058 to e3f450e Compare August 28, 2025 02:36
@alive4ever

Copy link
Copy Markdown
Contributor Author

Currently unable to extract n-signature for player 2b83d2e0, so switching to non js player (innertube_client_id = 8) is needed for working video playback.

The issue also happens with third party yt frontend out there.

@alive4ever

Copy link
Copy Markdown
Contributor Author

Currently, the hardcoded player version causes 403 Forbidden errors. The js code required to decrypt the signatures for recent player versions are getting more complicated and breaking current workaround using regex based js code extraction.

The robust solution for js code extraction is using AST based parser, which acts like a JS Minifier to create JS script containing only necessary code to run signature decryption.

I've looked at the result of js code extraction by @LuanRT (https://github.com/LuanRT/YouTube.js/pull/1052) and I've verified that dukpy is able to execute the code.

It's time to get AST based code extractor implemented to get the JS innertube clients working again.

For anyone out there experiencing 403 Forbidden issues using this pr-branch or a release from my repo, the possible workaround is by switching innertube_client_id to 8, which corresponds to android_vr.

@alive4ever

alive4ever commented Oct 27, 2025

Copy link
Copy Markdown
Contributor Author

Ok, finally got ast-based signature solver working via yt-dlp-ejs module, which also raises python requirement to 3.10.

One of ['deno', 'bun', 'node'] is needed to run the signature solver, since dukpy isn't able to do it.

@alive4ever

Copy link
Copy Markdown
Contributor Author

Got hit by another YT changes that required new poToken to be generated for each videoId. So innertube clients such as mweb which requires poToken are currently got 403 Forbidden replies when using poToken bound to visitorId.

Still investigating the possibility to integrate poToken framework to make mweb client usable again.

@alive4ever

Copy link
Copy Markdown
Contributor Author

Ok, finally got mweb client working. The changes are available at feature-innertube-client-fix-plus-pot-integration branch of my repo.

It will fetch generate-po-token scripts and generate poToken as needed.

@alive4ever
alive4ever force-pushed the feature-innertube-client-fix branch from 3783319 to 6f807f7 Compare April 16, 2026 03:34
@alive4ever

Copy link
Copy Markdown
Contributor Author

Ok, finally got mweb client working. The changes are available at feature-innertube-client-fix-plus-pot-integration branch of my repo.

It will fetch generate-po-token scripts and generate poToken as needed.

Update: po_token fetching has been integrated into this branch, which requires bgutil pot provider to listen at localhost:4416.

Tested to work with mweb client.

@Zero3K20

Copy link
Copy Markdown

When will a new release be made?

@alive4ever

Copy link
Copy Markdown
Contributor Author

When will a new release be made?

A new release has been built, tagged v2.8.12-playground20_feature-my-local-changes-rebuild3, which includes changes in this branch and from #240

Feel free to try it.

@alive4ever

Copy link
Copy Markdown
Contributor Author

Looks like there are quite large changes in master branch after 2.8.14 that makes this branch requires more often rebase works.

@alive4ever

Copy link
Copy Markdown
Contributor Author

A full rework of this feature is on the way. Will force-push later if I think it's good enough.

@user234683 : by the way, how does a working js innertube client sound? Nice to have or unnecessary complicated?

Use mweb client definition from yt-dlp project.
Allow users to choose between non-js client and js client.
To use js client, bgutil-ytdlp-pot-provider server has to bind on
localhost:4416.
Add a new submodule to provide functions related to js signature
decryption.

One of [ 'deno', 'node', 'bun' ] runtime is required to be present on
$PATH to decrypt js signatures so the js player client is able to get
working stream url, in addition to bgutil-ytdlp-pot-provider server on
localhost:4416
Use settings.player_client to determine which client to use for player
api request.

The user is responsible to set up a js runtime and pot server for the js
player client to work.
Add yt-dlp-ejs>=0.8.0
Add yt-dlp-ejs>=0.8.0, which requires python>=3.10, potentially breaking
oldwin.
Add yt-dlp-ejs>=0.8.0
@alive4ever
alive4ever force-pushed the feature-innertube-client-fix branch from d4dd6bd to 3373f40 Compare August 26, 2026 14:21
@user234683

Copy link
Copy Markdown
Owner

A full rework of this feature is on the way. Will force-push later if I think it's good enough.

@user234683 : by the way, how does a working js innertube client sound? Nice to have or unnecessary complicated?

I think it is going to be necessary ultimately. I don't see this visionos workaround lasting very long. General structure of what you have so far looks promising (haven't reviewed in depth). Will review once you let me know it's ready

Catch yt-dlp-ejs `ImportError` exception and show a fallback warning when
js decryption is not possible.
Fall back to non-js client when js decryption is not possible.
@alive4ever

Copy link
Copy Markdown
Contributor Author

Thanks for expressing your impression of js client revival feature.

I added some minor changes so the failure of yt_dlp_ejs import is properly caught. I also added fall back mechanism to non js client in case js_runtime is unavailable.

With my last two commits, the app will fall back to non js client if yt_dlp_ejs is not installed (possibly on oldwin because it requires python>=3.10) or the user is enabling js client without suitable js runtime on $PATH.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Youtube-local doesn't work for some videos, mainly music videos

5 participants