Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use cookie instead of credentials for authentication #41

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: '[email protected]',
password: 'yourpassword'
cookies: '[value from console]'
}
netflix.login(credentials, callback)
```
Expand Down
2 changes: 1 addition & 1 deletion lib/constants.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
177 changes: 80 additions & 97 deletions lib/netflix2.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}

/**
Expand All @@ -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 {
Expand Down Expand Up @@ -115,7 +120,7 @@ class Netflix {

const endpoint = constants.pathEvaluatorEndpointUrl
const response = await this.__apiRequest(endpoint, options)
return response.body
return await response.json()
}

/**
Expand All @@ -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
*/
Expand All @@ -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)
}
Expand All @@ -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
Expand Down Expand Up @@ -284,7 +286,7 @@ class Netflix {
const endpoint = constants.viewingActivity

const response = await this.__apiRequest(endpoint, options)
return response.body
return await response.json()
}

/**
Expand All @@ -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()
}

/**
Expand All @@ -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.")
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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()
}

/**
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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.')
Expand All @@ -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
}
Expand All @@ -535,24 +515,20 @@ 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: {},
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) {
Expand Down Expand Up @@ -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.')
}
}
Expand Down
Loading