mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-18 23:02:07 +00:00
249 lines
7.7 KiB
JavaScript
249 lines
7.7 KiB
JavaScript
/* global gsignin */
|
|
import BitField from 'bitfield'
|
|
import sha1 from 'simple-sha1'
|
|
import Wire from 'bittorrent-protocol'
|
|
import { load, client, auth2 } from 'gapi'
|
|
import { keys } from './keys.js'
|
|
import { concat, sleep, Queue } from './util.js'
|
|
import halfmoon from 'halfmoon'
|
|
|
|
// i'll fucking give you 'thenable', shitass callbacks
|
|
export const token = new Promise(resolve => {
|
|
load('client:auth2', () => {
|
|
client.init(Object.assign({}, keys)).then(() => {
|
|
const auth = auth2.getAuthInstance()
|
|
auth.isSignedIn.listen(state => {
|
|
if (state === true) {
|
|
if (!auth.currentUser.get().getAuthResponse().scope.includes('https://www.googleapis.com/auth/drive.readonly')) {
|
|
halfmoon.initStickyAlert({
|
|
content: 'You didn\'t grant permission for Google Drive access, try again!',
|
|
title: 'Login Error',
|
|
alertType: 'alert-danger',
|
|
fillType: ''
|
|
})
|
|
return auth.signOut()
|
|
}
|
|
resolve(auth.currentUser.get().getAuthResponse().access_token)
|
|
gsignin.classList.add('d-none')
|
|
}
|
|
})
|
|
if (!auth.isSignedIn.get()) {
|
|
gsignin.onclick = auth.signIn
|
|
gsignin.classList.remove('d-none')
|
|
} else {
|
|
if (!auth.currentUser.get().getAuthResponse().scope.includes('https://www.googleapis.com/auth/drive.readonly')) {
|
|
halfmoon.initStickyAlert({
|
|
content: 'You didn\'t grant permission for Google Drive access, try again!',
|
|
title: 'Login Error',
|
|
alertType: 'alert-danger',
|
|
fillType: ''
|
|
})
|
|
return auth.signOut()
|
|
}
|
|
resolve(auth.currentUser.get().getAuthResponse().access_token)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
export async function getFileNameID (name) {
|
|
return new Promise(resolve => {
|
|
token.then(() => {
|
|
client.drive.files.list({
|
|
pageSize: 1,
|
|
fields: 'files(id)',
|
|
includeItemsFromAllDrives: true,
|
|
supportsAllDrives: true,
|
|
corpora: 'allDrives',
|
|
q: `name = '${name}'`
|
|
}).then(res => {
|
|
console.log(res)
|
|
resolve(res?.result?.files?.length && res?.result?.files[0].id)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
export async function GDHandleTorrent (torrent) {
|
|
await token
|
|
for await (const file of torrent.files) {
|
|
const gDriveID = await getFileNameID(file.name)
|
|
if (gDriveID) {
|
|
file.gDriveID = gDriveID
|
|
} else {
|
|
halfmoon.initStickyAlert({
|
|
content: `Couldn't find a GDrive file for ${file.name}! Can't create webseed!`,
|
|
title: 'GDrive Error!',
|
|
alertType: 'alert-secondary',
|
|
fillType: ''
|
|
})
|
|
return null // if a single entry doesnt exist, I can't create a reliable webseed
|
|
}
|
|
await sleep(1000)
|
|
}
|
|
torrent.addWebSeed(new WebConn(torrent))
|
|
}
|
|
|
|
class WebConn extends Wire {
|
|
constructor (torrent) {
|
|
super()
|
|
|
|
this.connId = torrent.infoHash + ' gdrive' // Unique id to deduplicate web seeds
|
|
this.webPeerId = sha1.sync(this.connId)
|
|
this._torrent = torrent
|
|
this.lastRequest = {}
|
|
|
|
this.sleep = null
|
|
|
|
this.queue = new Queue()
|
|
|
|
this._init()
|
|
}
|
|
|
|
_init () {
|
|
this.setKeepAlive(true)
|
|
|
|
this.once('handshake', (infoHash, peerId) => {
|
|
if (this.destroyed) return
|
|
this.handshake(infoHash, this.webPeerId)
|
|
const numPieces = this._torrent.pieces.length
|
|
const bitfield = new BitField(numPieces)
|
|
for (let i = 0; i <= numPieces; i++) {
|
|
bitfield.set(i, true)
|
|
}
|
|
this.bitfield(bitfield)
|
|
})
|
|
|
|
this.once('interested', () => {
|
|
this.unchoke()
|
|
})
|
|
|
|
this.on('request', (pieceIndex, offset, length, callback) => {
|
|
const request = () => this.queue.add({
|
|
fn: async () => await this.httpRequest(pieceIndex, offset, length, (err, data) => {
|
|
if (err || data?.length !== length) return request()
|
|
callback(err, data)
|
|
}),
|
|
index: pieceIndex
|
|
})
|
|
request()
|
|
})
|
|
}
|
|
|
|
async httpRequest (pieceIndex, offset, length, cb) {
|
|
const pieceOffset = pieceIndex * this._torrent.pieceLength
|
|
const rangeStart = pieceOffset + offset
|
|
const rangeEnd = rangeStart + length - 1
|
|
const files = this._torrent.files
|
|
let requests
|
|
if (files.length <= 1) {
|
|
requests = [{
|
|
url: `https://www.googleapis.com/drive/v3/files/${files[0].gDriveID}?supportsAllDrives=true&alt=media&key=${keys.apiKey}`,
|
|
start: rangeStart,
|
|
end: rangeEnd
|
|
}]
|
|
} else {
|
|
const requestedFiles = files.filter(file => file.offset <= rangeEnd && (file.offset + file.length) > rangeStart)
|
|
if (requestedFiles.length < 1) {
|
|
return cb(new Error('Could not find file corresponding to web seed range request'))
|
|
}
|
|
|
|
requests = requestedFiles.map(file => {
|
|
const fileEnd = file.offset + file.length - 1
|
|
const url = `https://www.googleapis.com/drive/v3/files/${file.gDriveID}?supportsAllDrives=true&alt=media&key=${keys.apiKey}`
|
|
return {
|
|
url,
|
|
fileOffsetInRange: Math.max(file.offset - rangeStart, 0),
|
|
start: Math.max(rangeStart - file.offset, 0),
|
|
end: Math.min(fileEnd, rangeEnd - file.offset)
|
|
}
|
|
})
|
|
}
|
|
let numRequestsSucceeded = 0
|
|
let hasError = false
|
|
|
|
let ret
|
|
if (requests.length > 1) {
|
|
ret = Buffer.alloc(length)
|
|
}
|
|
|
|
let { res, reader, endRange, setSize, ctrl } = this.lastRequest
|
|
for await (const request of requests) {
|
|
const { url, start, end } = request
|
|
function onResponse (res, data) {
|
|
if (!res || res.status < 200 || res.status >= 300) {
|
|
if (hasError) return
|
|
hasError = true
|
|
return cb(new Error(`Unexpected HTTP status code ${res?.status}`))
|
|
}
|
|
if (requests.length === 1) {
|
|
cb(null, data)
|
|
} else {
|
|
data.copy(ret, request.fileOffsetInRange)
|
|
if (++numRequestsSucceeded === requests.length) {
|
|
cb(null, ret)
|
|
}
|
|
}
|
|
}
|
|
if (endRange !== start - 1 || ctrl?.signal?.aborted) {
|
|
async function * read (reader) { // <3 Endless
|
|
let buffered = []
|
|
let bufferedBytes = 0
|
|
let done = false
|
|
let size = 512
|
|
const setSize = x => { size = x }
|
|
yield setSize
|
|
|
|
while (!done) {
|
|
const it = await reader.read()
|
|
done = it.done
|
|
if (done) {
|
|
yield concat(buffered, bufferedBytes)
|
|
return
|
|
} else {
|
|
bufferedBytes += it.value.byteLength
|
|
buffered.push(it.value)
|
|
|
|
while (bufferedBytes >= size) {
|
|
const b = concat(buffered)
|
|
bufferedBytes -= size
|
|
yield b.slice(0, size)
|
|
buffered = [b.slice(size, b.length)]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (ctrl) ctrl.abort()
|
|
ctrl = new AbortController()
|
|
await this.sleep
|
|
try {
|
|
res = await fetch(url, {
|
|
headers: {
|
|
range: `bytes=${start}-`,
|
|
authorization: 'Bearer ' + await token
|
|
},
|
|
signal: ctrl.signal
|
|
})
|
|
} catch (e) {
|
|
this.sleep = sleep(500)
|
|
onResponse()
|
|
throw e
|
|
}
|
|
this.sleep = sleep(500)
|
|
reader = read(res.body.getReader(), 1)
|
|
setSize = (await reader.next()).value // lazy, but 1st yield is callback x)
|
|
}
|
|
endRange = end
|
|
setSize(end - start + 1)
|
|
onResponse(res, (await reader.next()).value || new Uint8Array())
|
|
}
|
|
this.lastRequest = { res, reader, endRange, setSize, ctrl }
|
|
}
|
|
|
|
destroy () {
|
|
this.lastRequest?.ctrl?.abort()
|
|
this.queue.destroy()
|
|
this.lastRequest?.ctrl?.abort()
|
|
super.destroy()
|
|
this._torrent = null
|
|
}
|
|
}
|