feat: capacitor build

fix: separate IPC calls
This commit is contained in:
ThaUnknown 2023-11-12 21:12:28 +01:00
parent 832b3bb33d
commit 9a73497fc6
34 changed files with 2655 additions and 655 deletions

View file

@ -6,6 +6,9 @@ const config = {
plugins: {
SplashScreen: {
launchShowDuration: 0
},
CapacitorHttp: {
enabled: false
}
},
// remove server section before making production build
@ -13,7 +16,7 @@ const config = {
// for android only, below settings will work out of the box
// for iOS or both, change the url to http://your-device-ip
// To discover your workstation IP, just run ifconfig
url: 'http://10.0.2.2:5001',
url: 'http://localhost:5001/app.html',
cleartext: true
}
}

View file

@ -1,26 +1,43 @@
{
"name": "capacitor",
"private": true,
"scripts": {
"build:app": "vite build",
"build:android": "run-s build:app cap-run:android",
"build:ios": "run-s build:app cap-run:ios",
"cap-run:android": "cap sync android && cap open android",
"cap-run:android": "cap run android --target=Pixel_XL_API_33 --external --public-host=10.5.0.2",
"cap-run:ios": "cap sync ios && cap open ios",
"dev:ios": "run-p dev:start cap-run:ios",
"dev:android": "run-p dev:start cap-run:android",
"dev:preview": "vite preview",
"dev:start": "run-p dev:vite",
"dev:vite": "vite --host --port 5001"
"dev:start": "run-p dev:webpack cap-run:android",
"dev:webpack": "webpack serve",
"dev:localhost-bind": "adb reverse tcp:5001 tcp:5001"
},
"devDependencies": {
"cordova-res": "^0.15.4"
"assert": "^2.1.0",
"buffer": "^6.0.3",
"chrome-dgram": "^3.0.6",
"chrome-net": "^3.3.4",
"cordova-res": "^0.15.4",
"crypto-browserify": "^3.12.0",
"hybrid-chunk-store": "^1.2.2",
"path-esm": "^1.0.0",
"querystring": "^0.2.1",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"timers-browserify": "^2.0.12",
"util": "^0.12.5",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@capacitor/android": "^5.5.1",
"@capacitor/cli": "^5.5.1",
"@capacitor/core": "^5.5.1",
"@capacitor/ios": "^5.5.1",
"@superfrogbe/cordova-plugin-chrome-apps-sockets-udp": "github:superfrogbe/cordova-plugin-chrome-apps-sockets-udp",
"common": "workspace:*",
"cordova-plugin-chrome-apps-common": "^1.0.7",
"cordova-plugin-chrome-apps-sockets-tcp": "github:KoenLav/cordova-plugin-chrome-apps-sockets-tcp",
"cordova-plugin-chrome-apps-sockets-udp": "^1.3.0"
"webpack-merge": "^5.10.0"
}
}

View file

@ -1 +0,0 @@
<h1>uwu</h1>

View file

