mirror of
https://github.com/NoCrypt/migu.git
synced 2026-04-19 23:52:06 +00:00
feat: spawn custom video player process #415
This commit is contained in:
parent
58209181bf
commit
17498e8e69
13 changed files with 135 additions and 32 deletions
|
|
@ -11,5 +11,6 @@ export const SUPPORTS = {
|
|||
torrentPath: false,
|
||||
torrentPersist: false,
|
||||
keybinds: false,
|
||||
isAndroid: true
|
||||
isAndroid: true,
|
||||
externalPlayer: false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,5 +12,6 @@ export const SUPPORTS = {
|
|||
torrentPersist: true,
|
||||
keybinds: true,
|
||||
extensions: true,
|
||||
isAndroid: false
|
||||
isAndroid: false,
|
||||
externalPlayer: true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { spawn } from 'node:child_process'
|
||||
import WebTorrent from 'webtorrent'
|
||||
import HTTPTracker from 'bittorrent-tracker/lib/client/http-tracker.js'
|
||||
import { hex2bin, arr2hex, text2arr } from 'uint8-util'
|
||||
|
|
@ -33,6 +34,10 @@ try {
|
|||
export default class TorrentClient extends WebTorrent {
|
||||
static excludedErrorMessages = ['WebSocket', 'User-Initiated Abort, reason=', 'Connection failed.']
|
||||
|
||||
player = ''
|
||||
/** @type {ReturnType<spawn>} */
|
||||
playerProcess = null
|
||||
|
||||
constructor (ipc, storageQuota, serverMode, settingOverrides = {}, controller) {
|
||||
const settings = { ...defaults, ...storedSettings, ...settingOverrides }
|
||||
super({
|
||||
|
|
@ -55,6 +60,9 @@ export default class TorrentClient extends WebTorrent {
|
|||
})
|
||||
ipc.on('destroy', this.destroy.bind(this))
|
||||
})
|
||||
ipc.on('player', (event, data) => {
|
||||
this.player = data
|
||||
})
|
||||
this.settings = settings
|
||||
|
||||
this.serverMode = serverMode
|
||||
|
|
@ -219,18 +227,34 @@ export default class TorrentClient extends WebTorrent {
|
|||
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)
|
||||
const torrent = await this.get(data.data.current.infoHash)
|
||||
const found = torrent?.files.find(file => file.path === data.data.current.path)
|
||||
if (!found) return
|
||||
if (this.playerProcess) {
|
||||
this.playerProcess.kill()
|
||||
this.playerProcess = null
|
||||
}
|
||||
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)
|
||||
if (data.data.external && this.player) {
|
||||
this.playerProcess = spawn(this.player, ['http://localhost:' + this.server.address().port + found.streamURL])
|
||||
this.playerProcess.stdout.on('data', () => {})
|
||||
const startTime = Date.now()
|
||||
this.playerProcess.once('close', () => {
|
||||
this.playerProcess = null
|
||||
const seconds = (Date.now() - startTime) / 1000
|
||||
console.log(seconds)
|
||||
this.dispatch('externalWatched', seconds)
|
||||
})
|
||||
} else {
|
||||
this.parser = new Parser(this, found)
|
||||
this.findSubtitleFiles(found)
|
||||
this.findFontFiles(found)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,6 +155,8 @@
|
|||
}
|
||||
$: loadDeband($settings.playerDeband, video)
|
||||
|
||||
let watchedListener
|
||||
|
||||
async function handleCurrent (file) {
|
||||
if (file) {
|
||||
if (thumbnailData.video?.src) URL.revokeObjectURL(video?.src)
|
||||
|
|
@ -168,14 +170,26 @@
|
|||
chapters = []
|
||||
currentSkippable = null
|
||||
completed = false
|
||||
if (subs) subs.destroy()
|
||||
if (subs) {
|
||||
subs.destroy()
|
||||
subs = null
|
||||
}
|
||||
current = file
|
||||
emit('current', current)
|
||||
src = file.url
|
||||
client.send('current', file)
|
||||
subs = new Subtitles(video, files, current, handleHeaders)
|
||||
video.load()
|
||||
await loadAnimeProgress()
|
||||
client.send('current', { current: file, external: settings.value.enableExternal })
|
||||
if (!settings.value.enableExternal) {
|
||||
src = file.url
|
||||
subs = new Subtitles(video, files, current, handleHeaders)
|
||||
video.load()
|
||||
await loadAnimeProgress()
|
||||
} else if (current.media?.media?.duration) {
|
||||
const duration = current.media?.media?.duration
|
||||
client.removeEventListener('externalWatched', watchedListener)
|
||||
watchedListener = ({ detail }) => {
|
||||
checkCompletionByTime(detail, duration)
|
||||
}
|
||||
client.addEventListener('externalWatched', watchedListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -882,13 +896,17 @@
|
|||
let completed = false
|
||||
function checkCompletion () {
|
||||
if (!completed && $settings.playerAutocomplete) {
|
||||
const fromend = Math.max(180, safeduration / 10)
|
||||
if (safeduration && currentTime && video?.readyState && safeduration - fromend < currentTime) {
|
||||
if (media?.media?.episodes || media?.media?.nextAiringEpisode?.episode) {
|
||||
if (media.media.episodes || media.media.nextAiringEpisode?.episode > media.episode) {
|
||||
completed = true
|
||||
anilistClient.alEntry(media)
|
||||
}
|
||||
checkCompletionByTime(currentTime, safeduration)
|
||||
}
|
||||
}
|
||||
|
||||
function checkCompletionByTime (time, duration) {
|
||||
const fromend = Math.max(180, duration / 10)
|
||||
if (time && time && video?.readyState && time - fromend < time) {
|
||||
if (media?.media?.episodes || media?.media?.nextAiringEpisode?.episode) {
|
||||
if (media.media.episodes || media.media.nextAiringEpisode?.episode > media.episode) {
|
||||
completed = true
|
||||
anilistClient.alEntry(media)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
import { toast } from 'svelte-sonner'
|
||||
import FontSelect from 'simple-font-select'
|
||||
import SettingCard from './SettingCard.svelte'
|
||||
import { SUPPORTS } from '@/modules/support.js'
|
||||
import { click } from '@/modules/click.js'
|
||||
import IPC from '@/modules/ipc.js'
|
||||
export let settings
|
||||
|
||||
async function changeFont ({ detail }) {
|
||||
|
|
@ -21,6 +24,9 @@
|
|||
})
|
||||
}
|
||||
}
|
||||
function handleExecutable () {
|
||||
IPC.emit('player')
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if ('queryLocalFonts' in self)}
|
||||
|
|
@ -131,3 +137,22 @@
|
|||
<label for='player-deband'>{settings.playerDeband ? 'On' : 'Off'}</label>
|
||||
</div>
|
||||
</SettingCard>
|
||||
|
||||
{#if SUPPORTS.externalPlayer}
|
||||
<h4 class='mb-10 font-weight-bold'>External Player Settings</h4>
|
||||
<SettingCard title='Enable External Player' description='Tells Miru to open a custom user-defined video player to play video, instead of using the built-in one.'>
|
||||
<div class='custom-switch'>
|
||||
<input type='checkbox' id='player-external-enabled' bind:checked={settings.enableExternal} />
|
||||
<label for='player-external-enabled'>{settings.enableExternal ? 'On' : 'Off'}</label>
|
||||
</div>
|
||||
</SettingCard>
|
||||
<SettingCard title='External Video Player' description='Executable for an external video player. Make sure the player supports HTTP sources.'>
|
||||
<div
|
||||
class='input-group w-300 mw-full'>
|
||||
<div class='input-group-prepend'>
|
||||
<button type='button' use:click={handleExecutable} class='btn btn-primary input-group-append'>Select Executable</button>
|
||||
</div>
|
||||
<input type='url' class='form-control bg-dark' readonly value={settings.playerPath} />
|
||||
</div>
|
||||
</SettingCard>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,10 @@
|
|||
$settings.torrentPath = data
|
||||
}
|
||||
|
||||
function playerListener (data) {
|
||||
$settings.playerPath = data
|
||||
}
|
||||
|
||||
function loginButton () {
|
||||
if (anilistClient.userID) {
|
||||
$logout = true
|
||||
|
|
@ -95,9 +99,11 @@
|
|||
}
|
||||
onDestroy(() => {
|
||||
IPC.off('path', pathListener)
|
||||
IPC.off('player', playerListener)
|
||||
})
|
||||
$: IPC.emit('show-discord-status', $settings.showDetailsInRPC)
|
||||
IPC.on('path', pathListener)
|
||||
IPC.on('player', playerListener)
|
||||
</script>
|
||||
|
||||
<Tabs>
|
||||
|
|
|
|||
25
electron/src/main/dialog.js
Normal file
25
electron/src/main/dialog.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { basename, extname } from 'node:path'
|
||||
import { ipcMain, dialog } from 'electron'
|
||||
import store from './store.js'
|
||||
|
||||
export default class Dialog {
|
||||
/**
|
||||
* @param {import('electron').BrowserWindow} torrentWindow
|
||||
*/
|
||||
constructor (torrentWindow) {
|
||||
ipcMain.on('player', async ({ sender }) => {
|
||||
const { filePaths, canceled } = await dialog.showOpenDialog({
|
||||
title: 'Select video player executable',
|
||||
properties: ['openFile']
|
||||
})
|
||||
if (canceled) return
|
||||
if (filePaths.length) {
|
||||
const path = filePaths[0]
|
||||
|
||||
store.set('player', path)
|
||||
torrentWindow.webContents.send('player', path)
|
||||
sender.send('player', basename(path, extname(path)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { Client } from 'discord-rpc'
|
|||
import { ipcMain } from 'electron'
|
||||
import { debounce } from '@/modules/util.js'
|
||||
|
||||
export default class {
|
||||
export default class Discord {
|
||||
defaultStatus = {
|
||||
activity: {
|
||||
timestamps: { start: Date.now() },
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import Discord from './discord.js'
|
|||
import Updater from './updater.js'
|
||||
import Protocol from './protocol.js'
|
||||
import { development } from './util.js'
|
||||
import Dialog from './dialog.js'
|
||||
import store from './store.js'
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
|
|
@ -44,6 +46,7 @@ function createWindow () {
|
|||
new Discord(mainWindow)
|
||||
new Protocol(mainWindow)
|
||||
new Updater(mainWindow)
|
||||
new Dialog(webtorrentWindow)
|
||||
mainWindow.setMenuBarVisibility(false)
|
||||
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived(({ responseHeaders }, fn) => {
|
||||
|
|
@ -107,6 +110,7 @@ function createWindow () {
|
|||
const { port1, port2 } = new MessageChannelMain()
|
||||
await torrentLoad
|
||||
webtorrentWindow.webContents.postMessage('port', null, [port1])
|
||||
webtorrentWindow.webContents.postMessage('player', store.get('player'))
|
||||
sender.postMessage('port', null, [port2])
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ if (process.defaultApp) {
|
|||
app.setAsDefaultProtocolClient('miru')
|
||||
}
|
||||
|
||||
export default class {
|
||||
export default class Protocol {
|
||||
// schema: miru://key/value
|
||||
protocolMap = {
|
||||
auth: token => this.sendToken(token),
|
||||
|
|
|
|||
|
|
@ -27,4 +27,4 @@ function parseDataFile (filePath, defaults) {
|
|||
}
|
||||
}
|
||||
|
||||
export default new Store('settings', { angle: 'default' })
|
||||
export default new Store('settings', { angle: 'default', player: '' })
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ ipcMain.on('update', () => {
|
|||
})
|
||||
|
||||
autoUpdater.checkForUpdatesAndNotify()
|
||||
export default class {
|
||||
export default class Updater {
|
||||
/**
|
||||
* @param {import('electron').BrowserWindow} window
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -33,11 +33,9 @@ ipcMain.on('open', (event, url) => {
|
|||
|
||||
ipcMain.on('doh', (event, dns) => {
|
||||
try {
|
||||
const url = new URL(dns)
|
||||
|
||||
app.configureHostResolver({
|
||||
secureDnsMode: 'secure',
|
||||
secureDnsServers: [url.toString()]
|
||||
secureDnsServers: ['' + new URL(dns)]
|
||||
})
|
||||
} catch (e) {}
|
||||
})
|
||||
|
|
@ -50,10 +48,11 @@ ipcMain.on('close', () => {
|
|||
app.quit()
|
||||
})
|
||||
|
||||
ipcMain.on('dialog', async (event, data) => {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
ipcMain.on('dialog', async ({ sender }) => {
|
||||
const { filePaths, canceled } = await dialog.showOpenDialog({
|
||||
properties: ['openDirectory']
|
||||
})
|
||||
if (canceled) return
|
||||
if (filePaths.length) {
|
||||
let path = filePaths[0]
|
||||
if (!(path.endsWith('\\') || path.endsWith('/'))) {
|
||||
|
|
@ -63,12 +62,12 @@ ipcMain.on('dialog', async (event, data) => {
|
|||
path += '/'
|
||||
}
|
||||
}
|
||||
event.sender.send('path', path)
|
||||
sender.send('path', path)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('version', (event) => {
|
||||
event.sender.send('version', app.getVersion()) // fucking stupid
|
||||
ipcMain.on('version', ({ sender }) => {
|
||||
sender.send('version', app.getVersion()) // fucking stupid
|
||||
})
|
||||
|
||||
app.setJumpList?.([
|
||||
|
|
|
|||
Loading…
Reference in a new issue