@ohmykreee/dsh-remote-control 0.0.0-stage → 1.0.3

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.
Files changed (373) hide show
  1. package/AGENTS.md +556 -0
  2. package/INSTALLATION.md +310 -0
  3. package/LICENSE +21 -0
  4. package/README.md +195 -2
  5. package/client.js +3 -0
  6. package/components.json +20 -0
  7. package/cordis.patch.yml +3 -0
  8. package/docs/README.en.md +196 -0
  9. package/docs/screenshots/en/overview.png +0 -0
  10. package/docs/screenshots/en/question.png +0 -0
  11. package/docs/screenshots/zh/overview.png +0 -0
  12. package/docs/screenshots/zh/question.png +0 -0
  13. package/icon.svg +24 -0
  14. package/index.js +2 -0
  15. package/lib/account.js +1 -0
  16. package/lib/app-mapping.js +1 -0
  17. package/lib/i18n.js +1 -0
  18. package/lib/media-enrich.js +1 -0
  19. package/lib/notifications.js +1 -0
  20. package/lib/performance.js +1 -0
  21. package/lib/platform/index.js +1 -0
  22. package/lib/probe-adapter.js +1 -0
  23. package/lib/probe-daemon.js +1 -0
  24. package/lib/probe.js +1 -0
  25. package/lib/qr.js +1 -0
  26. package/lib/runtime.js +1 -0
  27. package/lib/service.js +1 -0
  28. package/lib/sessions.js +1 -0
  29. package/lib/settings.js +1 -0
  30. package/lib/types.js +1 -0
  31. package/lib/util.js +1 -0
  32. package/lib/weather.js +1 -0
  33. package/lib/ws.js +1 -0
  34. package/locale/en.json +6 -0
  35. package/locale/zh.json +6 -0
  36. package/package.json +118 -4
  37. package/scripts/browser-shot.mjs +217 -0
  38. package/scripts/build-dsh.mjs +228 -0
  39. package/scripts/build-rust.mjs +319 -0
  40. package/scripts/build-web.mjs +17 -0
  41. package/scripts/check-npm-package.mjs +322 -0
  42. package/scripts/mock-preview.py +574 -0
  43. package/scripts/screenshot-compose.py +89 -0
  44. package/scripts/screenshot-device.sh +75 -0
  45. package/src/dsh/client/api.ts +68 -0
  46. package/src/dsh/client/i18n.ts +573 -0
  47. package/src/dsh/client/icons.ts +56 -0
  48. package/src/dsh/client/index.ts +95 -0
  49. package/src/dsh/client/logic.ts +310 -0
  50. package/src/dsh/client/panel.ts +483 -0
  51. package/src/dsh/client/sections/apps.ts +40 -0
  52. package/src/dsh/client/sections/balance.ts +38 -0
  53. package/src/dsh/client/sections/connection.ts +97 -0
  54. package/src/dsh/client/sections/notifications.ts +43 -0
  55. package/src/dsh/client/sections/pomodoro.ts +184 -0
  56. package/src/dsh/client/sections/preferences.ts +138 -0
  57. package/src/dsh/client/sections/system.ts +120 -0
  58. package/src/dsh/client/sections/weather.ts +276 -0
  59. package/src/dsh/client/styles.ts +1119 -0
  60. package/src/dsh/client/types.ts +366 -0
  61. package/src/dsh/client/ui.ts +142 -0
  62. package/src/dsh/index.ts +108 -0
  63. package/src/dsh/lib/account.ts +520 -0
  64. package/src/dsh/lib/app-mapping.ts +366 -0
  65. package/src/dsh/lib/i18n.ts +82 -0
  66. package/src/dsh/lib/media-enrich.ts +202 -0
  67. package/src/dsh/lib/notifications.ts +322 -0
  68. package/src/dsh/lib/performance.ts +279 -0
  69. package/src/dsh/lib/platform/index.ts +43 -0
  70. package/src/dsh/lib/probe-adapter.ts +275 -0
  71. package/src/dsh/lib/probe-daemon.ts +359 -0
  72. package/src/dsh/lib/probe.ts +395 -0
  73. package/src/dsh/lib/qr.ts +42 -0
  74. package/src/dsh/lib/runtime.ts +126 -0
  75. package/src/dsh/lib/service.ts +1645 -0
  76. package/src/dsh/lib/sessions.ts +907 -0
  77. package/src/dsh/lib/settings.ts +468 -0
  78. package/src/dsh/lib/types.ts +231 -0
  79. package/src/dsh/lib/util.ts +115 -0
  80. package/src/dsh/lib/weather.ts +532 -0
  81. package/src/dsh/lib/ws.ts +328 -0
  82. package/src/dsh/types/qrcode-generator.d.ts +16 -0
  83. package/src/rust/Cargo.lock +1431 -0
  84. package/src/rust/Cargo.toml +68 -0
  85. package/src/rust/src/cli.rs +233 -0
  86. package/src/rust/src/daemon.rs +140 -0
  87. package/src/rust/src/location/linux.rs +118 -0
  88. package/src/rust/src/location/macos.rs +26 -0
  89. package/src/rust/src/location/mod.rs +170 -0
  90. package/src/rust/src/location/windows.rs +205 -0
  91. package/src/rust/src/main.rs +42 -0
  92. package/src/rust/src/media/catalog.rs +454 -0
  93. package/src/rust/src/media/linux.rs +350 -0
  94. package/src/rust/src/media/macos/accessibility.rs +59 -0
  95. package/src/rust/src/media/macos.rs +366 -0
  96. package/src/rust/src/media/mod.rs +298 -0
  97. package/src/rust/src/media/vendor/apple_music.rs +90 -0
  98. package/src/rust/src/media/vendor/common.rs +186 -0
  99. package/src/rust/src/media/vendor/kugou.rs +52 -0
  100. package/src/rust/src/media/vendor/kuwo.rs +49 -0
  101. package/src/rust/src/media/vendor/mod.rs +75 -0
  102. package/src/rust/src/media/vendor/netease.rs +1715 -0
  103. package/src/rust/src/media/vendor/qishui.rs +36 -0
  104. package/src/rust/src/media/vendor/qqmusic.rs +33 -0
  105. package/src/rust/src/media/vendor/spotify.rs +46 -0
  106. package/src/rust/src/media/vendor/wesing.rs +39 -0
  107. package/src/rust/src/media/vendor/youtube_music.rs +49 -0
  108. package/src/rust/src/media/windows/accessibility.rs +85 -0
  109. package/src/rust/src/media/windows/audio.rs +116 -0
  110. package/src/rust/src/media/windows/private.rs +291 -0
  111. package/src/rust/src/media/windows.rs +775 -0
  112. package/src/rust/src/notifications/linux.rs +276 -0
  113. package/src/rust/src/notifications/macos.rs +382 -0
  114. package/src/rust/src/notifications/mod.rs +244 -0
  115. package/src/rust/src/notifications/windows.rs +565 -0
  116. package/src/rust/src/perf/common.rs +109 -0
  117. package/src/rust/src/perf/linux.rs +282 -0
  118. package/src/rust/src/perf/macos.rs +28 -0
  119. package/src/rust/src/perf/mod.rs +157 -0
  120. package/src/rust/src/perf/windows.rs +981 -0
  121. package/src/rust/src/util.rs +360 -0
  122. package/src/rust/src/volume/linux.rs +195 -0
  123. package/src/rust/src/volume/macos.rs +54 -0
  124. package/src/rust/src/volume/mod.rs +116 -0
  125. package/src/rust/src/volume/windows.rs +75 -0
  126. package/src/rust/tests/cli.rs +79 -0
  127. package/src/rust/tests/daemon.rs +107 -0
  128. package/src/svelte/App.svelte +385 -0
  129. package/src/svelte/index.html +18 -0
  130. package/src/svelte/lib/billing.svelte.ts +228 -0
  131. package/src/svelte/lib/components/BalanceCard.svelte +173 -0
  132. package/src/svelte/lib/components/ClockCard.svelte +183 -0
  133. package/src/svelte/lib/components/CountdownCard.svelte +261 -0
  134. package/src/svelte/lib/components/CpuMemoryCard.svelte +238 -0
  135. package/src/svelte/lib/components/EmptyState.svelte +34 -0
  136. package/src/svelte/lib/components/GpuCard.svelte +245 -0
  137. package/src/svelte/lib/components/HomePage.svelte +34 -0
  138. package/src/svelte/lib/components/MusicControls.svelte +256 -0
  139. package/src/svelte/lib/components/MusicCover.svelte +88 -0
  140. package/src/svelte/lib/components/MusicPage.svelte +116 -0
  141. package/src/svelte/lib/components/NotificationsCard.svelte +109 -0
  142. package/src/svelte/lib/components/Panel.svelte +22 -0
  143. package/src/svelte/lib/components/PerformancePage.svelte +55 -0
  144. package/src/svelte/lib/components/PomodoroCard.svelte +250 -0
  145. package/src/svelte/lib/components/QuestionSheet.svelte +354 -0
  146. package/src/svelte/lib/components/RadialGauge.svelte +99 -0
  147. package/src/svelte/lib/components/Rail.svelte +217 -0
  148. package/src/svelte/lib/components/SectionHeader.svelte +40 -0
  149. package/src/svelte/lib/components/SeekBar.svelte +421 -0
  150. package/src/svelte/lib/components/SessionList.svelte +364 -0
  151. package/src/svelte/lib/components/SessionStats.svelte +98 -0
  152. package/src/svelte/lib/components/SessionsPage.svelte +61 -0
  153. package/src/svelte/lib/components/SpendCard.svelte +105 -0
  154. package/src/svelte/lib/components/ThermalCard.svelte +249 -0
  155. package/src/svelte/lib/components/Toast.svelte +32 -0
  156. package/src/svelte/lib/components/WeatherIcon.svelte +40 -0
  157. package/src/svelte/lib/components/WeatherPanel.svelte +420 -0
  158. package/src/svelte/lib/components/ui/alert/alert-action.svelte +20 -0
  159. package/src/svelte/lib/components/ui/alert/alert-description.svelte +23 -0
  160. package/src/svelte/lib/components/ui/alert/alert-title.svelte +23 -0
  161. package/src/svelte/lib/components/ui/alert/alert.svelte +43 -0
  162. package/src/svelte/lib/components/ui/alert/index.ts +17 -0
  163. package/src/svelte/lib/components/ui/avatar/avatar-badge.svelte +26 -0
  164. package/src/svelte/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
  165. package/src/svelte/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
  166. package/src/svelte/lib/components/ui/avatar/avatar-group.svelte +23 -0
  167. package/src/svelte/lib/components/ui/avatar/avatar-image.svelte +17 -0
  168. package/src/svelte/lib/components/ui/avatar/avatar.svelte +26 -0
  169. package/src/svelte/lib/components/ui/avatar/index.ts +22 -0
  170. package/src/svelte/lib/components/ui/badge/badge.svelte +49 -0
  171. package/src/svelte/lib/components/ui/badge/index.ts +2 -0
  172. package/src/svelte/lib/components/ui/button/button.svelte +82 -0
  173. package/src/svelte/lib/components/ui/button/index.ts +17 -0
  174. package/src/svelte/lib/components/ui/card/card-action.svelte +23 -0
  175. package/src/svelte/lib/components/ui/card/card-content.svelte +20 -0
  176. package/src/svelte/lib/components/ui/card/card-description.svelte +20 -0
  177. package/src/svelte/lib/components/ui/card/card-footer.svelte +20 -0
  178. package/src/svelte/lib/components/ui/card/card-header.svelte +23 -0
  179. package/src/svelte/lib/components/ui/card/card-title.svelte +20 -0
  180. package/src/svelte/lib/components/ui/card/card.svelte +22 -0
  181. package/src/svelte/lib/components/ui/card/index.ts +25 -0
  182. package/src/svelte/lib/components/ui/dialog/dialog-close.svelte +11 -0
  183. package/src/svelte/lib/components/ui/dialog/dialog-content.svelte +48 -0
  184. package/src/svelte/lib/components/ui/dialog/dialog-description.svelte +17 -0
  185. package/src/svelte/lib/components/ui/dialog/dialog-footer.svelte +32 -0
  186. package/src/svelte/lib/components/ui/dialog/dialog-header.svelte +20 -0
  187. package/src/svelte/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
  188. package/src/svelte/lib/components/ui/dialog/dialog-portal.svelte +7 -0
  189. package/src/svelte/lib/components/ui/dialog/dialog-title.svelte +17 -0
  190. package/src/svelte/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
  191. package/src/svelte/lib/components/ui/dialog/dialog.svelte +7 -0
  192. package/src/svelte/lib/components/ui/dialog/index.ts +34 -0
  193. package/src/svelte/lib/components/ui/empty/empty-content.svelte +23 -0
  194. package/src/svelte/lib/components/ui/empty/empty-description.svelte +23 -0
  195. package/src/svelte/lib/components/ui/empty/empty-header.svelte +20 -0
  196. package/src/svelte/lib/components/ui/empty/empty-media.svelte +41 -0
  197. package/src/svelte/lib/components/ui/empty/empty-title.svelte +20 -0
  198. package/src/svelte/lib/components/ui/empty/empty.svelte +23 -0
  199. package/src/svelte/lib/components/ui/empty/index.ts +22 -0
  200. package/src/svelte/lib/components/ui/input/index.ts +7 -0
  201. package/src/svelte/lib/components/ui/input/input.svelte +48 -0
  202. package/src/svelte/lib/components/ui/progress/index.ts +7 -0
  203. package/src/svelte/lib/components/ui/progress/progress.svelte +27 -0
  204. package/src/svelte/lib/components/ui/scroll-area/index.ts +10 -0
  205. package/src/svelte/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte +30 -0
  206. package/src/svelte/lib/components/ui/scroll-area/scroll-area.svelte +43 -0
  207. package/src/svelte/lib/components/ui/separator/index.ts +7 -0
  208. package/src/svelte/lib/components/ui/separator/separator.svelte +21 -0
  209. package/src/svelte/lib/components/ui/sheet/index.ts +34 -0
  210. package/src/svelte/lib/components/ui/sheet/sheet-close.svelte +7 -0
  211. package/src/svelte/lib/components/ui/sheet/sheet-content.svelte +55 -0
  212. package/src/svelte/lib/components/ui/sheet/sheet-description.svelte +17 -0
  213. package/src/svelte/lib/components/ui/sheet/sheet-footer.svelte +20 -0
  214. package/src/svelte/lib/components/ui/sheet/sheet-header.svelte +20 -0
  215. package/src/svelte/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
  216. package/src/svelte/lib/components/ui/sheet/sheet-portal.svelte +7 -0
  217. package/src/svelte/lib/components/ui/sheet/sheet-title.svelte +17 -0
  218. package/src/svelte/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
  219. package/src/svelte/lib/components/ui/sheet/sheet.svelte +7 -0
  220. package/src/svelte/lib/components/ui/skeleton/index.ts +7 -0
  221. package/src/svelte/lib/components/ui/skeleton/skeleton.svelte +17 -0
  222. package/src/svelte/lib/components/ui/slider/index.ts +7 -0
  223. package/src/svelte/lib/components/ui/slider/slider.svelte +52 -0
  224. package/src/svelte/lib/components/ui/tooltip/index.ts +19 -0
  225. package/src/svelte/lib/components/ui/tooltip/tooltip-content.svelte +51 -0
  226. package/src/svelte/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
  227. package/src/svelte/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
  228. package/src/svelte/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
  229. package/src/svelte/lib/components/ui/tooltip/tooltip.svelte +7 -0
  230. package/src/svelte/lib/countdown.svelte.ts +252 -0
  231. package/src/svelte/lib/format.ts +199 -0
  232. package/src/svelte/lib/i18n.svelte.ts +583 -0
  233. package/src/svelte/lib/media.ts +32 -0
  234. package/src/svelte/lib/pages.ts +18 -0
  235. package/src/svelte/lib/pomodoro.svelte.ts +223 -0
  236. package/src/svelte/lib/session-tree.ts +80 -0
  237. package/src/svelte/lib/state.svelte.ts +359 -0
  238. package/src/svelte/lib/timer-alarm.svelte.ts +319 -0
  239. package/src/svelte/lib/types.ts +306 -0
  240. package/src/svelte/lib/utils.ts +8 -0
  241. package/src/svelte/lib/weather.ts +84 -0
  242. package/src/svelte/lib/ws.ts +104 -0
  243. package/src/svelte/main.ts +13 -0
  244. package/src/svelte/styles/global.css +315 -0
  245. package/src/svelte/vite-env.d.ts +12 -0
  246. package/static/fonts/LICENSE.txt +287 -0
  247. package/static/icon.svg +24 -0
  248. package/static/manifest.webmanifest +18 -0
  249. package/svelte.config.js +5 -0
  250. package/tsconfig.dsh-client.json +18 -0
  251. package/tsconfig.dsh.json +21 -0
  252. package/tsconfig.json +20 -0
  253. package/vite.config.js +31 -0
  254. package/web/assets/index-DLdSfnJ3.css +1 -0
  255. package/web/assets/index-YNE8X0Uy.js +6 -0
  256. package/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  257. package/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  258. package/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  259. package/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  260. package/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  261. package/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  262. package/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  263. package/web/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
  264. package/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  265. package/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  266. package/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  267. package/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  268. package/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  269. package/web/assets/noto-sans-sc-100-wght-normal-DluiXYzi.woff2 +0 -0
  270. package/web/assets/noto-sans-sc-101-wght-normal-CPkZHnw0.woff2 +0 -0
  271. package/web/assets/noto-sans-sc-102-wght-normal-gzIulUH6.woff2 +0 -0
  272. package/web/assets/noto-sans-sc-103-wght-normal-CJfSBxFo.woff2 +0 -0
  273. package/web/assets/noto-sans-sc-104-wght-normal-B3QMglgE.woff2 +0 -0
  274. package/web/assets/noto-sans-sc-105-wght-normal-xXh_OMfk.woff2 +0 -0
  275. package/web/assets/noto-sans-sc-106-wght-normal-B9n3jMjP.woff2 +0 -0
  276. package/web/assets/noto-sans-sc-107-wght-normal-D7fvVprN.woff2 +0 -0
  277. package/web/assets/noto-sans-sc-108-wght-normal-J0_LhxVo.woff2 +0 -0
  278. package/web/assets/noto-sans-sc-109-wght-normal-Dsp-De_0.woff2 +0 -0
  279. package/web/assets/noto-sans-sc-110-wght-normal-DE_6NUrb.woff2 +0 -0
  280. package/web/assets/noto-sans-sc-111-wght-normal-C4S-TlTn.woff2 +0 -0
  281. package/web/assets/noto-sans-sc-112-wght-normal-BcFuP3w6.woff2 +0 -0
  282. package/web/assets/noto-sans-sc-113-wght-normal-Dk932kmj.woff2 +0 -0
  283. package/web/assets/noto-sans-sc-114-wght-normal-CO_5Xo-i.woff2 +0 -0
  284. package/web/assets/noto-sans-sc-115-wght-normal-BWy07H14.woff2 +0 -0
  285. package/web/assets/noto-sans-sc-116-wght-normal-B4HVAfHo.woff2 +0 -0
  286. package/web/assets/noto-sans-sc-117-wght-normal-siZzFqgK.woff2 +0 -0
  287. package/web/assets/noto-sans-sc-118-wght-normal-xhBB7f6V.woff2 +0 -0
  288. package/web/assets/noto-sans-sc-119-wght-normal-Ck_g2FGz.woff2 +0 -0
  289. package/web/assets/noto-sans-sc-21-wght-normal-D0x9-yBt.woff2 +0 -0
  290. package/web/assets/noto-sans-sc-22-wght-normal-BBUVTShg.woff2 +0 -0
  291. package/web/assets/noto-sans-sc-23-wght-normal-Du1oYWkE.woff2 +0 -0
  292. package/web/assets/noto-sans-sc-24-wght-normal-CmWjJ0gt.woff2 +0 -0
  293. package/web/assets/noto-sans-sc-25-wght-normal-BFHiqilg.woff2 +0 -0
  294. package/web/assets/noto-sans-sc-26-wght-normal-C0sF2Z2f.woff2 +0 -0
  295. package/web/assets/noto-sans-sc-27-wght-normal-COFdrquJ.woff2 +0 -0
  296. package/web/assets/noto-sans-sc-28-wght-normal-DMgdNu1o.woff2 +0 -0
  297. package/web/assets/noto-sans-sc-29-wght-normal-Cmq8em30.woff2 +0 -0
  298. package/web/assets/noto-sans-sc-30-wght-normal-CZWqZ3yU.woff2 +0 -0
  299. package/web/assets/noto-sans-sc-31-wght-normal-Wfi_lutW.woff2 +0 -0
  300. package/web/assets/noto-sans-sc-32-wght-normal-BRJQ4PY5.woff2 +0 -0
  301. package/web/assets/noto-sans-sc-33-wght-normal-CUVQXNAJ.woff2 +0 -0
  302. package/web/assets/noto-sans-sc-34-wght-normal-Bspba0wC.woff2 +0 -0
  303. package/web/assets/noto-sans-sc-35-wght-normal-K8fX_YPG.woff2 +0 -0
  304. package/web/assets/noto-sans-sc-36-wght-normal-CkITyjoT.woff2 +0 -0
  305. package/web/assets/noto-sans-sc-37-wght-normal-WCZ2zfEe.woff2 +0 -0
  306. package/web/assets/noto-sans-sc-38-wght-normal-DeM7CUce.woff2 +0 -0
  307. package/web/assets/noto-sans-sc-39-wght-normal-BMZNLWQU.woff2 +0 -0
  308. package/web/assets/noto-sans-sc-4-wght-normal-AR5bdDjz.woff2 +0 -0
  309. package/web/assets/noto-sans-sc-40-wght-normal-BQ5xpS5j.woff2 +0 -0
  310. package/web/assets/noto-sans-sc-41-wght-normal-Hthm7FFi.woff2 +0 -0
  311. package/web/assets/noto-sans-sc-42-wght-normal-eaS2gKds.woff2 +0 -0
  312. package/web/assets/noto-sans-sc-43-wght-normal-o-T53XXV.woff2 +0 -0
  313. package/web/assets/noto-sans-sc-44-wght-normal-BJH5J4Ez.woff2 +0 -0
  314. package/web/assets/noto-sans-sc-45-wght-normal-BKuxSCOJ.woff2 +0 -0
  315. package/web/assets/noto-sans-sc-46-wght-normal-BR_5Mu6e.woff2 +0 -0
  316. package/web/assets/noto-sans-sc-47-wght-normal-CyY7gKNX.woff2 +0 -0
  317. package/web/assets/noto-sans-sc-48-wght-normal-BNMfTR6k.woff2 +0 -0
  318. package/web/assets/noto-sans-sc-49-wght-normal-DX-0y7wg.woff2 +0 -0
  319. package/web/assets/noto-sans-sc-5-wght-normal-CfWugllB.woff2 +0 -0
  320. package/web/assets/noto-sans-sc-50-wght-normal-DAOGW-Dr.woff2 +0 -0
  321. package/web/assets/noto-sans-sc-51-wght-normal-BlFLOjcP.woff2 +0 -0
  322. package/web/assets/noto-sans-sc-52-wght-normal-OZjrybnw.woff2 +0 -0
  323. package/web/assets/noto-sans-sc-53-wght-normal-CwyWNT6x.woff2 +0 -0
  324. package/web/assets/noto-sans-sc-54-wght-normal-DM0nRe18.woff2 +0 -0
  325. package/web/assets/noto-sans-sc-55-wght-normal-DBK2wUOR.woff2 +0 -0
  326. package/web/assets/noto-sans-sc-56-wght-normal-C-VKK1LS.woff2 +0 -0
  327. package/web/assets/noto-sans-sc-57-wght-normal-Cdbfh5kq.woff2 +0 -0
  328. package/web/assets/noto-sans-sc-58-wght-normal-CflMKARZ.woff2 +0 -0
  329. package/web/assets/noto-sans-sc-59-wght-normal-BS4Kwzxx.woff2 +0 -0
  330. package/web/assets/noto-sans-sc-6-wght-normal-ChnZIQfQ.woff2 +0 -0
  331. package/web/assets/noto-sans-sc-60-wght-normal-B7QOYsXJ.woff2 +0 -0
  332. package/web/assets/noto-sans-sc-61-wght-normal-C8JjQS7w.woff2 +0 -0
  333. package/web/assets/noto-sans-sc-62-wght-normal-bS4wq1Gg.woff2 +0 -0
  334. package/web/assets/noto-sans-sc-63-wght-normal-BJr8P-OB.woff2 +0 -0
  335. package/web/assets/noto-sans-sc-64-wght-normal-DVB11t7j.woff2 +0 -0
  336. package/web/assets/noto-sans-sc-65-wght-normal-DMPCXBvK.woff2 +0 -0
  337. package/web/assets/noto-sans-sc-66-wght-normal-BobaG10V.woff2 +0 -0
  338. package/web/assets/noto-sans-sc-67-wght-normal-Dzk7WhCR.woff2 +0 -0
  339. package/web/assets/noto-sans-sc-68-wght-normal-DGbCD9xu.woff2 +0 -0
  340. package/web/assets/noto-sans-sc-69-wght-normal-Ctrv1EqA.woff2 +0 -0
  341. package/web/assets/noto-sans-sc-70-wght-normal-RgTO941D.woff2 +0 -0
  342. package/web/assets/noto-sans-sc-71-wght-normal-D9sG-CWv.woff2 +0 -0
  343. package/web/assets/noto-sans-sc-72-wght-normal-C4oWy3ul.woff2 +0 -0
  344. package/web/assets/noto-sans-sc-73-wght-normal-B1ZoOTgM.woff2 +0 -0
  345. package/web/assets/noto-sans-sc-74-wght-normal-F7dH_rZJ.woff2 +0 -0
  346. package/web/assets/noto-sans-sc-75-wght-normal-Cp2wp4si.woff2 +0 -0
  347. package/web/assets/noto-sans-sc-76-wght-normal-B494D7xS.woff2 +0 -0
  348. package/web/assets/noto-sans-sc-77-wght-normal-B3MVY9Ax.woff2 +0 -0
  349. package/web/assets/noto-sans-sc-78-wght-normal-5jX7iNMC.woff2 +0 -0
  350. package/web/assets/noto-sans-sc-79-wght-normal-BIoZy8fr.woff2 +0 -0
  351. package/web/assets/noto-sans-sc-80-wght-normal-Bje1Fxy1.woff2 +0 -0
  352. package/web/assets/noto-sans-sc-81-wght-normal-B2NN7Qi3.woff2 +0 -0
  353. package/web/assets/noto-sans-sc-82-wght-normal-CUggNZi4.woff2 +0 -0
  354. package/web/assets/noto-sans-sc-83-wght-normal-99rD8YO-.woff2 +0 -0
  355. package/web/assets/noto-sans-sc-84-wght-normal-zMBmUocm.woff2 +0 -0
  356. package/web/assets/noto-sans-sc-85-wght-normal-CjI3qdxG.woff2 +0 -0
  357. package/web/assets/noto-sans-sc-86-wght-normal-BHY8TVEH.woff2 +0 -0
  358. package/web/assets/noto-sans-sc-87-wght-normal-DioZSHnU.woff2 +0 -0
  359. package/web/assets/noto-sans-sc-88-wght-normal-BXYxwf2t.woff2 +0 -0
  360. package/web/assets/noto-sans-sc-89-wght-normal-ClMFrFaL.woff2 +0 -0
  361. package/web/assets/noto-sans-sc-90-wght-normal-BnOlPreP.woff2 +0 -0
  362. package/web/assets/noto-sans-sc-91-wght-normal-DTr9RQcS.woff2 +0 -0
  363. package/web/assets/noto-sans-sc-97-wght-normal-9u6Aiav-.woff2 +0 -0
  364. package/web/assets/noto-sans-sc-98-wght-normal-BR-H2C0V.woff2 +0 -0
  365. package/web/assets/noto-sans-sc-99-wght-normal-BI6XPoxp.woff2 +0 -0
  366. package/web/assets/noto-sans-sc-cyrillic-wght-normal-BGmsQlw-.woff2 +0 -0
  367. package/web/assets/noto-sans-sc-latin-ext-wght-normal-DuJTpTg_.woff2 +0 -0
  368. package/web/assets/noto-sans-sc-latin-wght-normal-DDW8mKAR.woff2 +0 -0
  369. package/web/assets/noto-sans-sc-vietnamese-wght-normal-Bi3ATJ76.woff2 +0 -0
  370. package/web/fonts/LICENSE.txt +287 -0
  371. package/web/icon.svg +24 -0
  372. package/web/index.html +19 -0
  373. package/web/manifest.webmanifest +18 -0
