fix: webtorrent IPC, test version

This commit is contained in:
ThaUnknown 2022-04-02 17:38:12 +02:00
parent 3827aa74b0
commit 95e8c8e6cb
10 changed files with 351 additions and 224 deletions

View file

@ -1,6 +1,6 @@
{
"name": "Miru",
"version": "0.12.5",
"version": "1.0.0",
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
"main": "src/index.js",
"homepage": "https://github.com/ThaUnknown/miru#readme",
@ -27,7 +27,10 @@
"bundle.js",
"bundle.map.js"
],
"env": "browser"
"env": [
"browser",
"node"
]
},
"build": {
"publish": [
@ -84,12 +87,14 @@
}
},
"dependencies": {
"@electron/remote": "^2.0.7",
"anitomyscript": "^2.0.4",
"discord-rpc": "^4.0.1",
"electron-log": "^4.4.6",
"electron-updater": "^4.6.5",
"matroska-subtitles": "github:ThaUnknown/matroska-subtitles#patch",
"mime": "^3.0.0",
"pump": "^3.0.0",
"range-parser": "^1.2.1",
"webtorrent": "^1.5.0"
}
}

View file

@ -3,6 +3,8 @@ const path = require('path')
const remote = require('@electron/remote/main')
const log = require('electron-log')
const { autoUpdater } = require('electron-updater')
require('./main/torrent.js')
require('./main/misc.js')
autoUpdater.logger = log
autoUpdater.logger.transports.file.level = 'info'
@ -39,11 +41,9 @@ function createWindow () {
autoHideMenuBar: true,
experimentalFeatures: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
enableBlinkFeatures: 'AudioVideoTracks',
enableRemoteModule: true,
backgroundThrottling: false
backgroundThrottling: false,
preload: path.join(__dirname, '/preload.js')
},
icon: path.join(__dirname, '/renderer/public/logo.ico'),
show: false

53
src/main/misc.js Normal file
View file

@ -0,0 +1,53 @@
const { dialog, ipcMain, BrowserWindow } = require('electron')
ipcMain.on('dialog', async (event, data) => {
const { filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory']
})
if (filePaths.length) {
let path = filePaths[0]
if (!(path.endsWith('\\') || path.endsWith('/'))) {
if (path.indexOf('\\') !== -1) {
path += '\\'
} else if (path.indexOf('/') !== -1) {
path += '/'
}
}
event.sender.send('path', path)
}
})
ipcMain.on('minimize', () => {
BrowserWindow.getAllWindows()[0].minimize()
})
ipcMain.on('maximize', () => {
const window = BrowserWindow.getAllWindows()[0]
if (window.isMaximized()) {
window.unmaximize()
} else {
window.maximize()
}
})
let status = null
const { Client } = require('discord-rpc')
const discord = new Client({
transport: 'ipc'
})
function setDiscordRPC (event, data) {
status = data
if (discord?.user && status) {
status.pid = process.pid
discord.request('SET_ACTIVITY', status)
}
}
ipcMain.on('discord', setDiscordRPC)
discord.on('ready', () => {
setDiscordRPC(null, status)
})
function loginRPC () {
discord.login({ clientId: '954855428355915797' }).catch(() => {
setTimeout(loginRPC, 5000)
})
}
loginRPC()

194
src/main/torrent.js Normal file
View file

@ -0,0 +1,194 @@
const {
BrowserWindow,
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 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.listen(420)
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 window = BrowserWindow.getAllWindows()[0]
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) {
const window = BrowserWindow.getAllWindows()[0]
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) => {
settings = data
client = new WebTorrent({
dht: !settings.torrentDHT,
downloadLimit: settings.torrentSpeed * 1048576 || 0,
uploadLimit: settings.torrentSpeed * 1572864 || 0 // :trolled:
})
const window = BrowserWindow.getAllWindows()[0]
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).unref()
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:420/webtorrent/' + torrent.infoHash + '/' + file.path)
}
})
window.webContents.send('files', files)
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'
]
})
})

11
src/preload.js Normal file
View file

@ -0,0 +1,11 @@
/* eslint node/no-callback-literal: 0 */
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('IPC', {
emit: (event, data) => {
ipcRenderer.send(event, data)
},
on: (event, callback) => {
ipcRenderer.on(event, (event, ...args) => callback(...args))
}
})

View file

