From 40af589d38278db7a4687882f41b6114819236f2 Mon Sep 17 00:00:00 2001 From: RndDev123 Date: Tue, 26 Nov 2024 08:05:50 +0100 Subject: [PATCH 1/2] AniWorld, AnimeFenix, JKAnime and TioAnime: New host, fixes and helper lib update AniWorld - New Host: Speedfiles - New Host: Filemoon - Better Sorting for preferences AnimeFenix: - Added possible status string JKAnime - Get Studios TioAnime - Fix hasNextPage Helper library - Add new host: Speedfiles - Add new String function: reverse, swapcase --- javascript/anime/src/de/aniworld.js | 673 ++++++++++++++++++++++++-- javascript/anime/src/es/animefenix.js | 214 +++++--- javascript/anime/src/es/jkanime.js | 195 +++++--- javascript/anime/src/es/tioanime.js | 190 +++++--- 4 files changed, 1014 insertions(+), 258 deletions(-) diff --git a/javascript/anime/src/de/aniworld.js b/javascript/anime/src/de/aniworld.js index b5ee46a2..2032e284 100644 --- a/javascript/anime/src/de/aniworld.js +++ b/javascript/anime/src/de/aniworld.js @@ -7,7 +7,7 @@ const mangayomiSources = [{ "typeSource": "single", "isManga": false, "isNsfw": false, - "version": "0.0.28", + "version": "0.3.0", "dateFormat": "", "dateFormatLocale": "", "pkgPath": "anime/src/de/aniworld.js" @@ -137,20 +137,20 @@ class DefaultExtension extends MProvider { const videos = []; const redirectsElements = document.select("ul.row li"); - const hosterSelection = new SharedPreferences().get("hoster_selection_new"); + const hostFilter = new SharedPreferences().get("host_filter"); const dartClient = new Client({ 'useDartHttpClient': true, "followRedirects": false }); for (const element of redirectsElements) { const host = element.selectFirst("a h4").text; - if (hosterSelection.includes(host)) { + if (hostFilter.includes(host)) { const langkey = element.attr("data-lang-key"); const lang = (langkey == 1 || langkey == 3) ? 'Deutscher' : 'Englischer'; const type = (langkey == 1) ? 'Dub' : 'Sub'; const redirect = baseUrl + element.selectFirst("a.watchEpisode").attr("href"); promises.push((async (redirect, lang, type, host) => { const location = (await dartClient.get(redirect)).headers.location; - return await extractAny(location, host.toLowerCase(), lang, type, host); + return await extractAny(location, host.toLowerCase(), lang, type, host, {'Referer': this.source.baseUrl}); })(redirect, lang, type, host)); } } @@ -159,57 +159,164 @@ class DefaultExtension extends MProvider { videos.push.apply(videos, p.value); } } - return this.sortVideos(videos); - } - sortVideos(videos) { - const preference = new SharedPreferences(); - const hoster = RegExp(preference.get("preferred_hoster_new")); - const lang = RegExp(preference.get("preferred_lang")); - videos.sort((a, b) => { - let qualityMatchA = hoster.test(a.quality) * lang.test(a.quality); - let qualityMatchB = hoster.test(b.quality) * lang.test(b.quality); - return qualityMatchB - qualityMatchA; - }); - return videos; + return sortVideos(videos); } getSourcePreferences() { - const hosterOptions = ["Streamtape", "VOE", "Vidoza", "Doodstream"]; - const languageOptions = ["Deutscher Sub", "Deutscher Dub", "Englischer Sub"]; + const languages = ['Deutsch', 'Englisch']; + const languageValues = ['Deutscher', 'Englischer']; + const types = ['Dub', 'Sub']; + const resolutions = ['1080p', '720p', '480p']; + const hosts = ['Doodstream', 'Filemoon', 'SpeedFiles', 'Streamtape', 'Vidoza', 'VOE']; + return [ { - "key": "preferred_lang", - "listPreference": { - "title": "Bevorzugte Sprache", - "summary": "", - "valueIndex": 0, - "entries": languageOptions, - "entryValues": languageOptions + key: 'lang', + listPreference: { + title: 'Bevorzugte Sprache', + summary: 'Wenn verfügbar, wird diese Sprache ausgewählt. Priority = 0 (lower is better)', + valueIndex: 0, + entries: languages, + entryValues: languageValues } }, { - "key": "preferred_hoster_new", - "listPreference": { - "title": "Standard-Hoster", - "summary": "", - "valueIndex": 0, - "entries": hosterOptions, - "entryValues": hosterOptions + key: 'type', + listPreference: { + title: 'Bevorzugter Typ', + summary: 'Wenn verfügbar, wird dieser Typ ausgewählt. Priority = 1 (lower is better)', + valueIndex: 0, + entries: types, + entryValues: types } }, { - "key": "hoster_selection_new", - "multiSelectListPreference": { - "title": "Hoster auswählen", - "summary": "", - "entries": hosterOptions, - "entryValues": hosterOptions, - "values": hosterOptions + key: 'res', + listPreference: { + title: 'Bevorzugte Auflösung', + summary: 'Wenn verfügbar, wird diese Auflösung ausgewählt. Priority = 2 (lower is better)', + valueIndex: 0, + entries: resolutions, + entryValues: resolutions + } + }, + { + key: 'host', + listPreference: { + title: 'Bevorzugter Hoster', + summary: 'Wenn verfügbar, wird dieser Hoster ausgewählt. Priority = 3 (lower is better)', + valueIndex: 0, + entries: hosts, + entryValues: hosts + } + }, + { + key: "host_filter", + multiSelectListPreference: { + title: "Hoster auswählen", + summary: "Wähle aus welche Hoster dir angezeigt werden sollen. Weniger hoster zu laden beschleunigt den Start der Videos.", + entries: hosts, + entryValues: hosts, + values: hosts } } ]; } } +/*************************************************************************************************** +* +* mangayomi-js-helpers v1.1 +* +* # Video Extractors +* - vidGuardExtractor +* - doodExtractor +* - vidozaExtractor +* - okruExtractor +* - amazonExtractor +* - vidHideExtractor +* - filemoonExtractor +* - mixdropExtractor +* - speedfilesExtractor +* - burstcloudExtractor (not working, see description) +* +* # Video Extractor Wrappers +* - streamWishExtractor +* - voeExtractor +* - mp4UploadExtractor +* - yourUploadExtractor +* - streamTapeExtractor +* - sendVidExtractor +* +* # Video Extractor helpers +* - extractAny +* +* # Playlist Extractors +* - m3u8Extractor +* - jwplayerExtractor +* +* # Extension Helpers +* - sortVideos() +* +* # Uint8Array +* - Uint8Array.fromBase64() +* - Uint8Array.prototype.toBase64() +* - Uint8Array.prototype.decode() +* +* # String +* - String.prototype.encode() +* - String.decode() +* - String.prototype.reverse() +* - String.prototype.swapcase() +* - getRandomString() +* +* # Encode/Decode Functions +* - decodeUTF8 +* - encodeUTF8 +* +* # Url +* - absUrl() +* +***************************************************************************************************/ + +//-------------------------------------------------------------------------------------------------- +// Video Extractors +//-------------------------------------------------------------------------------------------------- + +async function vidGuardExtractor(url) { + // get html + const res = await new Client().get(url); + const doc = new Document(res.body); + const script = doc.selectFirst('script:contains(eval)'); + + // eval code + const code = script.text; + eval?.('var window = {};'); + eval?.(code); + const playlistUrl = globalThis.window.svg.stream; + + // decode sig + const encoded = playlistUrl.match(/sig=(.*?)&/)[1]; + const charCodes = []; + + for (let i = 0; i < encoded.length; i += 2) { + charCodes.push(parseInt(encoded.slice(i, i + 2), 16) ^ 2); + } + + let decoded = Uint8Array.fromBase64( + String.fromCharCode(...charCodes)) + .slice(5, -5) + .reverse(); + + for (let i = 0; i < decoded.length; i += 2) { + let tmp = decoded[i]; + decoded[i] = decoded[i + 1]; + decoded[i + 1] = tmp; + } + + decoded = decoded.decode(); + return await m3u8Extractor(playlistUrl.replace(encoded, decoded), null); +} + async function doodExtractor(url) { const dartClient = new Client({ 'useDartHttpClient': true, "followRedirects": false }); let response = await dartClient.get(url); @@ -235,9 +342,124 @@ async function vidozaExtractor(url) { return [{ url: videoUrl, originalUrl: videoUrl, quality: '' }]; } -_streamTapeExtractor = streamTapeExtractor; -streamTapeExtractor = async (url) => { - return await _streamTapeExtractor(url, ''); +async function okruExtractor(url) { + const res = await new Client().get(url); + const doc = new Document(res.body); + const tag = doc.selectFirst('div[data-options]'); + const playlistUrl = tag.attr('data-options').match(/hlsManifestUrl.*?(h.*?id=\d+)/)[1].replaceAll('\\\\u0026', '&'); + return await m3u8Extractor(playlistUrl, null); +} + +async function amazonExtractor(url) { + const res = await new Client().get(url); + const doc = new Document(res.body); + const videoUrl = doc.selectFirst('video').getSrc; + return videoUrl ? [{ url: videoUrl, originalUrl: videoUrl, headers: null, quality: '' }] : []; +} + +async function vidHideExtractor(url) { + const res = await new Client().get(url); + return await jwplayerExtractor(res.body); +} + +async function filemoonExtractor(url, headers) { + let res = await new Client().get(url, headers); + const src = res.body.match(/iframe src="(.*?)"/)?.[1]; + if (src) { + res = await new Client().get(src, { + 'Referer': url, + 'Accept-Language': 'de,en-US;q=0.7,en;q=0.3' + }); + } + return await jwplayerExtractor(res.body); +} + +async function mixdropExtractor(url) { + headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'}; + let res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(url, headers); + while ("location" in res.headers) { + res = await new Client({ 'useDartHttpClient': true, "followRedirects": false }).get(res.headers.location, headers); + } + const newUrl = res.request.url; + let doc = new Document(res.body); + + const code = doc.selectFirst('script:contains(MDCore):contains(eval)').text; + const unpacked = unpackJs(code); + let videoUrl = unpacked.match(/wurl="(.*?)"/)?.[1]; + + if (!videoUrl) return []; + + videoUrl = 'https:' + videoUrl; + headers.referer = newUrl; + + return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: headers}]; +} + +async function speedfilesExtractor(url) { + let res = await new Client().get(url); + let doc = new Document(res.body); + + const code = doc.selectFirst('script:contains(var)').text; + let b64; + + // Get b64 + for (const match of code.matchAll(/(?:var|let|const)\s*\w+\s*=\s*["']([^"']+)/g)) { + if (match[1].match(/[g-zG-Z]/)) { + b64 = match[1]; + break; + } + } + + // decode b64 => b64 + const step1 = Uint8Array.fromBase64(b64).reverse().decode().swapcase(); + // decode b64 => hex + const step2 = Uint8Array.fromBase64(step1).reverse().decode(); + // decode hex => b64 + let step3 = []; + for (let i = 0; i < step2.length; i += 2) { + step3.push(parseInt(step2.slice(i, i + 2), 16) - 3); + } + step3 = String.fromCharCode(...step3.reverse()).swapcase(); + // decode b64 => url + const videoUrl = Uint8Array.fromBase64(step3).decode(); + + return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: null}]; +} + +/** Does not work: Client always sets 'charset=utf-8' in Content-Type. */ +async function burstcloudExtractor(url) { + let client = new Client(); + let res = await client.get(url); + + const id = res.body.match(/data-file-id="(.*?)"/)[1]; + const headers = { + "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", + 'Referer': url, + }; + const data = { + 'fileId': id + }; + + res = await client.post(`https://www.burstcloud.co/file/play-request/`, headers, data); + const videoUrl = res.body.match(/cdnUrl":"(.*?)"/)[1]; + return [{ + url: videoUrl, + originalUrl: videoUrl, + headers: { 'Referer': url.match(/.*?:\/\/.*?\//) }, + quality: '' + }]; +} + +//-------------------------------------------------------------------------------------------------- +// Video Extractor Wrappers +//-------------------------------------------------------------------------------------------------- + +_streamWishExtractor = streamWishExtractor; +streamWishExtractor = async (url) => { + return (await _streamWishExtractor(url, '')).map(v => { + v.quality = v.quality.slice(3, -1); + return v; + }); } _voeExtractor = voeExtractor; @@ -248,26 +470,381 @@ voeExtractor = async (url) => { }); } -async function extractAny(link, method, lang, type, host) { +_mp4UploadExtractor = mp4UploadExtractor; +mp4UploadExtractor = async (url) => { + return (await _mp4UploadExtractor(url)).map(v => { + v.quality = v.quality.match(/\d+p/)?.[0] ?? ''; + return v; + }); +} + +_yourUploadExtractor = yourUploadExtractor; +yourUploadExtractor = async (url) => { + return (await _yourUploadExtractor(url)) + .filter(v => !v.url.includes('/novideo')) + .map(v => { + v.quality = ''; + return v; + }); +} + +_streamTapeExtractor = streamTapeExtractor; +streamTapeExtractor = async (url) => { + return await _streamTapeExtractor(url, ''); +} + +_sendVidExtractor = sendVidExtractor; +sendVidExtractor = async (url) => { + let res = await new Client().get(url); + var videoUrl, quality; + try { + videoUrl = res.body.match(/og:video" content="(.*?\.mp4.*?)"/)[1]; + quality = res.body.match(/og:video:height" content="(.*?)"/)?.[1]; + quality = quality ? quality + 'p' : ''; + } catch (error) { + + } + if (!videoUrl) { + return _sendVidExtractor(url, null, ''); + } + return [{url: videoUrl, originalUrl: videoUrl, quality: quality, headers: null}]; +} + +//-------------------------------------------------------------------------------------------------- +// Video Extractor Helpers +//-------------------------------------------------------------------------------------------------- + +async function extractAny(url, method, lang, type, host, headers = null) { const m = extractAny.methods[method]; - return (!m) ? [] : (await m(link)).map(v => { + return (!m) ? [] : (await m(url, headers)).map(v => { v.quality = v.quality ? `${lang} ${type} ${v.quality} ${host}` : `${lang} ${type} ${host}`; return v; }); }; extractAny.methods = { + 'amazon': amazonExtractor, + 'burstcloud': burstcloudExtractor, 'doodstream': doodExtractor, + 'filemoon': filemoonExtractor, + 'mixdrop': mixdropExtractor, + 'mp4upload': mp4UploadExtractor, + 'okru': okruExtractor, + 'sendvid': sendVidExtractor, + 'speedfiles': speedfilesExtractor, 'streamtape': streamTapeExtractor, + 'streamwish': vidHideExtractor, + 'vidguard': vidGuardExtractor, + 'vidhide': vidHideExtractor, 'vidoza': vidozaExtractor, - 'voe': voeExtractor + 'voe': voeExtractor, + 'yourupload': yourUploadExtractor }; +//-------------------------------------------------------------------------------------------------- +// Playlist Extractors +//-------------------------------------------------------------------------------------------------- + +async function m3u8Extractor(url, headers = null) { + // https://developer.apple.com/documentation/http-live-streaming/creating-a-multivariant-playlist + // https://developer.apple.com/documentation/http-live-streaming/adding-alternate-media-to-a-playlist + // define attribute lists + const streamAttributes = [ + ['avg_bandwidth', /AVERAGE-BANDWIDTH=(\d+)/], + ['bandwidth', /\bBANDWIDTH=(\d+)/], + ['resolution', /\bRESOLUTION=([\dx]+)/], + ['framerate', /\bFRAME-RATE=([\d\.]+)/], + ['codecs', /\bCODECS="(.*?)"/], + ['video', /\bVIDEO="(.*?)"/], + ['audio', /\bAUDIO="(.*?)"/], + ['subtitles', /\bSUBTITLES="(.*?)"/], + ['captions', /\bCLOSED-CAPTIONS="(.*?)"/] + ]; + const mediaAttributes = [ + ['type', /\bTYPE=([\w-]*)/], + ['group', /\bGROUP-ID="(.*?)"/], + ['lang', /\bLANGUAGE="(.*?)"/], + ['name', /\bNAME="(.*?)"/], + ['autoselect', /\bAUTOSELECT=(\w*)/], + ['default', /\bDEFAULT=(\w*)/], + ['instream-id', /\bINSTREAM-ID="(.*?)"/], + ['assoc-lang', /\bASSOC-LANGUAGE="(.*?)"/], + ['channels', /\bCHANNELS="(.*?)"/], + ['uri', /\bURI="(.*?)"/] + ]; + const streams = [], videos = {}, audios = {}, subtitles = {}, captions = {}; + const dict = { 'VIDEO': videos, 'AUDIO': audios, 'SUBTITLES': subtitles, 'CLOSED-CAPTIONS': captions }; + + const res = await new Client().get(url, headers); + const text = res.body; + + // collect media + for (const match of text.matchAll(/#EXT-X-MEDIA:(.*)/g)) { + const info = match[1], medium = {}; + for (const attr of mediaAttributes) { + const m = info.match(attr[1]); + medium[attr[0]] = m ? m[1] : null; + } + + const type = medium.type; + delete medium.type; + const group = medium.group; + delete medium.group; + + const typedict = dict[type]; + if (typedict[group] == undefined) + typedict[group] = []; + typedict[group].push(medium); + } + + // collect streams + for (const match of text.matchAll(/#EXT-X-STREAM-INF:(.*)\s*(.*)/g)) { + const info = match[1], stream = { 'url': absUrl(match[2], url) }; + for (const attr of streamAttributes) { + const m = info.match(attr[1]); + stream[attr[0]] = m ? m[1] : null; + } + + stream['video'] = videos[stream.video] ?? null; + stream['audio'] = audios[stream.audio] ?? null; + stream['subtitles'] = subtitles[stream.subtitles] ?? null; + stream['captions'] = captions[stream.captions] ?? null; + + // format resolution or bandwidth + let quality; + if (stream.resolution) { + quality = stream.resolution.match(/x(\d+)/)[1] + 'p'; + } else { + quality = (parseInt(stream.avg_bandwidth ?? stream.bandwidth) / 1000000) + 'Mb/s' + } + + // add stream to list + const subs = stream.subtitles?.map((s) => { + return { file: s.uri, label: s.name }; + }); + const auds = stream.audio?.map((a) => { + return { file: a.uri, label: a.name }; + }); + streams.push({ + url: stream.url, + quality: quality, + originalUrl: stream.url, + headers: headers, + subtitles: subs ?? null, + audios: auds ?? null + }); + } + return streams.length ? streams : [{ + url: url, + quality: '', + originalUrl: url, + headers: headers, + subtitles: null, + audios: null + }]; +} + +async function jwplayerExtractor(text, headers) { + // https://docs.jwplayer.com/players/reference/playlists + const getsetup = /setup\(({[\s\S]*?})\)/; + const getsources = /sources:\s*(\[[\s\S]*?\])/; + const gettracks = /tracks:\s*(\[[\s\S]*?\])/; + const unpacked = unpackJs(text); + + const videos = [], subtitles = []; + + const data = eval('(' + (getsetup.exec(text) || getsetup.exec(unpacked))?.[1] + ')'); + + if (data){ + var sources = data.sources; + var tracks = data.tracks; + } else { + var sources = eval('(' + (getsources.exec(text) || getsources.exec(unpacked))?.[1] + ')'); + var tracks = eval('(' + (gettracks.exec(text) || gettracks.exec(unpacked))?.[1] + ')'); + } + for (t of tracks) { + if (t.type == "captions") { + subtitles.push({file: t.file, label: t.label}); + } + } + for (s of sources) { + if (s.file.includes('master.m3u8')) { + videos.push(...(await m3u8Extractor(s.file, headers))); + } else if (s.file.includes('.mpd')) { + + } else { + videos.push({url: s.file, originalUrl: s.file, quality: '', headers: headers}); + } + } + return videos.map(v => { + v.subtitles = subtitles; + return v; + }); +} + +//-------------------------------------------------------------------------------------------------- +// Extension Helpers +//-------------------------------------------------------------------------------------------------- + +function sortVideos(videos) { + const pref = new SharedPreferences(); + const getres = RegExp('(\\d+)p?', 'i'); + const lang = RegExp(pref.get('lang'), 'i'); + const type = RegExp(pref.get('type'), 'i'); + const res = RegExp(getres.exec(pref.get('res'))[1], 'i'); + const host = RegExp(pref.get('host'), 'i'); + + let getScore = (q, hasRes) => { + const bLang = lang.test(q), bType = type.test(q), bRes = res.test(q), bHost = host.test(q); + if (hasRes) { + return bLang * (8 + bType * (4 + bRes * (2 + bHost * 1))); + } else { + return bLang * (8 + bType * (4 + (bHost * 3))); + } + } + + return videos.sort((a, b) => { + const resA = getres.exec(a.quality)?.[1]; + const resB = getres.exec(b.quality)?.[1]; + const score = getScore(b.quality, resB) - getScore(a.quality, resA); + + if (score) return score; + + const qA = resA ? a.quality.replace(resA, (9999 - parseInt(resA)).toString()) : a.quality; + const qB = resA ? b.quality.replace(resB, (9999 - parseInt(resB)).toString()) : b.quality; + + return qA.localeCompare(qB); + }); +} + +//-------------------------------------------------------------------------------------------------- +// Uint8Array +//-------------------------------------------------------------------------------------------------- + +Uint8Array.fromBase64 = function (b64) { + // [00,01,02,03,04,05,06,07,08,\t,\n,0b,0c,\r,0e,0f,10,11,12,13,14,15,16,17,18,19,1a,1b,1c,1d,1e,1f,' ', !, ", #, $, %, &, ', (, ), *, +,',', -, ., /, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <,'=', >, ?, @,A,B,C,D,E,F,G,H,I,J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~,7f] + const m = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1] + let data = [], val = 0, bits = -8 + for (const c of b64) { + let n = m[c.charCodeAt(0)]; + if (n == -1) break; + val = (val << 6) + n; + bits += 6; + for (; bits >= 0; bits -= 8) + data.push((val >> bits) & 0xFF); + } + return new Uint8Array(data); +} + +Uint8Array.prototype.toBase64 = function () { + const m = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + let b64 = '', val = 0, bits = -6; + for (const b of this) { + val = (val << 8) + b; + bits += 8; + while (bits >= 0) { + b64 += m[(val >> bits) & 0x3F]; + bits -= 6; + } + } + if (bits > -6) + b64 += m[(val << -bits) & 0x3F]; + return b64 + ['', '', '==', '='][b64.length % 4]; +} + +Uint8Array.prototype.decode = function (encoding = 'utf-8') { + encoding = encoding.toLowerCase(); + if (encoding == 'utf-8') { + return decodeUTF8(this); + } + return null; +} + +//-------------------------------------------------------------------------------------------------- +// String +//-------------------------------------------------------------------------------------------------- + +String.prototype.encode = function (encoding = 'utf-8') { + encoding = encoding.toLowerCase(); + if (encoding == 'utf-8') { + return encodeUTF8(this); + } + return null; +} + +String.decode = function (data, encoding = 'utf-8') { + encoding = encoding.toLowerCase(); + if (encoding == 'utf-8') { + return decodeUTF8(data); + } + return null; +} + +String.prototype.reverse = function () { + return this.split('').reverse().join(''); +} + +String.prototype.swapcase = function () { + const isAsciiLetter = /[A-z]/; + const result = []; + for (const l of this) + result.push(isAsciiLetter.test(l) ? String.fromCharCode(l.charCodeAt() ^ 32) : l); + return result.join(''); +} + function getRandomString(length) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; - const charArray = new Array(length); + let result = ""; for (let i = 0; i < length; i++) { - charArray[i] = chars[Math.floor(Math.random() * chars.length)]; + const random = Math.floor(Math.random() * 61); + result += chars[random]; } - return charArray.join(""); + return result; } + +//-------------------------------------------------------------------------------------------------- +// Encode/Decode Functions +//-------------------------------------------------------------------------------------------------- + +function decodeUTF8(data) { + const codes = []; + for (let i = 0; i < data.length;) { + const c = data[i++]; + const len = (c > 0xBF) + (c > 0xDF) + (c > 0xEF); + let val = c & (0xFF >> (len + 1)); + for (const end = i + len; i < end; i++) { + val = (val << 6) + (data[i] & 0x3F); + } + codes.push(val); + } + return String.fromCharCode(...codes); +} + +function encodeUTF8(string) { + const data = []; + for (const c of string) { + const code = c.charCodeAt(0); + const len = (code > 0x7F) + (code > 0x7FF) + (code > 0xFFFF); + let bits = len * 6; + + data.push((len ? ~(0xFF >> len + 1) : (0)) + (code >> bits)); + while (bits > 0) { + data.push(0x80 + ((code >> (bits -= 6)) & 0x3F)) + } + } + return new Uint8Array(data); +} + +//-------------------------------------------------------------------------------------------------- +// Url +//-------------------------------------------------------------------------------------------------- + +function absUrl(url, base) { + if (url.search(/^\w+:\/\//) == 0) { + return url; + } else if (url.startsWith('/')) { + return base.slice(0, base.lastIndexOf('/')) + url; + } else { + return base.slice(0, base.lastIndexOf('/') + 1) + url; + } +} \ No newline at end of file diff --git a/javascript/anime/src/es/animefenix.js b/javascript/anime/src/es/animefenix.js index df82d147..3b25c047 100644 --- a/javascript/anime/src/es/animefenix.js +++ b/javascript/anime/src/es/animefenix.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://www3.animefenix.tv/themes/fenix-neo/images/AveFenix.png", "typeSource": "single", "isManga": false, - "version": "0.1.0", + "version": "0.1.1", "dateFormat": "", "dateFormatLocale": "", "pkgPath": "anime/src/es/animefenix.js" @@ -37,10 +37,11 @@ class DefaultExtension extends MProvider { } statusFromString(status) { return { - "En emision": 0, // releasing - "Finalizado": 1, // finished - "Proximamente": 4, // unreleased - }[status] ?? 5; + "en emision": 0, // releasing + "emisión": 0, // releasing + "finalizado": 1, // finished + "proximamente": 4, // unreleased + }[status.toLowerCase()] ?? 5; } async getPopular(page) { return this.parseAnimeList(`${this.source.baseUrl}/animes?order=visits&page=${page}`); @@ -80,7 +81,7 @@ class DefaultExtension extends MProvider { const info = doc.selectFirst('main div.flex'); detail.name = info.selectFirst("h1").text; - detail.status = this.statusFromString(info.selectFirst("a").text); + detail.status = this.statusFromString(info.selectFirst("a").text.trim()); detail.imageUrl = info.selectFirst("img").getSrc; detail.description = info.selectFirst("h2 + p").text.trim(); detail.genre = info.select("h2 + div a").map(e => e.text.trim()); @@ -114,13 +115,8 @@ class DefaultExtension extends MProvider { }; // extract remote video links - const renameLUT = { - 'amazones': 'amazon', - 'burst': 'burstcloud', - 'hide': 'vidhide', - 'ru': 'okru', - 'stream2': 'vidhide', - }; + const renameLUT = { 'amazones': 'amazon', 'burst': 'burstcloud', 'hide': 'vidhide', + 'ru': 'okru', 'stream2': 'vidhide', }; for (let i = 0; i < hosts.length; i++) { const host = hosts[i].trim(); const lhost = host.toLowerCase(); @@ -509,6 +505,11 @@ class DefaultExtension extends MProvider { ]; } getSourcePreferences() { + const languages = ['Español']; + const types = ['Sub']; + const resolutions = ['1080p', '720p', '480p']; + const hosts = ['Amazon', 'AmazonEs', 'Burst', 'Mp4Upload', 'RU', 'Sendvid', 'STREAM2', 'HIDE', 'YourUpload']; + return [ { key: 'lang', @@ -516,12 +517,8 @@ class DefaultExtension extends MProvider { title: 'Preferred Language', summary: 'If available, this language will be chosen by default. Priority = 0 (lower is better)', valueIndex: 0, - entries: [ - 'Español' - ], - entryValues: [ - 'Español' - ] + entries: languages, + entryValues: languages } }, { @@ -530,12 +527,8 @@ class DefaultExtension extends MProvider { title: 'Preferred Type', summary: 'If available, this type will be chosen by default. Priority = 1 (lower is better)', valueIndex: 0, - entries: [ - 'Sub' - ], - entryValues: [ - 'Sub' - ] + entries: types, + entryValues: types } }, { @@ -544,44 +537,18 @@ class DefaultExtension extends MProvider { title: 'Preferred Resolution', summary: 'If available, this resolution will be chosen by default. Priority = 2 (lower is better)', valueIndex: 0, - entries: [ - '1080p', - '720p', - '480p' - ], - entryValues: [ - '1080p', - '720p', - '480p' - ] + entries: resolutions, + entryValues: resolutions } }, { key: 'host', listPreference: { - title: 'Preferred Hoster', + title: 'Preferred Host', summary: 'If available, this hoster will be chosen by default. Priority = 3 (lower is better)', valueIndex: 0, - entries: [ - 'Amazon', - 'Burst', - 'Mp4Upload', - 'RU', - 'Sendvid', - 'STREAM2', - 'HIDE', - 'YourUpload', - ], - entryValues: [ - 'Amazon', - 'Burst', - 'Mp4Upload', - 'RU', - 'Sendvid', - 'STREAM2', - 'HIDE', - 'YourUpload', - ] + entries: hosts, + entryValues: hosts } } ]; @@ -590,7 +557,7 @@ class DefaultExtension extends MProvider { /*************************************************************************************************** * -* mangayomi-js-helpers v1.0 +* mangayomi-js-helpers v1.1 * * # Video Extractors * - vidGuardExtractor @@ -601,9 +568,10 @@ class DefaultExtension extends MProvider { * - vidHideExtractor * - filemoonExtractor * - mixdropExtractor +* - speedfilesExtractor * - burstcloudExtractor (not working, see description) * -* # Video Extractor Format Wrappers +* # Video Extractor Wrappers * - streamWishExtractor * - voeExtractor * - mp4UploadExtractor @@ -618,24 +586,34 @@ class DefaultExtension extends MProvider { * - m3u8Extractor * - jwplayerExtractor * -* # Extension +* # Extension Helpers * - sortVideos() * -* # Encoding/Decoding -* - Uint8Array.fromBase64() -* - Uint8Array.prototype.toBase64() -* - Uint8Array.prototype.decode() +* # Uint8Array +* - Uint8Array.fromBase64() +* - Uint8Array.prototype.toBase64() +* - Uint8Array.prototype.decode() +* +* # String * - String.prototype.encode() -* - String.prototype.decode() -* -* # Random string +* - String.decode() +* - String.prototype.reverse() +* - String.prototype.swapcase() * - getRandomString() +* +* # Encode/Decode Functions +* - decodeUTF8 +* - encodeUTF8 * -* # URL +* # Url * - absUrl() * ***************************************************************************************************/ +//-------------------------------------------------------------------------------------------------- +// Video Extractors +//-------------------------------------------------------------------------------------------------- + async function vidGuardExtractor(url) { // get html const res = await new Client().get(url); @@ -716,8 +694,8 @@ async function vidHideExtractor(url) { return await jwplayerExtractor(res.body); } -async function filemoonExtractor(url) { - let res = await new Client().get(url); +async function filemoonExtractor(url, headers) { + let res = await new Client().get(url, headers); const src = res.body.match(/iframe src="(.*?)"/)?.[1]; if (src) { res = await new Client().get(src, { @@ -749,6 +727,37 @@ async function mixdropExtractor(url) { return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: headers}]; } +async function speedfilesExtractor(url) { + let res = await new Client().get(url); + let doc = new Document(res.body); + + const code = doc.selectFirst('script:contains(var)').text; + let b64; + + // Get b64 + for (const match of code.matchAll(/(?:var|let|const)\s*\w+\s*=\s*["']([^"']+)/g)) { + if (match[1].match(/[g-zG-Z]/)) { + b64 = match[1]; + break; + } + } + + // decode b64 => b64 + const step1 = Uint8Array.fromBase64(b64).reverse().decode().swapcase(); + // decode b64 => hex + const step2 = Uint8Array.fromBase64(step1).reverse().decode(); + // decode hex => b64 + let step3 = []; + for (let i = 0; i < step2.length; i += 2) { + step3.push(parseInt(step2.slice(i, i + 2), 16) - 3); + } + step3 = String.fromCharCode(...step3.reverse()).swapcase(); + // decode b64 => url + const videoUrl = Uint8Array.fromBase64(step3).decode(); + + return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: null}]; +} + /** Does not work: Client always sets 'charset=utf-8' in Content-Type. */ async function burstcloudExtractor(url) { let client = new Client(); @@ -773,6 +782,10 @@ async function burstcloudExtractor(url) { }]; } +//-------------------------------------------------------------------------------------------------- +// Video Extractor Wrappers +//-------------------------------------------------------------------------------------------------- + _streamWishExtractor = streamWishExtractor; streamWishExtractor = async (url) => { return (await _streamWishExtractor(url, '')).map(v => { @@ -829,9 +842,13 @@ sendVidExtractor = async (url) => { return [{url: videoUrl, originalUrl: videoUrl, quality: quality, headers: null}]; } -async function extractAny(url, method, lang, type, host) { +//-------------------------------------------------------------------------------------------------- +// Video Extractor Helpers +//-------------------------------------------------------------------------------------------------- + +async function extractAny(url, method, lang, type, host, headers = null) { const m = extractAny.methods[method]; - return (!m) ? [] : (await m(url)).map(v => { + return (!m) ? [] : (await m(url, headers)).map(v => { v.quality = v.quality ? `${lang} ${type} ${v.quality} ${host}` : `${lang} ${type} ${host}`; return v; }); @@ -846,6 +863,7 @@ extractAny.methods = { 'mp4upload': mp4UploadExtractor, 'okru': okruExtractor, 'sendvid': sendVidExtractor, + 'speedfiles': speedfilesExtractor, 'streamtape': streamTapeExtractor, 'streamwish': vidHideExtractor, 'vidguard': vidGuardExtractor, @@ -855,6 +873,10 @@ extractAny.methods = { 'yourupload': yourUploadExtractor }; +//-------------------------------------------------------------------------------------------------- +// Playlist Extractors +//-------------------------------------------------------------------------------------------------- + async function m3u8Extractor(url, headers = null) { // https://developer.apple.com/documentation/http-live-streaming/creating-a-multivariant-playlist // https://developer.apple.com/documentation/http-live-streaming/adding-alternate-media-to-a-playlist @@ -992,6 +1014,10 @@ async function jwplayerExtractor(text, headers) { }); } +//-------------------------------------------------------------------------------------------------- +// Extension Helpers +//-------------------------------------------------------------------------------------------------- + function sortVideos(videos) { const pref = new SharedPreferences(); const getres = RegExp('(\\d+)p?', 'i'); @@ -1023,6 +1049,10 @@ function sortVideos(videos) { }); } +//-------------------------------------------------------------------------------------------------- +// Uint8Array +//-------------------------------------------------------------------------------------------------- + Uint8Array.fromBase64 = function (b64) { // [00,01,02,03,04,05,06,07,08,\t,\n,0b,0c,\r,0e,0f,10,11,12,13,14,15,16,17,18,19,1a,1b,1c,1d,1e,1f,' ', !, ", #, $, %, &, ', (, ), *, +,',', -, ., /, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <,'=', >, ?, @,A,B,C,D,E,F,G,H,I,J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~,7f] const m = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1] @@ -1062,6 +1092,10 @@ Uint8Array.prototype.decode = function (encoding = 'utf-8') { return null; } +//-------------------------------------------------------------------------------------------------- +// String +//-------------------------------------------------------------------------------------------------- + String.prototype.encode = function (encoding = 'utf-8') { encoding = encoding.toLowerCase(); if (encoding == 'utf-8') { @@ -1078,6 +1112,32 @@ String.decode = function (data, encoding = 'utf-8') { return null; } +String.prototype.reverse = function () { + return this.split('').reverse().join(''); +} + +String.prototype.swapcase = function () { + const isAsciiLetter = /[A-z]/; + const result = []; + for (const l of this) + result.push(isAsciiLetter.test(l) ? String.fromCharCode(l.charCodeAt() ^ 32) : l); + return result.join(''); +} + +function getRandomString(length) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; + let result = ""; + for (let i = 0; i < length; i++) { + const random = Math.floor(Math.random() * 61); + result += chars[random]; + } + return result; +} + +//-------------------------------------------------------------------------------------------------- +// Encode/Decode Functions +//-------------------------------------------------------------------------------------------------- + function decodeUTF8(data) { const codes = []; for (let i = 0; i < data.length;) { @@ -1107,15 +1167,9 @@ function encodeUTF8(string) { return new Uint8Array(data); } -function getRandomString(length) { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; - let result = ""; - for (let i = 0; i < length; i++) { - const random = Math.floor(Math.random() * 61); - result += chars[random]; - } - return result; -} +//-------------------------------------------------------------------------------------------------- +// Url +//-------------------------------------------------------------------------------------------------- function absUrl(url, base) { if (url.search(/^\w+:\/\//) == 0) { diff --git a/javascript/anime/src/es/jkanime.js b/javascript/anime/src/es/jkanime.js index a1ab1bf9..27b57780 100644 --- a/javascript/anime/src/es/jkanime.js +++ b/javascript/anime/src/es/jkanime.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://cdn.jkdesu.com/assets2/css/img/favicon.ico", "typeSource": "single", "isManga": false, - "version": "0.1.0", + "version": "0.1.1", "dateFormat": "", "dateFormatLocale": "", "pkgPath": "anime/src/es/jkanime.js" @@ -102,6 +102,7 @@ class DefaultExtension extends MProvider { detail.description = info.selectFirst("p.sinopsis").text.trim(); detail.status = this.statusFromString(extInfo.selectFirst("span:contains(Estado) + span").text); detail.genre = extInfo.select("li:contains(Genero) a").map(e => e.text); + detail.author = extInfo.select("li:contains(Studios) a").map(e => e.text).join(', '); // get episodes detail.episodes = []; @@ -779,6 +780,11 @@ class DefaultExtension extends MProvider { ]; } getSourcePreferences() { + const languages = ['Español']; + const types = ['Sub']; + const resolutions = ['1080p', '720p', '480p']; + const hosts = ['Desu', 'Filemoon', 'Mixdrop', 'Mp4upload', 'Streamtape', 'Streamwish', 'Vidhide', 'VOE']; + return [ { key: 'lang', @@ -786,12 +792,8 @@ class DefaultExtension extends MProvider { title: 'Preferred Language', summary: 'If available, this language will be chosen by default. Priority = 0 (lower is better)', valueIndex: 0, - entries: [ - 'Español' - ], - entryValues: [ - 'Español' - ] + entries: languages, + entryValues: languages } }, { @@ -800,12 +802,8 @@ class DefaultExtension extends MProvider { title: 'Preferred Type', summary: 'If available, this type will be chosen by default. Priority = 1 (lower is better)', valueIndex: 0, - entries: [ - 'Sub' - ], - entryValues: [ - 'Sub' - ] + entries: types, + entryValues: types } }, { @@ -814,44 +812,18 @@ class DefaultExtension extends MProvider { title: 'Preferred Resolution', summary: 'If available, this resolution will be chosen by default. Priority = 2 (lower is better)', valueIndex: 0, - entries: [ - '1080p', - '720p', - '480p' - ], - entryValues: [ - '1080p', - '720p', - '480p' - ] + entries: resolutions, + entryValues: resolutions } }, { key: 'host', listPreference: { - title: 'Preferred Hoster', + title: 'Preferred Host', summary: 'If available, this hoster will be chosen by default. Priority = 3 (lower is better)', valueIndex: 0, - entries: [ - 'Desu', - 'Filemoon', - 'Mixdrop', - 'Mp4Upload', - 'Streamtape', - 'Streamwish', - 'Vidhide', - 'Voe' - ], - entryValues: [ - 'Desu', - 'Filemoon', - 'Mixdrop', - 'Mp4Upload', - 'Streamtape', - 'Streamwish', - 'Vidhide', - 'Voe' - ] + entries: hosts, + entryValues: hosts } } ]; @@ -860,7 +832,7 @@ class DefaultExtension extends MProvider { /*************************************************************************************************** * -* mangayomi-js-helpers v1.0 +* mangayomi-js-helpers v1.1 * * # Video Extractors * - vidGuardExtractor @@ -871,9 +843,10 @@ class DefaultExtension extends MProvider { * - vidHideExtractor * - filemoonExtractor * - mixdropExtractor +* - speedfilesExtractor * - burstcloudExtractor (not working, see description) * -* # Video Extractor Format Wrappers +* # Video Extractor Wrappers * - streamWishExtractor * - voeExtractor * - mp4UploadExtractor @@ -888,24 +861,34 @@ class DefaultExtension extends MProvider { * - m3u8Extractor * - jwplayerExtractor * -* # Extension +* # Extension Helpers * - sortVideos() * -* # Encoding/Decoding -* - Uint8Array.fromBase64() -* - Uint8Array.prototype.toBase64() -* - Uint8Array.prototype.decode() +* # Uint8Array +* - Uint8Array.fromBase64() +* - Uint8Array.prototype.toBase64() +* - Uint8Array.prototype.decode() +* +* # String * - String.prototype.encode() -* - String.prototype.decode() -* -* # Random string +* - String.decode() +* - String.prototype.reverse() +* - String.prototype.swapcase() * - getRandomString() +* +* # Encode/Decode Functions +* - decodeUTF8 +* - encodeUTF8 * -* # URL +* # Url * - absUrl() * ***************************************************************************************************/ +//-------------------------------------------------------------------------------------------------- +// Video Extractors +//-------------------------------------------------------------------------------------------------- + async function vidGuardExtractor(url) { // get html const res = await new Client().get(url); @@ -986,8 +969,8 @@ async function vidHideExtractor(url) { return await jwplayerExtractor(res.body); } -async function filemoonExtractor(url) { - let res = await new Client().get(url); +async function filemoonExtractor(url, headers) { + let res = await new Client().get(url, headers); const src = res.body.match(/iframe src="(.*?)"/)?.[1]; if (src) { res = await new Client().get(src, { @@ -1019,6 +1002,37 @@ async function mixdropExtractor(url) { return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: headers}]; } +async function speedfilesExtractor(url) { + let res = await new Client().get(url); + let doc = new Document(res.body); + + const code = doc.selectFirst('script:contains(var)').text; + let b64; + + // Get b64 + for (const match of code.matchAll(/(?:var|let|const)\s*\w+\s*=\s*["']([^"']+)/g)) { + if (match[1].match(/[g-zG-Z]/)) { + b64 = match[1]; + break; + } + } + + // decode b64 => b64 + const step1 = Uint8Array.fromBase64(b64).reverse().decode().swapcase(); + // decode b64 => hex + const step2 = Uint8Array.fromBase64(step1).reverse().decode(); + // decode hex => b64 + let step3 = []; + for (let i = 0; i < step2.length; i += 2) { + step3.push(parseInt(step2.slice(i, i + 2), 16) - 3); + } + step3 = String.fromCharCode(...step3.reverse()).swapcase(); + // decode b64 => url + const videoUrl = Uint8Array.fromBase64(step3).decode(); + + return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: null}]; +} + /** Does not work: Client always sets 'charset=utf-8' in Content-Type. */ async function burstcloudExtractor(url) { let client = new Client(); @@ -1043,6 +1057,10 @@ async function burstcloudExtractor(url) { }]; } +//-------------------------------------------------------------------------------------------------- +// Video Extractor Wrappers +//-------------------------------------------------------------------------------------------------- + _streamWishExtractor = streamWishExtractor; streamWishExtractor = async (url) => { return (await _streamWishExtractor(url, '')).map(v => { @@ -1099,9 +1117,13 @@ sendVidExtractor = async (url) => { return [{url: videoUrl, originalUrl: videoUrl, quality: quality, headers: null}]; } -async function extractAny(url, method, lang, type, host) { +//-------------------------------------------------------------------------------------------------- +// Video Extractor Helpers +//-------------------------------------------------------------------------------------------------- + +async function extractAny(url, method, lang, type, host, headers = null) { const m = extractAny.methods[method]; - return (!m) ? [] : (await m(url)).map(v => { + return (!m) ? [] : (await m(url, headers)).map(v => { v.quality = v.quality ? `${lang} ${type} ${v.quality} ${host}` : `${lang} ${type} ${host}`; return v; }); @@ -1116,6 +1138,7 @@ extractAny.methods = { 'mp4upload': mp4UploadExtractor, 'okru': okruExtractor, 'sendvid': sendVidExtractor, + 'speedfiles': speedfilesExtractor, 'streamtape': streamTapeExtractor, 'streamwish': vidHideExtractor, 'vidguard': vidGuardExtractor, @@ -1125,6 +1148,10 @@ extractAny.methods = { 'yourupload': yourUploadExtractor }; +//-------------------------------------------------------------------------------------------------- +// Playlist Extractors +//-------------------------------------------------------------------------------------------------- + async function m3u8Extractor(url, headers = null) { // https://developer.apple.com/documentation/http-live-streaming/creating-a-multivariant-playlist // https://developer.apple.com/documentation/http-live-streaming/adding-alternate-media-to-a-playlist @@ -1262,6 +1289,10 @@ async function jwplayerExtractor(text, headers) { }); } +//-------------------------------------------------------------------------------------------------- +// Extension Helpers +//-------------------------------------------------------------------------------------------------- + function sortVideos(videos) { const pref = new SharedPreferences(); const getres = RegExp('(\\d+)p?', 'i'); @@ -1293,6 +1324,10 @@ function sortVideos(videos) { }); } +//-------------------------------------------------------------------------------------------------- +// Uint8Array +//-------------------------------------------------------------------------------------------------- + Uint8Array.fromBase64 = function (b64) { // [00,01,02,03,04,05,06,07,08,\t,\n,0b,0c,\r,0e,0f,10,11,12,13,14,15,16,17,18,19,1a,1b,1c,1d,1e,1f,' ', !, ", #, $, %, &, ', (, ), *, +,',', -, ., /, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <,'=', >, ?, @,A,B,C,D,E,F,G,H,I,J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~,7f] const m = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1] @@ -1332,6 +1367,10 @@ Uint8Array.prototype.decode = function (encoding = 'utf-8') { return null; } +//-------------------------------------------------------------------------------------------------- +// String +//-------------------------------------------------------------------------------------------------- + String.prototype.encode = function (encoding = 'utf-8') { encoding = encoding.toLowerCase(); if (encoding == 'utf-8') { @@ -1348,6 +1387,32 @@ String.decode = function (data, encoding = 'utf-8') { return null; } +String.prototype.reverse = function () { + return this.split('').reverse().join(''); +} + +String.prototype.swapcase = function () { + const isAsciiLetter = /[A-z]/; + const result = []; + for (const l of this) + result.push(isAsciiLetter.test(l) ? String.fromCharCode(l.charCodeAt() ^ 32) : l); + return result.join(''); +} + +function getRandomString(length) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; + let result = ""; + for (let i = 0; i < length; i++) { + const random = Math.floor(Math.random() * 61); + result += chars[random]; + } + return result; +} + +//-------------------------------------------------------------------------------------------------- +// Encode/Decode Functions +//-------------------------------------------------------------------------------------------------- + function decodeUTF8(data) { const codes = []; for (let i = 0; i < data.length;) { @@ -1377,15 +1442,9 @@ function encodeUTF8(string) { return new Uint8Array(data); } -function getRandomString(length) { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; - let result = ""; - for (let i = 0; i < length; i++) { - const random = Math.floor(Math.random() * 61); - result += chars[random]; - } - return result; -} +//-------------------------------------------------------------------------------------------------- +// Url +//-------------------------------------------------------------------------------------------------- function absUrl(url, base) { if (url.search(/^\w+:\/\//) == 0) { diff --git a/javascript/anime/src/es/tioanime.js b/javascript/anime/src/es/tioanime.js index 729e30a2..514154e9 100644 --- a/javascript/anime/src/es/tioanime.js +++ b/javascript/anime/src/es/tioanime.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://tioanime.com/assets/img/tio_fb.jpg", "typeSource": "single", "isManga": false, - "version": "0.1.0", + "version": "0.1.1", "dateFormat": "", "dateFormatLocale": "", "pkgPath": "anime/src/es/tioanime.js" @@ -32,8 +32,8 @@ class DefaultExtension extends MProvider { const link = element.selectFirst("a").getHref; list.push({ name, imageUrl, link }); } - const hasNextPage = doc.selectFirst("li.page-item.active + li").text == "»"; - return { "list": list, "hasNextPage": hasNextPage }; + const isLastPage = doc.selectFirst("li.page-item.active + li").text == "»"; + return { "list": list, "hasNextPage": !isLastPage }; } statusFromString(status) { return { @@ -106,6 +106,11 @@ class DefaultExtension extends MProvider { throw new Error("getFilterList not implemented"); } getSourcePreferences() { + const languages = ['Español']; + const types = ['Sub']; + const resolutions = ['1080p', '720p', '480p']; + const hosts = ['Okru', 'VidGuard', 'Voe', 'YourUpload']; + return [ { key: 'lang', @@ -113,12 +118,8 @@ class DefaultExtension extends MProvider { title: 'Preferred Language', summary: 'If available, this language will be chosen by default. Priority = 0 (lower is better)', valueIndex: 0, - entries: [ - 'Español' - ], - entryValues: [ - 'Español' - ] + entries: languages, + entryValues: languages } }, { @@ -127,12 +128,8 @@ class DefaultExtension extends MProvider { title: 'Preferred Type', summary: 'If available, this type will be chosen by default. Priority = 1 (lower is better)', valueIndex: 0, - entries: [ - 'Sub' - ], - entryValues: [ - 'Sub' - ] + entries: types, + entryValues: types } }, { @@ -141,36 +138,18 @@ class DefaultExtension extends MProvider { title: 'Preferred Resolution', summary: 'If available, this resolution will be chosen by default. Priority = 2 (lower is better)', valueIndex: 0, - entries: [ - '1080p', - '720p', - '480p' - ], - entryValues: [ - '1080p', - '720p', - '480p' - ] + entries: resolutions, + entryValues: resolutions } }, { key: 'host', listPreference: { - title: 'Preferred Hoster', + title: 'Preferred Host', summary: 'If available, this hoster will be chosen by default. Priority = 3 (lower is better)', valueIndex: 0, - entries: [ - 'Okru', - 'VidGuard', - 'Voe', - 'YourUpload' - ], - entryValues: [ - 'Okru', - 'VidGuard', - 'Voe', - 'YourUpload' - ] + entries: hosts, + entryValues: hosts } } ]; @@ -179,7 +158,7 @@ class DefaultExtension extends MProvider { /*************************************************************************************************** * -* mangayomi-js-helpers v1.0 +* mangayomi-js-helpers v1.1 * * # Video Extractors * - vidGuardExtractor @@ -190,9 +169,10 @@ class DefaultExtension extends MProvider { * - vidHideExtractor * - filemoonExtractor * - mixdropExtractor +* - speedfilesExtractor * - burstcloudExtractor (not working, see description) * -* # Video Extractor Format Wrappers +* # Video Extractor Wrappers * - streamWishExtractor * - voeExtractor * - mp4UploadExtractor @@ -207,24 +187,34 @@ class DefaultExtension extends MProvider { * - m3u8Extractor * - jwplayerExtractor * -* # Extension +* # Extension Helpers * - sortVideos() * -* # Encoding/Decoding -* - Uint8Array.fromBase64() -* - Uint8Array.prototype.toBase64() -* - Uint8Array.prototype.decode() +* # Uint8Array +* - Uint8Array.fromBase64() +* - Uint8Array.prototype.toBase64() +* - Uint8Array.prototype.decode() +* +* # String * - String.prototype.encode() -* - String.prototype.decode() -* -* # Random string +* - String.decode() +* - String.prototype.reverse() +* - String.prototype.swapcase() * - getRandomString() +* +* # Encode/Decode Functions +* - decodeUTF8 +* - encodeUTF8 * -* # URL +* # Url * - absUrl() * ***************************************************************************************************/ +//-------------------------------------------------------------------------------------------------- +// Video Extractors +//-------------------------------------------------------------------------------------------------- + async function vidGuardExtractor(url) { // get html const res = await new Client().get(url); @@ -305,8 +295,8 @@ async function vidHideExtractor(url) { return await jwplayerExtractor(res.body); } -async function filemoonExtractor(url) { - let res = await new Client().get(url); +async function filemoonExtractor(url, headers) { + let res = await new Client().get(url, headers); const src = res.body.match(/iframe src="(.*?)"/)?.[1]; if (src) { res = await new Client().get(src, { @@ -338,6 +328,37 @@ async function mixdropExtractor(url) { return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: headers}]; } +async function speedfilesExtractor(url) { + let res = await new Client().get(url); + let doc = new Document(res.body); + + const code = doc.selectFirst('script:contains(var)').text; + let b64; + + // Get b64 + for (const match of code.matchAll(/(?:var|let|const)\s*\w+\s*=\s*["']([^"']+)/g)) { + if (match[1].match(/[g-zG-Z]/)) { + b64 = match[1]; + break; + } + } + + // decode b64 => b64 + const step1 = Uint8Array.fromBase64(b64).reverse().decode().swapcase(); + // decode b64 => hex + const step2 = Uint8Array.fromBase64(step1).reverse().decode(); + // decode hex => b64 + let step3 = []; + for (let i = 0; i < step2.length; i += 2) { + step3.push(parseInt(step2.slice(i, i + 2), 16) - 3); + } + step3 = String.fromCharCode(...step3.reverse()).swapcase(); + // decode b64 => url + const videoUrl = Uint8Array.fromBase64(step3).decode(); + + return [{url: videoUrl, originalUrl: videoUrl, quality: '', headers: null}]; +} + /** Does not work: Client always sets 'charset=utf-8' in Content-Type. */ async function burstcloudExtractor(url) { let client = new Client(); @@ -362,6 +383,10 @@ async function burstcloudExtractor(url) { }]; } +//-------------------------------------------------------------------------------------------------- +// Video Extractor Wrappers +//-------------------------------------------------------------------------------------------------- + _streamWishExtractor = streamWishExtractor; streamWishExtractor = async (url) => { return (await _streamWishExtractor(url, '')).map(v => { @@ -418,9 +443,13 @@ sendVidExtractor = async (url) => { return [{url: videoUrl, originalUrl: videoUrl, quality: quality, headers: null}]; } -async function extractAny(url, method, lang, type, host) { +//-------------------------------------------------------------------------------------------------- +// Video Extractor Helpers +//-------------------------------------------------------------------------------------------------- + +async function extractAny(url, method, lang, type, host, headers = null) { const m = extractAny.methods[method]; - return (!m) ? [] : (await m(url)).map(v => { + return (!m) ? [] : (await m(url, headers)).map(v => { v.quality = v.quality ? `${lang} ${type} ${v.quality} ${host}` : `${lang} ${type} ${host}`; return v; }); @@ -435,6 +464,7 @@ extractAny.methods = { 'mp4upload': mp4UploadExtractor, 'okru': okruExtractor, 'sendvid': sendVidExtractor, + 'speedfiles': speedfilesExtractor, 'streamtape': streamTapeExtractor, 'streamwish': vidHideExtractor, 'vidguard': vidGuardExtractor, @@ -444,6 +474,10 @@ extractAny.methods = { 'yourupload': yourUploadExtractor }; +//-------------------------------------------------------------------------------------------------- +// Playlist Extractors +//-------------------------------------------------------------------------------------------------- + async function m3u8Extractor(url, headers = null) { // https://developer.apple.com/documentation/http-live-streaming/creating-a-multivariant-playlist // https://developer.apple.com/documentation/http-live-streaming/adding-alternate-media-to-a-playlist @@ -581,6 +615,10 @@ async function jwplayerExtractor(text, headers) { }); } +//-------------------------------------------------------------------------------------------------- +// Extension Helpers +//-------------------------------------------------------------------------------------------------- + function sortVideos(videos) { const pref = new SharedPreferences(); const getres = RegExp('(\\d+)p?', 'i'); @@ -612,6 +650,10 @@ function sortVideos(videos) { }); } +//-------------------------------------------------------------------------------------------------- +// Uint8Array +//-------------------------------------------------------------------------------------------------- + Uint8Array.fromBase64 = function (b64) { // [00,01,02,03,04,05,06,07,08,\t,\n,0b,0c,\r,0e,0f,10,11,12,13,14,15,16,17,18,19,1a,1b,1c,1d,1e,1f,' ', !, ", #, $, %, &, ', (, ), *, +,',', -, ., /, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, :, ;, <,'=', >, ?, @,A,B,C,D,E,F,G,H,I,J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, [, \, ], ^, _, `, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, {, |, }, ~,7f] const m = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1] @@ -651,6 +693,10 @@ Uint8Array.prototype.decode = function (encoding = 'utf-8') { return null; } +//-------------------------------------------------------------------------------------------------- +// String +//-------------------------------------------------------------------------------------------------- + String.prototype.encode = function (encoding = 'utf-8') { encoding = encoding.toLowerCase(); if (encoding == 'utf-8') { @@ -667,6 +713,32 @@ String.decode = function (data, encoding = 'utf-8') { return null; } +String.prototype.reverse = function () { + return this.split('').reverse().join(''); +} + +String.prototype.swapcase = function () { + const isAsciiLetter = /[A-z]/; + const result = []; + for (const l of this) + result.push(isAsciiLetter.test(l) ? String.fromCharCode(l.charCodeAt() ^ 32) : l); + return result.join(''); +} + +function getRandomString(length) { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; + let result = ""; + for (let i = 0; i < length; i++) { + const random = Math.floor(Math.random() * 61); + result += chars[random]; + } + return result; +} + +//-------------------------------------------------------------------------------------------------- +// Encode/Decode Functions +//-------------------------------------------------------------------------------------------------- + function decodeUTF8(data) { const codes = []; for (let i = 0; i < data.length;) { @@ -696,15 +768,9 @@ function encodeUTF8(string) { return new Uint8Array(data); } -function getRandomString(length) { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890"; - let result = ""; - for (let i = 0; i < length; i++) { - const random = Math.floor(Math.random() * 61); - result += chars[random]; - } - return result; -} +//-------------------------------------------------------------------------------------------------- +// Url +//-------------------------------------------------------------------------------------------------- function absUrl(url, base) { if (url.search(/^\w+:\/\//) == 0) { From 773fc0bc3ccec08daa89d389730acd466459874d Mon Sep 17 00:00:00 2001 From: RndDev123 Date: Tue, 26 Nov 2024 10:32:43 +0100 Subject: [PATCH 2/2] JKAnime: Change Logo --- javascript/anime/src/es/jkanime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/anime/src/es/jkanime.js b/javascript/anime/src/es/jkanime.js index 27b57780..b03c8a13 100644 --- a/javascript/anime/src/es/jkanime.js +++ b/javascript/anime/src/es/jkanime.js @@ -3,7 +3,7 @@ const mangayomiSources = [{ "lang": "es", "baseUrl": "https://jkanime.net", "apiUrl": "", - "iconUrl": "https://cdn.jkdesu.com/assets2/css/img/favicon.ico", + "iconUrl": "https://cdn.jkanime.net/logo_jk.png", "typeSource": "single", "isManga": false, "version": "0.1.1",