no1b4me's picture
Upload 5037 files
95f4e64 verified
import EventEmitter from 'events'
import { Readable } from 'streamx'
import { chunkStoreRead } from 'chunk-store-iterator'
import mime from 'mime/lite.js'
import FileIterator from './file-iterator.js'
export default class File extends EventEmitter {
constructor (torrent, file) {
super()
this._torrent = torrent
this._destroyed = false
this._fileStreams = new Set()
this._iterators = new Set()
this.name = file.name
this.path = file.path
this.length = file.length
this.size = file.length
this.type = mime.getType(this.name) || 'application/octet-stream'
this.offset = file.offset
this.done = false
const start = file.offset
const end = start + file.length - 1
this._startPiece = start / this._torrent.pieceLength | 0
this._endPiece = end / this._torrent.pieceLength | 0
if (this.length === 0) {
this.done = true
this.emit('done')
}
this._client = torrent.client
}
get downloaded () {
if (this._destroyed || !this._torrent.bitfield) return 0
const { pieces, bitfield, pieceLength, lastPieceLength } = this._torrent
const { _startPiece: start, _endPiece: end } = this
const getPieceLength = (pieceIndex) => (
pieceIndex === pieces.length - 1 ? lastPieceLength : pieceLength
)
const getPieceDownloaded = (pieceIndex) => {
const len = pieceIndex === pieces.length - 1 ? lastPieceLength : pieceLength
if (bitfield.get(pieceIndex)) {
// verified data
return len
} else {
// "in progress" data
return len - pieces[pieceIndex].missing
}
}
let downloaded = 0
for (let index = start; index <= end; index += 1) {
const pieceDownloaded = getPieceDownloaded(index)
downloaded += pieceDownloaded
if (index === start) {
// First piece may have an offset, e.g. irrelevant bytes from the end of
// the previous file
const irrelevantFirstPieceBytes = this.offset % pieceLength
downloaded -= Math.min(irrelevantFirstPieceBytes, pieceDownloaded)
}
if (index === end) {
// Last piece may have an offset, e.g. irrelevant bytes from the start
// of the next file
const irrelevantLastPieceBytes = getPieceLength(end) - (this.offset + this.length) % pieceLength
downloaded -= Math.min(irrelevantLastPieceBytes, pieceDownloaded)
}
}
return downloaded
}
get progress () {
return this.length ? this.downloaded / this.length : 0
}
select (priority) {
if (this.length === 0) return
this._torrent.select(this._startPiece, this._endPiece, priority)
}
deselect () {
if (this.length === 0) return
this._torrent.deselect(this._startPiece, this._endPiece)
}
[Symbol.asyncIterator] (opts = {}) {
if (this.length === 0) return (async function * empty () {})()
const { start = 0 } = opts ?? {}
const end = (opts?.end && opts.end < this.length)
? opts.end
: this.length - 1
if (this.done) {
return chunkStoreRead(this._torrent.store, { offset: start + this.offset, length: end - start + 1 })
}
const iterator = new FileIterator(this, { start, end })
this._iterators.add(iterator)
iterator.once('return', () => {
this._iterators.delete(iterator)
})
return iterator
}
createReadStream (opts) {
const iterator = this[Symbol.asyncIterator](opts)
const fileStream = Readable.from(iterator)
this._fileStreams.add(fileStream)
fileStream.once('close', () => {
this._fileStreams.delete(fileStream)
})
return fileStream
}
async arrayBuffer (opts) {
const data = new Uint8Array(this.length)
let offset = 0
for await (const chunk of this[Symbol.asyncIterator](opts)) {
data.set(chunk, offset)
offset += chunk.length
}
return data.buffer
}
async blob (opts) {
return new Blob([await this.arrayBuffer(opts)], { type: this.type })
}
stream (opts) {
let iterator
return new ReadableStream({
start: () => {
iterator = this[Symbol.asyncIterator](opts)
},
async pull (controller) {
const { value, done } = await iterator.next()
if (done) {
controller.close()
} else {
controller.enqueue(value)
}
},
cancel () {
iterator.return()
}
})
}
get streamURL () {
if (!this._client._server) throw new Error('No server created')
return `${this._client._server.pathname}/${this._torrent.infoHash}/${this.path}`
}
streamTo (elem) {
elem.src = this.streamURL
return elem
}
includes (piece) {
return this._startPiece <= piece && this._endPiece >= piece
}
_destroy () {
this._destroyed = true
this._torrent = null
for (const fileStream of this._fileStreams) {
fileStream.destroy()
}
this._fileStreams.clear()
for (const iterator of this._iterators) {
iterator.destroy()
}
this._iterators.clear()
}
}