LuanRT / YouTube.js

A wrapper around YouTube's internal API — reverse engineering InnerTube
https://www.npmjs.com/package/youtubei.js
MIT License
3.48k stars 219 forks source link

The `account_index` parameter does not work #735

Open dnicolson opened 3 weeks ago

dnicolson commented 3 weeks ago

Steps to reproduce

  1. Navigate to https://www.youtube.com/playlist?list=WL while logged into two accounts
  2. Attempt to fetch the Watch Later playlist from the secondary account
const youtube = await Innertube.create({
  cookie: document.cookie,
  account_index: 1,
  fetch: (...args) => fetch(...args),
})

const playlist = await youtube.getPlaylist('WL')
const videoIds = playlist.videos.map((video) => video.id)
console.log('Video IDs', videoIds)

In most cases, it is required to remove these lines: https://github.com/LuanRT/YouTube.js/blob/4b60b97132b0ee42b41838f3336c582a7f7f7aec/src/utils/HTTPClient.ts#L75-L76

To prevent this response from /playlist requests:

)]}'
{"reload":"now"}

Failure Logs

The playlist does not exist.

Expected behavior

The playlist contents will be retrieved from the secondary account.

Current behavior

The X-Goog-AuthUser header doesn't appear to indicate the active account as described in #229 and https://github.com/yt-dlp/yt-dlp/commit/34917076ad9844eddfa4ea97656d81a7fefe5d59.

Looking at the source of desktop_polymer.js, the X-Goog-AuthUser header comes from the SESSION_INDEX value:

{
    Authorization: "AUTHORIZATION",
    "X-Goog-EOM-Visitor-Id": "EOM_VISITOR_DATA",
    "X-Goog-Visitor-Id": "SANDBOXED_VISITOR_ID",
    "X-Youtube-Domain-Admin-State": "DOMAIN_ADMIN_STATE",
    "X-Youtube-Chrome-Connected": "CHROME_CONNECTED_HEADER",
    "X-YouTube-Client-Name": "INNERTUBE_CONTEXT_CLIENT_NAME",
    "X-YouTube-Client-Version": "INNERTUBE_CONTEXT_CLIENT_VERSION",
    "X-YouTube-Delegation-Context": "INNERTUBE_CONTEXT_SERIALIZED_DELEGATION_CONTEXT",
    "X-YouTube-Device": "DEVICE",
    "X-Youtube-Identity-Token": "ID_TOKEN",
    "X-YouTube-Page-CL": "PAGE_CL",
    "X-YouTube-Page-Label": "PAGE_BUILD_LABEL",
    "X-YouTube-Variants-Checksum": "VARIANTS_CHECKSUM",
    "X-Goog-AuthUser": "SESSION_INDEX",
    "X-Goog-PageId": "DELEGATED_SESSION_ID"
}

Switching between accounts does not change the ytcfg.data_.SESSION_INDEX value, it is always 0. A search on GitHub only found code using the SESSION_INDEX value or hard coding 0 or 1.

Version

Default

Anything else?

This code successfully gets the contents of the Watch Later playlist of the active account:

const youtube = await Innertube.create({
  cookie: document.cookie,
  fetch: (input, init = {}) => {
    const headers = new Headers()
    headers.append('X-YouTube-Client-Name', window.ytcfg.data_.INNERTUBE_CONTEXT_CLIENT_NAME)
    headers.append('X-YouTube-Client-Version', window.ytcfg.data_.INNERTUBE_CONTEXT_CLIENT_VERSION)
    headers.append('X-YouTube-Device', window.ytcfg.data_.DEVICE)
    headers.append('X-YouTube-Identity-Token', window.ytcfg.data_.ID_TOKEN)
    const options = { ...init, headers }
    return fetch(input, options)
  },
})

const response = await youtube.session.http.fetch(`/playlist?list=WL&pbj=1`, {
  baseURL: 'https://www.youtube.com',
})
const json = await response.json()

const playlist =
  json.response.contents.twoColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0]
    .itemSectionRenderer.contents[0].playlistVideoListRenderer

console.log('Video IDs', playlist.contents.map((video) => video.playlistVideoRenderer.videoId))

Checklist

dnicolson commented 3 weeks ago

To obtain the account index in the browser, the /getAccountSwitcherEndpoint endpoint can be used:

const activeHandle = window.ytInitialData.header.playlistHeaderRenderer.ownerText.runs[0].navigationEndpoint.commandMetadata.webCommandMetadata.url.slice(1)

const youtube = await Innertube.create({
  cookie: document.cookie,
  fetch: (...arguments_) => fetch(...arguments_),
})

const response = await youtube.session.http.fetch(`/getAccountSwitcherEndpoint`, {
  baseURL: 'https://www.youtube.com',
})

const text = await response.text()
const accountSwitcher = JSON.parse(text.slice(5))
const accounts = accountSwitcher.data.actions[0].getMultiPageMenuAction.menu.multiPageMenuRenderer.sections[0].accountSectionListRenderer.contents[0].accountItemSectionRenderer.contents

return accounts.findIndex(item => item.accountItem && item.accountItem.channelHandle.simpleText === activeHandle)