@ -0,0 +1,497 @@
/*! chrome-dgram. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/* global chrome */
/**
* UDP / Datagram Sockets
* ======================
*
* Datagram sockets are available through require('chrome-dgram').
*/
exports.Socket = Socket
const EventEmitter = require('events').EventEmitter
const inherits = require('inherits')
const series = require('run-series')
const BIND_STATE_UNBOUND = 0
const BIND_STATE_BINDING = 1
const BIND_STATE_BOUND = 2
// Track open sockets to route incoming data (via onReceive) to the right handlers.
const sockets = {}
// Thorough check for Chrome App since both Edge and Chrome implement dummy chrome object
if (
typeof chrome === 'object' &&
typeof chrome.sockets === 'object' &&
typeof chrome.sockets.udp === 'object'
) {
chrome.sockets.udp.onReceive.addListener(onReceive)
chrome.sockets.udp.onReceiveError.addListener(onReceiveError)
}
function onReceive (info) {
if (info.socketId in sockets) {
sockets[info.socketId]._onReceive(info)
} else {
console.error('Unknown socket id: ' + info.socketId)
}
}
function onReceiveError (info) {
if (info.socketId in sockets) {
sockets[info.socketId]._onReceiveError(info.resultCode)
} else {
console.error('Unknown socket id: ' + info.socketId)
}
}
/**
* dgram.createSocket(type, [callback])
*
* Creates a datagram Socket of the specified types. Valid types are `udp4`
* and `udp6`.
*
* Takes an optional callback which is added as a listener for message events.
*
* Call socket.bind if you want to receive datagrams. socket.bind() will bind
* to the "all interfaces" address on a random port (it does the right thing
* for both udp4 and udp6 sockets). You can then retrieve the address and port
* with socket.address().address and socket.address().port.
*
* @param {string} type Either 'udp4' or 'udp6'
* @param {function} listener Attached as a listener to message events.
* Optional
* @return {Socket} Socket object
*/
exports.createSocket = function (type, listener) {
return new Socket(type, listener)
}
inherits(Socket, EventEmitter)
/**
* Class: dgram.Socket
*
* The dgram Socket class encapsulates the datagram functionality. It should
* be created via `dgram.createSocket(type, [callback])`.
*
* Event: 'message'
* - msg Buffer object. The message
* - rinfo Object. Remote address information
* Emitted when a new datagram is available on a socket. msg is a Buffer and
* rinfo is an object with the sender's address information and the number
* of bytes in the datagram.
*
* Event: 'listening'
* Emitted when a socket starts listening for datagrams. This happens as soon
* as UDP sockets are created.
*
* Event: 'close'
* Emitted when a socket is closed with close(). No new message events will
* be emitted on this socket.
*
* Event: 'error'
* - exception Error object
* Emitted when an error occurs.
*/
function Socket (options, listener) {
const self = this
EventEmitter.call(self)
if (typeof options === 'string') options = { type: options }
if (options.type !== 'udp4') throw new Error('Bad socket type specified. Valid types are: udp4')
if (typeof listener === 'function') self.on('message', listener)
self._destroyed = false
self._bindState = BIND_STATE_UNBOUND
self._bindTasks = []
}
/**
* socket.bind(port, [address], [callback])
*
* For UDP sockets, listen for datagrams on a named port and optional address.
* If address is not specified, the OS will try to listen on all addresses.
* After binding is done, a "listening" event is emitted and the callback(if
* specified) is called. Specifying both a "listening" event listener and
* callback is not harmful but not very useful.
*
* A bound datagram socket keeps the node process running to receive
* datagrams.
*
* If binding fails, an "error" event is generated. In rare case (e.g. binding
* a closed socket), an Error may be thrown by this method.
*
* @param {number} port
* @param {string} address Optional
* @param {function} callback Function with no parameters, Optional. Callback
* when binding is done.
*/
Socket.prototype.bind = function (port, address, callback) {
const self = this
if (typeof address === 'function') {
callback = address
address = undefined
}
if (!address) address = '0.0.0.0'
if (!port) port = 0
if (self._bindState !== BIND_STATE_UNBOUND) throw new Error('Socket is already bound')
self._bindState = BIND_STATE_BINDING
if (typeof callback === 'function') self.once('listening', callback)
chrome.sockets.udp.create(function (createInfo) {
self.id = createInfo.socketId
sockets[self.id] = self
const bindFns = self._bindTasks.map(function (t) { return t.fn })
series(bindFns, function (err) {
if (err) return self.emit('error', err)
chrome.sockets.udp.bind(self.id, address, port, function (result) {
if (result < 0) {
self.emit('error', new Error('Socket ' + self.id + ' failed to bind. ' +
chrome.runtime.lastError.message))
return
}
chrome.sockets.udp.getInfo(self.id, function (socketInfo) {
if (!socketInfo.localPort || !socketInfo.localAddress) {
self.emit('error', new Error('Cannot get local port/address for Socket ' + self.id))
return
}
self._port = socketInfo.localPort
self._address = socketInfo.localAddress
self._bindState = BIND_STATE_BOUND
self.emit('listening')
self._bindTasks.map(function (t) {
t.callback()
})
})
})
})
})
}
/**
* Internal function to receive new messages and emit `message` events.
*/
Socket.prototype._onReceive = function (info) {
const self = this
const buf = Buffer.from(new Uint8Array(info.data))
const rinfo = {
address: info.remoteAddress,
family: 'IPv4',
port: info.remotePort,
size: buf.length
}
self.emit('message', buf, rinfo)
}
Socket.prototype._onReceiveError = function (resultCode) {
const self = this
self.emit('error', new Error('Socket ' + self.id + ' receive error ' + resultCode))
}
/**
* socket.send(buf, offset, length, port, address, [callback])
*
* For UDP sockets, the destination port and IP address must be
* specified. A string may be supplied for the address parameter, and it will
* be resolved with DNS. An optional callback may be specified to detect any
* DNS errors and when buf may be re-used. Note that DNS lookups will delay
* the time that a send takes place, at least until the next tick. The only
* way to know for sure that a send has taken place is to use the callback.
*
* If the socket has not been previously bound with a call to bind, it's
* assigned a random port number and bound to the "all interfaces" address
* (0.0.0.0 for udp4 sockets, ::0 for udp6 sockets).
*
* @param {Buffer|Arrayish|string} buf Message to be sent
* @param {number} offset Offset in the buffer where the message starts. Optional.
* @param {number} length Number of bytes in the message. Optional.
* @param {number} port destination port
* @param {string} address destination IP
* @param {function} callback Callback when message is done being delivered.
* Optional.
*
* Valid combinations:
* send(buffer, offset, length, port, address, callback)
* send(buffer, offset, length, port, address)
* send(buffer, offset, length, port)
* send(bufferOrList, port, address, callback)
* send(bufferOrList, port, address)
* send(bufferOrList, port)
*
*/
Socket.prototype.send = function (buffer, offset, length, port, address, callback) {
const self = this
let list
if (address || (port && typeof port !== 'function')) {
buffer = sliceBuffer(buffer, offset, length)
} else {
callback = port
port = offset
address = length
}
if (!Array.isArray(buffer)) {
if (typeof buffer === 'string') {
list = [Buffer.from(buffer)]
} else if (!(buffer instanceof Uint8Array)) {
throw new TypeError('First argument must be a buffer or a string')
} else {
list = [Buffer.from(buffer)]
}
} else if (!(list = fixBufferList(buffer))) {
throw new TypeError('Buffer list arguments must be buffers or strings')
}
port = port >>> 0
if (port === 0 || port > 65535) {
throw new RangeError('Port should be > 0 and < 65536')
}
// Normalize callback so it's always a function
if (typeof callback !== 'function') {
callback = function () {}
}
if (self._bindState === BIND_STATE_UNBOUND) self.bind(0)
// If the socket hasn't been bound yet, push the outbound packet onto the
// send queue and send after binding is complete.
if (self._bindState !== BIND_STATE_BOUND) {
// If the send queue hasn't been initialized yet, do it, and install an
// event handler that flishes the send queue after binding is done.
if (!self._sendQueue) {
self._sendQueue = []
self.once('listening', function () {
// Flush the send queue.
for (let i = 0; i < self._sendQueue.length; i++) {
self.send.apply(self, self._sendQueue[i])
}
self._sendQueue = undefined
})
}
self._sendQueue.push([buffer, offset, length, port, address, callback])
return
}
const ab = Buffer.concat(list).buffer
chrome.sockets.udp.send(self.id, ab, address, port, function (sendInfo) {
if (sendInfo.resultCode < 0) {
const err = new Error('Socket ' + self.id + ' send error ' + sendInfo.resultCode)
callback(err)
self.emit('error', err)
} else {
callback(null)
}
})
}
function sliceBuffer (buffer, offset, length) {
buffer = Buffer.from(buffer)
offset = offset >>> 0
length = length >>> 0
// assuming buffer is browser implementation (`buffer` package on npm)
let buf = buffer.buffer
if (buffer.byteOffset || buffer.byteLength !== buf.byteLength) {
buf = buf.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)
}
if (offset || length !== buffer.length) {
buf = buf.slice(offset, length)
}
return Buffer.from(buf)
}
function fixBufferList (list) {
const newlist = new Array(list.length)
for (let i = 0, l = list.length; i < l; i++) {
const buf = list[i]
if (typeof buf === 'string') {
newlist[i] = Buffer.from(buf)
} else if (!(buf instanceof Uint8Array)) {
return null
} else {
newlist[i] = Buffer.from(buf)
}
}
return newlist
}
/**
* Close the underlying socket and stop listening for data on it.
*/
Socket.prototype.close = function () {
const self = this
if (self._destroyed) return
delete sockets[self.id]
chrome.sockets.udp.close(self.id)
self._destroyed = true
self.emit('close')
}
/**
* Returns an object containing the address information for a socket. For UDP
* sockets, this object will contain address, family and port.
*
* @return {Object} information
*/
Socket.prototype.address = function () {
const self = this
return {
address: self._address,
port: self._port,
family: 'IPv4'
}
}
Socket.prototype.setBroadcast = function (flag) {
// No chrome.sockets equivalent
}
Socket.prototype.setTTL = function (ttl) {
// No chrome.sockets equivalent
}
// NOTE: Multicast code is untested. Pull requests accepted for bug fixes and to
// add tests!
/**
* Sets the IP_MULTICAST_TTL socket option. TTL stands for "Time to Live," but
* in this context it specifies the number of IP hops that a packet is allowed
* to go through, specifically for multicast traffic. Each router or gateway
* that forwards a packet decrements the TTL. If the TTL is decremented to 0
* by a router, it will not be forwarded.
*
* The argument to setMulticastTTL() is a number of hops between 0 and 255.
* The default on most systems is 1.
*
* NOTE: The Chrome version of this function is async, whereas the node
* version is sync. Keep this in mind.
*
* @param {number} ttl
* @param {function} callback CHROME-SPECIFIC: Called when the configuration
* operation is done.
*/
Socket.prototype.setMulticastTTL = function (ttl, callback) {
const self = this
if (!callback) callback = function () {}
if (self._bindState === BIND_STATE_BOUND) {
setMulticastTTL(callback)
} else {
self._bindTasks.push({
fn: setMulticastTTL,
callback
})
}
function setMulticastTTL (callback) {
chrome.sockets.udp.setMulticastTimeToLive(self.id, ttl, callback)
}
}
/**
* Sets or clears the IP_MULTICAST_LOOP socket option. When this option is
* set, multicast packets will also be received on the local interface.
*
* NOTE: The Chrome version of this function is async, whereas the node
* version is sync. Keep this in mind.
*
* @param {boolean} flag
* @param {function} callback CHROME-SPECIFIC: Called when the configuration
* operation is done.
*/
Socket.prototype.setMulticastLoopback = function (flag, callback) {
const self = this
if (!callback) callback = function () {}
if (self._bindState === BIND_STATE_BOUND) {
setMulticastLoopback(callback)
} else {
self._bindTasks.push({
fn: setMulticastLoopback,
callback
})
}
function setMulticastLoopback (callback) {
chrome.sockets.udp.setMulticastLoopbackMode(self.id, flag, callback)
}
}
/**
* Tells the kernel to join a multicast group with IP_ADD_MEMBERSHIP socket
* option.
*
* If multicastInterface is not specified, the OS will try to add membership
* to all valid interfaces.
*
* NOTE: The Chrome version of this function is async, whereas the node
* version is sync. Keep this in mind.
*
* @param {string} multicastAddress
* @param {string} [multicastInterface] Optional
* @param {function} callback CHROME-SPECIFIC: Called when the configuration
* operation is done.
*/
Socket.prototype.addMembership = function (multicastAddress,
multicastInterface,
callback) {
const self = this
if (!callback) callback = function () {}
chrome.sockets.udp.joinGroup(self.id, multicastAddress, callback)
}
/**
* Opposite of addMembership - tells the kernel to leave a multicast group
* with IP_DROP_MEMBERSHIP socket option. This is automatically called by the
* kernel when the socket is closed or process terminates, so most apps will
* never need to call this.
*
* NOTE: The Chrome version of this function is async, whereas the node
* version is sync. Keep this in mind.
*
* If multicastInterface is not specified, the OS will try to drop membership
* to all valid interfaces.
*
* @param {[type]} multicastAddress
* @param {[type]} multicastInterface Optional
* @param {function} callback CHROME-SPECIFIC: Called when the configuration
* operation is done.
*/
Socket.prototype.dropMembership = function (multicastAddress,
multicastInterface,
callback) {
const self = this
if (!callback) callback = function () {}
chrome.sockets.udp.leaveGroup(self.id, multicastAddress, callback)
}
Socket.prototype.unref = function () {
// No chrome.sockets equivalent
}
Socket.prototype.ref = function () {
// No chrome.sockets equivalent
}

