Skip to content

Commit

Permalink
FIX: Lezhin - fix CDN, purchased, and unscramble images when needed (#…
Browse files Browse the repository at this point in the history
…5378)

* Lezhin : fix case not getting pictures

* coin == 0 doesn't mean purchased. If we pass purchased  = true when its not we get 403 errors when pictures should be accessible.
* _getPages() : Read "purchased" value in chapter page __LZ_PRODUCT__ and provide it in payload data
* _hancleConnectorURI : provided purchased value

* Lezhin : cdn change & use given cdn

Turns out cdn is dead, ccdn is just for website stuff, and rcdn is the one with pictures.
Paid and free chapters now are downloaded without problems.

* add image unscrambling

* Update Lezhin.mjs

* better version 

* remove the need for the bigint lib. Use native BigInt.
* use pagesinfos toe get pages list, with scrollsinfos as fallback (both are identical, first one may not exists)
  • Loading branch information
MikeZeDev authored Oct 3, 2023
1 parent 4e92a37 commit db51966
Showing 1 changed file with 249 additions and 43 deletions.
292 changes: 249 additions & 43 deletions src/web/mjs/connectors/templates/Lezhin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export default class Lezhin extends Connector {
this.tags = [];
this.url = undefined;
this.apiURL = 'https://www.lezhinus.com';
this.cdnURL = 'https://cdn.lezhin.com';
this.userID = undefined;
this.cdnURL = 'https://rcdn.lezhin.com';
this.token = undefined;
this.mangasPerPage = 36;
this.config = {
username: {
Expand All @@ -35,33 +35,27 @@ export default class Lezhin extends Connector {
};
}

async _initializeAccount() {
if(this.userID) {
async _initializeConnector() {
const data = await this.getLzConfig();
this.cdnURL = data.contentsCdnUrl ? data.contentsCdnUrl : this.cdnURL;
}

async _initializeAccount() {
if(this.token) {
//check if user disconnected
const uri = new URL(this.url);
const checkscript = `
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(__LZ_CONFIG__);
},5000);
});
`;
const request = new Request(uri, this.requestOptions);
const data = await Engine.Request.fetchUI(request, checkscript);
const data = await this.getLzConfig();
if (!data.token) {
this.requestOptions.headers.delete('Authorization');
this.userID = '';
this.token = '';
}
}

if(this.userID || !this.config.username.value || !this.config.password.value) {
if(this.token || !this.config.username.value || !this.config.password.value) {
return;
}
const password = this.config.password.value.replace("'", "\\'"); //escape the password, because if it contains a single quote the script will fail
let script = `
new Promise((resolve, reject) => {
//try {
if($('#log-nav-email').length) {
return resolve();
}
Expand All @@ -75,25 +69,15 @@ export default class Lezhin extends Connector {
success: resolve,
error: reject
});
// }
// catch(error) {
// reject(error);
// }
});
`;
let request = new Request(new URL(this.url + '/login'), this.requestOptions);
await Engine.Request.fetchUI(request, script);
let response = await fetch(new Request(new URL(this.url + '/account'), this.requestOptions));
let data = await response.text();
let cdn = data.match(/cdnUrl\s*:\s*['"]([^'"]+)['"]/);
let user = data.match(/userId\s*:\s*['"](\d+)['"]/);
let token = data.match(/token\s*:\s*['"]([^'"]+)['"]/);
this.requestOptions.headers.set('Authorization', 'Bearer '+token[1]);
this.cdnURL = cdn ? cdn[1] : this.cdnURL;
this.userID = user ? user[1] : undefined;
if(this.userID) {
await fetch(this.url + '/adultkind?path=&sw=all', this.requestOptions);
}

const data = await this.getLzConfig();
this.token = data.token;
this.requestOptions.headers.set('Authorization', 'Bearer '+ data.token);

// force user locale user setting to be the same as locale from the currently used website ...
// => prevent a warning webpage that would appear otherwise when loading chapters / pages
return fetch(this.url + '/locale/' + this.locale, this.requestOptions);
Expand Down Expand Up @@ -140,7 +124,6 @@ export default class Lezhin extends Connector {
new Promise((resolve, reject) => {
// wait until episodes have been updated with purchase info ...
setTimeout(() => {
// try {
let chapters = __LZ_PRODUCT__.all // __LZ_PRODUCT__.product.episodes
.filter(chapter => {
if(chapter.purchased) {
Expand All @@ -165,32 +148,43 @@ export default class Lezhin extends Connector {
};
});
resolve(chapters);
// }
// catch(error) {
// reject(error);
// }
}, 2500);
});
`;
let request = new Request(new URL('/comic/' + manga.id, this.url), this.requestOptions);
return Engine.Request.fetchUI(request, script);
return await Engine.Request.fetchUI(request, script);
}

async _getPages(chapter) {
await this._initializeAccount();

//check if chapter is purchased
let script = `
new Promise((resolve, reject) => {
// wait until episodes have been updated with purchase info ...
setTimeout(() => {
let chapter = __LZ_PRODUCT__.all.filter(chapter => chapter.name == "${chapter.id}");
resolve(chapter[0].purchased);
}, 2500);
});
`;
let request = new Request(new URL('/comic/' + chapter.manga.id, this.url), this.requestOptions);
const purchased = await Engine.Request.fetchUI(request, script);

let uri = new URL('https://www.lezhin.com/lz-api/v2/inventory_groups/comic_viewer');
uri.searchParams.set('platform', 'web');
uri.searchParams.set('store', 'web');
uri.searchParams.set('alias', chapter.manga.id);
uri.searchParams.set('name', chapter.id);
uri.searchParams.set('preload', false);
uri.searchParams.set('type', 'comic_episode');
let request = new Request(uri, this.requestOptions);
request = new Request(uri, this.requestOptions);
let data = await this.fetchJSON(request);

return data.data.extra.episode.scrollsInfo.map(scroll => {
return this.createConnectorURI({url : scroll.path, infos : JSON.stringify(data)});
//default to scrollsInfo if pagesInfo doesnt exists (same structure)
const content = data.data.extra.episode.pagesInfo ? data.data.extra.episode.pagesInfo : data.data.extra.episode.scrollsInfo;
return content.map(scroll => {
return this.createConnectorURI({url : scroll.path, infos : JSON.stringify(data), purchased : purchased});
});
}

Expand All @@ -208,7 +202,7 @@ export default class Lezhin extends Connector {
const episode = data.data.extra.episode;
const extension = this.config.forceJPEG.value ? '.jpg' : '.webp';
let imageurl = new URL('/v2' + payload.url + extension, this.cdnURL);
let purchased = episode.coin == 0;
let purchased = payload.purchased ? payload.purchased : false;
//purchased = purchased || (episode.freedAt && episode.freedAt < Date.now());
const subscribed = data.data.extra.subscribed;
const updatedAt = episode.updatedAt;
Expand All @@ -232,12 +226,224 @@ export default class Lezhin extends Connector {
imageurl.searchParams.set('Policy', response.data.Policy);
imageurl.searchParams.set('Signature', response.data.Signature);
imageurl.searchParams.set('Key-Pair-Id', response.data['Key-Pair-Id']);

request = new Request(imageurl, this.requestOptions);
response = await fetch(request);

const scrambled = data.data.extra.comic.metadata && data.data.extra.comic.metadata.imageShuffle;

data = await response.blob();
if (scrambled) {//if image is scrambled
data = await this.descrambleimage(data, episode.id);
}
data = await this._blobToBuffer(data);
this._applyRealMime(data);
return data;
}

async getLzConfig() {
const uri = new URL(this.url);
const checkscript = `
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(__LZ_CONFIG__);
},2500);
});
`;
const request = new Request(uri, this.requestOptions);
return await Engine.Request.fetchUI(request, checkscript);
}

async descrambleimage(imageBlob, episodeid) {
const bitmap = await createImageBitmap(imageBlob);
return new Promise(resolve => {

const canvas = document.createElement('canvas');
canvas.width = bitmap.width,
canvas.height = bitmap.height;
const ctx = canvas.getContext('2d');
let scrambleTable = _generateScrambletable(episodeid, 5);
const i = Math.floor(Math.sqrt(scrambleTable.length));
const dimensions = {width : canvas.width, height : canvas.height};

scrambleTable = _addLength(scrambleTable);
scrambleTable = createSuperArray(scrambleTable);

const piecesData = scrambleTable.map(entry => {
const n = entry[0];
const r = entry[1];
return {
from: calculatePieces(dimensions, i, parseInt(n)),
to: calculatePieces(dimensions, i, r)
};
}).filter(entry => {
return !!entry.from && !!entry.to;
});

for (const piece of piecesData) {
const e = piece.from;
const n = piece.to;
ctx.drawImage(bitmap, n.left, n.top, n.width, n.height, e.left, e.top, e.width, e.height );
}

canvas.toBlob(data => {
resolve(data);
}, Engine.Settings.recompressionFormat.value, parseFloat( Engine.Settings.recompressionQuality.value )/100);
});

}
}

//*************************
// LEHZIN SCRAMBLING
//************************

function _generateScrambletable(episodeid, e) {
return episodeid ? new Randomizer(episodeid, e).get() : [];
}

const Randomizer = function e(t, n) {
var r = this;
!function (t) {
if (!(t instanceof e)) throw new TypeError('Cannot call a class as a function');
}(this),

this.random = function (t) {

// eslint-disable-next-line no-undef
const BIGT = BigInt(t);
// eslint-disable-next-line no-undef
const big12 = BigInt(12);
// eslint-disable-next-line no-undef
const big25 = BigInt(25);
// eslint-disable-next-line no-undef
const big27 = BigInt(27);
// eslint-disable-next-line no-undef
const big32 = BigInt(32);
// eslint-disable-next-line no-undef
const BigXXX = BigInt('18446744073709551615');

let e = r.state;
e = e ^ e >> big12;
const shifter = e << big25 & BigXXX;
e = e ^ shifter;
e = e ^ e >>big27;
r.state = e & BigXXX;

return parseInt((e >> big32) % BIGT, 10); //22

/*
// Detailled code using bigint js library, for references purposes. Test manga : Jinx, Chapter 1, Lezhin EN
var BIGT = bigInt(t);
var big12 = bigInt(12);
var big25 = bigInt(25);
var big27 = bigInt(27);
var big32 = bigInt(32);
var BigXXX = bigInt('18446744073709551615');
var e = r.state; //6252351865552896n
e = e.xor(e.shiftRight(big12)); //6253027791257424n
var shifter = e.shiftLeft(big25).and(BigXXX); //3528721484988022784n
e = e.xor(shifter); //3525964907269111632n
e = e.xor(e.shiftRight(big27)); //3525964881051480971n
r.state = e.and(BigXXX); //3525964881051480971n
return e.shiftRight(big32).remainder(BIGT).toJSNumber(); //22
*/

},

this.get = function () {
return r.order;
},

this.seed = t,
// eslint-disable-next-line no-undef
this.state = BigInt(this.seed);
for (
var i = n * n,
o = Array.from({
length: i
}, function (t, e) {
return e;
}),
a = 0;
a < o.length;
a++
) {
var s = this.random(i),
u = o[a];
o[a] = o[s],
o[s] = u;
}
this.order = o;
};

function _addLength(t) {
return [].concat(t, [
t.length,
t.length + 1
]);
}

function createSuperArray(array) {
//generate "0", "arraylength" array
const indexArray = Array(array.length).fill().map((_, index) => index.toString());
const resultArray = [];
indexArray.map(element => resultArray.push([element, array[element]]));
return resultArray;
}

function calculatePieces(t, e, n) {
var r,
i,
o,
a,
s,
u,
c,
l,
f,
h,
d,
p,
v,
g,
m,
y,
b,
w = e * e;
return n < w ? (
p = e,
v = n,
g = (d = t).width,
m = d.height,
y = Math.floor(g / p),
b = Math.floor(m / p),
{
left: v % p * y,
top: Math.floor(v / p) * b,
width: y,
height: b
}
) : n === w ? (
c = e,
l = (u = t).width,
f = u.height,
0 === (h = l % c) ? null : {
left: l - h,
top: 0,
width: h,
height: f
}
) : (
i = e,
o = (r = t).width,
a = r.height,
0 === (s = a % i) ? null : {
left: 0,
top: a - s,
width: o - o % i,
height: s
}
);
}

0 comments on commit db51966

Please sign in to comment.