mirror of
https://github.com/qier222/YesPlayMusic.git
synced 2025-03-01 14:08:40 +08:00
284 lines
6.3 KiB
TypeScript
284 lines
6.3 KiB
TypeScript
![]() |
import { $ } from 'zx'
|
||
|
import store from './store'
|
||
|
import { ipcMain, BrowserWindow } from 'electron'
|
||
|
import { getNetworkInfo, sleep } from './utils'
|
||
|
import { spawn, ChildProcessWithoutNullStreams } from 'child_process'
|
||
|
import log from './log'
|
||
|
|
||
|
type Protocol = 'dmap' | 'mrp' | 'airplay' | 'companion' | 'raop'
|
||
|
interface Device {
|
||
|
name: string
|
||
|
address: string
|
||
|
identifier: string
|
||
|
services: { protocol: Protocol; port: number }[]
|
||
|
}
|
||
|
|
||
|
class Airplay {
|
||
|
devices: Device[] = []
|
||
|
pairProcess: ChildProcessWithoutNullStreams | null = null
|
||
|
window: BrowserWindow
|
||
|
|
||
|
constructor(window: BrowserWindow) {
|
||
|
log.debug('[airplay] ini')
|
||
|
this.window = window
|
||
|
this.initIpc()
|
||
|
}
|
||
|
|
||
|
async checkIsInstalled() {
|
||
|
const help = (await $`atvscript -h`).toString()
|
||
|
return String(help).includes('usage: atvscript')
|
||
|
}
|
||
|
|
||
|
async scanDevices(excludeThisDevice: boolean = true) {
|
||
|
if (!this.checkIsInstalled()) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: 'pyatv is not installed',
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let scanResult = null
|
||
|
try {
|
||
|
scanResult = await $`atvscript scan`
|
||
|
} catch (p: any) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: p.stderr,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let json = null
|
||
|
try {
|
||
|
json = JSON.parse(scanResult.toString())
|
||
|
} catch (e) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: String(e),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (excludeThisDevice) {
|
||
|
const macAddress = getNetworkInfo()?.mac
|
||
|
if (macAddress) {
|
||
|
json.devices = json.devices.filter(
|
||
|
(device: Device) =>
|
||
|
device.identifier.toLowerCase() !== macAddress.toLowerCase()
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (json.result === 'success') {
|
||
|
this.devices = json.devices
|
||
|
}
|
||
|
|
||
|
return json
|
||
|
}
|
||
|
|
||
|
async pairDevice(deviceID: string, protocol: Protocol) {
|
||
|
this.pairProcess = spawn('atvremote', [
|
||
|
'--id',
|
||
|
deviceID,
|
||
|
'--protocol',
|
||
|
protocol,
|
||
|
'pair',
|
||
|
])
|
||
|
|
||
|
let paired = false
|
||
|
let done = false
|
||
|
this.pairProcess.stdout.on('data', (data: any) => {
|
||
|
console.log('stdout', String(data))
|
||
|
if (data.includes('You may now use these credentials:')) {
|
||
|
store.set(
|
||
|
`airplay.credentials.${deviceID}`,
|
||
|
String(data).split('credentials:')[1].trim()
|
||
|
)
|
||
|
paired = true
|
||
|
done = true
|
||
|
}
|
||
|
if (data.includes('Pairing failed')) {
|
||
|
paired = false
|
||
|
done = true
|
||
|
}
|
||
|
})
|
||
|
|
||
|
while (!done) {
|
||
|
console.log('not done yet')
|
||
|
await sleep(1000)
|
||
|
}
|
||
|
|
||
|
return paired
|
||
|
}
|
||
|
|
||
|
async enterPairPin(pin: string) {
|
||
|
if (!this.pairProcess) {
|
||
|
return false
|
||
|
}
|
||
|
this.pairProcess.stdin.write(`${pin}\n`)
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
async playUrl(deviceID: string, url: string) {
|
||
|
log.debug(`[airplay] playUrl ${url}`)
|
||
|
const credentials: string = store.get(`airplay.credentials.${deviceID}`)
|
||
|
if (url.includes('127.0.0.1')) {
|
||
|
const ip = getNetworkInfo()?.address
|
||
|
if (ip) url = url.replace('127.0.0.1', ip)
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
spawn('atvremote', [
|
||
|
'--id',
|
||
|
deviceID,
|
||
|
'--airplay-credentials',
|
||
|
credentials,
|
||
|
`play_url=${url}`,
|
||
|
])
|
||
|
} catch (p: any) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: p.stderr,
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async getPlaying(deviceID: string) {
|
||
|
if (!this.checkIsInstalled()) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: 'pyatv is not installed',
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||
|
|
||
|
let playing = null
|
||
|
try {
|
||
|
playing =
|
||
|
await $`atvscript --id ${deviceID} --airplay-credentials=${credentials} playing`
|
||
|
} catch (p: any) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: p.stderr,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let json = null
|
||
|
try {
|
||
|
json = JSON.parse(playing.toString())
|
||
|
} catch (e) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: String(e),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return json
|
||
|
}
|
||
|
|
||
|
async play(deviceID: string) {
|
||
|
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||
|
try {
|
||
|
$`atvscript --id ${deviceID} --airplay-credentials ${credentials} play`
|
||
|
} catch (p: any) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: p.stderr,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
result: 'success',
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async pause(deviceID: string) {
|
||
|
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||
|
try {
|
||
|
$`atvscript --id ${deviceID} --airplay-credentials ${credentials} pause`
|
||
|
} catch (p: any) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: p.stderr,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
result: 'success',
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async playOrPause(deviceID: string) {
|
||
|
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||
|
try {
|
||
|
$`atvscript --id ${deviceID} --airplay-credentials ${credentials} play_pause`
|
||
|
} catch (p: any) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: p.stderr,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
result: 'success',
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async setProgress(deviceID: string, progress: number) {
|
||
|
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||
|
try {
|
||
|
$`atvremote --id ${deviceID} --airplay-credentials ${credentials} set_position=${progress}`
|
||
|
} catch (p: any) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: p.stderr,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
result: 'success',
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async pushUpdates(deviceID: string) {
|
||
|
const credentials = store.get(`airplay.credentials.${deviceID}`)
|
||
|
let updates = null
|
||
|
try {
|
||
|
updates = $`atvscript --id ${deviceID} --airplay-credentials ${credentials} push_updates`
|
||
|
} catch (p: any) {
|
||
|
return {
|
||
|
result: 'failure',
|
||
|
error: p.stderr,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for await (const chunk of updates.stdout) {
|
||
|
this.window.webContents.send('airplay-updates', chunk)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async initIpc() {
|
||
|
ipcMain.handle('airplay-scan-devices', () => {
|
||
|
return this.scanDevices()
|
||
|
})
|
||
|
|
||
|
ipcMain.handle('airplay-pair', async () => {
|
||
|
console.log('airplay-pair')
|
||
|
return this.pairDevice('58:D3:49:F0:C9:71', 'airplay')
|
||
|
})
|
||
|
|
||
|
ipcMain.handle('airplay-pair-enter-pin', async (e, pin) => {
|
||
|
return this.enterPairPin(pin)
|
||
|
})
|
||
|
|
||
|
ipcMain.handle('airplay-play-url', async (e, { deviceID, url }) => {
|
||
|
return this.playUrl(deviceID, url)
|
||
|
})
|
||
|
|
||
|
ipcMain.handle('airplay-get-playing', async (e, { deviceID }) => {
|
||
|
return this.getPlaying(deviceID)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export default Airplay
|