mirror of
https://github.com/NoCrypt/migu.git
synced 2026-03-11 17:45:32 +00:00
feat: ESM, webtorrent v2
This commit is contained in:
parent
7fb6a83138
commit
764c5a1519
18 changed files with 2805 additions and 876 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
50
package.json
50
package.json
|
|
@ -1,26 +1,32 @@
|
|||
{
|
||||
"name": "Miru",
|
||||
"version": "3.11.6",
|
||||
"version": "4.0.0",
|
||||
"author": "ThaUnknown_ <ThaUnknown@users.noreply.github.com>",
|
||||
"description": "Stream anime torrents, real-time with no waiting for downloads.",
|
||||
"main": "src/index.js",
|
||||
"main": "build/main.js",
|
||||
"homepage": "https://github.com/ThaUnknown/miru#readme",
|
||||
"scripts": {
|
||||
"start": "SET NODE_ENV=development & concurrently --kill-others \"pnpm run web:watch\" \"pnpm run electron:start\"",
|
||||
"web:watch": "vite",
|
||||
"electron:start": "electron src",
|
||||
"build": "vite build && electron-builder",
|
||||
"publish": "vite build && electron-builder -p always"
|
||||
"start": "SET NODE_ENV=development & concurrently --kill-others \"npm run web:watch\" \"npm run electron:start\"",
|
||||
"web:watch": "webpack serve",
|
||||
"web:build": "SET NODE_ENV=production & webpack build",
|
||||
"electron:start": "electron ./build/main.js",
|
||||
"build": "npm run web:build && electron-builder",
|
||||
"publish": "npm run web:build && electron-builder -p always"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^1.4.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"electron": "23.1.0",
|
||||
"electron-builder": "^23.6.0",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"svelte": "^3.55.0",
|
||||
"vite": "4.2.1",
|
||||
"vite-plugin-commonjs": "^0.5.3"
|
||||
"html-webpack-plugin": "^5.5.1",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"svelte": "^3.59.1",
|
||||
"svelte-loader": "^3.1.8",
|
||||
"webpack": "^5.85.0",
|
||||
"webpack-cli": "^5.1.3",
|
||||
"webpack-dev-server": "^4.15.0"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
|
|
@ -33,6 +39,9 @@
|
|||
]
|
||||
},
|
||||
"build": {
|
||||
"directories": {
|
||||
"buildResources": "buildResources"
|
||||
},
|
||||
"asarUnpack": "**/*.node",
|
||||
"electronDownload": {
|
||||
"mirror": "https://github.com/aa910d571134/feb7c2e1a10f/releases/download/",
|
||||
|
|
@ -52,20 +61,17 @@
|
|||
"repo": "miru"
|
||||
}
|
||||
],
|
||||
"afterSign": "./build/notarize.js",
|
||||
"afterSign": "./buildResources/notarize.js",
|
||||
"appId": "com.github.thaunknown.miru",
|
||||
"productName": "Miru",
|
||||
"files": [
|
||||
"src/*",
|
||||
"src/main/*",
|
||||
"src/renderer/dist/**/*",
|
||||
"!node_modules/**/*.{mk,a,o,h}"
|
||||
"build/**/*"
|
||||
],
|
||||
"mac": {
|
||||
"artifactName": "${os}-${name}-${version}.${ext}",
|
||||
"singleArchFiles": "node_modules/+(register-scheme|utp-native)/**",
|
||||
"category": "public.app-category.video",
|
||||
"icon": "build/icon.icns",
|
||||
"icon": "buildResources/icon.icns",
|
||||
"target": [
|
||||
{
|
||||
"arch": "universal",
|
||||
|
|
@ -105,23 +111,21 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"anitomyscript": "github:ThaUnknown/anitomyscript",
|
||||
"anitomyscript": "github:ThaUnknown/anitomyscript#42290c4b3f256893be08a4e89051f448ff5e9d00",
|
||||
"bottleneck": "^2.19.5",
|
||||
"browser-event-target-emitter": "^1.0.0",
|
||||
"discord-rpc": "4.0.1",
|
||||
"ebml-iterator": "^1.0.2",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-updater": "^4.6.5",
|
||||
"jassub": "1.7.1",
|
||||
"js-levenshtein": "^1.1.6",
|
||||
"matroska-subtitles": "github:ThaUnknown/matroska-subtitles#redist",
|
||||
"mime": "^3.0.0",
|
||||
"pako": "^2.1.0",
|
||||
"perfect-seekbar": "^1.1.0",
|
||||
"pump": "^3.0.0",
|
||||
"quartermoon": "^1.2.3",
|
||||
"range-parser": "^1.2.1",
|
||||
"simple-store-svelte": "^1.0.0",
|
||||
"svelte-keybinds": "1.0.5",
|
||||
"svelte-miniplayer": "1.0.3",
|
||||
"webtorrent": "^1.9.6"
|
||||
"webtorrent": "^2.0.29"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2975
pnpm-lock.yaml
2975
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,6 @@
|
|||
const WebTorrent = require('webtorrent')
|
||||
const http = require('http')
|
||||
const pump = require('pump')
|
||||
const rangeParser = require('range-parser')
|
||||
const mime = require('mime')
|
||||
const { SubtitleParser, SubtitleStream } = require('matroska-subtitles')
|
||||
const { ipcRenderer } = require('electron')
|
||||
import WebTorrent from 'webtorrent'
|
||||
import { SubtitleParser, SubtitleStream } from './matroska.js'
|
||||
import { ipcRenderer } from 'electron'
|
||||
|
||||
class TorrentClient extends WebTorrent {
|
||||
constructor (settings) {
|
||||
|
|
@ -33,56 +29,7 @@ class TorrentClient extends WebTorrent {
|
|||
}, 2000)
|
||||
this.on('torrent', this.handleTorrent.bind(this))
|
||||
|
||||
this.server = http.createServer((request, response) => {
|
||||
if (!request.url) return null
|
||||
let [infoHash, ...filePath] = request.url.slice(request.url.indexOf('/webtorrent/') + 12).split('/')
|
||||
filePath = decodeURI(filePath.join('/'))
|
||||
if (!infoHash || !filePath) return null
|
||||
|
||||
const file = this.get(infoHash)?.files.find(file => file.path === filePath)
|
||||
if (!file) return null
|
||||
|
||||
response.setHeader('Access-Control-Allow-Origin', '*')
|
||||
response.setHeader('Content-Type', mime.getType(file.name) || 'application/octet-stream')
|
||||
|
||||
response.setHeader('Accept-Ranges', 'bytes')
|
||||
|
||||
let range = rangeParser(file.length, request.headers.range || '')
|
||||
|
||||
if (Array.isArray(range)) {
|
||||
response.statusCode = 206
|
||||
range = range[0]
|
||||
|
||||
response.setHeader(
|
||||
'Content-Range',
|
||||
`bytes ${range.start}-${range.end}/${file.length}`
|
||||
)
|
||||
response.setHeader('Content-Length', range.end - range.start + 1)
|
||||
} else {
|
||||
response.statusCode = 200
|
||||
range = null
|
||||
response.setHeader('Content-Length', file.length)
|
||||
}
|
||||
|
||||
if (response.method === 'HEAD') {
|
||||
return response.end()
|
||||
}
|
||||
|
||||
let stream = file.createReadStream(range)
|
||||
|
||||
if (stream && !this.parsed) {
|
||||
if (file.name.endsWith('.mkv')) {
|
||||
this.parserInstance = new SubtitleStream(this.parserInstance)
|
||||
this.handleSubtitleParser(this.parserInstance, true)
|
||||
stream = pump(stream, this.parserInstance)
|
||||
}
|
||||
}
|
||||
|
||||
pump(stream, response)
|
||||
})
|
||||
|
||||
this.server.on('error', console.warn)
|
||||
|
||||
this.server = this.createServer(undefined, 'node')
|
||||
this.server.listen(0)
|
||||
}
|
||||
|
||||
|
|
@ -91,10 +38,10 @@ class TorrentClient extends WebTorrent {
|
|||
return {
|
||||
infoHash: torrent.infoHash,
|
||||
name: file.name,
|
||||
type: file._getMimeType(),
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
path: file.path,
|
||||
url: encodeURI(`http://localhost:${this.server.address().port}/webtorrent/${torrent.infoHash}/${file.path}`)
|
||||
url: 'http://localhost:' + this.server.address().port + file.streamURL
|
||||
}
|
||||
})
|
||||
this.dispatch('files', files)
|
||||
|
|
@ -102,7 +49,7 @@ class TorrentClient extends WebTorrent {
|
|||
this.dispatch('torrent', Array.from(torrent.torrentFile))
|
||||
}
|
||||
|
||||
handleMessage ({ data }) {
|
||||
async handleMessage ({ data }) {
|
||||
switch (data.type) {
|
||||
case 'current': {
|
||||
this.current?.removeListener('done', this.boundParse)
|
||||
|
|
@ -112,11 +59,18 @@ class TorrentClient extends WebTorrent {
|
|||
this.current = null
|
||||
this.parsed = false
|
||||
if (data.data) {
|
||||
this.current = this?.get(data.data.infoHash)?.files.find(file => file.path === data.data.path)
|
||||
this.current = (await this.get(data.data.infoHash))?.files.find(file => file.path === data.data.path)
|
||||
if (this.current?.name.endsWith('.mkv')) {
|
||||
// if (this.current.done) this.parseSubtitles()
|
||||
// this.current.once('done', this.boundParse)
|
||||
this.parseFonts(this.current)
|
||||
this.current.on('iterator', ({ iterator, req }, cb) => {
|
||||
if (!this.parsed) {
|
||||
this.stream = new SubtitleStream(this.stream, iterator)
|
||||
this.handleSubtitleParser(this.stream, true)
|
||||
cb(this.stream)
|
||||
}
|
||||
})
|
||||
}
|
||||
// TODO: findSubtitleFiles(current)
|
||||
}
|
||||
|
|
@ -124,7 +78,7 @@ class TorrentClient extends WebTorrent {
|
|||
}
|
||||
case 'torrent': {
|
||||
const id = typeof data.data !== 'string' ? Buffer.from(data.data) : data.data
|
||||
const existing = this.get(id)
|
||||
const existing = await this.get(id)
|
||||
if (existing) {
|
||||
if (existing.ready) return this.handleTorrent(existing)
|
||||
existing.once('ready', this.handleTorrent.bind(this))
|
||||
|
|
@ -179,21 +133,17 @@ class TorrentClient extends WebTorrent {
|
|||
}
|
||||
|
||||
parseFonts (file) {
|
||||
const stream = new SubtitleParser()
|
||||
const stream = new SubtitleParser(file)
|
||||
this.handleSubtitleParser(stream)
|
||||
stream.once('tracks', tracks => {
|
||||
if (!tracks.length) {
|
||||
this.parsed = true
|
||||
stream.destroy()
|
||||
fileStreamStream.destroy()
|
||||
}
|
||||
})
|
||||
stream.once('subtitle', () => {
|
||||
fileStreamStream.destroy()
|
||||
stream.destroy()
|
||||
})
|
||||
const fileStreamStream = file.createReadStream()
|
||||
fileStreamStream.pipe(stream)
|
||||
}
|
||||
|
||||
handleSubtitleParser (parser, skipFile) {
|
||||
261
src/background/matroska.js
Normal file
261
src/background/matroska.js
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
import { EbmlIteratorDecoder, EbmlTagId } from 'ebml-iterator'
|
||||
import { EventEmitter } from 'events'
|
||||
import join from 'join-async-iterator'
|
||||
import { inflate } from 'pako'
|
||||
|
||||
const SSA_TYPES = new Set(['ssa', 'ass'])
|
||||
const SSA_KEYS = ['readOrder', 'layer', 'style', 'name', 'marginL', 'marginR', 'marginV', 'effect', 'text']
|
||||
|
||||
function getChild (chunk, tag) {
|
||||
return chunk?.Children.find(({ id }) => id === tag)
|
||||
}
|
||||
function getData (chunk, tag) {
|
||||
return getChild(chunk, tag)?.data
|
||||
}
|
||||
|
||||
export class SubtitleParserBase extends EventEmitter {
|
||||
constructor () {
|
||||
super()
|
||||
this.subtitleTracks = new Map()
|
||||
this.timecodeScale = 1
|
||||
|
||||
this._currentClusterTimecode = null
|
||||
|
||||
this.destroyed = false
|
||||
|
||||
this._tagMap = {
|
||||
// Segment Information
|
||||
[EbmlTagId.TimecodeScale]: ({ data }) => {
|
||||
this.timecodeScale = data / 1000000
|
||||
},
|
||||
// Assumption: This is a Cluster `Timecode`
|
||||
[EbmlTagId.Timecode]: ({ data }) => {
|
||||
this._currentClusterTimecode = data
|
||||
},
|
||||
// Parse attached files, mainly to allow extracting subtitle font files.
|
||||
[EbmlTagId.AttachedFile]: chunk => {
|
||||
this.emit('file', {
|
||||
filename: getData(chunk, EbmlTagId.FileName),
|
||||
mimetype: getData(chunk, EbmlTagId.FileMimeType),
|
||||
data: getData(chunk, EbmlTagId.FileData)
|
||||
})
|
||||
},
|
||||
// Duration for chapters which don't specify an end position
|
||||
[EbmlTagId.Duration]: ({ data }) => {
|
||||
if (this.chapters) {
|
||||
this.chapters[this.chapters.length - 1].end = data
|
||||
this.emit('chapters', this.chapters)
|
||||
} else {
|
||||
this.duration = data
|
||||
}
|
||||
},
|
||||
[EbmlTagId.Tracks]: this.handleTracks.bind(this),
|
||||
[EbmlTagId.BlockGroup]: this.handleBlockGroup.bind(this),
|
||||
[EbmlTagId.Chapters]: this.handleChapters.bind(this)
|
||||
}
|
||||
}
|
||||
|
||||
async * [Symbol.asyncIterator] (stream) {
|
||||
const decoder = new EbmlIteratorDecoder({
|
||||
bufferTagIds: [
|
||||
EbmlTagId.TimecodeScale,
|
||||
EbmlTagId.Tracks,
|
||||
EbmlTagId.BlockGroup,
|
||||
EbmlTagId.AttachedFile,
|
||||
EbmlTagId.Chapters,
|
||||
EbmlTagId.Duration
|
||||
],
|
||||
stream
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if (this.destroyed) return null
|
||||
const tags = decoder.parseTags(chunk)
|
||||
for (const tag of tags) {
|
||||
this._tagMap[tag.id]?.(tag)
|
||||
if (tag.id === EbmlTagId.Tracks) {
|
||||
if (!tag.Children.some(({ id }) => id === EbmlTagId.TrackEntry)) return this.destroy()
|
||||
}
|
||||
}
|
||||
yield chunk
|
||||
}
|
||||
}
|
||||
|
||||
handleTracks (chunk) {
|
||||
for (const entry of chunk.Children.filter(c => c.id === EbmlTagId.TrackEntry)) {
|
||||
// Skip non subtitle tracks
|
||||
if (getData(entry, EbmlTagId.TrackType) !== 0x11) continue
|
||||
|
||||
const codecID = getData(entry, EbmlTagId.CodecID) || ''
|
||||
if (codecID.startsWith('S_TEXT')) {
|
||||
const track = {
|
||||
number: getData(entry, EbmlTagId.TrackNumber),
|
||||
language: getData(entry, EbmlTagId.Language),
|
||||
type: codecID.substring(7).toLowerCase()
|
||||
}
|
||||
|
||||
const name = getData(entry, EbmlTagId.Name)
|
||||
if (name) track.name = name
|
||||
|
||||
const header = getData(entry, EbmlTagId.CodecPrivate)
|
||||
if (header) track.header = header.toString()
|
||||
|
||||
// TODO: Assume zlib deflate compression
|
||||
const compressed = entry.Children.find(c =>
|
||||
c.id === EbmlTagId.ContentEncodings &&
|
||||
c.Children.find(cc =>
|
||||
cc.id === EbmlTagId.ContentEncoding &&
|
||||
getChild(cc, EbmlTagId.ContentCompression)
|
||||
)
|
||||
)
|
||||
|
||||
if (compressed) track._compressed = true
|
||||
|
||||
this.subtitleTracks.set(track.number, track)
|
||||
}
|
||||
}
|
||||
|
||||
this.emit('tracks', [...this.subtitleTracks.values()])
|
||||
}
|
||||
|
||||
handleBlockGroup (chunk) {
|
||||
const block = getChild(chunk, EbmlTagId.Block)
|
||||
|
||||
if (block && this.subtitleTracks.has(block.track)) {
|
||||
const blockDuration = getData(chunk, EbmlTagId.BlockDuration)
|
||||
const track = this.subtitleTracks.get(block.track)
|
||||
|
||||
const payload = track._compressed
|
||||
? inflate(Buffer.from(block.payload), { to: 'string' })
|
||||
: block.payload
|
||||
|
||||
const subtitle = {
|
||||
text: payload.toString('utf8'),
|
||||
time: (block.value + this._currentClusterTimecode) * this.timecodeScale,
|
||||
duration: blockDuration * this.timecodeScale
|
||||
}
|
||||
|
||||
if (SSA_TYPES.has(track.type)) {
|
||||
// extract SSA/ASS keys
|
||||
const values = subtitle.text.split(',')
|
||||
|
||||
// ignore read-order, and skip layer if ssa
|
||||
for (let i = track.type === 'ssa' ? 2 : 1; i < 8; i++) {
|
||||
subtitle[SSA_KEYS[i]] = values[i]
|
||||
}
|
||||
|
||||
subtitle.text = values.slice(8).join(',')
|
||||
}
|
||||
|
||||
this.emit('subtitle', subtitle, block.track)
|
||||
}
|
||||
}
|
||||
|
||||
handleChapters ({ Children }) {
|
||||
const editions = Children.filter(c => c.id === EbmlTagId.EditionEntry)
|
||||
// https://www.matroska.org/technical/chapters.html#default-edition
|
||||
// finds first default edition, or first entry
|
||||
const defaultEdition = editions.find(c => {
|
||||
return c.Children.some(cc => {
|
||||
return cc.id === EbmlTagId.EditionFlagDefault && Boolean(cc.data)
|
||||
})
|
||||
}) || editions[0]
|
||||
|
||||
// exclude hidden atoms
|
||||
const atoms = defaultEdition.Children.filter(c => c.id === EbmlTagId.ChapterAtom && !getData(c, EbmlTagId.ChapterFlagHidden))
|
||||
const chapters = []
|
||||
for (let i = atoms.length - 1; i >= 0; --i) {
|
||||
const start = getData(atoms[i], EbmlTagId.ChapterTimeStart) / this.timecodeScale / 1000000
|
||||
const end = (getData(atoms[i], EbmlTagId.ChapterTimeEnd) / this.timecodeScale / 1000000)
|
||||
const disp = getChild(atoms[i], EbmlTagId.ChapterDisplay)
|
||||
|
||||
chapters[i] = {
|
||||
start,
|
||||
end,
|
||||
text: getData(disp, EbmlTagId.ChapString),
|
||||
language: getData(disp, EbmlTagId.ChapLanguage)
|
||||
}
|
||||
}
|
||||
|
||||
chapters.sort((a, b) => a.start - b.start)
|
||||
for (let i = chapters.length - 1; i >= 0; --i) {
|
||||
chapters[i].end ||= chapters[i + 1]?.start || this.duration
|
||||
}
|
||||
|
||||
if (this.duration) {
|
||||
this.emit('chapters', chapters)
|
||||
} else {
|
||||
this.chapters = chapters
|
||||
}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.destroyed = true
|
||||
this.emit('finish')
|
||||
}
|
||||
}
|
||||
|
||||
export class SubtitleParser extends SubtitleParserBase {
|
||||
constructor (stream) {
|
||||
super()
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for await (const _ of super[Symbol.asyncIterator](stream)) {
|
||||
if (this.destroyed) break
|
||||
}
|
||||
} catch (e) {}
|
||||
this.emit('finish')
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
export class SubtitleStream extends SubtitleParserBase {
|
||||
constructor (prevInstance, stream) {
|
||||
super()
|
||||
|
||||
this._stream = stream
|
||||
|
||||
if (prevInstance instanceof SubtitleParserBase) {
|
||||
// copy previous metadata
|
||||
this.subtitleTracks = prevInstance.subtitleTracks
|
||||
this.timecodeScale = prevInstance.timecodeScale
|
||||
|
||||
// may not be at ebml tag offset
|
||||
this.unstable = true
|
||||
}
|
||||
}
|
||||
|
||||
async * [Symbol.asyncIterator] (stream = this._stream) {
|
||||
while (true) {
|
||||
if (this.destroyed) return
|
||||
if (this.unstable) {
|
||||
const iterator = stream[Symbol.asyncIterator]()
|
||||
const { value: chunk } = await iterator.next()
|
||||
if (!chunk) return
|
||||
// the ebml decoder expects to see a tag, so we won't use it until we find a cluster
|
||||
for (let i = 0; i < chunk.length - 12; i++) {
|
||||
// cluster id 0x1F43B675
|
||||
// https://matroska.org/technical/elements.html#LevelCluster
|
||||
if (chunk[i] === 0x1f && chunk[i + 1] === 0x43 && chunk[i + 2] === 0xb6 && chunk[i + 3] === 0x75) {
|
||||
// length of cluster size tag
|
||||
const len = 8 - Math.floor(Math.log2(chunk[i + 4]))
|
||||
// first tag in cluster is a valid EbmlTag
|
||||
if (EbmlTagId[chunk[i + 4 + len]]) {
|
||||
// okay this is probably a cluster
|
||||
this.unstable = false
|
||||
yield chunk.slice(0, i)
|
||||
yield * super[Symbol.asyncIterator](join([[chunk.slice(i)], iterator]))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
yield chunk
|
||||
} else {
|
||||
yield * super[Symbol.asyncIterator](stream)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,6 +116,7 @@ ipcMain.on('doh', (event, dns) => {
|
|||
})
|
||||
|
||||
function createWindow () {
|
||||
const development = process.env.NODE_ENV?.trim() === 'development'
|
||||
// Create the browser window.
|
||||
webtorrentWindow = new BrowserWindow({
|
||||
show: false,
|
||||
|
|
@ -140,19 +141,19 @@ function createWindow () {
|
|||
webPreferences: {
|
||||
enableBlinkFeatures: 'FontAccess, AudioVideoTracks',
|
||||
backgroundThrottling: false,
|
||||
preload: path.join(__dirname, '/preload.js')
|
||||
preload: development ? path.join(__dirname, '/preload.js') : path.join(__dirname, '/preload.js')
|
||||
},
|
||||
icon: path.join(__dirname, '/renderer/public/logo.ico'),
|
||||
icon: path.join(__dirname, '/logo.ico'),
|
||||
show: false
|
||||
})
|
||||
mainWindow.setMenuBarVisibility(false)
|
||||
|
||||
protocol.registerHttpProtocol('miru', (req, cb) => {
|
||||
const token = req.url.slice(7)
|
||||
if (process.env.NODE_ENV !== 'development ') {
|
||||
mainWindow.loadURL(path.join(__dirname, '/renderer/dist/index.html' + token))
|
||||
if (development) {
|
||||
mainWindow.loadURL(path.join(__dirname, '/index.html' + token))
|
||||
} else {
|
||||
mainWindow.loadURL('http://localhost:5173/' + token)
|
||||
mainWindow.loadURL('http://localhost:5000/' + token)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -168,16 +169,16 @@ function createWindow () {
|
|||
|
||||
let torrentLoad = null
|
||||
|
||||
if (process.env.NODE_ENV !== 'development ') {
|
||||
if (process.env.NODE_ENV?.trim() !== 'development') {
|
||||
// Load production build
|
||||
torrentLoad = webtorrentWindow.loadFile(path.join(__dirname, '/renderer/dist/webtorrent.html'))
|
||||
mainWindow.loadFile(path.join(__dirname, '/renderer/dist/index.html'))
|
||||
torrentLoad = webtorrentWindow.loadFile(path.join(__dirname, '/background.html'))
|
||||
mainWindow.loadFile(path.join(__dirname, '/index.html'))
|
||||
} else {
|
||||
// Load vite dev server page
|
||||
console.log('Development mode')
|
||||
torrentLoad = webtorrentWindow.loadURL('http://localhost:5173/webtorrent.html')
|
||||
torrentLoad = webtorrentWindow.loadURL('http://localhost:5000/background.html')
|
||||
webtorrentWindow.webContents.openDevTools()
|
||||
mainWindow.loadURL('http://localhost:5173/')
|
||||
mainWindow.loadURL('http://localhost:5000/index.html')
|
||||
mainWindow.webContents.openDevTools()
|
||||
}
|
||||
|
||||
|
|
@ -250,31 +251,7 @@ let status = null
|
|||
const discord = new Client({
|
||||
transport: 'ipc'
|
||||
})
|
||||
|
||||
function setDiscordRPC (data) {
|
||||
if (!data) {
|
||||
data = {
|
||||
activity: {
|
||||
timestamps: {
|
||||
start: Date.now()
|
||||
},
|
||||
details: 'Stream anime torrents, real-time.',
|
||||
state: 'Watching anime',
|
||||
assets: {
|
||||
small_image: 'logo',
|
||||
small_text: 'https://github.com/ThaUnknown/miru'
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
label: 'Download app',
|
||||
url: 'https://github.com/ThaUnknown/miru/releases/latest'
|
||||
}
|
||||
],
|
||||
instance: true,
|
||||
type: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
function setDiscordRPC (event, data) {
|
||||
status = data
|
||||
if (discord?.user && status) {
|
||||
status.pid = process.pid
|
||||
|
|
@ -282,39 +259,9 @@ function setDiscordRPC (data) {
|
|||
}
|
||||
}
|
||||
|
||||
let allowDiscordDetails = false
|
||||
let requestedDiscordDetails = false
|
||||
let rpcStarted = false
|
||||
let cachedPresence = null
|
||||
|
||||
ipcMain.on('discord_status', (event, data) => {
|
||||
requestedDiscordDetails = data;
|
||||
if (!rpcStarted) {
|
||||
handleRPC()
|
||||
setInterval(handleRPC, 5000) //According to Discord documentation, clients can only update their presence 5 times per 20 seconds. We will add an extra second to be safe.
|
||||
rpcStarted = true
|
||||
}
|
||||
})
|
||||
|
||||
function handleRPC() {
|
||||
if (allowDiscordDetails === requestedDiscordDetails) return
|
||||
|
||||
allowDiscordDetails = requestedDiscordDetails
|
||||
if (!allowDiscordDetails) {
|
||||
setDiscordRPC(null)
|
||||
} else if (cachedPresence) {
|
||||
setDiscordRPC(cachedPresence)
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.on('discord', (event, data) => {
|
||||
cachedPresence = data
|
||||
if (allowDiscordDetails) {
|
||||
setDiscordRPC(data)
|
||||
}
|
||||
})
|
||||
ipcMain.on('discord', setDiscordRPC)
|
||||
discord.on('ready', async () => {
|
||||
setDiscordRPC(status)
|
||||
setDiscordRPC(null, status)
|
||||
discord.subscribe('ACTIVITY_JOIN_REQUEST')
|
||||
discord.subscribe('ACTIVITY_JOIN')
|
||||
discord.subscribe('ACTIVITY_SPECTATE')
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint n/no-callback-literal: 0 */
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
contextBridge.exposeInMainWorld('IPC', {
|
||||
emit: (event, data) => {
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
<meta name="theme-color" content="#191c20">
|
||||
<title>Miru</title>
|
||||
|
||||
<link rel='icon' href='/logo.ico'>
|
||||
<link href="/lib/Material-Icons.css" rel="stylesheet">
|
||||
|
||||
<script defer type="module" src="./src/main.js" async></script>
|
||||
</head>
|
||||
|
||||
<body class="dark-mode with-custom-webkit-scrollbars with-custom-css-scrollbars">
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -7,7 +7,8 @@
|
|||
import { page } from '@/App.svelte'
|
||||
import 'browser-event-target-emitter'
|
||||
|
||||
import P2PT from 'https://esm.sh/p2pt?bundle'
|
||||
// import P2PT from 'https://esm.sh/p2pt?bundle'
|
||||
const P2PT = {}
|
||||
|
||||
export const w2gEmitter = new EventTarget()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { add } from './torrent.js'
|
||||
import { DOMPARSER, PromiseBatch } from './util.js'
|
||||
import { alRequest, alSearch } from './anilist.js'
|
||||
import 'anitomyscript/dist/anitomyscript.wasm?url'
|
||||
import anitomyscript from 'anitomyscript'
|
||||
import { media } from '@/lib/Player/MediaHandler.svelte'
|
||||
import { addToast } from '@/lib/Toasts.svelte'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import JASSUB from 'jassub'
|
||||
import workerUrl from 'jassub/dist/jassub-worker.js?url'
|
||||
import modernWasmUrl from 'jassub/dist/jassub-worker-modern.wasm?url'
|
||||
import wasmUrl from 'jassub/dist/jassub-worker.wasm?url'
|
||||
import { toTS, videoRx, subRx } from './util.js'
|
||||
import { set } from '@/lib/Settings.svelte'
|
||||
|
||||
|
|
@ -163,9 +160,10 @@ export default class Subtitles {
|
|||
availableFonts: {
|
||||
'roboto medium': './Roboto.ttf'
|
||||
},
|
||||
workerUrl,
|
||||
wasmUrl,
|
||||
modernWasmUrl,
|
||||
workerUrl: new URL('jassub/dist/jassub-worker.js', import.meta.url).toString(),
|
||||
wasmUrl: new URL('jassub/dist/jassub-worker.wasm', import.meta.url).toString(),
|
||||
legacyWasmUrl: new URL('jassub/dist/jassub-worker.wasm.js', import.meta.url).toString(),
|
||||
modernWasmUrl: new URL('jassub/dist/jassub-worker-modern.wasm', import.meta.url).toString(),
|
||||
useLocalFonts: set.missingFont,
|
||||
dropAllBlur: set.disableSubtitleBlur
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
<meta name="theme-color" content="#191c20">
|
||||
<title>WebTorrent Hidden Window</title>
|
||||
|
||||
<script defer src="./lib/webtorrent.js" async></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import path from 'path'
|
||||
import process from 'process'
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import commonjs from 'vite-plugin-commonjs'
|
||||
|
||||
const root = path.resolve(process.cwd(), 'src/renderer')
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve('src/renderer/src')
|
||||
}
|
||||
},
|
||||
plugins: [mode !== 'development' && commonjs(), svelte()],
|
||||
root,
|
||||
server: {
|
||||
hmr: false
|
||||
},
|
||||
base: './',
|
||||
build: {
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: '[name].[ext]'
|
||||
},
|
||||
input: {
|
||||
index: root + '/index.html',
|
||||
torrent: root + '/webtorrent.html'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
146
webpack.config.cjs
Normal file
146
webpack.config.cjs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
const { join, resolve } = require('path')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
|
||||
const mode = process.env.NODE_ENV?.trim() || 'development'
|
||||
const isDev = mode === 'development'
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
entry: join(__dirname, 'src', 'background', 'background.js'),
|
||||
output: {
|
||||
path: join(__dirname, 'build'),
|
||||
filename: 'background.js'
|
||||
},
|
||||
mode,
|
||||
externals: {
|
||||
'utp-native': 'require("utp-native")'
|
||||
},
|
||||
resolve: {
|
||||
aliasFields: [],
|
||||
mainFields: ['module', 'main', 'node'],
|
||||
alias: {
|
||||
'node-fetch': false
|
||||
}
|
||||
},
|
||||
plugins: [new HtmlWebpackPlugin({ filename: 'background.html' })],
|
||||
target: 'electron20.0-renderer',
|
||||
devServer: {
|
||||
devMiddleware: {
|
||||
writeToDisk: true
|
||||
},
|
||||
hot: true,
|
||||
client: {
|
||||
overlay: { errors: true, warnings: false, runtimeErrors: false }
|
||||
},
|
||||
port: 5000
|
||||
}
|
||||
},
|
||||
{
|
||||
entry: join(__dirname, 'src', 'renderer', 'src', 'main.js'),
|
||||
output: {
|
||||
path: join(__dirname, 'build'),
|
||||
filename: 'renderer.js'
|
||||
},
|
||||
mode,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.svelte$/,
|
||||
use: {
|
||||
loader: 'svelte-loader',
|
||||
options: {
|
||||
compilerOptions: {
|
||||
dev: isDev
|
||||
},
|
||||
emitCss: !isDev,
|
||||
hotReload: isDev
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
// required to prevent errors from Svelte on Webpack 5+
|
||||
test: /node_modules\/svelte\/.*\.mjs$/,
|
||||
resolve: {
|
||||
fullySpecified: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
aliasFields: [],
|
||||
alias: {
|
||||
'@': resolve('src/renderer/src'),
|
||||
module: false,
|
||||
url: false
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.svelte']
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].css'
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: 'src/renderer/public' }
|
||||
]
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
inject: false,
|
||||
templateContent: ({ htmlWebpackPlugin }) => /* html */`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
<meta name="theme-color" content="#191c20">
|
||||
<title>Miru</title>
|
||||
|
||||
<link rel='icon' href='/logo.ico'>
|
||||
<link href="./lib/Material-Icons.css" rel="stylesheet">
|
||||
|
||||
${htmlWebpackPlugin.tags.headTags}
|
||||
</head>
|
||||
|
||||
<body class="dark-mode with-custom-webkit-scrollbars with-custom-css-scrollbars">
|
||||
${htmlWebpackPlugin.tags.bodyTags}
|
||||
</body>
|
||||
|
||||
</html> `
|
||||
})],
|
||||
target: 'web'
|
||||
},
|
||||
{
|
||||
entry: join(__dirname, 'src', 'preload', 'preload.js'),
|
||||
output: {
|
||||
path: join(__dirname, 'build'),
|
||||
filename: 'preload.js'
|
||||
},
|
||||
resolve: {
|
||||
aliasFields: []
|
||||
},
|
||||
mode,
|
||||
target: 'electron20.0-preload'
|
||||
},
|
||||
{
|
||||
entry: join(__dirname, 'src', 'main', 'main.js'),
|
||||
output: {
|
||||
path: join(__dirname, 'build'),
|
||||
filename: 'main.js'
|
||||
},
|
||||
resolve: {
|
||||
aliasFields: []
|
||||
},
|
||||
mode,
|
||||
target: 'electron20.0-main'
|
||||
}
|
||||
]
|
||||
Loading…
Reference in a new issue