From eeee086b21a73aa946dd43eb96787d31260df18f Mon Sep 17 00:00:00 2001 From: Michael Kuckuk Date: Sun, 24 Jan 2021 18:48:16 +0100 Subject: [PATCH 1/9] Rely on cookie instead of credentials for login --- lib/constants.json | 2 +- lib/netflix2.js | 146 +++++++++++++++++++-------------------------- package-lock.json | 5 ++ package.json | 1 + 4 files changed, 67 insertions(+), 87 deletions(-) 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..527f972 100644 --- a/lib/netflix2.js +++ b/lib/netflix2.js @@ -15,13 +15,12 @@ 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') /** @namespace */ class Netflix { @@ -34,24 +33,28 @@ 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 = { 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, - }) + redirect: 'follow', + ...options + } + console.log(`fetch('${url}', ${JSON.stringify({ ...config, headers: { ...config.headers, cookie: 'cookie' } })})`) + return await fetch(url, config) } /** @@ -60,14 +63,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 +119,7 @@ class Netflix { const endpoint = constants.pathEvaluatorEndpointUrl const response = await this.__apiRequest(endpoint, options) - return response.body + return await response.json() } /** @@ -129,7 +133,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 +147,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 +163,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 +285,7 @@ class Netflix { const endpoint = constants.viewingActivity const response = await this.__apiRequest(endpoint, options) - return response.body + return await response.json() } /** @@ -306,8 +307,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 +317,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.") @@ -357,7 +352,8 @@ class Netflix { 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 +389,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 +405,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 +418,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 +453,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 +478,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 +496,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 +514,10 @@ class Netflix { * @param {string} url */ async __getContextData(url) { - const options = { - url: url, - method: 'GET', - followAllRedirects: true, - } - 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 +525,8 @@ class Netflix { } vm.createContext(context) - const $ = cheerio.load(response.body) + const body = await response.text() + const $ = cheerio.load(body) $('script').map((index, element) => { // don't run external scripts if (!element.attribs.src) { 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..49dd346 100644 --- a/package.json +++ b/package.json @@ -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" From 3639f2db74f1d21a2b12a234c458818edf453552 Mon Sep 17 00:00:00 2001 From: Michael Kuckuk Date: Sun, 24 Jan 2021 19:49:57 +0100 Subject: [PATCH 2/9] Update README --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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) ``` From 9d2c1cdf55bcb5cc39a5db2c67580394a9a95668 Mon Sep 17 00:00:00 2001 From: Michael Kuckuk Date: Sun, 24 Jan 2021 19:52:00 +0100 Subject: [PATCH 3/9] 2.0.0-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 49dd346..6a99a20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netflix2", - "version": "1.0.1", + "version": "2.0.0-beta.0", "description": "A client library to access the not-so-public Netflix Shakti API.", "keywords": [ "netflix", From a7fe6b06fb17a239584c6e7a976f7af339126f48 Mon Sep 17 00:00:00 2001 From: Michael Kuckuk Date: Sun, 24 Jan 2021 19:57:31 +0100 Subject: [PATCH 4/9] Remove constant fetch logging --- lib/netflix2.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/netflix2.js b/lib/netflix2.js index 527f972..69f2f01 100644 --- a/lib/netflix2.js +++ b/lib/netflix2.js @@ -53,7 +53,6 @@ class Netflix { redirect: 'follow', ...options } - console.log(`fetch('${url}', ${JSON.stringify({ ...config, headers: { ...config.headers, cookie: 'cookie' } })})`) return await fetch(url, config) } From e6f034fafd5e04445565f7c39bdc4d1a7f835b6a Mon Sep 17 00:00:00 2001 From: Michael Kuckuk Date: Sun, 24 Jan 2021 19:57:59 +0100 Subject: [PATCH 5/9] 2.0.0-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a99a20..f027bf1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netflix2", - "version": "2.0.0-beta.0", + "version": "2.0.0-beta.1", "description": "A client library to access the not-so-public Netflix Shakti API.", "keywords": [ "netflix", From 2ee532ab73093b0e27171785992929542e431795 Mon Sep 17 00:00:00 2001 From: Michael Kuckuk Date: Thu, 4 Feb 2021 16:42:35 +0100 Subject: [PATCH 6/9] Save response body when auth request fails --- lib/netflix2.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/netflix2.js b/lib/netflix2.js index 69f2f01..b99ce81 100644 --- a/lib/netflix2.js +++ b/lib/netflix2.js @@ -21,6 +21,8 @@ const HttpError = require('./httpError') const errorLogger = require('./errorLogger') const constants = require('./constants') const fetch = require('node-fetch') +const fs = require('fs').promises +const path = require('path') /** @namespace */ class Netflix { @@ -513,6 +515,7 @@ class Netflix { * @param {string} url */ async __getContextData(url) { + let body try { const response = await this.__request(constants.baseUrl + url) if (response.status !== 200) { @@ -524,7 +527,7 @@ class Netflix { } vm.createContext(context) - const body = await response.text() + body = await response.text() const $ = cheerio.load(body) $('script').map((index, element) => { // don't run external scripts @@ -571,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.') } } From 7ad6a03e1ad4bd50fa39756ca067e9263d0d0aa2 Mon Sep 17 00:00:00 2001 From: Michael Kuckuk Date: Thu, 4 Feb 2021 16:43:06 +0100 Subject: [PATCH 7/9] 2.0.0-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f027bf1..17b3d2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netflix2", - "version": "2.0.0-beta.1", + "version": "2.0.0-beta.2", "description": "A client library to access the not-so-public Netflix Shakti API.", "keywords": [ "netflix", From c9c1fc29dee24f60861cce6420994714cf51e2bc Mon Sep 17 00:00:00 2001 From: Michael Kuckuk Date: Sun, 7 Feb 2021 19:26:53 +0100 Subject: [PATCH 8/9] Fix requests for setting ratings --- lib/netflix2.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/netflix2.js b/lib/netflix2.js index b99ce81..5d8fb32 100644 --- a/lib/netflix2.js +++ b/lib/netflix2.js @@ -48,12 +48,12 @@ class Netflix { async __request(url, options = {}) { const config = { + redirect: 'follow', + ...options, headers: { cookie: this.cookie, ...options.headers }, - redirect: 'follow', - ...options } return await fetch(url, config) } @@ -336,19 +336,19 @@ 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 { From 66c662d3e75df1fc8c0f04602ad69e573e7827c5 Mon Sep 17 00:00:00 2001 From: Michael Kuckuk Date: Sun, 7 Feb 2021 19:28:57 +0100 Subject: [PATCH 9/9] 2.0.0-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17b3d2b..e79da4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netflix2", - "version": "2.0.0-beta.2", + "version": "2.0.0-beta.3", "description": "A client library to access the not-so-public Netflix Shakti API.", "keywords": [ "netflix",