mirror of
https://github.com/NoCrypt/migu.git
synced 2026-04-20 16:12:31 +00:00
feat: capacitor build
fix: separate IPC calls
This commit is contained in:
parent
832b3bb33d
commit
9a73497fc6
34 changed files with 2655 additions and 655 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<h1>uwu</h1>
|
||||
497
capacitor/src/chrome-dgram.js
Normal file
497
capacitor/src/chrome-dgram.js
Normal 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
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
40
capacitor/src/ipc.js
Normal 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'
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import App from './App.svelte'
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app')
|
||||
})
|
||||
|
||||
export default app
|
||||
21
capacitor/src/webtorrent.js
Normal file
21
capacitor/src/webtorrent.js
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
1
common/modules/ipc.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default window.IPC
|
||||
67
common/modules/parser.js
Normal file
67
common/modules/parser.js
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
232
common/modules/webtorrent.js
Normal file
232
common/modules/webtorrent.js
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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')
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
709
pnpm-lock.yaml
709
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue