pinokiod 3.130.0 → 3.131.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,25 @@
1
1
  /*
2
2
  {
3
3
  method: "push",
4
- params: {
4
+ params: {
5
5
  title: <string>,
6
6
  subtitle: <string>,
7
7
  message: <string>,
8
8
  image: <image path>,
9
- sound: true|false,
10
- }
9
+ sound: true|false|string,
11
10
  }
11
+ }
12
12
  (*/
13
13
  const util = require('../../util')
14
14
  const path = require('path');
15
15
  module.exports = async (req, ondata, kernel) => {
16
- let params = req.params
17
- if (params.image) {
16
+ const params = { ...(req.params || {}) }
17
+ if (typeof params.image === 'string') {
18
18
  params.image = path.resolve(req.cwd, params.image)
19
19
  }
20
+ if (typeof params.sound === 'string') {
21
+ const trimmed = params.sound.trim()
22
+ params.sound = trimmed || undefined
23
+ }
20
24
  util.push(params)
21
25
  }
package/kernel/util.js CHANGED
@@ -1,8 +1,8 @@
1
1
  const fs = require('fs')
2
- const { spawn } = require('child_process')
2
+ const { spawn, spawnSync } = require('child_process')
3
3
  const Clipboard = require('copy-paste/promises');
4
4
  const http = require('http');
5
- const notifier = require('node-notifier');
5
+ const notifier = require('toasted-notifier');
6
6
  const os = require('os')
7
7
  const net = require('node:net')
8
8
  const path = require('path')
@@ -12,6 +12,11 @@ const retry = require('async-retry');
12
12
  const child_process = require('node:child_process');
13
13
  const {auto: normalizeEOL} = require("eol");
14
14
  const {EOL} = require("os");
15
+ const axios = require('axios');
16
+ const { pipeline } = require('stream/promises');
17
+ const { pathToFileURL } = require('url');
18
+ const { randomUUID } = require('crypto');
19
+ const fsp = fs.promises;
15
20
  const breakPattern = /\n/g;
16
21
  const breakReplacement = "\\n";
17
22
  const groupPattern = /\$/g;
@@ -24,7 +29,26 @@ const {
24
29
  } = require('glob')
25
30
 
26
31
 
27
- const platform = os.platform()
32
+ const platform = os.platform()
33
+ function ensureNotifierBinaries() {
34
+ if (platform !== 'darwin') {
35
+ return
36
+ }
37
+ try {
38
+ const notifierModulePath = require.resolve('toasted-notifier/notifiers/notificationcenter')
39
+ const binaryPath = path.resolve(path.dirname(notifierModulePath), '../vendor/mac.noindex/terminal-notifier.app/Contents/MacOS/terminal-notifier')
40
+ if (!fs.existsSync(binaryPath)) {
41
+ return
42
+ }
43
+ const mode = fs.statSync(binaryPath).mode & 0o777
44
+ if ((mode & 0o111) === 0) {
45
+ fs.chmodSync(binaryPath, mode | 0o755)
46
+ }
47
+ } catch (err) {
48
+ console.warn('Warning: unable to update terminal-notifier permissions:', err.message)
49
+ }
50
+ }
51
+ ensureNotifierBinaries()
28
52
  // asar handling for go-get-folder-size
29
53
  let g
30
54
  if( __dirname.includes(".asar") ) {
@@ -530,6 +554,147 @@ function fill_object(obj, pattern, list, cache) {
530
554
  let res = recurse(obj);
531
555
  return { result: res, replaced_map }
532
556
  }
557
+ const isNonEmptyString = (value) => typeof value === 'string' && value.trim().length > 0
558
+ const HTTP_URL_REGEX = /^https?:\/\//i
559
+ const linuxSoundCandidates = [
560
+ { cmd: 'paplay', args: (filePath) => [filePath] },
561
+ { cmd: 'aplay', args: (filePath) => [filePath] },
562
+ { cmd: 'ffplay', args: (filePath) => ['-autoexit', '-nodisp', filePath] },
563
+ { cmd: 'mpg123', args: (filePath) => [filePath] },
564
+ { cmd: 'mplayer', args: (filePath) => [filePath] },
565
+ { cmd: 'cvlc', args: (filePath) => ['--play-and-exit', filePath] },
566
+ ]
567
+ let cachedLinuxSoundPlayer
568
+ const DEFAULT_CHIME_PATH = path.resolve(__dirname, '../server/public/chime.mp3')
569
+ function pickLinuxSoundPlayer() {
570
+ if (cachedLinuxSoundPlayer !== undefined) {
571
+ return cachedLinuxSoundPlayer
572
+ }
573
+ for (const candidate of linuxSoundCandidates) {
574
+ try {
575
+ const lookup = spawnSync('which', [candidate.cmd], { stdio: 'ignore' })
576
+ if (lookup.status === 0) {
577
+ cachedLinuxSoundPlayer = candidate
578
+ return cachedLinuxSoundPlayer
579
+ }
580
+ } catch (_) {
581
+ // ignore lookup errors and try next candidate
582
+ }
583
+ }
584
+ cachedLinuxSoundPlayer = null
585
+ return cachedLinuxSoundPlayer
586
+ }
587
+ function extractUrlExtension(rawUrl) {
588
+ try {
589
+ const parsed = new URL(rawUrl)
590
+ const ext = path.extname(parsed.pathname)
591
+ if (ext && ext.length <= 6) {
592
+ return ext
593
+ }
594
+ } catch (_) {
595
+ // ignore URL parse errors
596
+ }
597
+ return ''
598
+ }
599
+ async function downloadSoundToTemp(url) {
600
+ const tempPath = path.join(os.tmpdir(), `pinokio-notify-${randomUUID()}${extractUrlExtension(url)}`)
601
+ const response = await axios.get(url, {
602
+ responseType: 'stream',
603
+ timeout: 15000,
604
+ })
605
+ await pipeline(response.data, fs.createWriteStream(tempPath))
606
+ return {
607
+ filePath: tempPath,
608
+ cleanup: async () => {
609
+ try {
610
+ await fsp.unlink(tempPath)
611
+ } catch (_) {
612
+ // best-effort cleanup
613
+ }
614
+ }
615
+ }
616
+ }
617
+ function playSoundFile(filePath) {
618
+ if (!filePath) {
619
+ return null
620
+ }
621
+ if (platform === 'darwin') {
622
+ return spawn('afplay', [filePath], { stdio: 'ignore' })
623
+ }
624
+ if (platform === 'win32') {
625
+ const fileUrl = pathToFileURL(filePath).href.replace(/'/g, "''")
626
+ const script = [
627
+ "$ErrorActionPreference = 'Stop'",
628
+ "Add-Type -AssemblyName presentationCore",
629
+ "$player = New-Object system.windows.media.mediaplayer",
630
+ `$player.Open([Uri]'${fileUrl}')`,
631
+ "$player.Play()",
632
+ "while ($player.NaturalDuration.HasTimeSpan -eq $false) { Start-Sleep -Milliseconds 100 }",
633
+ "Start-Sleep -Seconds $player.NaturalDuration.TimeSpan.TotalSeconds",
634
+ ].join('; ')
635
+ return spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script], {
636
+ stdio: 'ignore'
637
+ })
638
+ }
639
+ const candidate = pickLinuxSoundPlayer()
640
+ if (!candidate) {
641
+ throw new Error('No supported audio player found for Linux (expected one of paplay, aplay, ffplay, mpg123, mplayer, cvlc).')
642
+ }
643
+ return spawn(candidate.cmd, candidate.args(filePath), { stdio: 'ignore' })
644
+ }
645
+ function scheduleSoundPlayback(target) {
646
+ if (!target) {
647
+ return
648
+ }
649
+ ;(async () => {
650
+ let cleanup = null
651
+ try {
652
+ let filePath
653
+ if (target.kind === 'file') {
654
+ filePath = target.value
655
+ } else if (target.kind === 'url') {
656
+ if (!HTTP_URL_REGEX.test(target.value)) {
657
+ throw new Error(`Unsupported sound URL: ${target.value}`)
658
+ }
659
+ const downloaded = await downloadSoundToTemp(target.value)
660
+ filePath = downloaded.filePath
661
+ cleanup = downloaded.cleanup
662
+ } else {
663
+ return
664
+ }
665
+
666
+ const child = playSoundFile(filePath)
667
+ if (!child) {
668
+ if (cleanup) {
669
+ cleanup().catch(() => {})
670
+ }
671
+ return
672
+ }
673
+
674
+ const tidy = () => {
675
+ if (!cleanup) {
676
+ return
677
+ }
678
+ cleanup().catch((err) => {
679
+ console.error('Failed to clean up temporary sound file:', err)
680
+ })
681
+ }
682
+
683
+ child.once('close', tidy)
684
+ child.once('error', tidy)
685
+ child.on('error', (err) => {
686
+ console.error('Failed to play notification sound:', err)
687
+ })
688
+ } catch (err) {
689
+ if (cleanup) {
690
+ cleanup().catch(() => {})
691
+ }
692
+ console.error('Failed to play notification sound:', err)
693
+ }
694
+ })().catch((err) => {
695
+ console.error('Failed to play notification sound:', err)
696
+ })
697
+ }
533
698
  function push(params) {
534
699
  /*
535
700
  params :- {
@@ -537,23 +702,39 @@ function push(params) {
537
702
  subtitle: <string>,
538
703
  message: <string>,
539
704
  image: <image path>,
540
- sound: true|false,
705
+ sound: true|false|string, // true uses default chime
541
706
  }
542
707
  */
543
- if (params.sound) {
544
- params.sound = "Funk"
708
+ const notifyParams = { ...(params || {}) }
709
+ const customSoundTarget = (() => {
710
+ if (notifyParams.sound === true) {
711
+ return { kind: 'file', value: DEFAULT_CHIME_PATH }
712
+ }
713
+ if (isNonEmptyString(notifyParams.sound)) {
714
+ const trimmed = notifyParams.sound.trim()
715
+ if (!trimmed) {
716
+ return null
717
+ }
718
+ return { kind: 'url', value: trimmed }
719
+ }
720
+ return null
721
+ })()
722
+
723
+ if (notifyParams.image && !notifyParams.contentImage) {
724
+ notifyParams.contentImage = notifyParams.image
545
725
  }
546
- // Blow, Bottle, Funk, Glass, Morse, Tink.
547
- if (params.image && !params.contentImage) {
548
- params.contentImage = params.image
726
+ if (!notifyParams.title) {
727
+ notifyParams.title = "Pinokio"
549
728
  }
550
- if (!params.title) {
551
- params.title = "Pinokio"
729
+ if (!notifyParams.contentImage) {
730
+ notifyParams.contentImage = path.resolve(__dirname, "../server/public/pinokio-black.png")
552
731
  }
553
- if (!params.contentImage) {
554
- params.contentImage = path.resolve(__dirname, "../server/public/pinokio-black.png")
732
+ if (customSoundTarget) {
733
+ notifyParams.sound = false
734
+ scheduleSoundPlayback(customSoundTarget)
555
735
  }
556
- notifier.notify(params)
736
+ console.log("notifyParams", notifyParams)
737
+ notifier.notify(notifyParams)
557
738
  }
558
739
  function p2u(localPath) {
559
740
  /*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "3.130.0",
3
+ "version": "3.131.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -60,7 +60,6 @@
60
60
  "multer": "^1.4.5-lts.1",
61
61
  "node-downloader-helper": "^2.1.9",
62
62
  "node-gradio-client": "^0.14.6",
63
- "node-notifier": "^10.0.1",
64
63
  "normalize-path": "^3.0.0",
65
64
  "p-limit": "^3.1.0",
66
65
  "parseurl": "^1.3.3",
@@ -77,6 +76,7 @@
77
76
  "sudo-prompt-programfiles-x86": "^9.2.10",
78
77
  "symlink-dir": "^5.2.1",
79
78
  "systeminformation": "^5.18.4",
79
+ "toasted-notifier": "^10.1.0",
80
80
  "twitter-api-v2": "^1.23.2",
81
81
  "uuid": "^9.0.0",
82
82
  "wait-on": "^7.2.0",
package/server/index.js CHANGED
@@ -4521,64 +4521,88 @@ class Server {
4521
4521
  console.log("Push", req.body)
4522
4522
  try {
4523
4523
  const payload = { ...(req.body || {}) }
4524
- if (typeof payload.image === 'string' && payload.image.trim()) {
4525
- const resolveAssetPath = (raw) => {
4526
- if (typeof raw !== 'string') {
4527
- return null
4528
- }
4529
- const trimmed = raw.trim()
4530
- if (!trimmed) {
4531
- return null
4532
- }
4533
- let candidate = trimmed
4534
- if (/^https?:\/\//i.test(trimmed)) {
4535
- try {
4536
- const parsed = new URL(trimmed)
4537
- candidate = parsed.pathname
4538
- } catch (_) {
4539
- return null
4540
- }
4541
- }
4542
- if (!candidate.startsWith('/asset/')) {
4543
- return null
4544
- }
4545
- const pathPart = candidate.split('?')[0].split('#')[0]
4546
- const rel = pathPart.replace(/^\/asset\/+/, '')
4547
- if (!rel) {
4548
- return null
4549
- }
4550
- const parts = rel.split('/').filter(Boolean)
4551
- if (!parts.length || parts.some((part) => part === '..')) {
4552
- return null
4553
- }
4524
+ const resolveAssetPath = (raw) => {
4525
+ if (typeof raw !== 'string') {
4526
+ return null
4527
+ }
4528
+ const trimmed = raw.trim()
4529
+ if (!trimmed) {
4530
+ return null
4531
+ }
4532
+ let candidate = trimmed
4533
+ if (/^https?:\/\//i.test(trimmed)) {
4554
4534
  try {
4555
- return this.kernel.path(...parts)
4535
+ const parsed = new URL(trimmed)
4536
+ candidate = parsed.pathname
4556
4537
  } catch (_) {
4557
4538
  return null
4558
4539
  }
4559
4540
  }
4560
- const resolvedImage = resolveAssetPath(payload.image)
4541
+ if (!candidate.startsWith('/asset/')) {
4542
+ return null
4543
+ }
4544
+ const pathPart = candidate.split('?')[0].split('#')[0]
4545
+ const rel = pathPart.replace(/^\/asset\/+/, '')
4546
+ if (!rel) {
4547
+ return null
4548
+ }
4549
+ const parts = rel.split('/').filter(Boolean)
4550
+ if (!parts.length || parts.some((part) => part === '..')) {
4551
+ return null
4552
+ }
4553
+ try {
4554
+ return this.kernel.path(...parts)
4555
+ } catch (_) {
4556
+ return null
4557
+ }
4558
+ }
4559
+ const resolvePublicAsset = async (raw) => {
4560
+ if (typeof raw !== 'string') {
4561
+ return null
4562
+ }
4563
+ const trimmed = raw.trim()
4564
+ if (!trimmed || !trimmed.startsWith('/')) {
4565
+ return null
4566
+ }
4567
+ const relative = trimmed.replace(/^\/+/, '')
4568
+ if (!relative) {
4569
+ return null
4570
+ }
4571
+ const publicRoot = path.resolve(__dirname, 'public')
4572
+ const candidate = path.resolve(publicRoot, relative)
4573
+ if (!candidate.startsWith(publicRoot)) {
4574
+ return null
4575
+ }
4576
+ try {
4577
+ await fs.promises.access(candidate, fs.constants.R_OK)
4578
+ return candidate
4579
+ } catch (_) {
4580
+ return null
4581
+ }
4582
+ }
4583
+ const normaliseNotificationAsset = async (raw) => {
4584
+ const asset = resolveAssetPath(raw)
4585
+ if (asset) {
4586
+ return asset
4587
+ }
4588
+ const fallback = await resolvePublicAsset(raw)
4589
+ if (fallback) {
4590
+ return fallback
4591
+ }
4592
+ return null
4593
+ }
4594
+ if (typeof payload.image === 'string' && payload.image.trim()) {
4595
+ const resolvedImage = await normaliseNotificationAsset(payload.image)
4561
4596
  if (resolvedImage) {
4562
4597
  payload.image = resolvedImage
4563
- } else {
4564
- const normalised = payload.image.trim()
4565
- if (normalised.startsWith('/')) {
4566
- const relative = normalised.replace(/^\/+/, '')
4567
- if (relative) {
4568
- const publicRoot = path.resolve(__dirname, 'public')
4569
- const candidate = path.resolve(publicRoot, relative)
4570
- if (candidate.startsWith(publicRoot)) {
4571
- try {
4572
- await fs.promises.access(candidate, fs.constants.R_OK)
4573
- payload.image = candidate
4574
- } catch (_) {
4575
- // ignore missing fallback asset
4576
- }
4577
- }
4578
- }
4579
- }
4580
4598
  }
4581
4599
  }
4600
+ if (typeof payload.sound === 'string') {
4601
+ const trimmedSound = payload.sound.trim()
4602
+ payload.sound = trimmedSound || undefined
4603
+ }
4604
+ delete payload.soundUrl
4605
+ delete payload.soundPath
4582
4606
  Util.push(payload)
4583
4607
  res.json({ success: true })
4584
4608
  } catch (e) {