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
};