diff --git a/js/littleapple_open.js b/js/littleapple_open.js new file mode 100644 index 00000000..dfc8b53e --- /dev/null +++ b/js/littleapple_open.js @@ -0,0 +1,384 @@ +import { Crypto, Uri, _ } from 'assets://js/lib/cat.js'; + +let key = 'littleapple'; +let HOST = '' +let siteKey = ''; +let siteType = 0; + +const APP_VER = '1.3.3'; +const FULL_VER = 'XPGBOX com.phoenix.tv' + APP_VER; +const UA = 'okhttp/3.12.11'; +let SIGN; +let TOKEN; +let TOKEN2; + +async function request(reqUrl, method, data) { + const res = await req(reqUrl, { + method: method || 'get', + headers: getHeaders(), + data: data, + postType: method === 'post' ? 'form' : '', + }); + return res.content; +} + +function getHeaders() { + if (!SIGN || !TOKEN || !TOKEN2) { + SIGN = generateSignContent(); + TOKEN = generateToken(SIGN); + TOKEN2 = generateToken('eT/G/wE7YQpBbk02LN1uGw8s2cOPPAj9jVVkjWjY6BQ='); + } + const timestamp = Math.floor(new Date().getTime() / 1000).toString(); + const hash = getHash(SIGN, timestamp); + const headers = { + 'token': TOKEN, + 'token2': TOKEN2, + 'user_id': 'XPGBOX', + 'version': FULL_VER, + 'timestamp': timestamp, + 'hash': hash, + 'screenx': 2331, + 'screeny': 1121, + 'User-Agent': UA, + }; + return headers; +} + +function getHash(signContent, timestamp) { + const signStr = signContent + FULL_VER + timestamp; + const md5 = Crypto.MD5(signStr).toString(); + return md5.substring(8, 12); +} + +function generateSignContent() { + const separator = '||'; + const serial = 'unknown'; + const fingerprint = 'HUAWEI/INE-LX2/HWINE:9/HUAWEIINE-LX2/9.1.0.318C636:user/release-keys'; + + const strArr = []; + strArr.push(randStr(16)); + strArr.push(randStr(16)); + + const intCount = 6; + const int8Arr = new Int8Array(intCount); + for (let i = 0; i < intCount; i++) { + const random = _.random(0, 127); + int8Arr[i] = random; + } + const byte = int8Arr[0]; + int8Arr[0] = (byte & (byte ^ 1)); + const byteStr = _.map(int8Arr, (value) => value.toString(16).padStart(2, '0')).join('').toUpperCase(); + strArr.push(byteStr); + + strArr.push(randStr(16)); + strArr.push(serial); + strArr.push(fingerprint); + return strArr.join(separator); +} + +const charStr = 'abacdefghjklmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ0123456789'; +function randStr(len, withNum) { + var _str = ''; + let containsNum = withNum === undefined ? true : withNum; + for (var i = 0; i < len; i++) { + let idx = _.random(0, containsNum ? charStr.length - 1 : charStr.length - 11); + _str += charStr[idx]; + } + return _str; +} + +function generateToken(text) { + const data = Crypto.enc.Utf8.parse(text); + const uint8Array = wordArrayToUint8Array(data); + const encoded = encodeBytes(uint8Array); + const words = uint8ArrayToWordArray(encoded); + return Crypto.enc.Base64.stringify(words); +} + +function wordArrayToUint8Array(wordArray) { + let words = wordArray.words; + let sigBytes = wordArray.sigBytes; + let uint8Array = new Uint8Array(sigBytes); + for (let i = 0; i < sigBytes; i++) { + let byte = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + uint8Array[i] = byte; + } + return uint8Array; +} + +function uint8ArrayToWordArray(uint8Array) { + let len = uint8Array.length; + let words = []; + for (let i = 0; i < len; i++) { + words[i >>> 2] |= (uint8Array[i] & 0xff) << (24 - (i % 4) * 8); + } + return Crypto.lib.WordArray.create(words, len); +} + +function encodeBytes(byteArray) { + let result = null; + const key = Crypto.enc.Utf8.parse('XPINGGUO'); + const keyBytes = wordArrayToUint8Array(key); + const table = new Int8Array(333); + for (let i = 0; i < 333; i++) { + table[i] = i; + } + let index1 = 0; + let index2 = 0; + const keyLength = keyBytes.length; + for (let i = 0; i < 333; i++) { + let keyByte = keyBytes[index2]; + let tableByte = table[i]; + index1 = ((index1 + (((((keyByte & (keyByte ^ (-256))) - 4) + (tableByte & (tableByte ^ (-256)))) + 4) + 17)) - 17) % 333; + let temp = table[i]; + table[i] = table[index1]; + table[index1] = temp; + index2 = (index2 + 1) % keyLength; + } + result = table; + const encryptedData = new Int8Array(byteArray.length); + index1 = 0; + index2 = 0; + for (let i = 0; i < byteArray.length; i++) { + let pos = (index1 + 1) % 333; + const tableByte = result[pos]; + index2 = ((tableByte & (tableByte ^ -256)) + index2) % 333; + const temp = result[pos]; + result[pos] = result[index2]; + result[index2] = temp; + const b6 = result[pos]; + const b7 = result[index2]; + const b8 = result[(-((-(b6 & (b6 ^ (-256)))) - (b7 & (b7 ^ (-256))))) % 333]; + const inputByte = byteArray[i]; + encryptedData[i] = (b8 & ~inputByte) | (~b8 & inputByte); + index1 = pos; + } + return encryptedData; +} + +// cfg = {skey: siteKey, ext: extend} +async function init(cfg) { + siteKey = cfg.skey; + siteType = cfg.stype; + await requestAppHost(); +} + +async function requestAppHost() { + const resp = await request('http://194.147.100.39/api.php/v3.user/tokenlogin?version=' + APP_VER + '&os=4', 'post', {}); + try { + const encrypted = JSON.parse(resp).data; + const key = Crypto.enc.Utf8.parse('XPINGGUOXPINGGUO'); + const decrypted = Crypto.AES.decrypt(encrypted, key, { + mode: Crypto.mode.ECB, + padding: Crypto.pad.Pkcs7 + }); + const json = Crypto.enc.Utf8.stringify(decrypted).substring(8); + const data = JSON.parse(json); + const uri = new Uri(data.apiurl); + uri.setPath('/api.php'); + HOST = uri.toString(); + } catch(e) { + HOST = 'http://tipu.xjqxz.top/api.php'; + } +} + +async function home(filter) { + const url = HOST + '/v2.vod/androidtypes'; + const homeResult = await request(url); + const json = JSON.parse(homeResult); + const filters = {}; + const classes = _.map(json.data, (item) => { + const filter = getFilters(item); + filters[item.type_id] = filter; + return { + type_id: item.type_id, + type_name: item.type_name, + }; + }); + return { + class: classes, + filters: filters, + }; +} + +function getFilters(data) { + const filterArray = []; + const clazz = convertTypeData(data, 'classes', '类型'); + if (clazz) filterArray.push(clazz); + const area = convertTypeData(data, 'areas', '地区'); + if (area) filterArray.push(area); + const year = convertTypeData(data, 'years', '年份'); + if (year) filterArray.push(year); + const by = { + key: 'by', + name: '排序', + init: '', + value: [ + {'n':'时间','v':'updatetime'}, + {'n':'人气','v':'hits'}, + {'n':'评分','v':'score'}, + ] + }; + filterArray.push(by); + return filterArray; +} + +function convertTypeData(typeData, typeKey, typeName) { + if (!typeData || !typeData[typeKey] || _.isEmpty(typeData[typeKey])) { + return null; + } + let valueList = typeData[typeKey]; + const values = _.map(valueList, (item) => { + return { + n: item, + v: item, + }; + }); + values.unshift({ + n: '全部', + v: '', + }); + const typeClass = { + key: typeKey, + name: typeName, + init: '', + value: values, + }; + return typeClass; +} + +async function homeVod() { + const url = HOST + '/v2.main/androidhome'; + const homeResult = await request(url); + const json = JSON.parse(homeResult); + const filters = {}; + const videos = []; + videos.push({ + vod_id: json.data.top.id, + vod_name: json.data.top.name, + vod_pic: json.data.top.pic, + vod_remarks: json.data.top.state, + }); + for (const list of json.data.list) { + for (const item of list.list) { + videos.push({ + vod_id: item.id, + vod_name: item.name, + vod_pic: item.pic, + vod_remarks: item.state, + }); + } + } + return { + list: videos, + }; +} + +async function category(tid, pg, filter, extend) { + const limit = 20; + const areaPath = extend.areas ? ('&area=' + extend.areas) : ''; + const classPath = extend.classes ? ('&class=' + extend.classes) : ''; + const yearPath = extend.years ? ('&year=' + extend.years) : ''; + const byPath = extend.by ? ('&sortby=' + extend.by) : ''; + const url = HOST + '/v2.vod/androidfilter?page=' + pg + '&type=' + tid + areaPath + yearPath + byPath + classPath; + const cateResult = await request(url); + const json = JSON.parse(cateResult); + const videos = _.map(json.data, (vObj) => { + return { + vod_id: vObj.id, + vod_name: vObj.name, + vod_pic: vObj.pic, + vod_remarks: vObj.state, + }; + }); + const hasMore = !_.isEmpty(videos); + const page = parseInt(pg); + const pageCount = hasMore ? page + 1 :page; + return { + page: page, + pagecount: pageCount, + limit: limit, + total: pageCount * limit, + list: videos, + }; +} + + +async function detail(id) { + const url = HOST + '/v3.vod/androiddetail2?vod_id=' + id; + const detailResult = await request(url); + const content = JSON.parse(detailResult); + const vObj = content.data; + const vodAtom = { + vod_id: id, + vod_name: vObj.name, + vod_pic: vObj.pic, + type_name: vObj.className, + vod_year: vObj.year, + vod_area: vObj.area, + vod_remarks: vObj.state, + vod_actor: vObj.actor, + vod_director: vObj.director, + vod_content: vObj.content, + } + const playInfo = vObj.urls; + const vodItems = _.map(playInfo, (epObj) => { + const epName = epObj.key; + const playUrl = epObj.url; + return epName + '$' + playUrl; + }); + vodAtom.vod_play_from = '主力源'; + vodAtom.vod_play_url = vodItems.join('#'); + return { + list: [vodAtom], + }; +} + +async function play(flag, id, flags) { + let playUrl = id; + if (!playUrl.startsWith('http')) { + playUrl = 'http://c.xpgtv.net/m3u8/' + id + '.m3u8'; + } + return { + parse: 0, + url: playUrl, + header: getHeaders(), + }; +} + +async function search(wd, quick, pg) { + const limit = 20; + const url = HOST + '/v2.vod/androidsearch10086?page=' + pg + '&wd=' + encodeURIComponent(wd); + const searchResult = await request(url); + const json = JSON.parse(searchResult); + const videos = _.map(json.data, (vObj) => { + return { + vod_id: vObj.id, + vod_name: vObj.name, + vod_pic: vObj.pic, + vod_remarks: vObj.state, + }; + }); + const page = parseInt(pg); + const hasMore = limit * page < json.totalcount; + const pageCount = hasMore ? page + 1 :page; + return { + page: page, + pagecount: pageCount, + limit: limit, + total: pageCount * limit, + list: videos, + }; +} + +export function __jsEvalReturn() { + return { + init: init, + home: home, + homeVod: homeVod, + category: category, + detail: detail, + play: play, + search: search, + }; +} \ No newline at end of file