@ -3,11 +3,6 @@
export const title = writable('Miru')
</script>
<script>
const { getCurrentWindow } = require('@electron/remote')
const window = getCurrentWindow()
</script>
<div class="w-full navbar border-0 bg-dark position-relative p-0">
<div class="menu-shadow shadow-lg position-absolute w-full h-full z-0" />
<div class="w-full h-full bg-dark z-10 d-flex">
@ -16,17 +11,17 @@
{$title}
</div>
<div class="controls d-flex h-full pointer">
<div class="d-flex align-items-center" on:click={() => window.minimize()}>
<div class="d-flex align-items-center" on:click={() => window.IPC.emit('minimize')}>
<svg viewBox="0 0 24 24">
<path d="M19 13H5v-2h14v2z" />
</svg>
</div>
<div class="d-flex align-items-center" on:click={() => (window.isMaximized() ? window.unmaximize() : window.maximize())}>
<div class="d-flex align-items-center" on:click={() => window.IPC.emit('maximize')}>
<svg viewBox="0 0 24 24">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
</svg>
</div>
<div class="d-flex align-items-center close" on:click={() => window.close()}>
<div class="d-flex align-items-center close" on:click={window.close}>
<svg viewBox="0 0 24 24">
<path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
</svg>

View file

@ -2,15 +2,11 @@
import { set } from './Settings.svelte'
import { playAnime } from '../RSSView.svelte'
import { title } from '../Menubar.svelte'
const { Client } = require('discord-rpc')
const discord = new Client({
transport: 'ipc'
})
export let media = null
let fileMedia = null
let hadImage = false
export function updateMedia(fileMed) {
if (discord.user && !fileMedia) {
if (!fileMedia) {
setDiscordRPC(fileMed)
}
fileMedia = fileMed
@ -36,9 +32,8 @@
navigator.mediaSession.metadata = metadata
}
function setDiscordRPC(fileMedia) {
if (fileMedia && !process.env.NODE_ENV !== 'development ') {
discord.request('SET_ACTIVITY', {
pid: process.pid,
if (fileMedia) {
window.IPC.emit('discord', {
activity: {
details: fileMedia.media.title.userPreferred,
state: 'Watching Episode' + (!fileMedia.media.episodes ? ` ${fileMedia.episodeNumber}` : ''),
@ -59,15 +54,6 @@
})
}
}
discord.on('ready', () => {
setDiscordRPC(fileMedia)
})
function loginRPC() {
discord.login({ clientId: '954855428355915797' }).catch(() => {
setTimeout(loginRPC, 5000)
})
}
loginRPC()
</script>
<script>
@ -119,7 +105,7 @@
let ended = false
let volume = localStorage.getItem('volume') || 1
let playbackRate = 1
$: localStorage.setItem('volume', volume)
$: localStorage.setItem('volume', volume || 0)
function getFPS() {
video.fps = new Promise(resolve => {
let lastmeta = null
@ -211,15 +197,14 @@
video: undefined
})
completed = false
file.getStreamURL((err, url) => {
src = url
current = file
video?.load()
checkAvail(current)
})
current = file
initSubs()
src = file.url
window.IPC.emit('current', file)
video?.load()
checkAvail(current)
}
}
$: initSubs(current, video)
let hasNext = false
let hasLast = false
@ -240,11 +225,9 @@
}
}
function initSubs(current, video) {
if (current && video) {
if (subs) subs.destroy()
subs = new Subtitles(video, files, current, handleHeaders)
}
function initSubs() {
if (subs) subs.destroy()
subs = new Subtitles(video, files, current, handleHeaders)
}
function cycleSubtitles() {
if (current && subs?.headers) {
@ -729,12 +712,12 @@
}
}
const torrent = {}
function updateStats() {
torrent.peers = (client?.torrents.length && client?.torrents[0].numPeers) || 0
torrent.up = (client?.torrents.length && client?.torrents[0].uploadSpeed) || 0
torrent.down = (client?.torrents.length && client?.torrents[0].downloadSpeed) || 0
window.IPC.on('stats', updateStats)
function updateStats(stats) {
torrent.peers = stats.numPeers || 0
torrent.up = stats.uploadSpeed || 0
torrent.down = stats.downloadSpeed || 0
}
setInterval(updateStats, 200)
</script>
<svelte:window on:keydown={handleKeydown} bind:innerWidth bind:innerHeight />
@ -758,6 +741,7 @@
on:keypress={resetImmerse}
on:mouseleave={immersePlayer}>
<video
crossorigin="anonymous"
class="position-absolute h-full w-full"
style={`margin-top: ${menubarOffset}px`}
preload="auto"

View file

@ -23,10 +23,12 @@
function removeRelations() {
localStorage.removeItem('relations')
}
window.IPC.on('path', data => {
set.torrentPath = data
})
</script>
<script>
const { dialog } = require('@electron/remote')
import { Tabs, TabLabel, Tab } from '../Tabination.js'
const groups = {
@ -55,21 +57,8 @@
localStorage.removeItem('settings')
settings = { ...defaults }
}
async function handleFolder() {
const { filePaths } = await dialog.showOpenDialog({
properties: ['openDirectory']
})
if (filePaths.length) {
let path = filePaths[0]
if (!(path.endsWith('\\') || path.endsWith('/'))) {
if (path.indexOf('\\') !== -1) {
path += '\\'
} else if (path.indexOf('/') !== -1) {
path += '/'
}
}
settings.torrentPath = path
}
function handleFolder() {
window.IPC.emit('dialog')
}
</script>

View file

@ -1,7 +1,6 @@
/* eslint-env browser */
import SubtitlesOctopus from './subtitles-octopus.js'
import { toTS, videoRx, subRx } from './util.js'
import { SubtitleParser, SubtitleStream } from 'matroska-subtitles'
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
@ -26,22 +25,50 @@ 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))
}
if (this.selected.name.endsWith('.mkv') && this.selected.createReadStream) {
if (this.selected.done) this.parseSubtitles()
this.selected.on('done', this.parseSubtitles.bind(this))
this.parseFonts(this.selected)
this.selected.on('stream', ({ stream, req }, cb) => {
if (req.destination === 'video' && !this.parsed) {
this.stream = new SubtitleStream(this.stream)
this.handleSubtitleParser(this.stream, true)
stream.pipe(this.stream)
cb(this.stream)
}
})
handleFile (file) {
if (this.selected) {
this.fonts.push(URL.createObjectURL(new Blob([file.data], { type: file.mimetype })))
}
}
handleFonts () {
if (this.selected) {
this.renderer?.destroy()
this.renderer = null
this.initSubtitleRenderer()
// re-create renderer with fonts
}
}
handleSubtitle ({ subtitle, trackNumber }) {
if (this.selected) {
if (!this.renderer) this.initSubtitleRenderer()
this.tracks[trackNumber].add(this.constructor.constructSub(subtitle, this.headers[trackNumber].type !== 'ass'))
if (this.current === trackNumber) this.selectCaptions(trackNumber) // yucky
}
}
handleTracks (tracks) {
if (this.selected) {
for (const track of tracks) {
if (!this.tracks[track.number]) {
// overwrite webvtt or other header with custom one
if (track.type !== 'ass') track.header = defaultHeader
if (!this.current) {
this.current = track.number
}
this.tracks[track.number] = new Set()
this.headers[track.number] = track
this.onHeader()
}
}
}
this.findSubtitleFiles(this.selected)
}
findSubtitleFiles (targetFile) {
@ -201,94 +228,6 @@ export default class Subtitles {
subtitle.text || ''
}
parseSubtitles () { // parse all existing subtitles for a file
return new Promise((resolve) => {
if (this.selected.name.endsWith('.mkv')) {
let parser = new SubtitleParser()
this.handleSubtitleParser(parser, true)
const finish = () => {
console.log('Sub parsing finished', toTS((performance.now() - t0) / 1000))
this.parsed = true
this.stream?.destroy()
fileStream?.destroy()
this.parser?.destroy()
this.stream = undefined
this.parser = undefined
this.selectCaptions(this.current)
parser = undefined
resolve()
}
parser.once('tracks', tracks => {
if (!tracks.length) finish()
})
parser.once('finish', finish)
const t0 = performance.now()
console.log('Sub parsing started')
const fileStream = this.selected.createReadStream()
this.parser = fileStream.pipe(parser)
} else {
resolve()
}
})
}
parseFonts (file) {
this.stream = new SubtitleParser()
this.handleSubtitleParser(this.stream)
this.stream.once('tracks', tracks => {
if (!tracks.length) {
this.parsed = true
this.stream.destroy()
fileStreamStream.destroy()
}
})
this.stream.once('subtitle', () => {
fileStreamStream.destroy()
this.renderer?.destroy()
this.renderer = null
this.initSubtitleRenderer()
// re-create renderer with fonts
})
const fileStreamStream = file.createReadStream()
fileStreamStream.pipe(this.stream)
}
handleSubtitleParser (parser, skipFile) {
parser.once('tracks', tracks => {
if (!tracks.length) {
this.parsed = true
parser?.destroy()
} else {
for (const track of tracks) {
if (!this.tracks[track.number]) {
// overwrite webvtt or other header with custom one
if (track.type !== 'ass') track.header = defaultHeader
if (!this.current) {
this.current = track.number
}
this.tracks[track.number] = new Set()
this.headers[track.number] = track
this.onHeader()
}
}
}
})
parser.on('subtitle', (subtitle, trackNumber) => {
if (!this.parsed) {
if (!this.renderer) this.initSubtitleRenderer()
this.tracks[trackNumber].add(this.constructor.constructSub(subtitle, this.headers[trackNumber].type !== 'ass'))
if (this.current === trackNumber) this.selectCaptions(trackNumber) // yucky
}
})
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.fonts.push(URL.createObjectURL(new Blob([file.data], { type: file.mimetype })))
}
})
}
}
selectCaptions (trackNumber) {
if (trackNumber !== undefined) {
this.current = Number(trackNumber)
@ -303,7 +242,6 @@ export default class Subtitles {
}
destroy () {
this.selected.removeListener('done', this.parseSubtitles.bind(this))
this.stream?.destroy()
this.parser?.destroy()
this.renderer?.destroy()

View file

@ -1,78 +1,36 @@
import WebTorrent from 'webtorrent'
// import WebTorrent from 'webtorrent'
import { set } from '@/lib/pages/Settings.svelte'
import { files } from '@/lib/Router.svelte'
import { page } from '@/App.svelte'
export const client = new WebTorrent({
dht: !set.torrentDHT,
downloadLimit: set.torrentSpeed * 1048576 || 0,
uploadLimit: set.torrentSpeed * 1572864 || 0 // :trolled:
})
// save loaded torrent for persistence
// should use HTTP createserver... oopps xd
const scope = location.pathname.substr(0, location.pathname.lastIndexOf('/') + 1)
const worker = location.origin + scope + 'sw.js' === navigator.serviceWorker?.controller?.scriptURL && navigator.serviceWorker.controller
const handleWorker = worker => {
const checkState = worker => {
return worker.state === 'activated' && client.loadWorker(worker)
}
if (!checkState(worker)) {
worker.addEventListener('statechange', ({ target }) => checkState(target))
}
}
if (worker) {
handleWorker(worker)
} else {
navigator.serviceWorker.register('sw.js', { scope }).then(reg => {
handleWorker(reg.active || reg.waiting || reg.installing)
}).catch(e => {
if (String(e) === 'InvalidStateError: Failed to register a ServiceWorker: The document is in an invalid state.') {
location.reload() // weird workaround for a weird bug
} else {
throw e
}
})
}
window.client = client
client.on('torrent', torrent => {
files.set(torrent.files)
export const client = null
window.IPC.emit('settings', { ...set })
window.IPC.on('files', arr => {
files.set(arr)
})
export async function add (torrentID, hide) {
if (torrentID) {
if (client.torrents.length) client.remove(client.torrents[0].infoHash)
files.set([])
if (!hide) page.set('player')
if (typeof torrentID === 'string' && !torrentID.startsWith('magnet:')) {
// IMPORTANT, this is because node's get bypasses proxies, wut????
const res = await fetch(torrentID)
torrentID = new File([await res.arrayBuffer()], 'file.torrent', {
type: 'application/x-bittorrent'
})
torrentID = Array.from(new Uint8Array(await res.arrayBuffer()))
}
client.add(torrentID, {
private: set.torrentPeX,
path: set.torrentPath,
destroyStoreOnDestroy: !set.torrentPersist,
announce: [
'wss://tracker.openwebtorrent.com',
'wss://spacetradersapi-chatbox.herokuapp.com:443/announce',
'wss://peertube.cpy.re:443/tracker/socket'
]
})
window.IPC.emit('torrent', torrentID)
}
}
client.on('torrent', torrent => {
console.log('ready', torrent.name)
const string = JSON.stringify(Array.from(torrent.torrentFile))
localStorage.setItem('torrent', string)
window.IPC.on('torrent', file => {
localStorage.setItem('torrent', JSON.stringify(file))
})
// load last used torrent
queueMicrotask(() => {
if (localStorage.getItem('torrent')) {
const buffer = Buffer.from(JSON.parse(localStorage.getItem('torrent')))
add(buffer, true)
window.IPC.emit('torrent', JSON.parse(localStorage.getItem('torrent')))
}
})