1205
capacitor/src/chrome-net.js Normal file

File diff suppressed because it is too large Load diff

40
capacitor/src/ipc.js Normal file
View file

@ -0,0 +1,40 @@
import EventEmitter from 'events'
const ipcRendererUI = new EventEmitter()
export default {
emit: (event, data) => {
ipcRendererUI.emit(event, data)
},
on: (event, callback) => {
ipcRendererUI.on(event, (event, ...args) => callback(...args))
},
once: (event, callback) => {
ipcRendererUI.once(event, (event, ...args) => callback(...args))
},
off: event => {
ipcRendererUI.removeAllListeners(event)
}
}
ipcRendererUI.on('portRequest', async () => {
const { port1, port2 } = new MessageChannel()
window.port = {
onmessage: cb => {
port2.onmessage = ({ type, data }) => cb({ type, data })
},
postMessage: (a, b) => {
port2.postMessage(a, b)
}
}
await window.controller
ipcRendererWebTorrent.emit('port', { ports: [port1] })
ipcRendererUI.emit('port', { ports: [port2] })
})
export const ipcRendererWebTorrent = new EventEmitter()
window.version = {
arch: 'uwu',
platform: 'nyaa'
}

View file

@ -1,7 +0,0 @@
import App from './App.svelte'
const app = new App({
target: document.getElementById('app')
})
export default app

View file

@ -0,0 +1,21 @@
import TorrentClient from 'common/modules/webtorrent.js'
import { ipcRendererWebTorrent } from './ipc.js'
globalThis.chrome.runtime = { lastError: false, id: 'something' }
const controller = (async () => {
const reg = await navigator.serviceWorker.register('./sw.js', { scope: './' })
const worker = reg.active || reg.waiting || reg.installing
return new Promise(resolve => {
function checkState (worker) {
return worker.state === 'activated' && resolve(reg)
}
if (!checkState(worker)) {
worker.addEventListener('statechange', ({ target }) => checkState(target))
}
})
})()
window.controller = controller
window.client = new TorrentClient(ipcRendererWebTorrent, () => ({ bsize: Infinity, bavail: Infinity }), 'browser', controller)

View file

