mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-19 19:52:04 +00:00
feat: move WT to worker, UI fixes, UI improvements
This commit is contained in:
parent
ec3dd4924f
commit
a11516e088
12 changed files with 291 additions and 332 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Miru",
|
||||
"version": "1.9.1",
|
||||
"version": "2.0.0",
|
||||
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
|
||||
"main": "src/index.js",
|
||||
"homepage": "https://github.com/ThaUnknown/miru#readme",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ const { app, BrowserWindow, protocol, shell, ipcMain } = require('electron')
|
|||
const path = require('path')
|
||||
const log = require('electron-log')
|
||||
const { autoUpdater } = require('electron-updater')
|
||||
require('./main/torrent.js')
|
||||
require('./main/misc.js')
|
||||
|
||||
if (process.defaultApp) {
|
||||
|
|
@ -81,6 +80,7 @@ function createWindow () {
|
|||
webPreferences: {
|
||||
enableBlinkFeatures: 'AudioVideoTracks',
|
||||
backgroundThrottling: false,
|
||||
nodeIntegrationInWorker: true,
|
||||
preload: path.join(__dirname, '/preload.js')
|
||||
},
|
||||
icon: path.join(__dirname, '/renderer/public/logo.ico'),
|
||||
|
|
|
|||
|
|
@ -1,204 +0,0 @@
|
|||
const { app, ipcMain } = require('electron')
|
||||
const WebTorrent = require('webtorrent')
|
||||
const http = require('http')
|
||||
const pump = require('pump')
|
||||
const rangeParser = require('range-parser')
|
||||
const mime = require('mime')
|
||||
const { SubtitleParser, SubtitleStream } = require('matroska-subtitles')
|
||||
|
||||
let window = null
|
||||
app.on('browser-window-created', (event, data) => {
|
||||
window = data
|
||||
window.on('closed', () => {
|
||||
window = null
|
||||
})
|
||||
})
|
||||
|
||||
let client = null
|
||||
|
||||
let settings = {}
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
if (!request.url) return null
|
||||
let [infoHash, ...filePath] = request.url.slice(request.url.indexOf('/webtorrent/') + 12).split('/')
|
||||
filePath = decodeURI(filePath.join('/'))
|
||||
if (!infoHash || !filePath) return null
|
||||
|
||||
const file = client?.get(infoHash)?.files.find(file => file.path === filePath)
|
||||
if (!file) return null
|
||||
|
||||
response.setHeader('Access-Control-Allow-Origin', '*')
|
||||
response.setHeader('Content-Type', mime.getType(file.name) || 'application/octet-stream')
|
||||
|
||||
response.setHeader('Accept-Ranges', 'bytes')
|
||||
|
||||
let range = rangeParser(file.length, request.headers.range || '')
|
||||
|
||||
if (Array.isArray(range)) {
|
||||
response.statusCode = 206
|
||||
range = range[0]
|
||||
|
||||
response.setHeader(
|
||||
'Content-Range',
|
||||
`bytes ${range.start}-${range.end}/${file.length}`
|
||||
)
|
||||
response.setHeader('Content-Length', range.end - range.start + 1)
|
||||
} else {
|
||||
response.statusCode = 200
|
||||
range = null
|
||||
response.setHeader('Content-Length', file.length)
|
||||
}
|
||||
|
||||
if (response.method === 'HEAD') {
|
||||
return response.end()
|
||||
}
|
||||
|
||||
let stream = file.createReadStream(range)
|
||||
|
||||
if (stream && !parsed) {
|
||||
if (file.name.endsWith('.mkv')) {
|
||||
parserInstance = new SubtitleStream(parserInstance)
|
||||
handleSubtitleParser(parserInstance, true)
|
||||
stream = pump(stream, parserInstance)
|
||||
}
|
||||
}
|
||||
|
||||
pump(stream, response)
|
||||
})
|
||||
|
||||
server.on('error', console.log)
|
||||
|
||||
server.listen(41785)
|
||||
|
||||
let current = null
|
||||
let parsed = false
|
||||
let parserInstance = null
|
||||
|
||||
function parseSubtitles () {
|
||||
if (current.name.endsWith('.mkv')) {
|
||||
const parser = new SubtitleParser()
|
||||
handleSubtitleParser(parser, true)
|
||||
const finish = () => {
|
||||
console.log('Sub parsing finished')
|
||||
parsed = true
|
||||
fileStream?.destroy()
|
||||
stream?.destroy()
|
||||
stream = undefined
|
||||
}
|
||||
parser.once('tracks', tracks => {
|
||||
if (!tracks.length) finish()
|
||||
})
|
||||
parser.once('finish', finish)
|
||||
console.log('Sub parsing started')
|
||||
const fileStream = current.createReadStream()
|
||||
let stream = fileStream.pipe(parser)
|
||||
}
|
||||
}
|
||||
|
||||
function parseFonts (file) {
|
||||
const stream = new SubtitleParser()
|
||||
handleSubtitleParser(stream)
|
||||
stream.once('tracks', tracks => {
|
||||
if (!tracks.length) {
|
||||
parsed = true
|
||||
stream.destroy()
|
||||
fileStreamStream.destroy()
|
||||
}
|
||||
})
|
||||
stream.once('subtitle', () => {
|
||||
fileStreamStream.destroy()
|
||||
stream.destroy()
|
||||
window?.webContents.send('fonts')
|
||||
})
|
||||
const fileStreamStream = file.createReadStream()
|
||||
fileStreamStream.pipe(stream)
|
||||
}
|
||||
|
||||
function handleSubtitleParser (parser, skipFile) {
|
||||
parser.once('tracks', tracks => {
|
||||
if (!tracks.length) {
|
||||
parsed = true
|
||||
parser?.destroy()
|
||||
} else {
|
||||
window?.webContents.send('tracks', tracks)
|
||||
}
|
||||
})
|
||||
parser.on('subtitle', (subtitle, trackNumber) => {
|
||||
window?.webContents.send('subtitle', { subtitle, trackNumber })
|
||||
})
|
||||
if (!skipFile) {
|
||||
parser.on('file', file => {
|
||||
if (file.mimetype === 'application/x-truetype-font' || file.mimetype === 'application/font-woff' || file.mimetype === 'application/vnd.ms-opentype') {
|
||||
window?.webContents.send('file', { mimetype: file.mimetype, data: Array.from(file.data) })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.on('current', (event, data) => {
|
||||
current?.removeListener('done', parseSubtitles)
|
||||
parserInstance?.destroy()
|
||||
parserInstance = null
|
||||
current = null
|
||||
parsed = false
|
||||
if (data) {
|
||||
current = client?.get(data.infoHash)?.files.find(file => file.path === data.path)
|
||||
if (current.name.endsWith('.mkv')) {
|
||||
if (current.done) parseSubtitles()
|
||||
current.on('done', parseSubtitles)
|
||||
parseFonts(current)
|
||||
}
|
||||
// findSubtitleFiles(current) TODO:
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('settings', (event, data) => {
|
||||
if (!client) {
|
||||
settings = data
|
||||
client = new WebTorrent({
|
||||
dht: !settings.torrentDHT,
|
||||
downloadLimit: settings.torrentSpeed * 1048576 || 0,
|
||||
uploadLimit: settings.torrentSpeed * 1572864 || 0 // :trolled:
|
||||
})
|
||||
setInterval(() => {
|
||||
window?.webContents?.send('stats', {
|
||||
numPeers: (client?.torrents.length && client?.torrents[0].numPeers) || 0,
|
||||
uploadSpeed: (client?.torrents.length && client?.torrents[0].uploadSpeed) || 0,
|
||||
downloadSpeed: (client?.torrents.length && client?.torrents[0].downloadSpeed) || 0
|
||||
})
|
||||
}, 200)
|
||||
setInterval(() => {
|
||||
if (client?.torrents[0]?.pieces) window?.webContents?.send('pieces', [...client?.torrents[0]?.pieces.map(piece => piece === null ? 77 : 33)])
|
||||
}, 2000)
|
||||
client.on('torrent', torrent => {
|
||||
const files = torrent.files.map(file => {
|
||||
return {
|
||||
infoHash: torrent.infoHash,
|
||||
name: file.name,
|
||||
type: file._getMimeType(),
|
||||
size: file.size,
|
||||
path: file.path,
|
||||
url: encodeURI('http://localhost:41785/webtorrent/' + torrent.infoHash + '/' + file.path)
|
||||
}
|
||||
})
|
||||
window?.webContents.send('files', files)
|
||||
window?.webContents.send('pieces', torrent.pieces.length)
|
||||
window?.webContents.send('torrent', Array.from(torrent.torrentFile))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('torrent', (event, data) => {
|
||||
if (client.torrents.length) client.remove(client.torrents[0].infoHash)
|
||||
if (typeof data !== 'string') data = Buffer.from(data)
|
||||
client.add(data, {
|
||||
private: settings.torrentPeX,
|
||||
path: settings.torrentPath,
|
||||
destroyStoreOnDestroy: !settings.torrentPersist,
|
||||
announce: [
|
||||
'wss://tracker.openwebtorrent.com',
|
||||
'wss://spacetradersapi-chatbox.herokuapp.com:443/announce',
|
||||
'wss://peertube.cpy.re:443/tracker/socket'
|
||||
]
|
||||
})
|
||||
})
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
self.addEventListener('install', () => {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
const res = proxyResponse(event)
|
||||
if (res) event.respondWith(res)
|
||||
})
|
||||
|
||||
self.addEventListener('activate', evt => {
|
||||
evt.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
const portTimeoutDuration = 5000
|
||||
function proxyResponse (event) {
|
||||
const { url } = event.request
|
||||
if (!(url.includes(self.registration.scope) && url.includes('/webtorrent/')) || url.includes('?')) return null
|
||||
if (url.includes(self.registration.scope) && url.includes('/webtorrent/keepalive/')) return new Response()
|
||||
|
||||
return serve(event)
|
||||
}
|
||||
|
||||
async function serve ({ request }) {
|
||||
const { url, method, headers, destination } = request
|
||||
const clientlist = await clients.matchAll({ type: 'window', includeUncontrolled: true })
|
||||
|
||||
const [data, port] = await new Promise(resolve => {
|
||||
// Use race condition for whoever controls the response stream
|
||||
for (const client of clientlist) {
|
||||
const messageChannel = new MessageChannel()
|
||||
const { port1, port2 } = messageChannel
|
||||
port1.onmessage = ({ data }) => {
|
||||
resolve([data, port1])
|
||||
}
|
||||
client.postMessage({
|
||||
url,
|
||||
method,
|
||||
headers: Object.fromEntries(headers.entries()),
|
||||
scope: self.registration.scope,
|
||||
destination,
|
||||
type: 'webtorrent'
|
||||
}, [port2])
|
||||
}
|
||||
})
|
||||
|
||||
if (data.body !== 'STREAM' && data.body !== 'DOWNLOAD') return new Response(data.body, data)
|
||||
|
||||
let timeOut = null
|
||||
return new Response(new ReadableStream({
|
||||
pull (controller) {
|
||||
return new Promise(resolve => {
|
||||
port.onmessage = ({ data }) => {
|
||||
if (data) {
|
||||
controller.enqueue(data) // event.data is Uint8Array
|
||||
} else {
|
||||
clearTimeout(timeOut)
|
||||
controller.close() // event.data is null, means the stream ended
|
||||
port.onmessage = null
|
||||
}
|
||||
resolve()
|
||||
}
|
||||
|
||||
// 'media player' does NOT signal a close on the stream and we cannot close it because it's locked to the reader,
|
||||
// so we just empty it after 5s of inactivity, the browser will request another port anyways
|
||||
clearTimeout(timeOut)
|
||||
if (data.body === 'STREAM') {
|
||||
timeOut = setTimeout(() => {
|
||||
controller.close()
|
||||
port.postMessage(false) // send timeout
|
||||
port.onmessage = null
|
||||
resolve()
|
||||
}, portTimeoutDuration)
|
||||
}
|
||||
port.postMessage(true) // send a pull request
|
||||
})
|
||||
},
|
||||
cancel () {
|
||||
// This event is never executed
|
||||
port.postMessage(false) // send a cancel request
|
||||
}
|
||||
}), data)
|
||||
}
|
||||
|
|
@ -256,9 +256,6 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.trailer {
|
||||
padding-bottom: 56.25%;
|
||||
}
|
||||
.close {
|
||||
top: 1rem !important;
|
||||
left: unset;
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@
|
|||
window.IPC.emit('open', 'https://anilist.co/api/v2/oauth/authorize?client_id=4254&response_type=token')
|
||||
if (platformMap[window.version.platform] === 'Linux') {
|
||||
addToast({
|
||||
text: 'If your linux distribution doesn\'t support custom protocol handlers, you can simply paste the full URL into the app.',
|
||||
text: "If your linux distribution doesn't support custom protocol handlers, you can simply paste the full URL into the app.",
|
||||
title: 'Support Notification',
|
||||
type: 'secondary',
|
||||
duration: '300000'
|
||||
|
|
@ -78,8 +78,10 @@
|
|||
]
|
||||
if (alID) {
|
||||
alID.then(result => {
|
||||
links[links.length - 1].image = result.data.Viewer.avatar.medium
|
||||
links[links.length - 1].text = result.data.Viewer.name
|
||||
if (result?.data?.Viewer) {
|
||||
links[links.length - 1].image = result.data.Viewer.avatar.medium
|
||||
links[links.length - 1].text = result.data.Viewer.name + '\nLogout'
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -159,6 +161,9 @@
|
|||
padding: 0.75rem 1.5rem;
|
||||
height: 5.5rem;
|
||||
}
|
||||
.sidebar-link::after {
|
||||
white-space: pre !important;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 2.2rem;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { playAnime } from '../RSSView.svelte'
|
||||
import { title } from '../Menubar.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import { client } from '@/modules/torrent.js'
|
||||
export let media = null
|
||||
let fileMedia = null
|
||||
let hadImage = false
|
||||
|
|
@ -222,7 +223,7 @@
|
|||
current = file
|
||||
initSubs()
|
||||
src = file.url
|
||||
window.IPC.emit('current', file)
|
||||
client.send('current', file)
|
||||
video?.load()
|
||||
checkAvail(current)
|
||||
clearCanvas()
|
||||
|
|
@ -739,11 +740,11 @@
|
|||
}
|
||||
}
|
||||
const torrent = {}
|
||||
window.IPC.on('stats', updateStats)
|
||||
function updateStats(stats) {
|
||||
torrent.peers = stats.numPeers || 0
|
||||
torrent.up = stats.uploadSpeed || 0
|
||||
torrent.down = stats.downloadSpeed || 0
|
||||
client.on('stats', updateStats)
|
||||
function updateStats({ detail }) {
|
||||
torrent.peers = detail.numPeers || 0
|
||||
torrent.up = detail.uploadSpeed || 0
|
||||
torrent.down = detail.downloadSpeed || 0
|
||||
}
|
||||
let bufferCanvas = null
|
||||
let ctx = null
|
||||
|
|
@ -757,23 +758,23 @@
|
|||
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)'
|
||||
}
|
||||
let imageData = null
|
||||
function handlePieces(data) {
|
||||
if (data.constructor === Array) {
|
||||
function handlePieces({ detail }) {
|
||||
if (detail.constructor === Array) {
|
||||
if (imageData) {
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
imageData.data[i * 4 + 3] = data[i]
|
||||
for (let i = 0; i < detail.length; ++i) {
|
||||
imageData.data[i * 4 + 3] = detail[i]
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
}
|
||||
} else {
|
||||
const uint32 = new Uint32Array(data)
|
||||
const uint32 = new Uint32Array(detail)
|
||||
uint32.fill(872415231) // rgba(255, 255, 255, 0.2) to HEX to DEC
|
||||
imageData = new ImageData(new Uint8ClampedArray(uint32.buffer), data, 1)
|
||||
bufferCanvas.width = data
|
||||
imageData = new ImageData(new Uint8ClampedArray(uint32.buffer), detail, 1)
|
||||
bufferCanvas.width = detail
|
||||
clearCanvas()
|
||||
}
|
||||
}
|
||||
window.IPC.on('pieces', handlePieces)
|
||||
client.on('pieces', handlePieces)
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} bind:innerWidth bind:innerHeight />
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
import { addToast } from '@/lib/Toasts.svelte'
|
||||
import 'browser-event-target-emitter'
|
||||
|
||||
import { client } from '@/modules/torrent.js'
|
||||
|
||||
export const w2gEmitter = new EventTarget()
|
||||
|
||||
const state = writable(null)
|
||||
|
|
@ -61,11 +63,13 @@
|
|||
if (setPlayerState(detail)) emit('player', detail)
|
||||
})
|
||||
|
||||
window.IPC.on('torrent', file => {
|
||||
if (!file.every((v, i) => v === playerState.file[i])) {
|
||||
playerState.file = file
|
||||
emit('torrent', { file })
|
||||
}
|
||||
queueMicrotask(() => {
|
||||
client.on('torrent', ({ detail }) => {
|
||||
if (!detail.every((v, i) => v === playerState.file[i])) {
|
||||
playerState.file = detail
|
||||
emit('torrent', { file: detail })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function emit(type, data) {
|
||||
|
|
|
|||
|
|
@ -264,7 +264,7 @@ query {
|
|||
}`
|
||||
break
|
||||
} case 'UserLists': {
|
||||
variables.id = (await alID).data.Viewer.id
|
||||
variables.id = (await alID)?.data?.Viewer?.id
|
||||
query = `
|
||||
query ($page: Int, $perPage: Int, $id: Int, $type: MediaType, $status_in: [MediaListStatus]){
|
||||
Page (page: $page, perPage: $perPage) {
|
||||
|
|
@ -280,7 +280,7 @@ query ($page: Int, $perPage: Int, $id: Int, $type: MediaType, $status_in: [Media
|
|||
}`
|
||||
break
|
||||
} case 'SearchIDStatus': {
|
||||
variables.id = (await alID).data.Viewer.id
|
||||
variables.id = (await alID)?.data?.Viewer?.id
|
||||
variables.mediaId = opts.id
|
||||
query = `
|
||||
query ($id: Int, $mediaId: Int){
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
import SubtitlesOctopus from './subtitles-octopus.js'
|
||||
import { toTS, videoRx, subRx } from './util.js'
|
||||
|
||||
import { client } from '@/modules/torrent.js'
|
||||
|
||||
const defaultHeader = `[V4+ Styles]
|
||||
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
||||
Style: Default, Roboto Medium,26,&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,1.3,0,2,20,20,23,1
|
||||
|
|
@ -25,15 +27,15 @@ export default class Subtitles {
|
|||
this.videoFiles = files.filter(file => videoRx.test(file.name))
|
||||
this.subtitleFiles = []
|
||||
this.timeout = null
|
||||
window.IPC.on('tracks', (...args) => this.handleTracks(...args))
|
||||
window.IPC.on('subtitle', (...args) => this.handleSubtitle(...args))
|
||||
window.IPC.on('fonts', (...args) => this.handleFonts(...args))
|
||||
window.IPC.on('file', (...args) => this.handleFile(...args))
|
||||
client.on('tracks', this.handleTracks.bind(this))
|
||||
client.on('subtitle', this.handleSubtitle.bind(this))
|
||||
client.on('fonts', this.handleFonts.bind(this))
|
||||
client.on('file', this.handleFile.bind(this))
|
||||
}
|
||||
|
||||
handleFile (file) {
|
||||
handleFile ({ detail }) {
|
||||
if (this.selected) {
|
||||
this.fonts.push(URL.createObjectURL(new Blob([file.data], { type: file.mimetype })))
|
||||
this.fonts.push(URL.createObjectURL(new Blob([detail.data], { type: detail.mimetype })))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +48,8 @@ export default class Subtitles {
|
|||
}
|
||||
}
|
||||
|
||||
handleSubtitle ({ subtitle, trackNumber }) {
|
||||
handleSubtitle ({ detail }) {
|
||||
const { subtitle, trackNumber } = detail
|
||||
if (this.selected) {
|
||||
if (!this.renderer) this.initSubtitleRenderer()
|
||||
this.tracks[trackNumber].add(this.constructor.constructSub(subtitle, this.headers[trackNumber].type !== 'ass'))
|
||||
|
|
@ -54,9 +57,9 @@ export default class Subtitles {
|
|||
}
|
||||
}
|
||||
|
||||
handleTracks (tracks) {
|
||||
handleTracks ({ detail }) {
|
||||
if (this.selected) {
|
||||
for (const track of tracks) {
|
||||
for (const track of detail) {
|
||||
if (!this.tracks[track.number]) {
|
||||
// overwrite webvtt or other header with custom one
|
||||
if (track.type !== 'ass') track.header = defaultHeader
|
||||
|
|
@ -242,6 +245,10 @@ export default class Subtitles {
|
|||
}
|
||||
|
||||
destroy () {
|
||||
client.removeListener('tracks', this.handleTracks.bind(this))
|
||||
client.removeListener('subtitle', this.handleSubtitle.bind(this))
|
||||
client.removeListener('fonts', this.handleFonts.bind(this))
|
||||
client.removeListener('file', this.handleFile.bind(this))
|
||||
this.stream?.destroy()
|
||||
this.parser?.destroy()
|
||||
this.renderer?.destroy()
|
||||
|
|
|
|||
|
|
@ -3,12 +3,27 @@ import { set } from '@/lib/pages/Settings.svelte'
|
|||
import { files } from '@/lib/Router.svelte'
|
||||
import { page } from '@/App.svelte'
|
||||
|
||||
export const client = null
|
||||
class TorrentWorker extends Worker {
|
||||
constructor (opts) {
|
||||
super(opts)
|
||||
this.onmessage = this.handleMessage.bind(this)
|
||||
}
|
||||
|
||||
window.IPC.emit('settings', { ...set })
|
||||
handleMessage ({ data }) {
|
||||
this.emit(data.type, data.data)
|
||||
}
|
||||
|
||||
window.IPC.on('files', arr => {
|
||||
files.set(arr)
|
||||
send (type, data) {
|
||||
this.postMessage({ type, data })
|
||||
}
|
||||
}
|
||||
|
||||
export const client = new TorrentWorker(new URL('./torrentworker.js', import.meta.url))
|
||||
|
||||
client.send('settings', { ...set })
|
||||
|
||||
client.on('files', ({ detail }) => {
|
||||
files.set(detail)
|
||||
})
|
||||
|
||||
export async function add (torrentID, hide) {
|
||||
|
|
@ -20,17 +35,17 @@ export async function add (torrentID, hide) {
|
|||
const res = await fetch(torrentID)
|
||||
torrentID = Array.from(new Uint8Array(await res.arrayBuffer()))
|
||||
}
|
||||
window.IPC.emit('torrent', torrentID)
|
||||
client.send('torrent', torrentID)
|
||||
}
|
||||
}
|
||||
|
||||
window.IPC.on('torrent', file => {
|
||||
localStorage.setItem('torrent', JSON.stringify(file))
|
||||
client.on('torrent', ({ detail }) => {
|
||||
localStorage.setItem('torrent', JSON.stringify(detail))
|
||||
})
|
||||
|
||||
// load last used torrent
|
||||
queueMicrotask(() => {
|
||||
if (localStorage.getItem('torrent')) {
|
||||
window.IPC.emit('torrent', JSON.parse(localStorage.getItem('torrent')))
|
||||
client.send('torrent', JSON.parse(localStorage.getItem('torrent')))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
216
src/renderer/src/modules/torrentworker.js
Normal file
216
src/renderer/src/modules/torrentworker.js
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
const WebTorrent = require('webtorrent')
|
||||
const http = require('http')
|
||||
const pump = require('pump')
|
||||
const rangeParser = require('range-parser')
|
||||
const mime = require('mime')
|
||||
const { SubtitleParser, SubtitleStream } = require('matroska-subtitles')
|
||||
|
||||
class TorrentClient extends WebTorrent {
|
||||
constructor (settings) {
|
||||
super({
|
||||
dht: !settings.torrentDHT,
|
||||
downloadLimit: settings.torrentSpeed * 1048576 || 0,
|
||||
uploadLimit: settings.torrentSpeed * 1572864 || 0 // :trolled:
|
||||
})
|
||||
this.settings = settings
|
||||
|
||||
this.current = null
|
||||
this.parsed = false
|
||||
this.parserInstance = null
|
||||
|
||||
setInterval(() => {
|
||||
this.dispatch('stats', {
|
||||
numPeers: (this.torrents.length && this.torrents[0].numPeers) || 0,
|
||||
uploadSpeed: (this.torrents.length && this.torrents[0].uploadSpeed) || 0,
|
||||
downloadSpeed: (this.torrents.length && this.torrents[0].downloadSpeed) || 0
|
||||
})
|
||||
}, 200)
|
||||
setInterval(() => {
|
||||
if (this.torrents[0]?.pieces) this.dispatch('pieces', [...this.torrents[0]?.pieces.map(piece => piece === null ? 77 : 33)])
|
||||
}, 2000)
|
||||
this.on('torrent', torrent => {
|
||||
const files = torrent.files.map(file => {
|
||||
return {
|
||||
infoHash: torrent.infoHash,
|
||||
name: file.name,
|
||||
type: file._getMimeType(),
|
||||
size: file.size,
|
||||
path: file.path,
|
||||
url: encodeURI('http://localhost:41785/webtorrent/' + torrent.infoHash + '/' + file.path)
|
||||
}
|
||||
})
|
||||
this.dispatch('files', files)
|
||||
this.dispatch('pieces', torrent.pieces.length)
|
||||
this.dispatch('torrent', Array.from(torrent.torrentFile))
|
||||
})
|
||||
|
||||
this.server = http.createServer((request, response) => {
|
||||
if (!request.url) return null
|
||||
let [infoHash, ...filePath] = request.url.slice(request.url.indexOf('/webtorrent/') + 12).split('/')
|
||||
filePath = decodeURI(filePath.join('/'))
|
||||
if (!infoHash || !filePath) return null
|
||||
|
||||
const file = this.get(infoHash)?.files.find(file => file.path === filePath)
|
||||
if (!file) return null
|
||||
|
||||
response.setHeader('Access-Control-Allow-Origin', '*')
|
||||
response.setHeader('Content-Type', mime.getType(file.name) || 'application/octet-stream')
|
||||
|
||||
response.setHeader('Accept-Ranges', 'bytes')
|
||||
|
||||
let range = rangeParser(file.length, request.headers.range || '')
|
||||
|
||||
if (Array.isArray(range)) {
|
||||
response.statusCode = 206
|
||||
range = range[0]
|
||||
|
||||
response.setHeader(
|
||||
'Content-Range',
|
||||
`bytes ${range.start}-${range.end}/${file.length}`
|
||||
)
|
||||
response.setHeader('Content-Length', range.end - range.start + 1)
|
||||
} else {
|
||||
response.statusCode = 200
|
||||
range = null
|
||||
response.setHeader('Content-Length', file.length)
|
||||
}
|
||||
|
||||
if (response.method === 'HEAD') {
|
||||
return response.end()
|
||||
}
|
||||
|
||||
let stream = file.createReadStream(range)
|
||||
|
||||
if (stream && !this.parsed) {
|
||||
if (file.name.endsWith('.mkv')) {
|
||||
this.parserInstance = new SubtitleStream(this.parserInstance)
|
||||
this.handleSubtitleParser(this.parserInstance, true)
|
||||
stream = pump(stream, this.parserInstance)
|
||||
}
|
||||
}
|
||||
|
||||
pump(stream, response)
|
||||
})
|
||||
|
||||
this.server.on('error', console.log)
|
||||
|
||||
this.server.listen(41785)
|
||||
|
||||
onmessage = this.handleMessage.bind(this)
|
||||
}
|
||||
|
||||
handleMessage ({ data }) {
|
||||
switch (data.type) {
|
||||
case 'current': {
|
||||
this.current?.removeListener('done', this.parseSubtitles.bind(this))
|
||||
this.parserInstance?.destroy()
|
||||
this.parserInstance = null
|
||||
this.current = null
|
||||
this.parsed = false
|
||||
if (data) {
|
||||
this.current = this?.get(data.data.infoHash)?.files.find(file => file.path === data.data.path)
|
||||
if (this.current.name.endsWith('.mkv')) {
|
||||
if (this.current.done) this.parseSubtitles()
|
||||
this.current.on('done', this.parseSubtitles.bind(this))
|
||||
this.parseFonts(this.current)
|
||||
}
|
||||
// findSubtitleFiles(current) TODO:
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'torrent': {
|
||||
if (this.torrents.length) this.remove(client.torrents[0].infoHash)
|
||||
|
||||
const id = typeof data.data !== 'string' ? Buffer.from(data.data) : data.data
|
||||
this.add(id, {
|
||||
private: this.settings.torrentPeX,
|
||||
path: this.settings.torrentPath,
|
||||
destroyStoreOnDestroy: !this.settings.torrentPersist,
|
||||
announce: [
|
||||
'wss://tracker.openwebtorrent.com',
|
||||
'wss://spacetradersapi-chatbox.herokuapp.com:443/announce',
|
||||
'wss://peertube.cpy.re:443/tracker/socket'
|
||||
]
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch (type, data) {
|
||||
postMessage({ type, data })
|
||||
}
|
||||
|
||||
parseSubtitles () {
|
||||
if (this.current.name.endsWith('.mkv')) {
|
||||
const parser = new SubtitleParser()
|
||||
this.handleSubtitleParser(parser, true)
|
||||
const finish = () => {
|
||||
console.log('Sub parsing finished')
|
||||
this.parsed = true
|
||||
fileStream?.destroy()
|
||||
stream?.destroy()
|
||||
stream = undefined
|
||||
}
|
||||
parser.once('tracks', tracks => {
|
||||
if (!tracks.length) finish()
|
||||
})
|
||||
parser.once('finish', finish)
|
||||
console.log('Sub parsing started')
|
||||
const fileStream = this.current.createReadStream()
|
||||
let stream = fileStream.pipe(parser)
|
||||
}
|
||||
}
|
||||
|
||||
parseFonts (file) {
|
||||
const stream = new SubtitleParser()
|
||||
this.handleSubtitleParser(stream)
|
||||
stream.once('tracks', tracks => {
|
||||
if (!tracks.length) {
|
||||
this.parsed = true
|
||||
stream.destroy()
|
||||
fileStreamStream.destroy()
|
||||
}
|
||||
})
|
||||
stream.once('subtitle', () => {
|
||||
fileStreamStream.destroy()
|
||||
stream.destroy()
|
||||
this.dispatch('fonts')
|
||||
})
|
||||
const fileStreamStream = file.createReadStream()
|
||||
fileStreamStream.pipe(stream)
|
||||
}
|
||||
|
||||
handleSubtitleParser (parser, skipFile) {
|
||||
parser.once('tracks', tracks => {
|
||||
if (!tracks.length) {
|
||||
this.parsed = true
|
||||
parser?.destroy()
|
||||
} else {
|
||||
this.dispatch('tracks', tracks)
|
||||
}
|
||||
})
|
||||
parser.on('subtitle', (subtitle, trackNumber) => {
|
||||
this.dispatch('subtitle', { subtitle, trackNumber })
|
||||
})
|
||||
if (!skipFile) {
|
||||
parser.on('file', file => {
|
||||
if (file.mimetype === 'application/x-truetype-font' || file.mimetype === 'application/font-woff' || file.mimetype === 'application/vnd.ms-opentype') {
|
||||
this.dispatch('file', { mimetype: file.mimetype, data: Array.from(file.data) })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
predestroy () {
|
||||
this.destroy()
|
||||
this.server.close()
|
||||
}
|
||||
}
|
||||
|
||||
let client = null
|
||||
|
||||
onmessage = ({ data }) => {
|
||||
if (!client && data.type === 'settings') client = new TorrentClient(data.data)
|
||||
if (data.type === 'destroy') client?.predestroy()
|
||||
}
|
||||
Loading…
Reference in a new issue