index.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. #!/usr/bin/env node
  2. // omg I am tired of code
  3. import {getPlayer} from './players.js'
  4. import {parseOptions} from './general-util.js'
  5. import {getItemPathString} from './playlist-utils.js'
  6. import Backend from './backend.js'
  7. import setupClient from './client.js'
  8. import TelnetServer from './telnet.js'
  9. import {CommandLineInterface} from 'tui-lib/util/interfaces'
  10. import * as ansi from 'tui-lib/util/ansi'
  11. import {readFile, writeFile} from 'node:fs/promises'
  12. import os from 'node:os'
  13. import path from 'node:path'
  14. // Hack to get around errors when piping many things to stdout/err
  15. // (from general-util promisifyProcess)
  16. process.stdout.setMaxListeners(Infinity)
  17. process.stderr.setMaxListeners(Infinity)
  18. process.on('unhandledRejection', error => {
  19. console.error(ansi.setForeground(ansi.C_RED) + "** There was an uncatched error! **" + ansi.resetAttributes())
  20. console.error("Don't worry, your music files are all okay.")
  21. console.error("This just means there was a bug in mtui.")
  22. console.error("In order to verify that the program won't run weirdly, it has stopped.")
  23. console.error(ansi.setForeground(ansi.C_RED) + "Error stack:" + ansi.resetAttributes())
  24. console.error(error.stack)
  25. console.error(ansi.setForeground(ansi.C_RED) + "Error object:" + ansi.resetAttributes())
  26. console.error(error)
  27. console.error("(End of error log.)")
  28. process.stdout.write(ansi.cleanCursor())
  29. process.exit(1)
  30. })
  31. async function main() {
  32. const playlistSources = []
  33. const options = await parseOptions(process.argv.slice(2), {
  34. 'player': {
  35. type: 'value',
  36. async validate(playerName) {
  37. if (await getPlayer(playerName)) {
  38. return true
  39. } else {
  40. return 'a known player identifier'
  41. }
  42. }
  43. },
  44. 'player-options': {type: 'series'},
  45. 'stress-test': {type: 'flag'},
  46. 'telnet-server': {type: 'flag'},
  47. 'skip-config-file': {type: 'flag'},
  48. 'config-file': {type: 'value'},
  49. [parseOptions.handleDashless](option) {
  50. playlistSources.push(option)
  51. },
  52. })
  53. if (options['player-options'] && !options['player']) {
  54. console.error('--player must be specified in order to use --player-options')
  55. process.exit(1)
  56. }
  57. let jsonConfig = {}
  58. let jsonError = null
  59. const jsonPath =
  60. (options['config-file']
  61. ? path.resolve(options['config-file'])
  62. : path.join(os.homedir(), '.mtui', 'config.json'))
  63. try {
  64. jsonConfig = JSON.parse(await readFile(jsonPath))
  65. } catch (error) {
  66. if (error.code !== 'ENOENT') {
  67. jsonError = error
  68. }
  69. }
  70. if (jsonError) {
  71. console.error(`Error loading JSON config:`)
  72. console.error(jsonError.message)
  73. console.error(`Edit the file below to fix the error, or run mtui with --skip-config-file.`)
  74. console.error(jsonPath)
  75. process.exit(1)
  76. }
  77. const backend = new Backend({
  78. playerName: options['player'],
  79. playerOptions: options['player-options']
  80. })
  81. const result = await backend.setup()
  82. if (result.error) {
  83. console.error(result.error)
  84. process.exit(1)
  85. }
  86. backend.on('playing', track => {
  87. if (track) {
  88. writeFile(backend.rootDirectory + '/current-track.txt',
  89. getItemPathString(track))
  90. writeFile(backend.rootDirectory + '/current-track.json',
  91. JSON.stringify(track, null, 2))
  92. }
  93. })
  94. const { appElement, dirtyTerminal, flushable, root } = await setupClient({
  95. backend,
  96. screenInterface: new CommandLineInterface(),
  97. writable: process.stdout
  98. })
  99. appElement.on('quitRequested', () => {
  100. if (telnetServer) {
  101. telnetServer.disconnectAllSockets('User closed mtui - see you!')
  102. }
  103. process.exit(0)
  104. })
  105. appElement.on('suspendRequested', () => {
  106. process.kill(process.pid, 'SIGTSTP')
  107. })
  108. process.on('SIGCONT', () => {
  109. flushable.resizeScreen({lines: flushable.screenLines, cols: flushable.screenCols})
  110. process.stdin.setRawMode(false)
  111. process.stdin.setRawMode(true)
  112. dirtyTerminal()
  113. root.renderNow()
  114. })
  115. if (playlistSources.length === 0) {
  116. if (jsonConfig.defaultPlaylists) {
  117. playlistSources.push(...jsonConfig.defaultPlaylists)
  118. } else {
  119. playlistSources.push({
  120. name: 'My ~/Music Library',
  121. comment: (
  122. '(Add tracks and folders to ~/Music to make them show up here,' +
  123. ' or pass mtui your own playlist.json file!)'),
  124. source: ['crawl-local', os.homedir() + '/Music']
  125. })
  126. }
  127. }
  128. const loadPlaylists = async () => {
  129. for (const source of playlistSources) {
  130. await appElement.loadPlaylistOrSource(source, true)
  131. }
  132. }
  133. const loadPlaylistPromise = loadPlaylists()
  134. let telnetServer
  135. if (options['telnet-server']) {
  136. telnetServer = new TelnetServer(backend)
  137. await telnetServer.listen(1244)
  138. appElement.attachAsServerHost(telnetServer)
  139. }
  140. if (options['stress-test']) {
  141. await loadPlaylistPromise
  142. const w = 80
  143. const h = 40
  144. flushable.resizeScreen({lines: w, cols: h})
  145. root.w = w
  146. root.h = h
  147. root.fixAllLayout()
  148. /* eslint-disable-next-line no-unused-vars */
  149. const XXstress = func => '[disabled]'
  150. const stress = func => {
  151. const start = Date.now()
  152. let n = 0
  153. while (Date.now() < start + 1000) {
  154. func()
  155. n++
  156. }
  157. return n
  158. }
  159. const nRenderAndFlush = stress(() => {
  160. root.renderTo(flushable)
  161. flushable.flush()
  162. })
  163. const nFixAllLayout = stress(() => {
  164. root.fixAllLayout()
  165. })
  166. const listings = appElement.tabber.tabberElements
  167. const lastListing = listings[listings.length - 1]
  168. const nBuildItems = stress(() => {
  169. lastListing.buildItems()
  170. })
  171. process.stdout.write(ansi.cleanCursor() + ansi.clearScreen() + '\n')
  172. console.log('# of times we can render & flush:', nRenderAndFlush)
  173. console.log('# of times we can fix all layout:', nFixAllLayout)
  174. console.log('# of times we can build items:', nBuildItems)
  175. process.exit(0)
  176. return
  177. }
  178. }
  179. main().catch(err => {
  180. console.error(err)
  181. process.exit(1)
  182. })