About the project
Disney+ Client is a NodeJS library that can be used to interact with the Disney+ private API. The library does not facilitate piracy and still requires a valid Disney+ paid account to work. It was made by reverse engineering the API as used on an iPhone 8+ running iOS 14. Example usage below showing a basic Discord bot that lets users search for content on Disney+
// bot.js
require('dotenv').config();
const DisneyPlusClient = require('disneyplus-client');
const Discord = require('discord.js');
const commands = require('./commands');
const disneyClient = new DisneyPlusClient();
const discordClient = new Discord.Client();
disneyClient.on('ready', async () => {
// Login the user as if we have never connected before
// Normally you would reuse a refresh token
await disneyClient.createDeviceGrant(); // Get device grant token
await disneyClient.exchangeDeviceToken(); // Convert that token into an OAuth token (Disney+ requires an OAuth token even for logging in)
await disneyClient.login(process.env.DISNEY_PLUS_EMAIL, process.env.DISNEY_PLUS_PASSWORD); // Login the user (this does more stuff under the hood)
discordClient.login(process.env.DISCORD_BOT_TOKEN);
});
discordClient.on('ready', async () => {
console.log(`Logged in as ${discordClient.user.tag}!`);
});
discordClient.on('message', message => {
if (message.author.id === discordClient.user.id) return;
if (message.content.startsWith('d+')) {
const commandName = message.content.split(' ')[1];
const commandArgs = message.content.split(' ').slice(2).join(' ');
if (!commands[commandName]) {
console.warn('No command ' + commandName + ' found');
return;
}
return commands[commandName](commandArgs, disneyClient, message);
}
});
function start() {
disneyClient.init();
}
module.exports = {
start
};
// commands.js
const Discord = require('discord.js');
async function search(query, disneyClient, message) {
const searchResults = await disneyClient.search(query);
const embed = new Discord.MessageEmbed()
.setColor(0x113CCF)
.setTitle(`Search results for "${query}"`)
.setAuthor('Disney+', 'https://cdn.discordapp.com/avatars/756233648486744085/894c5202b4bf59211f186dd31ae4d99a.webp');
embed.addField('\u200B', '\u200B');
for (const { hit } of searchResults.hits) {
for (const text of hit.texts) {
if (text.field === 'title' && text.type === 'full') {
embed.addField(text.content, '\u200B');
}
}
}
message.reply(embed);
}
async function details(title, disneyClient, message) {
const searchResults = await disneyClient.search(title);
let content;
let description;
let titleFull;
let imageURL;
let slug;
// Filter the search results
if (searchResults.meta.hits > 1) {
for (const { hit } of searchResults.hits) {
for (const text of hit.texts) {
if (text.field === 'title' && text.type === 'full') {
if (text.content.toLowerCase() === title.toLowerCase()) {
content = hit;
break;
}
}
}
}
} else {
content = searchResults.hits[0].hit;
}
if (!content) {
console.warn('Failed to find media');
return;
}
for (const text of content.texts) {
if (text.language !== 'en') continue;
if (text.field === 'title' && text.type === 'full') {
titleFull = text.content;
}
if (text.field === 'title' && text.type === 'slug') {
slug = text.content;
}
if (text.field === 'description' && text.type === 'medium') {
description = text.content;
}
}
for (const image of content.images) {
if (image.purpose === 'tile' && image.sourceEntity === 'program' && image.aspectRatio === 1.78) {
imageURL = image.url;
break;
}
}
const type = content.programType + 's'; // "movie" -> "movies". Probably a better way to do this
const encodedFamilyId = content.family.encodedFamilyId;
const embed = new Discord.MessageEmbed()
.setColor(0x113CCF)
.setTitle(`Click here to watch "${titleFull}" on Disney+`)
.setURL(`https://www.disneyplus.com/${type}/${slug}/${encodedFamilyId}`)
.setDescription(description)
.setImage(imageURL)
.setThumbnail('https://cdn.discordapp.com/avatars/756233648486744085/894c5202b4bf59211f186dd31ae4d99a.webp')
.setAuthor('Disney+', 'https://cdn.discordapp.com/avatars/756233648486744085/894c5202b4bf59211f186dd31ae4d99a.webp');
message.reply(embed);
}
module.exports = {
search,
details
};