@ -1,54 +1,57 @@
import webpack from 'webpack'
import TerserPlugin from 'terser-webpack-plugin'
import info from 'webtorrent/package.json' assert { type: 'json' }
const webpack = require('webpack')
const commonConfig = require('common/webpack.config.cjs')
const { merge } = require('webpack-merge')
const { join, resolve } = require('path')
/** @type {import('webpack').Configuration} */
export default {
entry: './index.js',
devtool: 'source-map',
resolve: {
aliasFields: ['chromeapp'],
alias: {
...info.chromeapp,
path: 'path-esm',
stream: 'stream-browserify',
timers: 'timers-browserify',
crypto: 'crypto-browserify',
buffer: 'buffer',
querystring: 'querystring',
zlib: '/polyfills/inflate-sync-web.js'
}
},
output: {
chunkFormat: 'module',
filename: 'webtorrent.chromeapp.js',
library: {
type: 'module'
}
},
mode: 'production',
target: 'web',
experiments: {
outputModule: true
},
const capacitorConfig = {
entry: [join(__dirname, 'src', 'webtorrent.js')],
plugins: [
new webpack.ProvidePlugin({
process: '/polyfills/process-fast.js',
process: 'webtorrent/polyfills/process-fast.js',
Buffer: ['buffer', 'Buffer']
}),
new webpack.DefinePlugin({
global: 'globalThis'
})
],
optimization: {
minimize: true,
minimizer: [new TerserPlugin({
terserOptions: {
format: {
comments: false
}
},
extractComments: false
})]
devServer: {
devMiddleware: {
writeToDisk: true
},
hot: true,
client: {
overlay: { errors: true, warnings: false, runtimeErrors: false }
},
port: 5001
}
}
const alias = {
'@/modules/ipc.js': join(__dirname, 'src', 'ipc.js'),
'webtorrent/lib/utp.cjs': false,
'@silentbot1/nat-api': false,
fs: false,
http: 'stream-http',
https: 'stream-http',
'load-ip-set': false,
net: join(__dirname, 'src', 'chrome-net.js'),
dgram: join(__dirname, 'src', 'chrome-dgram.js'),
util: 'util',
assert: 'assert',
os: false,
ws: false,
ut_pex: 'ut_pex',
'bittorrent-dht': false,
path: 'path-esm',
'fs-chunk-store': 'hybrid-chunk-store',
stream: 'stream-browserify',
timers: 'timers-browserify',
crypto: 'crypto-browserify',
buffer: 'buffer',
'bittorrent-tracker': 'bittorrent-tracker',
querystring: 'querystring',
zlib: 'webtorrent/polyfills/inflate-sync-web.js',
'bittorrent-tracker/lib/client/http-tracker.js': resolve('../node_modules/bittorrent-tracker/lib/client/http-tracker.js')
}
module.exports = merge(commonConfig(__dirname, alias, 'chromeapp'), capacitorConfig)

View file

@ -2,6 +2,7 @@
import { setContext } from 'svelte'
import { writable } from 'simple-store-svelte'
import { alRequest } from '@/modules/anilist.js'
import IPC from '@/modules/ipc.js'
export const page = writable('home')
export const view = writable(null)
@ -9,8 +10,8 @@
view.set(null)
view.set((await alRequest({ method: 'SearchIDSingle', id: anime })).data.Media)
}
window.IPC.on('open-anime', handleAnime)
window.IPC.on('schedule', () => {
IPC.on('open-anime', handleAnime)
IPC.on('schedule', () => {
page.set('schedule')
})
</script>

View file

@ -1,6 +1,7 @@
<script>
import { getContext } from 'svelte'
import { click } from '@/modules/click.js'
import IPC from '@/modules/ipc.js'
export let page
const view = getContext('view')
@ -18,7 +19,7 @@
</div>
<div class='h-full bg-dark flex-grow-1'>
{#if window.version.platform === 'linux'}
<div class='d-flex align-items-center close h-full' use:click={() => window.IPC.emit('close')}>
<div class='d-flex align-items-center close h-full' use:click={() => IPC.emit('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

@ -7,6 +7,7 @@
import { toast } from 'svelte-sonner'
import { click } from '@/modules/click.js'
import { logout } from './Logout.svelte'
import IPC from '@/modules/ipc.js'
const view = getContext('view')
export let page
const links = [
@ -15,7 +16,7 @@
if (alID) {
$logout = true
} else {
window.IPC.emit('open', 'https://anilist.co/api/v2/oauth/authorize?client_id=4254&response_type=token') // Change redirect_url to miru://auth
IPC.emit('open', 'https://anilist.co/api/v2/oauth/authorize?client_id=4254&response_type=token') // Change redirect_url to miru://auth
if (platformMap[window.version.platform] === 'Linux') {
toast('Support Notification', {
description: "If your linux distribution doesn't support custom protocol handlers, you can simply paste the full URL into the app.",
@ -69,7 +70,7 @@
},
{
click: () => {
window.IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/')
IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/')
},
icon: 'favorite',
text: 'Support This App',

1
common/modules/ipc.js Normal file
View file

@ -0,0 +1 @@
export default window.IPC

67
common/modules/parser.js Normal file
View file

@ -0,0 +1,67 @@
import { fontRx } from './util.js'
import Metadata from 'matroska-metadata'
export default class Parser {
parsed = false
/** @type {Metadata} */
metadata = null
client = null
file = null
destroyed = false
constructor (client, file) {
this.client = client
this.file = file
this.metadata = new Metadata(file)
this.metadata.getTracks().then(tracks => {
if (this.destroyed) return
if (!tracks.length) {
this.parsed = true
this.destroy()
} else {
this.client.dispatch('tracks', tracks)
}
})
this.metadata.getChapters().then(chapters => {
if (this.destroyed) return
this.client.dispatch('chapters', chapters)
})
this.metadata.getAttachments().then(files => {
if (this.destroyed) return
for (const file of files) {
if (fontRx.test(file.filename) || file.mimetype.toLowerCase().includes('font')) {
const data = new Uint8Array(file.data)
this.client.dispatch('file', { data }, [data.buffer])
}
}
})
this.metadata.on('subtitle', (subtitle, trackNumber) => {
if (this.destroyed) return
this.client.dispatch('subtitle', { subtitle, trackNumber })
})
if (this.file.name.endsWith('.mkv') || this.file.name.endsWith('.webm')) {
this.file.on('iterator', ({ iterator }, cb) => {
if (this.destroyed) return cb(iterator)
cb(this.metadata.parseStream(iterator))
})
}
}
async parseSubtitles () {
if (this.file.name.endsWith('.mkv') || this.file.name.endsWith('.webm')) {
console.log('Sub parsing started')
await this.metadata.parseFile()
console.log('Sub parsing finished')
}
}
destroy () {
this.destroyed = true
this.metadata?.destroy()
this.metadata = undefined
}
}

View file

@ -1,5 +1,6 @@
import { writable } from 'simple-store-svelte'
import { defaults } from '@/../common/util.js'
import { defaults } from './util.js'
import IPC from '@/modules/ipc.js'
export let alToken = localStorage.getItem('ALtoken') || null
let storedSettings = { ...defaults }
@ -38,7 +39,7 @@ window.addEventListener('paste', ({ clipboardData }) => {
}
}
})
window.IPC.on('altoken', handleToken)
IPC.on('altoken', handleToken)
function handleToken (data) {
localStorage.setItem('ALtoken', data)
alToken = data

View file

@ -1,6 +1,5 @@
import JASSUB from 'jassub'
import { toTS } from './util.js'
import { subRx, videoRx } from '@/../common/util.js'
import { toTS, subRx, videoRx } from './util.js'
import { settings } from '@/modules/settings.js'
import { client } from '@/modules/torrent.js'
import clipboard from './clipboard.js'

View file

@ -2,6 +2,7 @@ import { files, media } from '../views/Player/MediaHandler.svelte'
import { page } from '@/App.svelte'
import { toast } from 'svelte-sonner'
import clipboard from './clipboard.js'
import IPC from '@/modules/ipc.js'
import 'browser-event-target-emitter'
const torrentRx = /(^magnet:){1}|(^[A-F\d]{8,40}$){1}|(.*\.torrent$){1}/i
@ -10,12 +11,12 @@ class TorrentWorker extends EventTarget {
constructor () {
super()
this.ready = new Promise(resolve => {
window.IPC.once('port', () => {
IPC.once('port', () => {
this.port = window.port
this.port.onmessage(this.handleMessage.bind(this))
resolve()
})
window.IPC.emit('portRequest')
IPC.emit('portRequest')
})
clipboard.on('text', ({ detail }) => {
for (const { text } of detail) {

View file

@ -141,3 +141,43 @@ export function binarySearch (arr, el) {
return false
}
export const defaults = {
playerAutoplay: true,
playerPause: true,
playerAutocomplete: true,
rssQuality: '1080',
rssFeedsNew: [['New Releases', 'SubsPlease']],
rssAutoplay: true,
torrentSpeed: 10,
torrentPersist: false,
torrentDHT: false,
torrentPeX: false,
torrentPort: 0,
dhtPort: 0,
missingFont: true,
maxConns: 20,
subtitleLanguage: 'eng',
audioLanguage: 'jpn',
enableDoH: false,
doHURL: 'https://cloudflare-dns.com/dns-query',
disableSubtitleBlur: false,
toshoURL: decodeURIComponent(atob('aHR0cHM6Ly9mZWVkLmFuaW1ldG9zaG8ub3JnLw==')),
showDetailsInRPC: true,
smoothScroll: true,
cards: 'small',
expandingSidebar: true,
torrentPath: undefined,
font: undefined,
angle: 'default'
}
export const subtitleExtensions = ['srt', 'vtt', 'ass', 'ssa', 'sub', 'txt']
export const subRx = new RegExp(`.(${subtitleExtensions.join('|')})$`, 'i')
export const videoExtensions = ['3g2', '3gp', 'asf', 'avi', 'dv', 'flv', 'gxf', 'm2ts', 'm4a', 'm4b', 'm4p', 'm4r', 'm4v', 'mkv', 'mov', 'mp4', 'mpd', 'mpeg', 'mpg', 'mxf', 'nut', 'ogm', 'ogv', 'swf', 'ts', 'vob', 'webm', 'wmv', 'wtv']
export const videoRx = new RegExp(`.(${videoExtensions.join('|')})$`, 'i')
// freetype supported
export const fontExtensions = ['ttf', 'ttc', 'woff', 'woff2', 'otf', 'cff', 'otc', 'pfa', 'pfb', 'pcf', 'fnt', 'bdf', 'pfr', 'eot']
export const fontRx = new RegExp(`.(${fontExtensions.join('|')})$`, 'i')

View file

@ -0,0 +1,232 @@
import WebTorrent from 'webtorrent'
import HTTPTracker from 'bittorrent-tracker/lib/client/http-tracker.js'
import { hex2bin, arr2hex, text2arr } from 'uint8-util'
import Parser from './parser.js'
import { defaults, fontRx, subRx, videoRx } from './util.js'
const LARGE_FILESIZE = 32_000_000_000
export default class TorrentClient extends WebTorrent {
static excludedErrorMessages = ['WebSocket', 'User-Initiated Abort, reason=', 'Connection failed.']
constructor (ipc, statfs, serverMode, controller) {
const settings = { ...defaults, ...(JSON.parse(localStorage.getItem('settings')) || {}) }
super({
dht: !settings.torrentDHT,
maxConns: settings.maxConns,
downloadLimit: settings.torrentSpeed * 1048576 || 0,
uploadLimit: settings.torrentSpeed * 1572864 || 0, // :trolled:
torrentPort: settings.torrentPort || 0,
dhtPort: settings.dhtPort || 0
})
this._ready = new Promise(resolve => {
ipc.on('port', ({ ports }) => {
this.message = ports[0].postMessage.bind(ports[0])
resolve()
ports[0].onmessage = ({ data }) => {
if (data.type === 'load') this.loadLastTorrent()
if (data.type === 'destroy') this.destroy()
this.handleMessage({ data })
}
})
ipc.on('destroy', this.destroy.bind(this))
})
this.settings = settings
this.serverMode = serverMode
this.statfs = statfs
this.current = null
this.parsed = false
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('progress', this.current?.progress)
}, 2000)
this.on('torrent', this.handleTorrent.bind(this))
const createServer = controller => {
this.server = this.createServer({ controller }, serverMode)
this.server.listen(0, () => {})
}
if (controller) {
controller.then(createServer)
} else {
createServer()
}
this.trackers = {
cat: new HTTPTracker({}, atob('aHR0cDovL255YWEudHJhY2tlci53Zjo3Nzc3L2Fubm91bmNl'))
}
this.on('error', this.dispatchError.bind(this))
}
loadLastTorrent () {
const torrent = localStorage.getItem('torrent')
if (torrent) this.addTorrent(new Uint8Array(JSON.parse(torrent)), JSON.parse(localStorage.getItem('lastFinished')))
}
async handleTorrent (torrent) {
const files = torrent.files.map(file => {
return {
infoHash: torrent.infoHash,
name: file.name,
type: file.type,
size: file.size,
path: file.path,
url: this.serverMode === 'node' ? 'http://localhost:' + this.server.address().port + file.streamURL : file.streamURL
}
})
if (torrent.length > LARGE_FILESIZE) {
for (const file of torrent.files) {
file.deselect()
}
this.dispatch('warn', 'Detected Large Torrent! To Conserve Drive Space Files Will Be Downloaded Selectively Instead Of Downloading The Entire Torrent.')
}
this.dispatch('files', files)
this.dispatch('magnet', { magnet: torrent.magnetURI, hash: torrent.infoHash })
localStorage.setItem('torrent', JSON.stringify([...torrent.torrentFile]))
const { bsize, bavail } = await this.statfs(torrent.path)
if (torrent.length > bsize * bavail) {
this.dispatch('error', 'Torrent Too Big! This Torrent Exceeds The Selected Drive\'s Available Space. Change Download Location In Torrent Settings To A Drive With More Space And Restart The App!')
}
}
async findFontFiles (targetFile) {
const files = this.torrents[0].files
const fontFiles = files.filter(file => fontRx.test(file.name))
const map = {}
// deduplicate fonts
// some releases have duplicate fonts for diff languages
// if they have different chars, we can't find that out anyways
// so some chars might fail, on REALLY bad releases
for (const file of fontFiles) {
map[file.name] = file
}
for (const file of Object.values(map)) {
const data = await file.arrayBuffer()
if (targetFile !== this.current) return
this.dispatch('file', { data: new Uint8Array(data) }, [data])
}
}
async findSubtitleFiles (targetFile) {
const files = this.torrents[0].files
const videoFiles = files.filter(file => videoRx.test(file.name))
const videoName = targetFile.name.substring(0, targetFile.name.lastIndexOf('.')) || targetFile.name
// array of subtitle files that match video name, or all subtitle files when only 1 vid file
const subfiles = files.filter(file => {
return subRx.test(file.name) && (videoFiles.length === 1 ? true : file.name.includes(videoName))
})
for (const file of subfiles) {
const data = await file.arrayBuffer()
if (targetFile !== this.current) return
this.dispatch('subtitleFile', { name: file.name, data: new Uint8Array(data) }, [data])
}
}
_scrape ({ id, infoHashes }) {
this.trackers.cat._request(this.trackers.cat.scrapeUrl, { info_hash: infoHashes.map(infoHash => hex2bin(infoHash)) }, (err, data) => {
if (err) {
this.dispatch('error', err)
return this.dispatch('scrape', { id, result: [] })
}
const { files } = data
const result = []
for (const [key, data] of Object.entries(files || {})) {
result.push({ hash: key.length !== 40 ? arr2hex(text2arr(key)) : key, ...data })
}
this.dispatch('scrape', { id, result })
})
}
dispatchError (e) {
if (e instanceof ErrorEvent) return this.dispatchError(e.error)
if (e instanceof PromiseRejectionEvent) return this.dispatchError(e.reason)
for (const exclude of TorrentClient.excludedErrorMessages) {
if (e.message?.startsWith(exclude)) return
}
this.dispatch('error', e)
}
async addTorrent (data, skipVerify = false) {
const existing = await this.get(data)
if (existing) {
if (existing.ready) this.handleTorrent(existing)
return
}
localStorage.setItem('lastFinished', false)
if (this.torrents.length) await this.remove(this.torrents[0])
const torrent = await this.add(data, {
private: this.settings.torrentPeX,
path: this.settings.torrentPath,
destroyStoreOnDestroy: !this.settings.torrentPersist,
skipVerify,
announce: [
'wss://tracker.openwebtorrent.com',
'wss://tracker.webtorrent.dev',
'wss://tracker.files.fm:7073/announce',
'wss://tracker.btorrent.xyz/',
atob('aHR0cDovL255YWEudHJhY2tlci53Zjo3Nzc3L2Fubm91bmNl')
]
})
torrent.once('done', () => {
localStorage.setItem('lastFinished', true)
})
}
async handleMessage ({ data }) {
switch (data.type) {
case 'current': {
if (data.data) {
const torrent = await this.get(data.data.infoHash)
const found = torrent?.files.find(file => file.path === data.data.path)
if (!found) return
if (this.current) {
this.current.removeAllListeners('stream')
}
this.parser?.destroy()
found.select()
this.current = found
this.parser = new Parser(this, found)
this.findSubtitleFiles(found)
this.findFontFiles(found)
}
break
}
case 'scrape': {
this._scrape(data.data)
break
}
case 'torrent': {
this.addTorrent(data.data)
break
}
}
}
async dispatch (type, data, transfer) {
await this._ready
this.message?.({ type, data }, transfer)
}
destroy () {
this.parser?.destroy()
this.server.close()
super.destroy()
}
}

View file

@ -18,6 +18,7 @@
"svelte-keybinds": "^1.0.5",
"svelte-loader": "^3.1.9",
"svelte-miniplayer": "^1.0.3",
"svelte-sonner": "^0.3.3"
"svelte-sonner": "^0.3.3",
"webpack-merge": "^5.10.0"
}
}

View file

@ -1,39 +0,0 @@
export const defaults = {
playerAutoplay: true,
playerPause: true,
playerAutocomplete: true,
rssQuality: '1080',
rssFeedsNew: [['New Releases', 'SubsPlease']],
rssAutoplay: true,
torrentSpeed: 10,
torrentPersist: false,
torrentDHT: false,
torrentPeX: false,
torrentPort: 0,
dhtPort: 0,
missingFont: true,
maxConns: 20,
subtitleLanguage: 'eng',
audioLanguage: 'jpn',
enableDoH: false,
doHURL: 'https://cloudflare-dns.com/dns-query',
disableSubtitleBlur: false,
toshoURL: decodeURIComponent(atob('aHR0cHM6Ly9mZWVkLmFuaW1ldG9zaG8ub3JnLw==')),
showDetailsInRPC: true,
smoothScroll: true,
cards: 'small',
expandingSidebar: true,
torrentPath: undefined,
font: undefined,
angle: 'default'
}
export const subtitleExtensions = ['srt', 'vtt', 'ass', 'ssa', 'sub', 'txt']
export const subRx = new RegExp(`.(${subtitleExtensions.join('|')})$`, 'i')
export const videoExtensions = ['3g2', '3gp', 'asf', 'avi', 'dv', 'flv', 'gxf', 'm2ts', 'm4a', 'm4b', 'm4p', 'm4r', 'm4v', 'mkv', 'mov', 'mp4', 'mpd', 'mpeg', 'mpg', 'mxf', 'nut', 'ogm', 'ogv', 'swf', 'ts', 'vob', 'webm', 'wmv', 'wtv']
export const videoRx = new RegExp(`.(${videoExtensions.join('|')})$`, 'i')
// freetype supported
export const fontExtensions = ['ttf', 'ttc', 'woff', 'woff2', 'otf', 'cff', 'otc', 'pfa', 'pfb', 'pcf', 'fnt', 'bdf', 'pfr', 'eot']
export const fontRx = new RegExp(`.(${fontExtensions.join('|')})$`, 'i')

View file

@ -1,6 +1,7 @@
<script>
import { settings } from '@/modules/settings.js'
import { click } from '@/modules/click.js'
import IPC from '@/modules/ipc.js'
let block = false
async function testConnection () {
@ -26,10 +27,10 @@
<div class='font-size-16'>Most features of Miru will not function correctly without being able to connect to Tosho.</div>
<div class='font-size-16'>If you enable a VPN a restart might be required for it to take effect.</div>
<!-- eslint-disable-next-line svelte/valid-compile -->
<div class='font-size-16'>Visit <a class='text-primary pointer' use:click={() => { window.IPC.emit('open', 'https://thewiki.moe/tutorials/unblock/') }}>this guide</a> for a tutorial on how to bypass ISP blocks.</div>
<div class='font-size-16'>Visit <a class='text-primary pointer' use:click={() => { IPC.emit('open', 'https://thewiki.moe/tutorials/unblock/') }}>this guide</a> for a tutorial on how to bypass ISP blocks.</div>
<div class='d-flex w-full mt-20 pt-20'>
<button class='btn ml-auto mr-5' type='button' on:click={() => { block = false }}>I Understand</button>
<button class='btn btn-primary mr-5' type='button' on:click={() => { window.IPC.emit('open', 'https://thewiki.moe/tutorials/unblock/') }}>Open Guide</button>
<button class='btn btn-primary mr-5' type='button' on:click={() => { IPC.emit('open', 'https://thewiki.moe/tutorials/unblock/') }}>Open Guide</button>
<button class='btn btn-primary' type='button' on:click={testConnection}>Reconnect</button>
</div>
</div>

View file

@ -1,9 +1,10 @@
<script context='module'>
import { writable } from 'simple-store-svelte'
import { resolveFileMedia } from '@/modules/anime.js'
import { videoRx } from '@/../common/util.js'
import { videoRx } from '@/modules/util.js'
import { tick } from 'svelte'
import { state } from '../WatchTogether/WatchTogether.svelte'
import IPC from '@/modules/ipc.js'
const episodeRx = /Episode (\d+) - (.*)/
@ -180,6 +181,7 @@
})
function setMediaSession (nowPlaying) {
if (typeof MediaMetadata === 'undefined') return
const name = [nowPlaying.title, nowPlaying.episode, nowPlaying.episodeTitle, 'Miru'].filter(i => i).join(' - ')
const metadata =
@ -238,7 +240,7 @@
}
]
}
window.IPC.emit('discord', { activity })
IPC.emit('discord', { activity })
}
state.subscribe(() => {
setDiscordRPC()

View file

@ -5,8 +5,7 @@
import { createEventDispatcher } from 'svelte'
import { alEntry } from '@/modules/anilist.js'
import Subtitles from '@/modules/subtitles.js'
import { toTS, fastPrettyBytes } from '@/modules/util.js'
import { videoRx } from '@/../common/util.js'
import { toTS, fastPrettyBytes, videoRx } from '@/modules/util.js'
import { toast } from 'svelte-sonner'
import { getChaptersAniSkip } from '@/modules/anime.js'
import Seekbar from 'perfect-seekbar'

View file

@ -2,8 +2,9 @@
import { toast } from 'svelte-sonner'
import { click } from '@/modules/click.js'
import { resetSettings, settings } from '@/modules/settings.js'
import IPC from '@/modules/ipc.js'
if (settings.value.enableDoH) window.IPC.emit('doh', settings.value.doHURL)
if (settings.value.enableDoH) IPC.emit('doh', settings.value.doHURL)
export const platformMap = {
aix: 'Aix',
darwin: 'MacOS',
@ -14,14 +15,14 @@
win32: 'Windows'
}
let version = '1.0.0'
window.IPC.on('version', data => (version = data))
window.IPC.emit('version')
IPC.on('version', data => (version = data))
IPC.emit('version')
function updateAngle () {
window.IPC.emit('angle', settings.value.angle)
IPC.emit('angle', settings.value.angle)
}
let wasUpdated = false
window.IPC.on('update-available', () => {
IPC.on('update-available', () => {
if (!wasUpdated) {
wasUpdated = true
toast('Auto Updater', {
@ -29,13 +30,13 @@
})
}
})
window.IPC.on('update-downloaded', () => {
IPC.on('update-downloaded', () => {
toast.success('Auto Updater', {
description: 'A new version of Miru has downloaded. You can restart to update!'
})
})
function checkUpdate () {
window.IPC.emit('update')
IPC.emit('update')
}
setInterval(checkUpdate, 1200000)
@ -44,7 +45,7 @@
const json = await res.json()
return json.map(({ body, tag_name: version }) => ({ body, version }))
})()
window.IPC.emit('discord_status', settings.value.showDetailsInRPC)
IPC.emit('discord_status', settings.value.showDetailsInRPC)
</script>
<script>
@ -52,11 +53,11 @@
import FontSelect from '../components/FontSelect.svelte'
import { onDestroy } from 'svelte'
import { variables } from '@/modules/themes.js'
import { defaults } from '@/../common/util.js'
import { defaults } from '@/modules/util.js'
import HomeSections from './Settings/HomeSectionsSettings.svelte'
onDestroy(() => {
window.IPC.off('path')
IPC.off('path')
})
const groups = {
@ -91,11 +92,11 @@
desc: 'Version change log.'
}
}
$: window.IPC.emit('discord_status', $settings.showDetailsInRPC)
$: IPC.emit('discord_status', $settings.showDetailsInRPC)
function handleFolder () {
window.IPC.emit('dialog')
IPC.emit('dialog')
}
window.IPC.on('path', data => {
IPC.on('path', data => {
$settings.torrentPath = data
})
async function changeFont ({ detail }) {
@ -144,7 +145,7 @@
</TabLabel>
{/each}
<button
use:click={() => window.IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/')}
use:click={() => IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/')}
class='btn btn-primary mx-20 mt-auto'
type='button'>
Donate

View file

@ -1,6 +1,7 @@
<script>
import { alToken } from '../../views/Settings.svelte'
import { addToast } from '../../components/Toasts.svelte'
import IPC from '@/modules/ipc.js'
import { alRequest } from '@/modules/anilist.js'
import { getContext } from 'svelte'
import { getMediaMaxEp } from '@/modules/anime.js'
@ -73,7 +74,7 @@
})
}
function openInBrowser (url) {
window.IPC.emit('open', url)
IPC.emit('open', url)
}
function getPlayText (media) {
if (media.mediaListEntry) {

View file

@ -2,6 +2,7 @@
import { alToken } from '@/modules/settings.js'
import { alRequest } from '@/modules/anilist.js'
import { click } from '@/modules/click.js'
import IPC from '@/modules/ipc.js'
export let media = null
let following = null
async function updateFollowing (media) {
@ -25,7 +26,7 @@
<img src={friend.user.avatar.medium} alt='avatar' class='w-50 h-50 img-fluid rounded cover-img' />
<span class='my-0 pl-20 mr-auto text-truncate'>{friend.user.name}</span>
<span class='my-0 px-10 text-capitalize'>{friend.status.toLowerCase()}</span>
<span class='material-symbols-outlined pointer text-primary font-size-18' use:click={() => window.IPC.emit('open', 'https://anilist.co/user/' + friend.user.name)}> open_in_new </span>
<span class='material-symbols-outlined pointer text-primary font-size-18' use:click={() => IPC.emit('open', 'https://anilist.co/user/' + friend.user.name)}> open_in_new </span>
</div>
{/each}
</div>

View file

@ -10,6 +10,7 @@
import ToggleList from './ToggleList.svelte'
import Following from './Following.svelte'
import smoothScroll from '@/modules/scroll.js'
import IPC from '@/modules/ipc.js'
const view = getContext('view')
function close () {
@ -70,7 +71,7 @@
})
}
function openInBrowser (url) {
window.IPC.emit('open', url)
IPC.emit('open', url)
}
</script>

View file

@ -1,5 +1,6 @@
<script>
import { click } from '@/modules/click.js'
import IPC from '@/modules/ipc.js'
export let peers
export let invite
export let cleanup
@ -20,7 +21,7 @@
{/if}
<h4 class='my-0 pl-20 mr-auto'>{peer.user?.name || 'Anonymous'}</h4>
{#if peer.user?.name}
<span class='material-symbols-outlined pointer text-primary' use:click={() => window.IPC.emit('open', 'https://anilist.co/user/' + peer.user?.name)}> open_in_new </span>
<span class='material-symbols-outlined pointer text-primary' use:click={() => IPC.emit('open', 'https://anilist.co/user/' + peer.user?.name)}> open_in_new </span>
{/if}
<!-- {#if state === 'host'}
<span class='material-symbols-outlined ml-15 pointer text-danger' use:click={() => peer.peer.pc.close()}> logout </span>

View file

@ -7,6 +7,7 @@
import { page } from '@/App.svelte'
import P2PT from 'p2pt'
import { click } from '@/modules/click.js'
import IPC from '@/modules/ipc.js'
import 'browser-event-target-emitter'
export const w2gEmitter = new EventTarget()
@ -130,7 +131,7 @@
index: 0
}
window.IPC.on('w2glink', link => {
IPC.on('w2glink', link => {
joinLobby(link)
page.set('watchtogether')
})

View file

@ -6,9 +6,9 @@ const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = (parentDir, alias = {}) => ({
module.exports = (parentDir, alias = {}, aliasFields = 'browser') => ({
devtool: 'source-map',
entry: join(__dirname, 'main.js'),
entry: [join(__dirname, 'main.js')],
output: {
path: join(parentDir, 'build'),
filename: 'renderer.js'
@ -51,7 +51,7 @@ module.exports = (parentDir, alias = {}) => ({
]
},
resolve: {
aliasFields: ['browser'],
aliasFields: [aliasFields],
alias: {
...alias,
'@': __dirname,

View file

@ -24,7 +24,8 @@
"electron-updater": "^6.1.4"
},
"dependencies": {
"utp-native": "^2.5.3"
"utp-native": "^2.5.3",
"webpack-merge": "^5.10.0"
},
"standard": {
"ignore": [

View file

@ -1,237 +1,7 @@
import WebTorrent from 'webtorrent'
import { ipcRenderer } from 'electron'
import HTTPTracker from 'bittorrent-tracker/lib/client/http-tracker.js'
import { hex2bin, arr2hex, text2arr } from 'uint8-util'
import Parser from './parser.js'
import { defaults, fontRx, subRx, videoRx } from 'common/util.js'
import { statfs } from 'fs/promises'
const LARGE_FILESIZE = 32_000_000_000
class TorrentClient extends WebTorrent {
static excludedErrorMessages = ['WebSocket', 'User-Initiated Abort, reason=', 'Connection failed.']
constructor () {
const settings = { ...defaults, ...(JSON.parse(localStorage.getItem('settings')) || {}) }
super({
dht: !settings.torrentDHT,
maxConns: settings.maxConns,
downloadLimit: settings.torrentSpeed * 1048576 || 0,
uploadLimit: settings.torrentSpeed * 1572864 || 0, // :trolled:
torrentPort: settings.torrentPort || 0,
dhtPort: settings.dhtPort || 0
})
this._ready = new Promise(resolve => {
ipcRenderer.on('port', ({ ports }) => {
this.message = ports[0].postMessage.bind(ports[0])
resolve()
ports[0].onmessage = ({ data }) => {
if (data.type === 'load') this.loadLastTorrent()
if (data.type === 'destroy') this.destroy()
this.handleMessage({ data })
}
})
ipcRenderer.on('destroy', this.destroy.bind(this))
})
this.settings = settings
this.current = null
this.parsed = false
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('progress', this.current?.progress)
}, 2000)
this.on('torrent', this.handleTorrent.bind(this))
this.server = this.createServer(undefined, 'node')
this.server.listen(0)
this.trackers = {
cat: new HTTPTracker({}, atob('aHR0cDovL255YWEudHJhY2tlci53Zjo3Nzc3L2Fubm91bmNl'))
}
this.on('error', this.dispatchError.bind(this))
process.on('uncaughtException', this.dispatchError.bind(this))
window.addEventListener('error', this.dispatchError.bind(this))
window.addEventListener('unhandledrejection', this.dispatchError.bind(this))
}
loadLastTorrent () {
const torrent = localStorage.getItem('torrent')
if (torrent) this.addTorrent(new Uint8Array(JSON.parse(torrent)), JSON.parse(localStorage.getItem('lastFinished')))
}
async handleTorrent (torrent) {
const files = torrent.files.map(file => {
return {
infoHash: torrent.infoHash,
name: file.name,
type: file.type,
size: file.size,
path: file.path,
url: 'http://localhost:' + this.server.address().port + file.streamURL
}
})
if (torrent.length > LARGE_FILESIZE) {
for (const file of torrent.files) {
file.deselect()
}
this.dispatch('warn', 'Detected Large Torrent! To Conserve Drive Space Files Will Be Downloaded Selectively Instead Of Downloading The Entire Torrent.')
}
this.dispatch('files', files)
this.dispatch('magnet', { magnet: torrent.magnetURI, hash: torrent.infoHash })
localStorage.setItem('torrent', JSON.stringify([...torrent.torrentFile]))
const { bsize, bavail } = await statfs(torrent.path)
if (torrent.length > bsize * bavail) {
this.dispatch('error', 'Torrent Too Big! This Torrent Exceeds The Selected Drive\'s Available Space. Change Download Location In Torrent Settings To A Drive With More Space And Restart The App!')
}
}
async findFontFiles (targetFile) {
const files = this.torrents[0].files
const fontFiles = files.filter(file => fontRx.test(file.name))
const map = {}
// deduplicate fonts
// some releases have duplicate fonts for diff languages
// if they have different chars, we can't find that out anyways
// so some chars might fail, on REALLY bad releases
for (const file of fontFiles) {
map[file.name] = file
}
for (const file of Object.values(map)) {
const data = await file.arrayBuffer()
if (targetFile !== this.current) return
this.dispatch('file', { data: new Uint8Array(data) }, [data])
}
}
async findSubtitleFiles (targetFile) {
const files = this.torrents[0].files
const videoFiles = files.filter(file => videoRx.test(file.name))
const videoName = targetFile.name.substring(0, targetFile.name.lastIndexOf('.')) || targetFile.name
// array of subtitle files that match video name, or all subtitle files when only 1 vid file
const subfiles = files.filter(file => {
return subRx.test(file.name) && (videoFiles.length === 1 ? true : file.name.includes(videoName))
})
for (const file of subfiles) {
const data = await file.arrayBuffer()
if (targetFile !== this.current) return
this.dispatch('subtitleFile', { name: file.name, data: new Uint8Array(data) }, [data])
}
}
_scrape ({ id, infoHashes }) {
this.trackers.cat._request(this.trackers.cat.scrapeUrl, { info_hash: infoHashes.map(infoHash => hex2bin(infoHash)) }, (err, data) => {
if (err) {
this.dispatch('error', err)
return this.dispatch('scrape', { id, result: [] })
}
const { files } = data
const result = []
for (const [key, data] of Object.entries(files || {})) {
result.push({ hash: key.length !== 40 ? arr2hex(text2arr(key)) : key, ...data })
}
this.dispatch('scrape', { id, result })
})
}
dispatchError (e) {
if (e instanceof ErrorEvent) return this.dispatchError(e.error)
if (e instanceof PromiseRejectionEvent) return this.dispatchError(e.reason)
for (const exclude of TorrentClient.excludedErrorMessages) {
if (e.message?.startsWith(exclude)) return
}
this.dispatch('error', e)
}
async addTorrent (data, skipVerify = false) {
let id
if (typeof data === 'string' && data.startsWith('http')) {
// IMPORTANT, this is because node's get bypasses proxies, wut????
const res = await fetch(data)
id = new Uint8Array(await res.arrayBuffer())
} else {
id = data
}
const existing = await this.get(id)
if (existing) {
if (existing.ready) this.handleTorrent(existing)
return
}
localStorage.setItem('lastFinished', false)
if (this.torrents.length) await this.remove(this.torrents[0])
const torrent = await this.add(id, {
private: this.settings.torrentPeX,
path: this.settings.torrentPath,
destroyStoreOnDestroy: !this.settings.torrentPersist,
skipVerify,
announce: [
'wss://tracker.openwebtorrent.com',
'wss://tracker.webtorrent.dev',
'wss://tracker.files.fm:7073/announce',
'wss://tracker.btorrent.xyz/',
atob('aHR0cDovL255YWEudHJhY2tlci53Zjo3Nzc3L2Fubm91bmNl')
]
})
torrent.once('done', () => {
localStorage.setItem('lastFinished', true)
})
}
async handleMessage ({ data }) {
switch (data.type) {
case 'current': {
if (data.data) {
const torrent = await this.get(data.data.infoHash)
const found = torrent?.files.find(file => file.path === data.data.path)
if (!found) return
if (this.current) {
this.current.removeAllListeners('stream')
}
this.parser?.destroy()
found.select()
this.current = found
this.parser = new Parser(this, found)
this.findSubtitleFiles(found)
this.findFontFiles(found)
}
break
}
case 'scrape': {
this._scrape(data.data)
break
}
case 'torrent': {
this.addTorrent(data.data)
break
}
}
}
async dispatch (type, data, transfer) {
await this._ready
this.message?.({ type, data }, transfer)
}
destroy () {
this.parser?.destroy()
this.server.close()
super.destroy()
}
}
import TorrentClient from 'common/modules/webtorrent.js'
// @ts-ignore
window.client = new TorrentClient()
window.client = new TorrentClient(ipcRenderer, statfs, 'node')

File diff suppressed because it is too large Load diff