diff --git a/README.md b/README.md index 0711925..46446a3 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,14 @@ var netflix = require('netflix2')() ``` ### Login -You must call `login` before using any of the other below functions. This will set cookies, API endpoints, and the authURL that must used to make API calls. +First, login to your Netflix account manually and retrieve your session's cookie by entering `document.cookie` in +your console. + +Next, you must call `login` before using any of the other below functions. This will set API endpoints, and the authURL +that must used to make API calls. ```javascript var credentials = { - email: 'youremail@example.com', - password: 'yourpassword' + cookies: '[value from console]' } netflix.login(credentials, callback) ``` diff --git a/lib/constants.json b/lib/constants.json index e06f565..f03d380 100644 --- a/lib/constants.json +++ b/lib/constants.json @@ -32,7 +32,7 @@ ], "avatarUrl": "/ffe/profiles/avatars_v2/%1$dx%1$d/PICON_%2$03d.png", "baseSecureUrl": "https://secure.netflix.com/", - "baseUrl": "https://www.netflix.com/", + "baseUrl": "https://www.netflix.com", "loginUrl": "/Login", "manageProfilesUrl": "/ManageProfiles", "pathEvaluatorEndpointUrl": "/pathEvaluator", diff --git a/lib/netflix2.js b/lib/netflix2.js index aa012fb..5d8fb32 100644 --- a/lib/netflix2.js +++ b/lib/netflix2.js @@ -15,13 +15,14 @@ const cheerio = require('cheerio') const extend = require('extend') const request = require('request-promise-native') const { sprintf } = require('sprintf-js') -const util = require('util') const vm = require('vm') const HttpError = require('./httpError') const errorLogger = require('./errorLogger') const constants = require('./constants') -const manifest = require('../package') +const fetch = require('node-fetch') +const fs = require('fs').promises +const path = require('path') /** @namespace */ class Netflix { @@ -34,24 +35,27 @@ class Netflix { constructor(options) { console.warn('Using new Netflix2 class!') + const cookieJar = request.jar() options = extend(true, { - cookieJar: request.jar() + cookieJar }, options) this.cookieJar = options.cookieJar this.netflixContext = {} this.endpointIdentifiers = {} this.authUrls = {} this.activeProfile = null - this.__request = request.defaults({ - baseUrl: constants.baseUrl, + } + + async __request(url, options = {}) { + const config = { + redirect: 'follow', + ...options, headers: { - 'User-Agent': util.format('%s/%s', manifest.name, manifest.version) + cookie: this.cookie, + ...options.headers }, - gzip: true, - jar: this.cookieJar, - simple: false, - resolveWithFullResponse: true, - }) + } + return await fetch(url, config) } /** @@ -60,14 +64,15 @@ class Netflix { * * This must be called before using any other functions * - * @param {{email: string, password: string}} credentials + * @param {{email: string, password: string, cookies: string}} credentials * */ async login(credentials) { try { + this.cookie = credentials.cookies if (credentials) { - const loginForm = await this.__getLoginForm(credentials) - await this.__postLoginForm(loginForm) + // const loginForm = await this.__getLoginForm(credentials) + // await this.__postLoginForm(loginForm) await this.__getContextDataFromUrls([constants.yourAccountUrl, constants.manageProfilesUrl]) console.log('Login successful!') } else { @@ -115,7 +120,7 @@ class Netflix { const endpoint = constants.pathEvaluatorEndpointUrl const response = await this.__apiRequest(endpoint, options) - return response.body + return await response.json() } /** @@ -129,7 +134,8 @@ class Netflix { * @property {string} experience * @property {boolean} isAutoCreated * @property {string} avatarName - * @property {{32: string, 50: string, 64: string, 80: string, 100: string, 112: string, 160: string, 200: string, 320: string, }} avatarImages + * @property {{32: string, 50: string, 64: string, 80: string, 100: string, 112: string, 160: string, 200: string, + * 320: string, }} avatarImages * @property {boolean} canEdit * @property {boolean} isDefault */ @@ -142,11 +148,12 @@ class Netflix { const endpoint = constants.profilesEndpointUrl try { const response = await this.__apiRequest(endpoint, options) - if (response.statusCode !== 200) { - throw new HttpError(response.statusCode, response.statusMessage) + const body = await response.json() + if (response.status !== 200) { + throw new HttpError(response.status, response.statusText) } // TODO; check if status is 2xx - return response.body.profiles + return body.profiles } catch (err) { console.error(err) } @@ -157,16 +164,11 @@ class Netflix { * @param {string} guid - can be found from {} */ async switchProfile(guid) { - const options = { - qs: { - switchProfileGuid: guid - } - } - try { - const endpoint = constants.switchProfileEndpointUrl - const response = await this.__apiRequest(endpoint, options) - if (!response || !response.body || response.body.status !== 'success') { + const endpoint = `${constants.switchProfileEndpointUrl}?switchProfileGuid=${guid}` + const response = await this.__apiRequest(endpoint) + const body = await response.json() + if (!response || !body || body.status !== 'success') { throw new Error('There was an error while trying to switch profile') } else { this.activeProfile = guid @@ -284,7 +286,7 @@ class Netflix { const endpoint = constants.viewingActivity const response = await this.__apiRequest(endpoint, options) - return response.body + return await response.json() } /** @@ -306,8 +308,8 @@ class Netflix { } const endpoint = constants.viewingActivity - const result = await this.__apiRequest(endpoint, options) - return result.body + const response = await this.__apiRequest(endpoint, options) + return await response.json() } /** @@ -316,16 +318,10 @@ class Netflix { * @returns {Object} */ async __getViewingHistory(page) { - const options = { - qs: { - pg: page - } - } - - const endpoint = constants.viewingActivity + const endpoint = `${constants.viewingActivity}?pg=${page}` try { - const response = await this.__apiRequest(endpoint, options) - return response.body + const response = await this.__apiRequest(endpoint) + return await response.json() } catch (err) { errorLogger(err) throw new Error("Couldn't get your viewing history. For more information, see previous log statements.") @@ -340,24 +336,25 @@ class Netflix { */ async __setRating(isThumbRating, titleId, rating) { const endpoint = isThumbRating ? constants.setThumbRatingEndpointUrl : constants.setVideoRatindEndpointUrl - let options = { - body: { + const options = { + body: JSON.stringify({ rating: rating, - authURL: this.authUrls[constants.yourAccountUrl] - } - } + authURL: this.authUrls[constants.yourAccountUrl], - // Note the capital I in titleId in the if-case vs. the lower case i in the else-case. This is necessary - // due to the Shakti API. - if (isThumbRating) { - options.body.titleId = titleId - } else { - options.body.titleid = titleId + // Note the capital I in titleId in the if-case vs. the lower case i in the else-case. This is necessary + // due to the Shakti API. + [isThumbRating ? 'titleId' : 'titleid']: titleId, + }), + headers: { + 'content-type': 'application/json', + }, + method: 'POST', } try { const response = await this.__apiRequest(endpoint, options) - if (response.body.newRating !== rating) { + const body = await response.json() + if (body.newRating !== rating) { throw new Error('Something went wrong! The saved rating does not match the rating that was supposed to be saved.') } } catch (err) { @@ -393,7 +390,8 @@ class Netflix { const options = {} const response = await this.__apiRequest(endpoint, options) - return response.body.active + const body = await response.json() + return body.active } getAvatarUrl(avatarName, size) { @@ -408,12 +406,11 @@ class Netflix { params: [null, null, null, avatarName, null], authURL: this.authUrls[constants.manageProfilesUrl] }, - method: 'POST', - qs: { method: 'call' } + method: 'POST' } - const response = await this.__apiRequest(endpoint, options) - return response.body + const response = await this.__apiRequest(`${endpoint}?method=call`, options) + return await response.json() } /** @@ -422,21 +419,16 @@ class Netflix { * @returns {Object} */ async __getLoginForm(credentials) { - const options = { - url: constants.loginUrl, - method: 'GET', - } - try { - const response = await this.__request(options) + const response = await this.__request(constants.baseUrl + constants.loginUrl) // When the statusCode is 403, that means we have been trying to login too many times in succession with incorrect credentials. - if (response.statusCode === 403) { + if (response.status === 403) { throw new Error('Your credentials are either incorrect or you have attempted to login too many times.') - } else if (response.statusCode !== 200) { - throw new HttpError(response.statusCode, response.statusMessage) + } else if (response.status !== 200) { + throw new HttpError(response.status, response.statusText) } else { - const $ = cheerio.load(response.body) + const $ = cheerio.load(await response.text()) let form = $('.login-input-email') .parent('form') .serializeArray() @@ -462,16 +454,15 @@ class Netflix { */ async __postLoginForm(form) { const options = { - url: constants.loginUrl, method: 'POST', form: form, } try { - const response = await this.__request(options) + const response = await this.__request(constants.baseUrl + constants.loginUrl, options) if (response.statusCode !== 302) { // we expect a 302 redirect upon success - const $ = cheerio.load(response.body) + const $ = cheerio.load(await response.text()) // This will always get the correct error message that is displayed on the Netflix website. const message = $('.ui-message-contents', '.hybrid-login-form-main').text() @@ -488,16 +479,11 @@ class Netflix { * @param {number} page */ async __getRatingHistory(page) { - const options = { - qs: { - pg: page - } - } - const endpoint = constants.ratingHistoryEndpointUrl + const endpoint = `${constants.ratingHistoryEndpointUrl}?pg=${page}` try { - const response = await this.__apiRequest(endpoint, options) - return response.body + const response = await this.__apiRequest(endpoint) + return await response.json() } catch (err) { errorLogger(err) throw new Error('There was something wrong getting your rating history. For more information, see previous log statements.') @@ -511,16 +497,10 @@ class Netflix { * @returns {Object} */ async __apiRequest(endpoint, options) { - const extendedOptions = extend(true, options, { - baseUrl: this.apiRoot, - url: endpoint, - json: true - }) - try { - const response = await this.__request(extendedOptions) - if (response.statusCode !== 200) { - throw new HttpError(response.statusCode, response.statusMessage) + const response = await this.__request(this.apiRoot + endpoint, options) + if (response.status !== 200) { + throw new HttpError(response.status, response.statusText) } else { return response } @@ -535,16 +515,11 @@ class Netflix { * @param {string} url */ async __getContextData(url) { - const options = { - url: url, - method: 'GET', - followAllRedirects: true, - } - + let body try { - const response = await this.__request(options) - if (response.statusCode !== 200) { - throw new HttpError(response.statusCode, response.statusMessage) + const response = await this.__request(constants.baseUrl + url) + if (response.status !== 200) { + throw new HttpError(response.status, response.statusText) } else { const context = { window: {}, @@ -552,7 +527,8 @@ class Netflix { } vm.createContext(context) - const $ = cheerio.load(response.body) + body = await response.text() + const $ = cheerio.load(body) $('script').map((index, element) => { // don't run external scripts if (!element.attribs.src) { @@ -598,6 +574,13 @@ class Netflix { } } catch (err) { errorLogger(err) + + if (body) { + const filePath = path.join(process.cwd(), 'errorResponsePage.html') + await fs.writeFile(filePath, body) + console.error(`The exact response HTML file was saved to ${filePath}`) + } + throw new Error('There was a problem fetching user data. For more information, see previous log statements.') } } diff --git a/package-lock.json b/package-lock.json index f32b333..de6110a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1118,6 +1118,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", diff --git a/package.json b/package.json index 4b3c152..e79da4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netflix2", - "version": "1.0.1", + "version": "2.0.0-beta.3", "description": "A client library to access the not-so-public Netflix Shakti API.", "keywords": [ "netflix", @@ -17,6 +17,7 @@ "dependencies": { "cheerio": "^0.20.0", "extend": "^3.0.0", + "node-fetch": "^2.6.1", "request": "^2.72.0", "request-promise-native": "^1.0.7", "sprintf-js": "^1.0.3"