@@ -0,0 +1,36 @@
1
+ //! Soda Music (汽水音乐) private adapter.
2
+ //!
3
+ //! The desktop client normally provides SMTC; only the title fallback lives
4
+ //! here because the reference project does not expose a useful private API.
5
+
6
+ use super::common;
7
+ use crate::media::catalog::ParsedTitle;
8
+
9
+ const SUFFIXES: &[&str] = &[" - 汽水音乐", " - Soda Music"];
10
+ const PRODUCT_NAMES: &[&str] = &["汽水音乐", "soda music", "sodamusic"];
11
+
12
+ pub fn parse_title(raw: &str) -> Option<ParsedTitle> {
13
+ let text = common::strip_suffixes(common::normalize_spaces(raw), SUFFIXES);
14
+ if common::is_generic_title(&text, PRODUCT_NAMES) {
15
+ return None;
16
+ }
17
+ let mut parsed = common::split_title_artist(&text, false);
18
+ parsed.artist = common::normalize_artist(&parsed.artist, &[(", ", " / ")]);
19
+ if parsed.title.is_empty() {
20
+ return None;
21
+ }
22
+ Some(parsed)
23
+ }
24
+
25
+ #[cfg(test)]
26
+ mod tests {
27
+ use super::*;
28
+
29
+ #[test]
30
+ fn parses_soda_music_title() {
31
+ let parsed = parse_title("晴天 - 周杰伦 - 汽水音乐").unwrap();
32
+ assert_eq!(parsed.title, "晴天");
33
+ assert_eq!(parsed.artist, "周杰伦");
34
+ assert!(parse_title("汽水音乐").is_none());
35
+ }
36
+ }
@@ -0,0 +1,33 @@
1
+ //! QQ Music private window-title adapter.
2
+
3
+ use super::common;
4
+ use crate::media::catalog::ParsedTitle;
5
+
6
+ const SUFFIXES: &[&str] = &[" - QQ音乐", " - QQ Music"];
7
+ const PRODUCT_NAMES: &[&str] = &["qq音乐", "qq music"];
8
+
9
+ pub fn parse_title(raw: &str) -> Option<ParsedTitle> {
10
+ let text = common::strip_suffixes(common::normalize_spaces(raw), SUFFIXES);
11
+ if common::is_generic_title(&text, PRODUCT_NAMES) {
12
+ return None;
13
+ }
14
+ let mut parsed = common::split_title_artist(&text, false);
15
+ parsed.artist = common::normalize_artist(&parsed.artist, &[("/", " / ")]);
16
+ if parsed.title.is_empty() {
17
+ return None;
18
+ }
19
+ Some(parsed)
20
+ }
21
+
22
+ #[cfg(test)]
23
+ mod tests {
24
+ use super::*;
25
+
26
+ #[test]
27
+ fn parses_qq_music_title() {
28
+ let parsed = parse_title("晴天 - 周杰伦 - QQ音乐").unwrap();
29
+ assert_eq!(parsed.title, "晴天");
30
+ assert_eq!(parsed.artist, "周杰伦");
31
+ assert!(parse_title("QQ音乐").is_none());
32
+ }
33
+ }
@@ -0,0 +1,46 @@
1
+ //! Spotify private window-title adapter.
2
+ //!
3
+ //! Spotify normally provides a complete SMTC/MPRIS session. The window title
4
+ //! fallback is only used when that session is missing; Spotify puts the artist
5
+ //! first (`歌手 - 歌名`).
6
+
7
+ use super::common;
8
+ use crate::media::catalog::ParsedTitle;
9
+
10
+ const SUFFIXES: &[&str] = &[
11
+ " - Spotify Premium",
12
+ " - Spotify Free",
13
+ " - Spotify",
14
+ " – Spotify",
15
+ ];
16
+ const PRODUCT_NAMES: &[&str] = &[
17
+ "spotify",
18
+ "spotify free",
19
+ "spotify premium",
20
+ "spotify - free",
21
+ ];
22
+
23
+ pub fn parse_title(raw: &str) -> Option<ParsedTitle> {
24
+ let text = common::strip_suffixes(common::normalize_spaces(raw), SUFFIXES);
25
+ if common::is_generic_title(&text, PRODUCT_NAMES) {
26
+ return None;
27
+ }
28
+ let parsed = common::split_title_artist(&text, true);
29
+ if parsed.title.is_empty() {
30
+ return None;
31
+ }
32
+ Some(parsed)
33
+ }
34
+
35
+ #[cfg(test)]
36
+ mod tests {
37
+ use super::*;
38
+
39
+ #[test]
40
+ fn parses_spotify_title() {
41
+ let parsed = parse_title("Daft Punk - Starboy - Spotify").unwrap();
42
+ assert_eq!(parsed.title, "Starboy");
43
+ assert_eq!(parsed.artist, "Daft Punk");
44
+ assert!(parse_title("Spotify Free").is_none());
45
+ }
46
+ }
@@ -0,0 +1,39 @@
1
+ //! WeSing (全民 K 歌) private adapter.
2
+ //!
3
+ //! WeSing's window title is `全民K歌 - 歌名`. Progress is read from its
4
+ //! accessibility text (`MM:SS | MM:SS`), but that lives in the platform
5
+ //! accessibility module because it is a generic tree scan.
6
+
7
+ use super::common;
8
+ use crate::media::catalog::ParsedTitle;
9
+
10
+ const PREFIX: &str = "全民K歌 - ";
11
+ const SUFFIXES: &[&str] = &[" - 全民K歌", " - WeSing"];
12
+ const PRODUCT_NAMES: &[&str] = &["全民k歌", "wesing"];
13
+
14
+ pub fn parse_title(raw: &str) -> Option<ParsedTitle> {
15
+ let normalized = common::normalize_spaces(raw);
16
+ let stripped = common::strip_prefix_ci(&normalized, PREFIX)
17
+ .map(str::to_string)
18
+ .unwrap_or_else(|| common::strip_suffixes(normalized, SUFFIXES));
19
+ if common::is_generic_title(&stripped, PRODUCT_NAMES) {
20
+ return None;
21
+ }
22
+ let parsed = common::split_title_artist(&stripped, false);
23
+ if parsed.title.is_empty() {
24
+ return None;
25
+ }
26
+ Some(parsed)
27
+ }
28
+
29
+ #[cfg(test)]
30
+ mod tests {
31
+ use super::*;
32
+
33
+ #[test]
34
+ fn parses_wesing_title() {
35
+ let parsed = parse_title("全民K歌 - 爱在西元前").unwrap();
36
+ assert_eq!(parsed.title, "爱在西元前");
37
+ assert!(parse_title("全民K歌").is_none());
38
+ }
39
+ }
@@ -0,0 +1,49 @@
1
+ //! YouTube Music private adapter.
2
+ //!
3
+ //! The desktop client and browsers usually bridge `navigator.mediaSession` to
4
+ //! SMTC/MPRIS. This module only parses the window title fallback.
5
+
6
+ use super::common;
7
+ use crate::media::catalog::ParsedTitle;
8
+
9
+ const SUFFIXES: &[&str] = &[
10
+ " - YouTube Music",
11
+ " - YouTube Music Premium",
12
+ " - YouTube",
13
+ " - YT Music",
14
+ ];
15
+ const PRODUCT_NAMES: &[&str] = &[
16
+ "youtube music",
17
+ "youtube music premium",
18
+ "youtube",
19
+ "yt music",
20
+ ];
21
+
22
+ pub fn parse_title(raw: &str) -> Option<ParsedTitle> {
23
+ let text = common::strip_suffixes(common::normalize_spaces(raw), SUFFIXES);
24
+ if common::is_generic_title(&text, PRODUCT_NAMES) {
25
+ return None;
26
+ }
27
+ let mut parsed = common::split_title_artist(&text, false);
28
+ parsed.artist = common::normalize_artist(
29
+ &parsed.artist,
30
+ &[("、", " / "), ("和", " / "), (", ", " / ")],
31
+ );
32
+ if parsed.title.is_empty() {
33
+ return None;
34
+ }
35
+ Some(parsed)
36
+ }
37
+
38
+ #[cfg(test)]
39
+ mod tests {
40
+ use super::*;
41
+
42
+ #[test]
43
+ fn parses_youtube_music_title() {
44
+ let parsed = parse_title("晴天 - 周杰伦 - YouTube Music").unwrap();
45
+ assert_eq!(parsed.title, "晴天");
46
+ assert_eq!(parsed.artist, "周杰伦");
47
+ assert!(parse_title("YouTube Music").is_none());
48
+ }
49
+ }
@@ -0,0 +1,85 @@
1
+ //! Windows UI Automation tier.
2
+ //!
3
+ //! This is the last resort for a read-only progress display. It is used only
4
+ //! for clients the reference project reads this way (NetEase and WeSing), and
5
+ //! it never performs a transport action or grants seek. The scan runs on a
6
+ //! short-lived worker thread with a timeout so a slow/hung accessibility
7
+ //! provider cannot stall the probe.
8
+
9
+ use std::ffi::c_void;
10
+ use std::sync::mpsc;
11
+ use std::time::Duration;
12
+
13
+ use windows::Win32::Foundation::HWND;
14
+ use windows::Win32::System::Com::{
15
+ CoCreateInstance, CoInitializeEx, CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED,
16
+ };
17
+ use windows::Win32::System::Variant::{VariantInit, VARIANT, VARIANT_0_0, VT_I4};
18
+ use windows::Win32::UI::Accessibility::{
19
+ CUIAutomation8, IUIAutomation, TreeScope_Descendants, UIA_ControlTypePropertyId,
20
+ UIA_TextControlTypeId,
21
+ };
22
+
23
+ const SCAN_TIMEOUT: Duration = Duration::from_millis(800);
24
+ const MAX_TEXT_ELEMENTS: i32 = 5000;
25
+
26
+ /// Read `MM:SS | MM:SS` progress from the window's accessibility text tree.
27
+ pub fn progress(hwnd: isize) -> Option<(f64, f64)> {
28
+ if hwnd == 0 {
29
+ return None;
30
+ }
31
+ let (sender, receiver) = mpsc::channel();
32
+ std::thread::spawn(move || {
33
+ let _ = sender.send(unsafe { read_progress(hwnd) });
34
+ });
35
+ receiver.recv_timeout(SCAN_TIMEOUT).ok().flatten()
36
+ }
37
+
38
+ fn ensure_com() {
39
+ unsafe {
40
+ let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
41
+ }
42
+ }
43
+
44
+ unsafe fn read_progress(hwnd: isize) -> Option<(f64, f64)> {
45
+ ensure_com();
46
+ let automation: IUIAutomation =
47
+ CoCreateInstance(&CUIAutomation8, None, CLSCTX_INPROC_SERVER).ok()?;
48
+ let window = automation
49
+ .ElementFromHandle(HWND(hwnd as *mut c_void))
50
+ .ok()?;
51
+ let condition_value = text_control_variant();
52
+ let condition = automation
53
+ .CreatePropertyCondition(UIA_ControlTypePropertyId, &condition_value)
54
+ .ok()?;
55
+ let elements = window.FindAll(TreeScope_Descendants, &condition).ok()?;
56
+ let count = elements.Length().ok()?.min(MAX_TEXT_ELEMENTS);
57
+ for index in 0..count {
58
+ let Ok(element) = elements.GetElement(index) else {
59
+ continue;
60
+ };
61
+ let Ok(name) = element.CurrentName() else {
62
+ continue;
63
+ };
64
+ if let Some(progress) = crate::media::catalog::parse_progress_text(&name.to_string()) {
65
+ return Some(progress);
66
+ }
67
+ }
68
+ None
69
+ }
70
+
71
+ fn text_control_variant() -> VARIANT {
72
+ // Build the inner VARIANT_0_0 separately and write it into the union; this
73
+ // avoids assigning through the `ManuallyDrop` field of VARIANT_0.
74
+ let mut inner: VARIANT_0_0 = unsafe { std::mem::zeroed() };
75
+ inner.vt = VT_I4;
76
+ inner.Anonymous.lVal = UIA_TextControlTypeId.0;
77
+ let mut value: VARIANT = unsafe { VariantInit() };
78
+ unsafe {
79
+ std::ptr::write(
80
+ std::ptr::addr_of_mut!(value.Anonymous.Anonymous),
81
+ std::mem::ManuallyDrop::new(inner),
82
+ );
83
+ }
84
+ value
85
+ }
@@ -0,0 +1,116 @@
1
+ //! Per-process audio activity on the default render endpoint.
2
+ //!
3
+ //! Several desktop players (QQ Music, KuGou, KuWo, Soda, Spotify) do not refresh
4
+ //! their SMTC playback status unless the Windows media flyout is open. The
5
+ //! reference project therefore uses the audio session meter/state to decide
6
+ //! whether the client is actually playing. The probe exposes the same signal so
7
+ //! the private tier and incomplete system sessions can correct their status.
8
+
9
+ use std::collections::HashMap;
10
+
11
+ use windows::core::Interface;
12
+ use windows::Win32::Media::Audio::Endpoints::IAudioMeterInformation;
13
+ use windows::Win32::Media::Audio::{
14
+ eMultimedia, eRender, AudioSessionStateActive, IAudioSessionControl2, IAudioSessionManager2,
15
+ IMMDeviceEnumerator, MMDeviceEnumerator,
16
+ };
17
+ use windows::Win32::System::Com::{
18
+ CoCreateInstance, CoInitializeEx, CLSCTX_ALL, COINIT_MULTITHREADED,
19
+ };
20
+
21
+ /// Aggregated state for one process.
22
+ #[derive(Debug, Clone, Default)]
23
+ pub struct Activity {
24
+ pub active: bool,
25
+ pub audible: bool,
26
+ pub exe: Option<String>,
27
+ }
28
+
29
+ impl Activity {
30
+ /// The client is producing sound or its audio session is active.
31
+ pub fn is_playing(&self) -> bool {
32
+ self.active || self.audible
33
+ }
34
+ }
35
+
36
+ fn ensure_com() {
37
+ static ONCE: std::sync::Once = std::sync::Once::new();
38
+ ONCE.call_once(|| unsafe {
39
+ let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
40
+ });
41
+ }
42
+
43
+ /// Snapshot every session on the default multimedia render endpoint.
44
+ ///
45
+ /// An empty map simply means Core Audio could not be queried; callers then keep
46
+ /// the status reported by the system media session.
47
+ pub fn snapshot() -> HashMap<u32, Activity> {
48
+ ensure_com();
49
+ let mut activities: HashMap<u32, Activity> = HashMap::new();
50
+ unsafe {
51
+ let enumerator: IMMDeviceEnumerator =
52
+ match CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL) {
53
+ Ok(value) => value,
54
+ Err(_) => return activities,
55
+ };
56
+ let device = match enumerator.GetDefaultAudioEndpoint(eRender, eMultimedia) {
57
+ Ok(value) => value,
58
+ Err(_) => return activities,
59
+ };
60
+ let manager: IAudioSessionManager2 = match device.Activate(CLSCTX_ALL, None) {
61
+ Ok(value) => value,
62
+ Err(_) => return activities,
63
+ };
64
+ let sessions = match manager.GetSessionEnumerator() {
65
+ Ok(value) => value,
66
+ Err(_) => return activities,
67
+ };
68
+ let count = sessions.GetCount().unwrap_or(0);
69
+ for index in 0..count {
70
+ let Ok(control) = sessions.GetSession(index) else {
71
+ continue;
72
+ };
73
+ let process_id = control
74
+ .cast::<IAudioSessionControl2>()
75
+ .ok()
76
+ .and_then(|control| control.GetProcessId().ok())
77
+ .unwrap_or(0);
78
+ if process_id == 0 {
79
+ continue;
80
+ }
81
+ let peak = control
82
+ .cast::<IAudioMeterInformation>()
83
+ .ok()
84
+ .and_then(|meter| meter.GetPeakValue().ok())
85
+ .unwrap_or(0.0);
86
+ let state = control.GetState().ok();
87
+ let entry = activities.entry(process_id).or_insert_with(|| Activity {
88
+ exe: super::private::process_image_name(process_id),
89
+ ..Activity::default()
90
+ });
91
+ if state == Some(AudioSessionStateActive) {
92
+ entry.active = true;
93
+ }
94
+ if peak > 0.00001 {
95
+ entry.audible = true;
96
+ }
97
+ }
98
+ }
99
+ activities
100
+ }
101
+
102
+ /// Audio activity for one executable (case-insensitive).
103
+ pub fn for_exe<'a>(activities: &'a HashMap<u32, Activity>, exe: &str) -> Option<&'a Activity> {
104
+ activities.values().find(|activity| {
105
+ activity
106
+ .exe
107
+ .as_deref()
108
+ .map(|candidate| candidate.eq_ignore_ascii_case(exe))
109
+ .unwrap_or(false)
110
+ })
111
+ }
112
+
113
+ /// Audio activity for one process id.
114
+ pub fn for_pid<'a>(activities: &'a HashMap<u32, Activity>, pid: u32) -> Option<&'a Activity> {
115
+ activities.get(&pid)
116
+ }
@@ -0,0 +1,291 @@
1
+ //! Windows private-interface tier.
2
+ //!
3
+ //! Many Chinese desktop players expose no usable SMTC session (or advertise one
4
+ //! without a position). The reference project reads their top-level window
5
+ //! titles instead, and NetEase/WeSing additionally expose `MM:SS | MM:SS`
6
+ //! progress through the UI Automation text tree. This module implements the
7
+ //! window-title part and Cider's `Plugins/title.txt`; the accessibility part
8
+ //! lives in [`super::accessibility`].
9
+ //!
10
+ //! Window titles are read with `EnumWindows` and process image names with
11
+ //! `QueryFullProcessImageNameW`; neither touches the network.
12
+
13
+ use std::collections::HashMap;
14
+
15
+ use windows::core::{BOOL, PWSTR};
16
+ use windows::Win32::Foundation::{CloseHandle, HWND, LPARAM};
17
+ use windows::Win32::System::Threading::{
18
+ OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
19
+ };
20
+ use windows::Win32::UI::WindowsAndMessaging::{
21
+ EnumWindows, GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible,
22
+ };
23
+
24
+ use super::super::catalog::{self, PrivateKind, Profile, Tier};
25
+ use super::super::{Media, ReadOptions};
26
+ use super::audio::{self, Activity};
27
+ use crate::util;
28
+
29
+ /// A visible or hidden top-level window belonging to a desktop process.
30
+ #[derive(Debug, Clone)]
31
+ pub struct WindowInfo {
32
+ pub hwnd: isize,
33
+ pub pid: u32,
34
+ pub exe: String,
35
+ pub title: String,
36
+ pub visible: bool,
37
+ }
38
+
39
+ pub fn enumerate_windows() -> Vec<WindowInfo> {
40
+ let mut windows: Vec<WindowInfo> = Vec::new();
41
+ unsafe {
42
+ let _ = EnumWindows(
43
+ Some(enum_window),
44
+ LPARAM(&mut windows as *mut Vec<WindowInfo> as isize),
45
+ );
46
+ }
47
+ windows
48
+ }
49
+
50
+ unsafe extern "system" fn enum_window(hwnd: HWND, lparam: LPARAM) -> BOOL {
51
+ let windows = &mut *(lparam.0 as *mut Vec<WindowInfo>);
52
+ let mut pid = 0u32;
53
+ GetWindowThreadProcessId(hwnd, Some(&mut pid));
54
+ if pid == 0 {
55
+ return BOOL(1);
56
+ }
57
+ let length = GetWindowTextLengthW(hwnd);
58
+ if length <= 0 {
59
+ return BOOL(1);
60
+ }
61
+ let mut buffer = vec![0u16; length as usize + 1];
62
+ let copied = GetWindowTextW(hwnd, &mut buffer);
63
+ if copied <= 0 {
64
+ return BOOL(1);
65
+ }
66
+ let title = String::from_utf16_lossy(&buffer[..copied as usize]);
67
+ if title.trim().is_empty() {
68
+ return BOOL(1);
69
+ }
70
+ windows.push(WindowInfo {
71
+ hwnd: hwnd.0 as isize,
72
+ pid,
73
+ exe: process_image_name(pid).unwrap_or_default(),
74
+ title,
75
+ visible: IsWindowVisible(hwnd).as_bool(),
76
+ });
77
+ BOOL(1)
78
+ }
79
+
80
+ /// Full executable name (no directory) for a process id.
81
+ pub fn process_image_name(pid: u32) -> Option<String> {
82
+ unsafe {
83
+ let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid).ok()?;
84
+ let mut buffer = [0u16; 1024];
85
+ let mut size = buffer.len() as u32;
86
+ let result = QueryFullProcessImageNameW(
87
+ handle,
88
+ PROCESS_NAME_WIN32,
89
+ PWSTR(buffer.as_mut_ptr()),
90
+ &mut size,
91
+ );
92
+ let _ = CloseHandle(handle);
93
+ result.ok()?;
94
+ let path = String::from_utf16_lossy(&buffer[..size as usize]);
95
+ path.rsplit(['\\', '/'])
96
+ .next()
97
+ .map(str::to_string)
98
+ .filter(|name| !name.is_empty())
99
+ }
100
+ }
101
+
102
+ /// Build a private-tier media payload for `profile`, if a matching window (or
103
+ /// Cider title file) exists.
104
+ pub fn read(
105
+ profile: &'static Profile,
106
+ windows: &[WindowInfo],
107
+ activities: &HashMap<u32, Activity>,
108
+ smtc: Option<&Media>,
109
+ options: &ReadOptions,
110
+ ) -> Option<Media> {
111
+ if matches!(profile.private, PrivateKind::None) {
112
+ return None;
113
+ }
114
+ let window = best_window(profile, windows)?;
115
+ let (parsed, detail) = match profile.private {
116
+ PrivateKind::CiderTitleFile => match crate::media::vendor::apple_music::cider_title() {
117
+ Some(parsed) => (parsed, "cider-title"),
118
+ None => (profile.parse_title(&window.title)?, "window-title"),
119
+ },
120
+ _ => (profile.parse_title(&window.title)?, "window-title"),
121
+ };
122
+
123
+ let app_id = if window.exe.is_empty() {
124
+ profile.app_id.to_string()
125
+ } else {
126
+ window.exe.clone()
127
+ };
128
+ let mut media = Media::new(app_id);
129
+ media.provider = Tier::Private.as_str().to_string();
130
+ media.provider_detail = detail.to_string();
131
+ media.title = parsed.title;
132
+ media.artist = parsed.artist;
133
+ media.playback_status = playback_status(profile, windows, activities, &window.exe, smtc);
134
+
135
+ // The private tier is read-only. If a partial SMTC session exists we can
136
+ // still use its transport controls, but never its seek grant.
137
+ if let Some(smtc) = smtc {
138
+ media.album = smtc.album.clone();
139
+ media.album_artist = smtc.album_artist.clone();
140
+ media.track_number = smtc.track_number;
141
+ media.auto_repeat_mode = smtc.auto_repeat_mode.clone();
142
+ media.shuffle_active = smtc.shuffle_active;
143
+ media.can_play_pause = smtc.can_play_pause;
144
+ media.can_next = smtc.can_next;
145
+ media.can_previous = smtc.can_previous;
146
+ if options.thumbnail {
147
+ media.thumbnail = smtc.thumbnail.clone();
148
+ media.thumbnail_type = smtc.thumbnail_type.clone();
149
+ }
150
+ } else {
151
+ media.can_play_pause = false;
152
+ media.can_next = false;
153
+ media.can_previous = false;
154
+ }
155
+ media.can_seek = catalog::seek_allowed(Tier::Private, false, false);
156
+ media.last_updated_at = util::now_ms();
157
+
158
+ // Accessibility is the last resort for a read-only progress display. It is
159
+ // never allowed to grant seek (see `catalog::seek_allowed`). NetEase defers
160
+ // this to `vendor::netease::resolve` so CDP can take precedence.
161
+ if profile.smtc_policy != catalog::SmtcPolicy::MetadataOnly {
162
+ attach_progress(profile, windows, &mut media);
163
+ }
164
+ Some(media)
165
+ }
166
+
167
+ /// Find `MM:SS | MM:SS` progress for a profile's window. The caller decides
168
+ /// whether accessibility is appropriate; this only performs the scan.
169
+ pub fn progress_for_profile(profile: &Profile, windows: &[WindowInfo]) -> Option<(f64, f64)> {
170
+ if !profile.accessibility {
171
+ return None;
172
+ }
173
+ let window = best_window(profile, windows)?;
174
+ super::accessibility::progress(window.hwnd)
175
+ }
176
+
177
+ /// Attach UI Automation progress to a media item.
178
+ pub fn attach_progress(profile: &Profile, windows: &[WindowInfo], media: &mut Media) -> bool {
179
+ let Some((position, total)) = progress_for_profile(profile, windows) else {
180
+ return false;
181
+ };
182
+ if total <= 0.0 {
183
+ return false;
184
+ }
185
+ media.position = position;
186
+ media.start_time = 0.0;
187
+ media.end_time = total;
188
+ media.min_seek_time = 0.0;
189
+ media.max_seek_time = total;
190
+ media.last_updated_at = util::now_ms();
191
+ if media.provider_detail.contains("smtc") {
192
+ media.provider_detail = "smtc+uia".to_string();
193
+ } else {
194
+ media.provider_detail = format!("{}+uia", media.provider_detail);
195
+ }
196
+ true
197
+ }
198
+
199
+ /// Audio activity for a profile, using the first matching window process.
200
+ pub fn profile_activity<'a>(
201
+ profile: &Profile,
202
+ windows: &[WindowInfo],
203
+ activities: &'a HashMap<u32, Activity>,
204
+ ) -> Option<&'a Activity> {
205
+ if let Some(activity) = windows
206
+ .iter()
207
+ .filter(|window| {
208
+ catalog::profile_for_process(&window.exe)
209
+ .map(|candidate| candidate.app == profile.app)
210
+ .unwrap_or(false)
211
+ })
212
+ .find_map(|window| {
213
+ audio::for_exe(activities, &window.exe)
214
+ .or_else(|| audio::for_pid(activities, window.pid))
215
+ })
216
+ {
217
+ return Some(activity);
218
+ }
219
+ // Some clients play audio through a helper process (KuWo's `KwService`)
220
+ // that owns no window. Match the audio session by its process profile.
221
+ activities.values().find(|activity| {
222
+ activity
223
+ .exe
224
+ .as_deref()
225
+ .and_then(catalog::profile_for_process)
226
+ .map(|candidate| candidate.app == profile.app)
227
+ .unwrap_or(false)
228
+ })
229
+ }
230
+
231
+ fn playback_status(
232
+ profile: &Profile,
233
+ windows: &[WindowInfo],
234
+ activities: &HashMap<u32, Activity>,
235
+ exe: &str,
236
+ smtc: Option<&Media>,
237
+ ) -> String {
238
+ if let Some(activity) = audio::for_exe(activities, exe) {
239
+ return if activity.is_playing() {
240
+ "Playing".to_string()
241
+ } else {
242
+ "Paused".to_string()
243
+ };
244
+ }
245
+ if let Some(activity) = profile_activity(profile, windows, activities) {
246
+ return if activity.is_playing() {
247
+ "Playing".to_string()
248
+ } else {
249
+ "Paused".to_string()
250
+ };
251
+ }
252
+ smtc.map(|media| media.playback_status.clone())
253
+ .unwrap_or_else(|| "Paused".to_string())
254
+ }
255
+
256
+ fn best_window<'a>(profile: &Profile, windows: &'a [WindowInfo]) -> Option<&'a WindowInfo> {
257
+ let mut candidates: Vec<&WindowInfo> = windows
258
+ .iter()
259
+ .filter(|window| {
260
+ catalog::profile_for_process(&window.exe)
261
+ .map(|candidate| candidate.app == profile.app)
262
+ .unwrap_or(false)
263
+ })
264
+ .collect();
265
+ // Keep enumeration (z-order) order inside each visibility class.
266
+ candidates.sort_by_key(|window| !window.visible);
267
+
268
+ for window in &candidates {
269
+ if profile.parse_title(&window.title).is_some() {
270
+ return Some(window);
271
+ }
272
+ }
273
+ if matches!(profile.private, PrivateKind::CiderTitleFile)
274
+ && crate::media::vendor::apple_music::cider_title().is_some()
275
+ && !candidates.is_empty()
276
+ {
277
+ return candidates.first().copied();
278
+ }
279
+ candidates.first().copied()
280
+ }
281
+
282
+ #[cfg(test)]
283
+ mod tests {
284
+ use super::*;
285
+
286
+ #[test]
287
+ fn process_image_name_of_current_process_exists() {
288
+ let name = process_image_name(std::process::id()).unwrap_or_default();
289
+ assert!(name.to_lowercase().ends_with(".exe"), "{name}");
290
+ }
291
+ }