diff --git a/addon/moch/delay.js b/addon/moch/delay.js new file mode 100644 index 0000000..69e7710 --- /dev/null +++ b/addon/moch/delay.js @@ -0,0 +1 @@ +module.exports = duration => new Promise((resolve) => setTimeout(resolve, duration)); \ No newline at end of file diff --git a/addon/moch/moch.js b/addon/moch/moch.js index eee8d4b..874362b 100644 --- a/addon/moch/moch.js +++ b/addon/moch/moch.js @@ -3,6 +3,7 @@ const options = require('./options'); const realdebrid = require('./realdebrid'); const premiumize = require('./premiumize'); const alldebrid = require('./alldebrid'); +const putio = require('./putio'); const StaticResponse = require('./static'); const { cacheWrapResolvedUrl } = require('../lib/cache'); @@ -22,6 +23,11 @@ const MOCHS = { key: 'alldebrid', instance: alldebrid, shortName: 'AD' + }, + 'putio': { + key: 'putio', + instance: putio, + shortName: 'Putio' } }; diff --git a/addon/moch/putio.js b/addon/moch/putio.js new file mode 100644 index 0000000..3cfc341 --- /dev/null +++ b/addon/moch/putio.js @@ -0,0 +1,146 @@ +const PutioAPI = require('@putdotio/api-client').default +const { encode } = require('magnet-uri'); +const isVideo = require('../lib/video'); +const delay = require('./delay'); +const StaticResponse = require('./static'); + +async function getCachedStreams(streams, apiKey) { + return streams + .reduce((mochStreams, stream) => { + const streamTitleParts = stream.title.replace(/\n👤.*/s, '').split('\n'); + const fileName = streamTitleParts[streamTitleParts.length - 1]; + const fileIndex = streamTitleParts.length === 2 ? stream.fileIdx : null; + const encodedFileName = encodeURIComponent(fileName); + mochStreams[stream.infoHash] = { + url: `${apiKey}/${stream.infoHash}/${encodedFileName}/${fileIndex}`, + cached: false + }; + return mochStreams; + }, {}); +} + +async function resolve({ ip, apiKey, infoHash, cachedEntryInfo, fileIndex }) { + console.log(`Unrestricting ${infoHash} [${fileIndex}]`); + const clientId = apiKey.replace(/@.*/, ''); + const token = apiKey.replace(/.*@/, ''); + const Putio = new PutioAPI({ clientID: clientId }); + Putio.setToken(token); + + const torrent = await _createOrFindTorrent(Putio, infoHash); + if (torrent && statusReady(torrent.status)) { + return _unrestrictLink(Putio, torrent, cachedEntryInfo, fileIndex); + } else if (torrent && statusDownloading(torrent.status)) { + return StaticResponse.DOWNLOADING; + } else if (torrent && statusError(torrent.status)) { + return _retryCreateTorrent(Putio, infoHash, cachedEntryInfo, fileIndex); + } + return Promise.reject("Failed Putio adding torrent"); +} + +async function _createOrFindTorrent(Putio, infoHash) { + return _findTorrent(Putio, infoHash) + .catch(() => _createTorrent(Putio, infoHash)) + .catch(error => { + console.warn('Failed Putio torrent retrieval', error); + return error; + }); +} + +async function _retryCreateTorrent(Putio, infoHash, encodedFileName, fileIndex) { + const newTorrent = await _createTorrent(Putio, infoHash); + return newTorrent && statusReady(newTorrent.status) + ? _unrestrictLink(Putio, newTorrent, encodedFileName, fileIndex) + : StaticResponse.FAILED_DOWNLOAD; +} + +async function _findTorrent(Putio, infoHash) { + const torrents = await Putio.Transfers.Query().then(response => response.data.transfers); + const foundTorrents = torrents.filter(torrent => torrent.source.toLowerCase().includes(infoHash)); + const nonFailedTorrent = foundTorrents.find(torrent => !statusError(torrent.status)); + const foundTorrent = nonFailedTorrent || foundTorrents[0]; + return foundTorrent || Promise.reject('No recent torrent found'); +} + +async function _createTorrent(Putio, infoHash) { + const magnetLink = encode({ infoHash }); + // Add the torrent and then delay for 3 secs for putio to process it and then check it's status. + return Putio.Transfers.Add({ url: magnetLink }) + .then(response => _getNewTorrent(Putio, response.data.transfer.id)); +} + +async function _getNewTorrent(Putio, torrentId, pollCounter = 0, pollRate = 2000, maxPollNumber = 15) { + return Putio.Transfers.Get(torrentId) + .then(response => response.data.transfer) + .then(torrent => statusProcessing(torrent.status) && pollCounter < maxPollNumber + ? delay(pollRate).then(() => _getNewTorrent(Putio, torrentId, pollCounter + 1)) + : torrent); +} + +async function _unrestrictLink(Putio, torrent, encodedFileName, fileIndex) { + const targetVideo = await _getTargetFile(Putio, torrent, encodedFileName, fileIndex) + const publicToken = await _getPublicToken(Putio, targetVideo.id); + const publicFile = await Putio.File.Public(publicToken).then(response => response.data.parent); + + if (!publicFile.stream_url || !publicFile.stream_url.length) { + return Promise.reject("No available links found"); + } + console.log(`Unrestricted ${torrent.hash} [${fileIndex}] to ${publicFile.stream_url}`); + return publicFile.stream_url; +} + +async function _getTargetFile(Putio, torrent, encodedFileName, fileIndex) { + const targetFileName = decodeURIComponent(encodedFileName); + let targetFile; + let files = await _getFiles(Putio, torrent.file_id); + let videos = []; + + while (!targetFile && files.length) { + const folders = files.filter(file => file.file_type === 'FOLDER'); + videos = videos.concat(files.filter(file => isVideo(file.name))); + // when specific file index is defined search by filename + // when it's not defined find all videos and take the largest one + targetFile = Number.isInteger(fileIndex) + ? videos.find(video => targetFileName.includes(video.name)) + : !folders.length && videos.sort((a, b) => b.size - a.size)[0]; + files = !targetFile + ? await Promise.all(folders.map(folder => _getFiles(Putio, folder.id))) + .then(results => results.reduce((a, b) => a.concat(b), [])) + : []; + } + return targetFile; +} + +async function _getFiles(Putio, fileId) { + return Putio.Files.Query(fileId).then(response => response.data.files); +} + +async function _getPublicToken(Putio, targetVideoId) { + const publicLinks = await Putio.Files.PublicShares().then(response => response.data.links); + const alreadySharedLink = publicLinks.find(link => link.user_file.id === targetVideoId); + if (alreadySharedLink) { + return alreadySharedLink.token; + } + if (publicLinks.length >= 10) { + // maximum public shares reached, revoke last one; + await Putio.File.RevokePublicLink(publicLinks[0].id); + } + return Putio.File.CreatePublicLink(targetVideoId).then(response => response.data.token); +} + +function statusError(status) { + return ['ERROR'].includes(status); +} + +function statusDownloading(status) { + return ['WAITING', 'IN_QUEUE', 'DOWNLOADING'].includes(status); +} + +function statusProcessing(status) { + return ['WAITING', 'IN_QUEUE', 'COMPLETING'].includes(status); +} + +function statusReady(status) { + return ['COMPLETED', 'SEEDING'].includes(status); +} + +module.exports = { getCachedStreams, resolve }; \ No newline at end of file diff --git a/addon/moch/realdebrid.js b/addon/moch/realdebrid.js index d955a05..5ce848e 100644 --- a/addon/moch/realdebrid.js +++ b/addon/moch/realdebrid.js @@ -1,6 +1,7 @@ const RealDebridClient = require('real-debrid-api'); const { encode } = require('magnet-uri'); const isVideo = require('../lib/video'); +const delay = require('./delay'); const StaticResponse = require('./static'); const { getRandomProxy, getRandomUserAgent } = require('../lib/request_helper'); const { cacheWrapProxy, cacheUserAgent } = require('../lib/cache'); @@ -103,8 +104,7 @@ async function _selectTorrentFiles(RD, torrent, cachedFileIds) { torrent = torrent.status ? torrent : await RD.torrents.info(torrent.id); if (torrent && statusOpening(torrent.status)) { // sleep for 2 seconds, maybe the torrent will be converted - await new Promise((resolve) => setTimeout(resolve, 2000)); - torrent = await RD.torrents.info(torrent.id); + torrent = await delay(2000).then(() => RD.torrents.info(torrent.id)); } if (torrent && torrent.files && statusWaitingSelection(torrent.status)) { const videoFileIds = torrent.files diff --git a/addon/package-lock.json b/addon/package-lock.json index d56dd5b..af8a536 100644 --- a/addon/package-lock.json +++ b/addon/package-lock.json @@ -24,6 +24,18 @@ "js-tokens": "^4.0.0" } }, + "@putdotio/api-client": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@putdotio/api-client/-/api-client-7.10.1.tgz", + "integrity": "sha512-JUNoAkTFsD9mgCuDYfgPZ+JXbRygPrgTp88dzCzIzoVUmH3iGTEgfrDLKM1fAzE3wu1B3UqwbhMi50W+677kvQ==", + "requires": { + "@types/js-base64": "^2.3.1", + "axios": "^0.19.1", + "event-emitter": "^0.3.5", + "js-base64": "^2.5.2", + "urijs": "^1.19.2" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -35,6 +47,11 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-1.0.6.tgz", "integrity": "sha512-Xqg/lIZMrUd0VRmSRbCAewtwGZiAk3mEUDvV4op1tGl+LvyPcb/MIOSxTl9z+9+J+R4/vpjiCAT4xeKzH9ji1w==" }, + "@types/js-base64": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@types/js-base64/-/js-base64-2.3.1.tgz", + "integrity": "sha512-4RKbhIDGC87s4EBy2Cp2/5S2O6kmCRcZnD5KRCq1q9z2GhBte1+BdsfVKCpG8yKpDGNyEE2G6IqFIh6W2YwWPA==" + }, "@types/node": { "version": "13.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.0.tgz", @@ -151,6 +168,14 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==" }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -391,6 +416,15 @@ "which": "^1.2.9" } }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -492,6 +526,35 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -888,6 +951,15 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -937,6 +1009,21 @@ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.1.1.tgz", "integrity": "sha512-puA1zcCx/quwWUOU6pT6daCt6t7SweD9wKChKhb+KSgFMKRwS81C224hiSAUANw/gnSHiwEhgozM/2ezEBZPeA==" }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", + "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==" + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1021,6 +1108,24 @@ "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", "dev": true }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -1290,6 +1395,11 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "js-base64": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz", + "integrity": "sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1524,6 +1634,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -2361,6 +2476,11 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -2416,6 +2536,11 @@ "punycode": "^2.1.0" } }, + "urijs": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.2.tgz", + "integrity": "sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w==" + }, "user-agents": { "version": "1.0.559", "resolved": "https://registry.npmjs.org/user-agents/-/user-agents-1.0.559.tgz", diff --git a/addon/package.json b/addon/package.json index 968dcec..b56fe6c 100644 --- a/addon/package.json +++ b/addon/package.json @@ -8,6 +8,7 @@ "author": "TheBeastLT ", "license": "MIT", "dependencies": { + "@putdotio/api-client": "^7.10.1", "all-debrid-api": "^1.0.0", "bottleneck": "^2.19.5", "cache-manager": "^2.11.1",