diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index b59fb9e..3aff557 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -8,8 +8,11 @@ copyrights, licenses, and trademarks remain with their respective owners.
| NexBox | Local source under `参考/NexBox-main` | GPL-3.0 | Reference implementation for hardware monitoring, optimization, overlays, music, network diagnostics, and installer workflows. NexBox branding and site assets are not included. |
| LibreHardwareMonitor | LibreHardwareMonitorLib 0.9.6 | MPL-2.0 | Optional sensor provider in the isolated hardware-monitor host. |
| QRCoder | 1.8.0 | MIT | QR code rendering for provider login. |
-| MoeKoeMusic | Commit [`4581660209c9cdd07669e8bd5bcbe136b7d91e02`](https://github.com/MoeKoeMusic/MoeKoeMusic/tree/4581660209c9cdd07669e8bd5bcbe136b7d91e02) | GPL-3.0 | Reference for the KuGou client login and account workflow. No MoeKoeMusic code or branding is bundled. |
-| KuGouMusicApi | Commit [`06560e3e053bda1ab830750db6f645bab703f824`](https://github.com/MakcRe/KuGouMusicApi/tree/06560e3e053bda1ab830750db6f645bab703f824) | MIT | Protocol reference for KuGou request signing, device registration, QR login, account playlists, and entitlement-aware HTTPS playback. The required protocol subset is independently implemented in C#. |
+| MoeKoeMusic | Commit [`4581660209c9cdd07669e8bd5bcbe136b7d91e02`](https://github.com/MoeKoeMusic/MoeKoeMusic/tree/4581660209c9cdd07669e8bd5bcbe136b7d91e02) | GPL-2.0 at the pinned commit | Pinned upstream project, README, license, and original disclaimer distributed with the local KuGou API integration. The fixed commit identifies `api` as the KuGouMusicApi submodule below. |
+| KuGouMusicApi | Commit [`06560e3e053bda1ab830750db6f645bab703f824`](https://github.com/MakcRe/KuGouMusicApi/tree/06560e3e053bda1ab830750db6f645bab703f824) | MIT | Bundled and modified local random-port API service for KuGou requests, login/account workflows, playback resolution, and KRC lyrics. Original source, lockfile, MIT license, README/disclaimer, and `YMhut_PATCHES.md` are distributed in `music-api`. |
+| Binaryify/NeteaseCloudMusicApi | [Repository](https://github.com/Binaryify/NeteaseCloudMusicApi) | Upstream project license | Attribution for the two-minute API response cache mechanism inherited by KuGouMusicApi. YMhut restricts the cache to an explicit anonymous public GET allowlist. |
+| Node.js | 22.18.0 x64 | Node.js license | Bundled runtime for the loopback-only music API child process. Production does not depend on a system Node installation. |
+| MoeKoeMusic API npm dependencies | Exact versions in `api/pnpm-lock.yaml` | Package-specific licenses | Production dependencies include axios, express, pako, qrcode, crypto-js, node-forge, big-integer, dotenv, safe-decode-uri-component, and url. |
| Microsoft Windows App SDK | 1.8.260416003 | Microsoft license | WinUI 3 desktop application and installer bootstrap. |
| Microsoft WebView2 | 1.0.3967.48 | Microsoft license | Embedded browser and tool surfaces. |
| Microsoft.Data.Sqlite | 10.0.9 | MIT | Local application data and logs. |
diff --git a/assets/data/tool-ui/age_calculator.dat b/assets/data/tool-ui/age_calculator.dat
new file mode 100644
index 0000000..dca91d3
Binary files /dev/null and b/assets/data/tool-ui/age_calculator.dat differ
diff --git a/assets/data/tool-ui/ai_latest_news.dat b/assets/data/tool-ui/ai_latest_news.dat
new file mode 100644
index 0000000..00b7976
Binary files /dev/null and b/assets/data/tool-ui/ai_latest_news.dat differ
diff --git a/assets/data/tool-ui/ai_translation.dat b/assets/data/tool-ui/ai_translation.dat
new file mode 100644
index 0000000..8eded05
Binary files /dev/null and b/assets/data/tool-ui/ai_translation.dat differ
diff --git a/assets/data/tool-ui/archive_tool.dat b/assets/data/tool-ui/archive_tool.dat
new file mode 100644
index 0000000..46fad0e
Binary files /dev/null and b/assets/data/tool-ui/archive_tool.dat differ
diff --git a/assets/data/tool-ui/area_converter.dat b/assets/data/tool-ui/area_converter.dat
new file mode 100644
index 0000000..7966b8a
Binary files /dev/null and b/assets/data/tool-ui/area_converter.dat differ
diff --git a/assets/data/tool-ui/ascii_art.dat b/assets/data/tool-ui/ascii_art.dat
new file mode 100644
index 0000000..cbd33d2
Binary files /dev/null and b/assets/data/tool-ui/ascii_art.dat differ
diff --git a/assets/data/tool-ui/average_calculator.dat b/assets/data/tool-ui/average_calculator.dat
new file mode 100644
index 0000000..6e14346
Binary files /dev/null and b/assets/data/tool-ui/average_calculator.dat differ
diff --git a/assets/data/tool-ui/baidu_hot.dat b/assets/data/tool-ui/baidu_hot.dat
new file mode 100644
index 0000000..396070a
Binary files /dev/null and b/assets/data/tool-ui/baidu_hot.dat differ
diff --git a/assets/data/tool-ui/base64_codec.dat b/assets/data/tool-ui/base64_codec.dat
new file mode 100644
index 0000000..f820b52
Binary files /dev/null and b/assets/data/tool-ui/base64_codec.dat differ
diff --git a/assets/data/tool-ui/batch_rename_preview.dat b/assets/data/tool-ui/batch_rename_preview.dat
new file mode 100644
index 0000000..9f2e82d
Binary files /dev/null and b/assets/data/tool-ui/batch_rename_preview.dat differ
diff --git a/assets/data/tool-ui/bili_hot.dat b/assets/data/tool-ui/bili_hot.dat
new file mode 100644
index 0000000..179f9cd
Binary files /dev/null and b/assets/data/tool-ui/bili_hot.dat differ
diff --git a/assets/data/tool-ui/blank_line_cleaner.dat b/assets/data/tool-ui/blank_line_cleaner.dat
new file mode 100644
index 0000000..6e210c6
Binary files /dev/null and b/assets/data/tool-ui/blank_line_cleaner.dat differ
diff --git a/assets/data/tool-ui/bmi_calculator.dat b/assets/data/tool-ui/bmi_calculator.dat
new file mode 100644
index 0000000..aa428df
Binary files /dev/null and b/assets/data/tool-ui/bmi_calculator.dat differ
diff --git a/assets/data/tool-ui/calculator_tool.dat b/assets/data/tool-ui/calculator_tool.dat
new file mode 100644
index 0000000..5508a87
Binary files /dev/null and b/assets/data/tool-ui/calculator_tool.dat differ
diff --git a/assets/data/tool-ui/car_info.dat b/assets/data/tool-ui/car_info.dat
new file mode 100644
index 0000000..536f5e5
Binary files /dev/null and b/assets/data/tool-ui/car_info.dat differ
diff --git a/assets/data/tool-ui/case_converter.dat b/assets/data/tool-ui/case_converter.dat
new file mode 100644
index 0000000..0b1664c
Binary files /dev/null and b/assets/data/tool-ui/case_converter.dat differ
diff --git a/assets/data/tool-ui/cctv_news.dat b/assets/data/tool-ui/cctv_news.dat
new file mode 100644
index 0000000..504e4b4
Binary files /dev/null and b/assets/data/tool-ui/cctv_news.dat differ
diff --git a/assets/data/tool-ui/charset_lookup.dat b/assets/data/tool-ui/charset_lookup.dat
new file mode 100644
index 0000000..4c6319b
Binary files /dev/null and b/assets/data/tool-ui/charset_lookup.dat differ
diff --git a/assets/data/tool-ui/chinese_converter.dat b/assets/data/tool-ui/chinese_converter.dat
new file mode 100644
index 0000000..74a5746
Binary files /dev/null and b/assets/data/tool-ui/chinese_converter.dat differ
diff --git a/assets/data/tool-ui/cidr_subnet_calculator.dat b/assets/data/tool-ui/cidr_subnet_calculator.dat
new file mode 100644
index 0000000..761895d
Binary files /dev/null and b/assets/data/tool-ui/cidr_subnet_calculator.dat differ
diff --git a/assets/data/tool-ui/city_route_query.dat b/assets/data/tool-ui/city_route_query.dat
new file mode 100644
index 0000000..bc1ef1e
Binary files /dev/null and b/assets/data/tool-ui/city_route_query.dat differ
diff --git a/assets/data/tool-ui/color_contrast_checker.dat b/assets/data/tool-ui/color_contrast_checker.dat
new file mode 100644
index 0000000..d395269
Binary files /dev/null and b/assets/data/tool-ui/color_contrast_checker.dat differ
diff --git a/assets/data/tool-ui/color_picker.dat b/assets/data/tool-ui/color_picker.dat
new file mode 100644
index 0000000..c1b8f57
Binary files /dev/null and b/assets/data/tool-ui/color_picker.dat differ
diff --git a/assets/data/tool-ui/column_extractor.dat b/assets/data/tool-ui/column_extractor.dat
new file mode 100644
index 0000000..a8a8165
Binary files /dev/null and b/assets/data/tool-ui/column_extractor.dat differ
diff --git a/assets/data/tool-ui/combination_calculator.dat b/assets/data/tool-ui/combination_calculator.dat
new file mode 100644
index 0000000..787d025
Binary files /dev/null and b/assets/data/tool-ui/combination_calculator.dat differ
diff --git a/assets/data/tool-ui/compound_interest_calculator.dat b/assets/data/tool-ui/compound_interest_calculator.dat
new file mode 100644
index 0000000..41aa443
Binary files /dev/null and b/assets/data/tool-ui/compound_interest_calculator.dat differ
diff --git a/assets/data/tool-ui/compression_codec.dat b/assets/data/tool-ui/compression_codec.dat
new file mode 100644
index 0000000..7eee023
Binary files /dev/null and b/assets/data/tool-ui/compression_codec.dat differ
diff --git a/assets/data/tool-ui/cron_helper.dat b/assets/data/tool-ui/cron_helper.dat
new file mode 100644
index 0000000..30b4948
Binary files /dev/null and b/assets/data/tool-ui/cron_helper.dat differ
diff --git a/assets/data/tool-ui/csv_json_converter.dat b/assets/data/tool-ui/csv_json_converter.dat
new file mode 100644
index 0000000..ac86fd2
Binary files /dev/null and b/assets/data/tool-ui/csv_json_converter.dat differ
diff --git a/assets/data/tool-ui/csv_tsv_converter.dat b/assets/data/tool-ui/csv_tsv_converter.dat
new file mode 100644
index 0000000..ecf377b
Binary files /dev/null and b/assets/data/tool-ui/csv_tsv_converter.dat differ
diff --git a/assets/data/tool-ui/data_size_converter.dat b/assets/data/tool-ui/data_size_converter.dat
new file mode 100644
index 0000000..1860182
Binary files /dev/null and b/assets/data/tool-ui/data_size_converter.dat differ
diff --git a/assets/data/tool-ui/date_difference_calculator.dat b/assets/data/tool-ui/date_difference_calculator.dat
new file mode 100644
index 0000000..121e8bc
Binary files /dev/null and b/assets/data/tool-ui/date_difference_calculator.dat differ
diff --git a/assets/data/tool-ui/delimiter_converter.dat b/assets/data/tool-ui/delimiter_converter.dat
new file mode 100644
index 0000000..abe6a1b
Binary files /dev/null and b/assets/data/tool-ui/delimiter_converter.dat differ
diff --git a/assets/data/tool-ui/dev_environment_config.dat b/assets/data/tool-ui/dev_environment_config.dat
new file mode 100644
index 0000000..656f479
Binary files /dev/null and b/assets/data/tool-ui/dev_environment_config.dat differ
diff --git a/assets/data/tool-ui/discount_calculator.dat b/assets/data/tool-ui/discount_calculator.dat
new file mode 100644
index 0000000..9596ba0
Binary files /dev/null and b/assets/data/tool-ui/discount_calculator.dat differ
diff --git a/assets/data/tool-ui/dns_query.dat b/assets/data/tool-ui/dns_query.dat
new file mode 100644
index 0000000..cb8000d
Binary files /dev/null and b/assets/data/tool-ui/dns_query.dat differ
diff --git a/assets/data/tool-ui/dns_record_lookup.dat b/assets/data/tool-ui/dns_record_lookup.dat
new file mode 100644
index 0000000..4a9a7a8
Binary files /dev/null and b/assets/data/tool-ui/dns_record_lookup.dat differ
diff --git a/assets/data/tool-ui/domain_extractor.dat b/assets/data/tool-ui/domain_extractor.dat
new file mode 100644
index 0000000..07b4f80
Binary files /dev/null and b/assets/data/tool-ui/domain_extractor.dat differ
diff --git a/assets/data/tool-ui/domain_price.dat b/assets/data/tool-ui/domain_price.dat
new file mode 100644
index 0000000..96864b5
Binary files /dev/null and b/assets/data/tool-ui/domain_price.dat differ
diff --git a/assets/data/tool-ui/duplicate_detector.dat b/assets/data/tool-ui/duplicate_detector.dat
new file mode 100644
index 0000000..388c78d
Binary files /dev/null and b/assets/data/tool-ui/duplicate_detector.dat differ
diff --git a/assets/data/tool-ui/earthquake_info.dat b/assets/data/tool-ui/earthquake_info.dat
new file mode 100644
index 0000000..60a1438
Binary files /dev/null and b/assets/data/tool-ui/earthquake_info.dat differ
diff --git a/assets/data/tool-ui/extension_extractor.dat b/assets/data/tool-ui/extension_extractor.dat
new file mode 100644
index 0000000..2260770
Binary files /dev/null and b/assets/data/tool-ui/extension_extractor.dat differ
diff --git a/assets/data/tool-ui/extension_statistics.dat b/assets/data/tool-ui/extension_statistics.dat
new file mode 100644
index 0000000..7641a12
Binary files /dev/null and b/assets/data/tool-ui/extension_statistics.dat differ
diff --git a/assets/data/tool-ui/external-tool-host.dat b/assets/data/tool-ui/external-tool-host.dat
new file mode 100644
index 0000000..9f4a79c
Binary files /dev/null and b/assets/data/tool-ui/external-tool-host.dat differ
diff --git a/assets/data/tool-ui/file_manifest_builder.dat b/assets/data/tool-ui/file_manifest_builder.dat
new file mode 100644
index 0000000..82420e2
Binary files /dev/null and b/assets/data/tool-ui/file_manifest_builder.dat differ
diff --git a/assets/data/tool-ui/filename_formatter.dat b/assets/data/tool-ui/filename_formatter.dat
new file mode 100644
index 0000000..bbad27f
Binary files /dev/null and b/assets/data/tool-ui/filename_formatter.dat differ
diff --git a/assets/data/tool-ui/football_news.dat b/assets/data/tool-ui/football_news.dat
new file mode 100644
index 0000000..55148f3
Binary files /dev/null and b/assets/data/tool-ui/football_news.dat differ
diff --git a/assets/data/tool-ui/form_urlencoded_builder.dat b/assets/data/tool-ui/form_urlencoded_builder.dat
new file mode 100644
index 0000000..3b8442a
Binary files /dev/null and b/assets/data/tool-ui/form_urlencoded_builder.dat differ
diff --git a/assets/data/tool-ui/gcd_lcm_calculator.dat b/assets/data/tool-ui/gcd_lcm_calculator.dat
new file mode 100644
index 0000000..5393016
Binary files /dev/null and b/assets/data/tool-ui/gcd_lcm_calculator.dat differ
diff --git a/assets/data/tool-ui/gold_price.dat b/assets/data/tool-ui/gold_price.dat
new file mode 100644
index 0000000..dcc1046
Binary files /dev/null and b/assets/data/tool-ui/gold_price.dat differ
diff --git a/assets/data/tool-ui/hardware.dat b/assets/data/tool-ui/hardware.dat
new file mode 100644
index 0000000..d5abe59
Binary files /dev/null and b/assets/data/tool-ui/hardware.dat differ
diff --git a/assets/data/tool-ui/hash_generator.dat b/assets/data/tool-ui/hash_generator.dat
new file mode 100644
index 0000000..43efb45
Binary files /dev/null and b/assets/data/tool-ui/hash_generator.dat differ
diff --git a/assets/data/tool-ui/hash_manifest_builder.dat b/assets/data/tool-ui/hash_manifest_builder.dat
new file mode 100644
index 0000000..9e17b9b
Binary files /dev/null and b/assets/data/tool-ui/hash_manifest_builder.dat differ
diff --git a/assets/data/tool-ui/hash_manifest_verify.dat b/assets/data/tool-ui/hash_manifest_verify.dat
new file mode 100644
index 0000000..b9dbc10
Binary files /dev/null and b/assets/data/tool-ui/hash_manifest_verify.dat differ
diff --git a/assets/data/tool-ui/history_today.dat b/assets/data/tool-ui/history_today.dat
new file mode 100644
index 0000000..5aeb681
Binary files /dev/null and b/assets/data/tool-ui/history_today.dat differ
diff --git a/assets/data/tool-ui/hmac_generator.dat b/assets/data/tool-ui/hmac_generator.dat
new file mode 100644
index 0000000..0e7a5b6
Binary files /dev/null and b/assets/data/tool-ui/hmac_generator.dat differ
diff --git a/assets/data/tool-ui/hotboard.dat b/assets/data/tool-ui/hotboard.dat
new file mode 100644
index 0000000..b3618de
Binary files /dev/null and b/assets/data/tool-ui/hotboard.dat differ
diff --git a/assets/data/tool-ui/html_entity.dat b/assets/data/tool-ui/html_entity.dat
new file mode 100644
index 0000000..a6fd70f
Binary files /dev/null and b/assets/data/tool-ui/html_entity.dat differ
diff --git a/assets/data/tool-ui/html_js_playground.dat b/assets/data/tool-ui/html_js_playground.dat
new file mode 100644
index 0000000..44bdeb1
Binary files /dev/null and b/assets/data/tool-ui/html_js_playground.dat differ
diff --git a/assets/data/tool-ui/html_minifier.dat b/assets/data/tool-ui/html_minifier.dat
new file mode 100644
index 0000000..aaeccf6
Binary files /dev/null and b/assets/data/tool-ui/html_minifier.dat differ
diff --git a/assets/data/tool-ui/html_text_extractor.dat b/assets/data/tool-ui/html_text_extractor.dat
new file mode 100644
index 0000000..d5571fa
Binary files /dev/null and b/assets/data/tool-ui/html_text_extractor.dat differ
diff --git a/assets/data/tool-ui/http_diagnostic.dat b/assets/data/tool-ui/http_diagnostic.dat
new file mode 100644
index 0000000..ead112d
Binary files /dev/null and b/assets/data/tool-ui/http_diagnostic.dat differ
diff --git a/assets/data/tool-ui/http_status_lookup.dat b/assets/data/tool-ui/http_status_lookup.dat
new file mode 100644
index 0000000..d0cb74c
Binary files /dev/null and b/assets/data/tool-ui/http_status_lookup.dat differ
diff --git a/assets/data/tool-ui/image_metadata_inspector.dat b/assets/data/tool-ui/image_metadata_inspector.dat
new file mode 100644
index 0000000..939c358
Binary files /dev/null and b/assets/data/tool-ui/image_metadata_inspector.dat differ
diff --git a/assets/data/tool-ui/image_processor.dat b/assets/data/tool-ui/image_processor.dat
new file mode 100644
index 0000000..8bf0686
Binary files /dev/null and b/assets/data/tool-ui/image_processor.dat differ
diff --git a/assets/data/tool-ui/indexed_rename_preview.dat b/assets/data/tool-ui/indexed_rename_preview.dat
new file mode 100644
index 0000000..180b64a
Binary files /dev/null and b/assets/data/tool-ui/indexed_rename_preview.dat differ
diff --git a/assets/data/tool-ui/ip_info.dat b/assets/data/tool-ui/ip_info.dat
new file mode 100644
index 0000000..3a0cf99
Binary files /dev/null and b/assets/data/tool-ui/ip_info.dat differ
diff --git a/assets/data/tool-ui/ip_lookup.dat b/assets/data/tool-ui/ip_lookup.dat
new file mode 100644
index 0000000..eef2ea9
Binary files /dev/null and b/assets/data/tool-ui/ip_lookup.dat differ
diff --git a/assets/data/tool-ui/js_obfuscator.dat b/assets/data/tool-ui/js_obfuscator.dat
new file mode 100644
index 0000000..90f5692
Binary files /dev/null and b/assets/data/tool-ui/js_obfuscator.dat differ
diff --git a/assets/data/tool-ui/json_formatter.dat b/assets/data/tool-ui/json_formatter.dat
new file mode 100644
index 0000000..b0b2b93
Binary files /dev/null and b/assets/data/tool-ui/json_formatter.dat differ
diff --git a/assets/data/tool-ui/json_path_helper.dat b/assets/data/tool-ui/json_path_helper.dat
new file mode 100644
index 0000000..48e0916
Binary files /dev/null and b/assets/data/tool-ui/json_path_helper.dat differ
diff --git a/assets/data/tool-ui/jwt_decoder.dat b/assets/data/tool-ui/jwt_decoder.dat
new file mode 100644
index 0000000..1ec58c0
Binary files /dev/null and b/assets/data/tool-ui/jwt_decoder.dat differ
diff --git a/assets/data/tool-ui/key_value_converter.dat b/assets/data/tool-ui/key_value_converter.dat
new file mode 100644
index 0000000..11958dc
Binary files /dev/null and b/assets/data/tool-ui/key_value_converter.dat differ
diff --git a/assets/data/tool-ui/keyword_counter.dat b/assets/data/tool-ui/keyword_counter.dat
new file mode 100644
index 0000000..94f3cac
Binary files /dev/null and b/assets/data/tool-ui/keyword_counter.dat differ
diff --git a/assets/data/tool-ui/length_converter.dat b/assets/data/tool-ui/length_converter.dat
new file mode 100644
index 0000000..0e1647d
Binary files /dev/null and b/assets/data/tool-ui/length_converter.dat differ
diff --git a/assets/data/tool-ui/line_column_transformer.dat b/assets/data/tool-ui/line_column_transformer.dat
new file mode 100644
index 0000000..c774366
Binary files /dev/null and b/assets/data/tool-ui/line_column_transformer.dat differ
diff --git a/assets/data/tool-ui/line_length_analyzer.dat b/assets/data/tool-ui/line_length_analyzer.dat
new file mode 100644
index 0000000..6f47f65
Binary files /dev/null and b/assets/data/tool-ui/line_length_analyzer.dat differ
diff --git a/assets/data/tool-ui/line_pair_zipper.dat b/assets/data/tool-ui/line_pair_zipper.dat
new file mode 100644
index 0000000..c342b3e
Binary files /dev/null and b/assets/data/tool-ui/line_pair_zipper.dat differ
diff --git a/assets/data/tool-ui/list_chunker.dat b/assets/data/tool-ui/list_chunker.dat
new file mode 100644
index 0000000..e5728e8
Binary files /dev/null and b/assets/data/tool-ui/list_chunker.dat differ
diff --git a/assets/data/tool-ui/list_difference.dat b/assets/data/tool-ui/list_difference.dat
new file mode 100644
index 0000000..d04ea92
Binary files /dev/null and b/assets/data/tool-ui/list_difference.dat differ
diff --git a/assets/data/tool-ui/list_group_statistics.dat b/assets/data/tool-ui/list_group_statistics.dat
new file mode 100644
index 0000000..10ce050
Binary files /dev/null and b/assets/data/tool-ui/list_group_statistics.dat differ
diff --git a/assets/data/tool-ui/list_intersection.dat b/assets/data/tool-ui/list_intersection.dat
new file mode 100644
index 0000000..c2073af
Binary files /dev/null and b/assets/data/tool-ui/list_intersection.dat differ
diff --git a/assets/data/tool-ui/list_sampler.dat b/assets/data/tool-ui/list_sampler.dat
new file mode 100644
index 0000000..95185ac
Binary files /dev/null and b/assets/data/tool-ui/list_sampler.dat differ
diff --git a/assets/data/tool-ui/list_union.dat b/assets/data/tool-ui/list_union.dat
new file mode 100644
index 0000000..cb93739
Binary files /dev/null and b/assets/data/tool-ui/list_union.dat differ
diff --git a/assets/data/tool-ui/loan_emi_calculator.dat b/assets/data/tool-ui/loan_emi_calculator.dat
new file mode 100644
index 0000000..401e3f8
Binary files /dev/null and b/assets/data/tool-ui/loan_emi_calculator.dat differ
diff --git a/assets/data/tool-ui/magic_number_lookup.dat b/assets/data/tool-ui/magic_number_lookup.dat
new file mode 100644
index 0000000..d6aff9f
Binary files /dev/null and b/assets/data/tool-ui/magic_number_lookup.dat differ
diff --git a/assets/data/tool-ui/manifest_deduplicator.dat b/assets/data/tool-ui/manifest_deduplicator.dat
new file mode 100644
index 0000000..f978737
Binary files /dev/null and b/assets/data/tool-ui/manifest_deduplicator.dat differ
diff --git a/assets/data/tool-ui/manifest_sorter.dat b/assets/data/tool-ui/manifest_sorter.dat
new file mode 100644
index 0000000..a7cafab
Binary files /dev/null and b/assets/data/tool-ui/manifest_sorter.dat differ
diff --git a/assets/data/tool-ui/manifest_template_export.dat b/assets/data/tool-ui/manifest_template_export.dat
new file mode 100644
index 0000000..49f4b20
Binary files /dev/null and b/assets/data/tool-ui/manifest_template_export.dat differ
diff --git a/assets/data/tool-ui/markdown_list_cleaner.dat b/assets/data/tool-ui/markdown_list_cleaner.dat
new file mode 100644
index 0000000..cedc97f
Binary files /dev/null and b/assets/data/tool-ui/markdown_list_cleaner.dat differ
diff --git a/assets/data/tool-ui/markdown_preview.dat b/assets/data/tool-ui/markdown_preview.dat
new file mode 100644
index 0000000..563307a
Binary files /dev/null and b/assets/data/tool-ui/markdown_preview.dat differ
diff --git a/assets/data/tool-ui/markdown_table_normalizer.dat b/assets/data/tool-ui/markdown_table_normalizer.dat
new file mode 100644
index 0000000..39a4433
Binary files /dev/null and b/assets/data/tool-ui/markdown_table_normalizer.dat differ
diff --git a/assets/data/tool-ui/media_player.dat b/assets/data/tool-ui/media_player.dat
new file mode 100644
index 0000000..5cb7bf0
Binary files /dev/null and b/assets/data/tool-ui/media_player.dat differ
diff --git a/assets/data/tool-ui/median_calculator.dat b/assets/data/tool-ui/median_calculator.dat
new file mode 100644
index 0000000..2d2829d
Binary files /dev/null and b/assets/data/tool-ui/median_calculator.dat differ
diff --git a/assets/data/tool-ui/mime_lookup.dat b/assets/data/tool-ui/mime_lookup.dat
new file mode 100644
index 0000000..00d173d
Binary files /dev/null and b/assets/data/tool-ui/mime_lookup.dat differ
diff --git a/assets/data/tool-ui/movie_box_office.dat b/assets/data/tool-ui/movie_box_office.dat
new file mode 100644
index 0000000..23c903d
Binary files /dev/null and b/assets/data/tool-ui/movie_box_office.dat differ
diff --git a/assets/data/tool-ui/music.dat b/assets/data/tool-ui/music.dat
new file mode 100644
index 0000000..fcb9f35
Binary files /dev/null and b/assets/data/tool-ui/music.dat differ
diff --git a/assets/data/tool-ui/nex_ace_optimize.dat b/assets/data/tool-ui/nex_ace_optimize.dat
new file mode 100644
index 0000000..7fa7c8b
Binary files /dev/null and b/assets/data/tool-ui/nex_ace_optimize.dat differ
diff --git a/assets/data/tool-ui/nex_audio_eq.dat b/assets/data/tool-ui/nex_audio_eq.dat
new file mode 100644
index 0000000..aa56ba3
Binary files /dev/null and b/assets/data/tool-ui/nex_audio_eq.dat differ
diff --git a/assets/data/tool-ui/nex_crosshair.dat b/assets/data/tool-ui/nex_crosshair.dat
new file mode 100644
index 0000000..a5462f1
Binary files /dev/null and b/assets/data/tool-ui/nex_crosshair.dat differ
diff --git a/assets/data/tool-ui/nex_delta_force.dat b/assets/data/tool-ui/nex_delta_force.dat
new file mode 100644
index 0000000..2875104
Binary files /dev/null and b/assets/data/tool-ui/nex_delta_force.dat differ
diff --git a/assets/data/tool-ui/nex_disk_health.dat b/assets/data/tool-ui/nex_disk_health.dat
new file mode 100644
index 0000000..3572239
Binary files /dev/null and b/assets/data/tool-ui/nex_disk_health.dat differ
diff --git a/assets/data/tool-ui/nex_display_filter.dat b/assets/data/tool-ui/nex_display_filter.dat
new file mode 100644
index 0000000..0de213e
Binary files /dev/null and b/assets/data/tool-ui/nex_display_filter.dat differ
diff --git a/assets/data/tool-ui/nex_epic_free.dat b/assets/data/tool-ui/nex_epic_free.dat
new file mode 100644
index 0000000..9a2d28c
Binary files /dev/null and b/assets/data/tool-ui/nex_epic_free.dat differ
diff --git a/assets/data/tool-ui/nex_event_log_diagnostics.dat b/assets/data/tool-ui/nex_event_log_diagnostics.dat
new file mode 100644
index 0000000..302ff2d
Binary files /dev/null and b/assets/data/tool-ui/nex_event_log_diagnostics.dat differ
diff --git a/assets/data/tool-ui/nex_file_lock_analysis.dat b/assets/data/tool-ui/nex_file_lock_analysis.dat
new file mode 100644
index 0000000..ba2e804
Binary files /dev/null and b/assets/data/tool-ui/nex_file_lock_analysis.dat differ
diff --git a/assets/data/tool-ui/nex_gpu_rename.dat b/assets/data/tool-ui/nex_gpu_rename.dat
new file mode 100644
index 0000000..ff89ca9
Binary files /dev/null and b/assets/data/tool-ui/nex_gpu_rename.dat differ
diff --git a/assets/data/tool-ui/nex_installed_apps.dat b/assets/data/tool-ui/nex_installed_apps.dat
new file mode 100644
index 0000000..d37ceaf
Binary files /dev/null and b/assets/data/tool-ui/nex_installed_apps.dat differ
diff --git a/assets/data/tool-ui/nex_memory_cleanup.dat b/assets/data/tool-ui/nex_memory_cleanup.dat
new file mode 100644
index 0000000..af9b93a
Binary files /dev/null and b/assets/data/tool-ui/nex_memory_cleanup.dat differ
diff --git a/assets/data/tool-ui/nex_memory_limit.dat b/assets/data/tool-ui/nex_memory_limit.dat
new file mode 100644
index 0000000..d5aac54
Binary files /dev/null and b/assets/data/tool-ui/nex_memory_limit.dat differ
diff --git a/assets/data/tool-ui/nex_mood.dat b/assets/data/tool-ui/nex_mood.dat
new file mode 100644
index 0000000..45b4d5f
Binary files /dev/null and b/assets/data/tool-ui/nex_mood.dat differ
diff --git a/assets/data/tool-ui/nex_network_optimizer.dat b/assets/data/tool-ui/nex_network_optimizer.dat
new file mode 100644
index 0000000..2c3a1ba
Binary files /dev/null and b/assets/data/tool-ui/nex_network_optimizer.dat differ
diff --git a/assets/data/tool-ui/nex_nvidia_driver.dat b/assets/data/tool-ui/nex_nvidia_driver.dat
new file mode 100644
index 0000000..2323e9d
Binary files /dev/null and b/assets/data/tool-ui/nex_nvidia_driver.dat differ
diff --git a/assets/data/tool-ui/nex_optional_features.dat b/assets/data/tool-ui/nex_optional_features.dat
new file mode 100644
index 0000000..7757b17
Binary files /dev/null and b/assets/data/tool-ui/nex_optional_features.dat differ
diff --git a/assets/data/tool-ui/nex_other_gun_codes.dat b/assets/data/tool-ui/nex_other_gun_codes.dat
new file mode 100644
index 0000000..f11b362
Binary files /dev/null and b/assets/data/tool-ui/nex_other_gun_codes.dat differ
diff --git a/assets/data/tool-ui/nex_overlay.dat b/assets/data/tool-ui/nex_overlay.dat
new file mode 100644
index 0000000..50c3d7a
Binary files /dev/null and b/assets/data/tool-ui/nex_overlay.dat differ
diff --git a/assets/data/tool-ui/nex_peripheral_optimize.dat b/assets/data/tool-ui/nex_peripheral_optimize.dat
new file mode 100644
index 0000000..7f734cc
Binary files /dev/null and b/assets/data/tool-ui/nex_peripheral_optimize.dat differ
diff --git a/assets/data/tool-ui/nex_power_management.dat b/assets/data/tool-ui/nex_power_management.dat
new file mode 100644
index 0000000..0e61b85
Binary files /dev/null and b/assets/data/tool-ui/nex_power_management.dat differ
diff --git a/assets/data/tool-ui/nex_resolution_converter.dat b/assets/data/tool-ui/nex_resolution_converter.dat
new file mode 100644
index 0000000..9b96dc8
Binary files /dev/null and b/assets/data/tool-ui/nex_resolution_converter.dat differ
diff --git a/assets/data/tool-ui/nex_restore_points.dat b/assets/data/tool-ui/nex_restore_points.dat
new file mode 100644
index 0000000..b0986a5
Binary files /dev/null and b/assets/data/tool-ui/nex_restore_points.dat differ
diff --git a/assets/data/tool-ui/nex_shader_cache.dat b/assets/data/tool-ui/nex_shader_cache.dat
new file mode 100644
index 0000000..9263f15
Binary files /dev/null and b/assets/data/tool-ui/nex_shader_cache.dat differ
diff --git a/assets/data/tool-ui/nex_startup_manager.dat b/assets/data/tool-ui/nex_startup_manager.dat
new file mode 100644
index 0000000..6fd8050
Binary files /dev/null and b/assets/data/tool-ui/nex_startup_manager.dat differ
diff --git a/assets/data/tool-ui/nex_storage_clean.dat b/assets/data/tool-ui/nex_storage_clean.dat
new file mode 100644
index 0000000..8170d6b
Binary files /dev/null and b/assets/data/tool-ui/nex_storage_clean.dat differ
diff --git a/assets/data/tool-ui/nex_system_optimizer.dat b/assets/data/tool-ui/nex_system_optimizer.dat
new file mode 100644
index 0000000..c91827b
Binary files /dev/null and b/assets/data/tool-ui/nex_system_optimizer.dat differ
diff --git a/assets/data/tool-ui/nex_third_party_tools.dat b/assets/data/tool-ui/nex_third_party_tools.dat
new file mode 100644
index 0000000..f1aaae4
Binary files /dev/null and b/assets/data/tool-ui/nex_third_party_tools.dat differ
diff --git a/assets/data/tool-ui/nex_tray_island.dat b/assets/data/tool-ui/nex_tray_island.dat
new file mode 100644
index 0000000..a72d04b
Binary files /dev/null and b/assets/data/tool-ui/nex_tray_island.dat differ
diff --git a/assets/data/tool-ui/nex_vertical_overlay.dat b/assets/data/tool-ui/nex_vertical_overlay.dat
new file mode 100644
index 0000000..a6e06c3
Binary files /dev/null and b/assets/data/tool-ui/nex_vertical_overlay.dat differ
diff --git a/assets/data/tool-ui/nex_windows_repair.dat b/assets/data/tool-ui/nex_windows_repair.dat
new file mode 100644
index 0000000..2337b6c
Binary files /dev/null and b/assets/data/tool-ui/nex_windows_repair.dat differ
diff --git a/assets/data/tool-ui/nex_windows_update.dat b/assets/data/tool-ui/nex_windows_update.dat
new file mode 100644
index 0000000..262bd12
Binary files /dev/null and b/assets/data/tool-ui/nex_windows_update.dat differ
diff --git a/assets/data/tool-ui/number_base.dat b/assets/data/tool-ui/number_base.dat
new file mode 100644
index 0000000..aa7564d
Binary files /dev/null and b/assets/data/tool-ui/number_base.dat differ
diff --git a/assets/data/tool-ui/number_base_converter.dat b/assets/data/tool-ui/number_base_converter.dat
new file mode 100644
index 0000000..22c658f
Binary files /dev/null and b/assets/data/tool-ui/number_base_converter.dat differ
diff --git a/assets/data/tool-ui/oil_price.dat b/assets/data/tool-ui/oil_price.dat
new file mode 100644
index 0000000..024e192
Binary files /dev/null and b/assets/data/tool-ui/oil_price.dat differ
diff --git a/assets/data/tool-ui/optimization.dat b/assets/data/tool-ui/optimization.dat
new file mode 100644
index 0000000..f458688
Binary files /dev/null and b/assets/data/tool-ui/optimization.dat differ
diff --git a/assets/data/tool-ui/password_generator.dat b/assets/data/tool-ui/password_generator.dat
new file mode 100644
index 0000000..7d466be
Binary files /dev/null and b/assets/data/tool-ui/password_generator.dat differ
diff --git a/assets/data/tool-ui/path_breakdown.dat b/assets/data/tool-ui/path_breakdown.dat
new file mode 100644
index 0000000..c5c2668
Binary files /dev/null and b/assets/data/tool-ui/path_breakdown.dat differ
diff --git a/assets/data/tool-ui/path_normalizer.dat b/assets/data/tool-ui/path_normalizer.dat
new file mode 100644
index 0000000..a6ce35b
Binary files /dev/null and b/assets/data/tool-ui/path_normalizer.dat differ
diff --git a/assets/data/tool-ui/pc_benchmark.dat b/assets/data/tool-ui/pc_benchmark.dat
new file mode 100644
index 0000000..96160f7
Binary files /dev/null and b/assets/data/tool-ui/pc_benchmark.dat differ
diff --git a/assets/data/tool-ui/percentage_calculator.dat b/assets/data/tool-ui/percentage_calculator.dat
new file mode 100644
index 0000000..7388256
Binary files /dev/null and b/assets/data/tool-ui/percentage_calculator.dat differ
diff --git a/assets/data/tool-ui/percentage_change_calculator.dat b/assets/data/tool-ui/percentage_change_calculator.dat
new file mode 100644
index 0000000..76d5f5d
Binary files /dev/null and b/assets/data/tool-ui/percentage_change_calculator.dat differ
diff --git a/assets/data/tool-ui/permutation_calculator.dat b/assets/data/tool-ui/permutation_calculator.dat
new file mode 100644
index 0000000..5179bfd
Binary files /dev/null and b/assets/data/tool-ui/permutation_calculator.dat differ
diff --git a/assets/data/tool-ui/plugin-host.dat b/assets/data/tool-ui/plugin-host.dat
new file mode 100644
index 0000000..d64ca32
Binary files /dev/null and b/assets/data/tool-ui/plugin-host.dat differ
diff --git a/assets/data/tool-ui/port_lookup.dat b/assets/data/tool-ui/port_lookup.dat
new file mode 100644
index 0000000..89378ba
Binary files /dev/null and b/assets/data/tool-ui/port_lookup.dat differ
diff --git a/assets/data/tool-ui/prefix_suffix_batch.dat b/assets/data/tool-ui/prefix_suffix_batch.dat
new file mode 100644
index 0000000..ea8cff1
Binary files /dev/null and b/assets/data/tool-ui/prefix_suffix_batch.dat differ
diff --git a/assets/data/tool-ui/profanity_check.dat b/assets/data/tool-ui/profanity_check.dat
new file mode 100644
index 0000000..535270a
Binary files /dev/null and b/assets/data/tool-ui/profanity_check.dat differ
diff --git a/assets/data/tool-ui/punycode_codec.dat b/assets/data/tool-ui/punycode_codec.dat
new file mode 100644
index 0000000..b6799b5
Binary files /dev/null and b/assets/data/tool-ui/punycode_codec.dat differ
diff --git a/assets/data/tool-ui/qq_avatar.dat b/assets/data/tool-ui/qq_avatar.dat
new file mode 100644
index 0000000..5f14233
Binary files /dev/null and b/assets/data/tool-ui/qq_avatar.dat differ
diff --git a/assets/data/tool-ui/qr_generator.dat b/assets/data/tool-ui/qr_generator.dat
new file mode 100644
index 0000000..a2785c5
Binary files /dev/null and b/assets/data/tool-ui/qr_generator.dat differ
diff --git a/assets/data/tool-ui/qrcode_scanner.dat b/assets/data/tool-ui/qrcode_scanner.dat
new file mode 100644
index 0000000..eef28e6
Binary files /dev/null and b/assets/data/tool-ui/qrcode_scanner.dat differ
diff --git a/assets/data/tool-ui/query_builder.dat b/assets/data/tool-ui/query_builder.dat
new file mode 100644
index 0000000..65e45a5
Binary files /dev/null and b/assets/data/tool-ui/query_builder.dat differ
diff --git a/assets/data/tool-ui/random_cinema.dat b/assets/data/tool-ui/random_cinema.dat
new file mode 100644
index 0000000..84a8535
Binary files /dev/null and b/assets/data/tool-ui/random_cinema.dat differ
diff --git a/assets/data/tool-ui/random_generator.dat b/assets/data/tool-ui/random_generator.dat
new file mode 100644
index 0000000..76e0edb
Binary files /dev/null and b/assets/data/tool-ui/random_generator.dat differ
diff --git a/assets/data/tool-ui/ratio_simplifier.dat b/assets/data/tool-ui/ratio_simplifier.dat
new file mode 100644
index 0000000..f155a46
Binary files /dev/null and b/assets/data/tool-ui/ratio_simplifier.dat differ
diff --git a/assets/data/tool-ui/rdap_domain_lookup.dat b/assets/data/tool-ui/rdap_domain_lookup.dat
new file mode 100644
index 0000000..0dbcab7
Binary files /dev/null and b/assets/data/tool-ui/rdap_domain_lookup.dat differ
diff --git a/assets/data/tool-ui/rdap_ip_lookup.dat b/assets/data/tool-ui/rdap_ip_lookup.dat
new file mode 100644
index 0000000..e27c6b5
Binary files /dev/null and b/assets/data/tool-ui/rdap_ip_lookup.dat differ
diff --git a/assets/data/tool-ui/regex_preset_lookup.dat b/assets/data/tool-ui/regex_preset_lookup.dat
new file mode 100644
index 0000000..c87e5a2
Binary files /dev/null and b/assets/data/tool-ui/regex_preset_lookup.dat differ
diff --git a/assets/data/tool-ui/regex_tool.dat b/assets/data/tool-ui/regex_tool.dat
new file mode 100644
index 0000000..46db02d
Binary files /dev/null and b/assets/data/tool-ui/regex_tool.dat differ
diff --git a/assets/data/tool-ui/safe_browser.dat b/assets/data/tool-ui/safe_browser.dat
new file mode 100644
index 0000000..0955443
Binary files /dev/null and b/assets/data/tool-ui/safe_browser.dat differ
diff --git a/assets/data/tool-ui/sanguosha_skin.dat b/assets/data/tool-ui/sanguosha_skin.dat
new file mode 100644
index 0000000..0277eab
Binary files /dev/null and b/assets/data/tool-ui/sanguosha_skin.dat differ
diff --git a/assets/data/tool-ui/serial_terminal.dat b/assets/data/tool-ui/serial_terminal.dat
new file mode 100644
index 0000000..aadc81c
Binary files /dev/null and b/assets/data/tool-ui/serial_terminal.dat differ
diff --git a/assets/data/tool-ui/shelf_life.dat b/assets/data/tool-ui/shelf_life.dat
new file mode 100644
index 0000000..2e26bba
Binary files /dev/null and b/assets/data/tool-ui/shelf_life.dat differ
diff --git a/assets/data/tool-ui/simple_interest_calculator.dat b/assets/data/tool-ui/simple_interest_calculator.dat
new file mode 100644
index 0000000..0f6dec6
Binary files /dev/null and b/assets/data/tool-ui/simple_interest_calculator.dat differ
diff --git a/assets/data/tool-ui/slug_generator.dat b/assets/data/tool-ui/slug_generator.dat
new file mode 100644
index 0000000..140d4dd
Binary files /dev/null and b/assets/data/tool-ui/slug_generator.dat differ
diff --git a/assets/data/tool-ui/smart_search.dat b/assets/data/tool-ui/smart_search.dat
new file mode 100644
index 0000000..7c5cf34
Binary files /dev/null and b/assets/data/tool-ui/smart_search.dat differ
diff --git a/assets/data/tool-ui/speed_converter.dat b/assets/data/tool-ui/speed_converter.dat
new file mode 100644
index 0000000..ce546c0
Binary files /dev/null and b/assets/data/tool-ui/speed_converter.dat differ
diff --git a/assets/data/tool-ui/sql_formatter.dat b/assets/data/tool-ui/sql_formatter.dat
new file mode 100644
index 0000000..f63f28f
Binary files /dev/null and b/assets/data/tool-ui/sql_formatter.dat differ
diff --git a/assets/data/tool-ui/system_info.dat b/assets/data/tool-ui/system_info.dat
new file mode 100644
index 0000000..be7284b
Binary files /dev/null and b/assets/data/tool-ui/system_info.dat differ
diff --git a/assets/data/tool-ui/system_tool.dat b/assets/data/tool-ui/system_tool.dat
new file mode 100644
index 0000000..71ecba0
Binary files /dev/null and b/assets/data/tool-ui/system_tool.dat differ
diff --git a/assets/data/tool-ui/tax_calculator.dat b/assets/data/tool-ui/tax_calculator.dat
new file mode 100644
index 0000000..c0e59ca
Binary files /dev/null and b/assets/data/tool-ui/tax_calculator.dat differ
diff --git a/assets/data/tool-ui/tech_news.dat b/assets/data/tool-ui/tech_news.dat
new file mode 100644
index 0000000..fd1a734
Binary files /dev/null and b/assets/data/tool-ui/tech_news.dat differ
diff --git a/assets/data/tool-ui/temperature_converter.dat b/assets/data/tool-ui/temperature_converter.dat
new file mode 100644
index 0000000..6ef79b2
Binary files /dev/null and b/assets/data/tool-ui/temperature_converter.dat differ
diff --git a/assets/data/tool-ui/template_filler.dat b/assets/data/tool-ui/template_filler.dat
new file mode 100644
index 0000000..305ae22
Binary files /dev/null and b/assets/data/tool-ui/template_filler.dat differ
diff --git a/assets/data/tool-ui/text_deduplicator.dat b/assets/data/tool-ui/text_deduplicator.dat
new file mode 100644
index 0000000..ddbe3c7
Binary files /dev/null and b/assets/data/tool-ui/text_deduplicator.dat differ
diff --git a/assets/data/tool-ui/text_diff.dat b/assets/data/tool-ui/text_diff.dat
new file mode 100644
index 0000000..6b74f0c
Binary files /dev/null and b/assets/data/tool-ui/text_diff.dat differ
diff --git a/assets/data/tool-ui/text_filter.dat b/assets/data/tool-ui/text_filter.dat
new file mode 100644
index 0000000..8d0af4e
Binary files /dev/null and b/assets/data/tool-ui/text_filter.dat differ
diff --git a/assets/data/tool-ui/text_merger.dat b/assets/data/tool-ui/text_merger.dat
new file mode 100644
index 0000000..433a4aa
Binary files /dev/null and b/assets/data/tool-ui/text_merger.dat differ
diff --git a/assets/data/tool-ui/text_numbering.dat b/assets/data/tool-ui/text_numbering.dat
new file mode 100644
index 0000000..c793ea7
Binary files /dev/null and b/assets/data/tool-ui/text_numbering.dat differ
diff --git a/assets/data/tool-ui/text_reverser.dat b/assets/data/tool-ui/text_reverser.dat
new file mode 100644
index 0000000..5f686ae
Binary files /dev/null and b/assets/data/tool-ui/text_reverser.dat differ
diff --git a/assets/data/tool-ui/text_sorter.dat b/assets/data/tool-ui/text_sorter.dat
new file mode 100644
index 0000000..4b3b267
Binary files /dev/null and b/assets/data/tool-ui/text_sorter.dat differ
diff --git a/assets/data/tool-ui/text_splitter.dat b/assets/data/tool-ui/text_splitter.dat
new file mode 100644
index 0000000..508f4ca
Binary files /dev/null and b/assets/data/tool-ui/text_splitter.dat differ
diff --git a/assets/data/tool-ui/text_statistics.dat b/assets/data/tool-ui/text_statistics.dat
new file mode 100644
index 0000000..cff05a1
Binary files /dev/null and b/assets/data/tool-ui/text_statistics.dat differ
diff --git a/assets/data/tool-ui/text_trimmer.dat b/assets/data/tool-ui/text_trimmer.dat
new file mode 100644
index 0000000..4786bff
Binary files /dev/null and b/assets/data/tool-ui/text_trimmer.dat differ
diff --git a/assets/data/tool-ui/time_duration_converter.dat b/assets/data/tool-ui/time_duration_converter.dat
new file mode 100644
index 0000000..340a86d
Binary files /dev/null and b/assets/data/tool-ui/time_duration_converter.dat differ
diff --git a/assets/data/tool-ui/timestamp_converter.dat b/assets/data/tool-ui/timestamp_converter.dat
new file mode 100644
index 0000000..4e20d65
Binary files /dev/null and b/assets/data/tool-ui/timestamp_converter.dat differ
diff --git a/assets/data/tool-ui/timezone_abbr_lookup.dat b/assets/data/tool-ui/timezone_abbr_lookup.dat
new file mode 100644
index 0000000..4ad984f
Binary files /dev/null and b/assets/data/tool-ui/timezone_abbr_lookup.dat differ
diff --git a/assets/data/tool-ui/totp_generator.dat b/assets/data/tool-ui/totp_generator.dat
new file mode 100644
index 0000000..16da476
Binary files /dev/null and b/assets/data/tool-ui/totp_generator.dat differ
diff --git a/assets/data/tool-ui/train_query.dat b/assets/data/tool-ui/train_query.dat
new file mode 100644
index 0000000..99b5b6b
Binary files /dev/null and b/assets/data/tool-ui/train_query.dat differ
diff --git a/assets/data/tool-ui/ulid_generator.dat b/assets/data/tool-ui/ulid_generator.dat
new file mode 100644
index 0000000..67f4216
Binary files /dev/null and b/assets/data/tool-ui/ulid_generator.dat differ
diff --git a/assets/data/tool-ui/unicode_block_lookup.dat b/assets/data/tool-ui/unicode_block_lookup.dat
new file mode 100644
index 0000000..b6f6389
Binary files /dev/null and b/assets/data/tool-ui/unicode_block_lookup.dat differ
diff --git a/assets/data/tool-ui/unicode_codec.dat b/assets/data/tool-ui/unicode_codec.dat
new file mode 100644
index 0000000..6946e1f
Binary files /dev/null and b/assets/data/tool-ui/unicode_codec.dat differ
diff --git a/assets/data/tool-ui/unit_converter.dat b/assets/data/tool-ui/unit_converter.dat
new file mode 100644
index 0000000..87164f0
Binary files /dev/null and b/assets/data/tool-ui/unit_converter.dat differ
diff --git a/assets/data/tool-ui/url_codec.dat b/assets/data/tool-ui/url_codec.dat
new file mode 100644
index 0000000..a0ca94d
Binary files /dev/null and b/assets/data/tool-ui/url_codec.dat differ
diff --git a/assets/data/tool-ui/url_inspector.dat b/assets/data/tool-ui/url_inspector.dat
new file mode 100644
index 0000000..e00b2e3
Binary files /dev/null and b/assets/data/tool-ui/url_inspector.dat differ
diff --git a/assets/data/tool-ui/url_redirect_trace.dat b/assets/data/tool-ui/url_redirect_trace.dat
new file mode 100644
index 0000000..5fa5063
Binary files /dev/null and b/assets/data/tool-ui/url_redirect_trace.dat differ
diff --git a/assets/data/tool-ui/uuid_generator.dat b/assets/data/tool-ui/uuid_generator.dat
new file mode 100644
index 0000000..93a9c46
Binary files /dev/null and b/assets/data/tool-ui/uuid_generator.dat differ
diff --git a/assets/data/tool-ui/volume_converter.dat b/assets/data/tool-ui/volume_converter.dat
new file mode 100644
index 0000000..b71e2d8
Binary files /dev/null and b/assets/data/tool-ui/volume_converter.dat differ
diff --git a/assets/data/tool-ui/weather.dat b/assets/data/tool-ui/weather.dat
new file mode 100644
index 0000000..0d51680
Binary files /dev/null and b/assets/data/tool-ui/weather.dat differ
diff --git a/assets/data/tool-ui/weight_converter.dat b/assets/data/tool-ui/weight_converter.dat
new file mode 100644
index 0000000..06939bf
Binary files /dev/null and b/assets/data/tool-ui/weight_converter.dat differ
diff --git a/assets/data/tool-ui/whitespace_normalizer.dat b/assets/data/tool-ui/whitespace_normalizer.dat
new file mode 100644
index 0000000..f722155
Binary files /dev/null and b/assets/data/tool-ui/whitespace_normalizer.dat differ
diff --git a/assets/data/tool-ui/workday_calculator.dat b/assets/data/tool-ui/workday_calculator.dat
new file mode 100644
index 0000000..c85f4f3
Binary files /dev/null and b/assets/data/tool-ui/workday_calculator.dat differ
diff --git a/assets/data/tool-ui/wx_domain_check.dat b/assets/data/tool-ui/wx_domain_check.dat
new file mode 100644
index 0000000..6770201
Binary files /dev/null and b/assets/data/tool-ui/wx_domain_check.dat differ
diff --git a/assets/data/tool-ui/xml_formatter.dat b/assets/data/tool-ui/xml_formatter.dat
new file mode 100644
index 0000000..91d0e09
Binary files /dev/null and b/assets/data/tool-ui/xml_formatter.dat differ
diff --git a/assets/data/tool-ui/yaml_json_converter.dat b/assets/data/tool-ui/yaml_json_converter.dat
new file mode 100644
index 0000000..f647fff
Binary files /dev/null and b/assets/data/tool-ui/yaml_json_converter.dat differ
diff --git a/assets/data/tool-ui/ymhut_uutool_suite.dat b/assets/data/tool-ui/ymhut_uutool_suite.dat
new file mode 100644
index 0000000..9f999d4
Binary files /dev/null and b/assets/data/tool-ui/ymhut_uutool_suite.dat differ
diff --git a/assets/data/tool-ui/zhihu_hot.dat b/assets/data/tool-ui/zhihu_hot.dat
new file mode 100644
index 0000000..c57ad9f
Binary files /dev/null and b/assets/data/tool-ui/zhihu_hot.dat differ
diff --git a/installer/YMhutBox-EULA.zh-CN.txt b/installer/YMhutBox-EULA.zh-CN.txt
index 7daa498..3bb5b77 100644
--- a/installer/YMhutBox-EULA.zh-CN.txt
+++ b/installer/YMhutBox-EULA.zh-CN.txt
@@ -46,3 +46,13 @@ YMhut Box 软件许可与服务协议
八、联系与反馈
您可以通过 YMhut Box 内置反馈页面提交功能建议、问题报告或协议相关咨询。
+
+九、网络音乐播放器特别声明
+
+1. 网络音乐播放器仅供学习、研究和个人非商业使用。请尊重音乐作品、表演、录音、歌词、封面和视频的著作权及相关权利。
+2. 您不得利用本软件从事商业行为、公开传播、批量抓取、倒卖数据、绕过会员或地区限制,或以任何方式破坏、规避第三方平台的版权保护和访问控制。
+3. 酷狗数据由随包的 MoeKoeMusic API 与 KuGouMusicApi 修改组件承接。客户端启动的 API 仅绑定随机回环端口并由主程序内部令牌保护,不向插件、浏览器或外部网络公开;该端口不构成任何对第三方服务的授权或保证。
+4. API 对公共匿名幂等 GET 使用约两分钟缓存;登录、二维码、账户、会员权益、个人歌单、收藏、播放地址和写操作不使用缓存。POST 请求会附加时间戳以避免重复 URL 被缓存。缓存可能导致内容短暂延迟,账户能力以平台实时返回为准。
+5. 请勿频繁调用登录接口。登录状态存在时软件会优先使用本地 DPAPI 保护的登录快照,仅在账户功能需要且快照过期时尝试刷新;平台可能因异常频率、设备或网络环境实施风控。
+6. MoeKoeMusic 使用固定提交 4581660209c9cdd07669e8bd5bcbe136b7d91e02,适用 GPL-2.0;KuGouMusicApi 使用固定提交 06560e3e053bda1ab830750db6f645bab703f824,适用 MIT。对应源码、锁文件、许可证、原始声明和 YMhut 修改说明随包分发,并在第三方声明页面列出。
+7. 软件不承诺歌曲、歌词、封面、MV、播放地址、会员权益或地区可用性,也不提供任何绕过平台付费、地区或版权限制的功能。因第三方服务规则、账户状态、网络或版权变化导致的不可用由相应服务规则决定。
diff --git a/scripts/build-winui.ps1 b/scripts/build-winui.ps1
index f902fb4..ad28815 100644
--- a/scripts/build-winui.ps1
+++ b/scripts/build-winui.ps1
@@ -388,6 +388,30 @@ function Find-WindowsSdkTool([string] $Name) {
($candidates | Sort-Object FullName -Descending | Select-Object -First 1).FullName
}
+function Assert-PerMonitorV2Manifest([string] $Executable) {
+ if (-not (Test-Path -LiteralPath $Executable)) {
+ throw "DPI manifest validation target does not exist: $Executable"
+ }
+
+ $mt = Find-WindowsSdkTool 'mt.exe'
+ if (-not $mt) {
+ throw 'Windows SDK mt.exe was not found, so the PerMonitorV2 application manifest cannot be validated.'
+ }
+
+ $manifestPath = Join-Path ([IO.Path]::GetTempPath()) ("ymhut-dpi-{0}.manifest" -f [Guid]::NewGuid().ToString('N'))
+ try {
+ Invoke-ToolQuiet $mt @("-inputresource:$Executable;#1", "-out:$manifestPath") 'DPI manifest extraction failed'
+ $manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8
+ if ($manifest -notmatch ']*>\s*PerMonitorV2\s*' -or
+ $manifest -notmatch ']*>\s*true/pm\s*') {
+ throw "Executable is missing the required PerMonitorV2 DPI declarations: $Executable"
+ }
+ Write-Host " DPI manifest: PerMonitorV2 ($Executable)"
+ } finally {
+ Remove-Item -LiteralPath $manifestPath -Force -ErrorAction SilentlyContinue
+ }
+}
+
function Find-InnoSetupCompiler {
$envCandidates = @()
if ($env:YMHUT_INNO_SETUP) {
@@ -1221,11 +1245,54 @@ function Remove-RootHelperDuplicates([string] $Directory) {
}
}
+function Assert-MusicApiPayload([string] $Directory) {
+ $required = @(
+ 'music-api\node.exe',
+ 'music-api\NODE_LICENSE',
+ 'music-api\MOEKOE_LICENSE',
+ 'music-api\UPSTREAM_COMMIT',
+ 'music-api\API_SUBMODULE_COMMIT',
+ 'music-api\YMhut_PATCHES.md',
+ 'music-api\api\ymhut-sidecar.js',
+ 'music-api\api\server.js',
+ 'music-api\api\package.json',
+ 'music-api\api\pnpm-lock.yaml',
+ 'music-api\api\LICENSE',
+ 'music-api\api\README.md',
+ 'music-api\api\node_modules\express\package.json'
+ )
+ $missing = $required | Where-Object { -not (Test-Path -LiteralPath (Join-Path $Directory $_) -PathType Leaf) }
+ if ($missing.Count -gt 0) {
+ throw "Release music-api payload is incomplete in '$Directory': $($missing -join ', ')"
+ }
+}
+
+function Assert-ToolUiPayload([string] $Directory) {
+ $folder = Join-Path $Directory 'Assets\data\tool-ui'
+ if (-not (Test-Path -LiteralPath $folder -PathType Container)) {
+ throw "Release tool UI payload is missing: $folder"
+ }
+
+ $files = @(Get-ChildItem -LiteralPath $folder -Filter '*.dat' -File -ErrorAction SilentlyContinue)
+ if ($files.Count -lt 202) {
+ throw "Release tool UI payload is incomplete in '$Directory': expected at least 202 .dat definitions, found $($files.Count)"
+ }
+
+ foreach ($file in $files) {
+ $bytes = [IO.File]::ReadAllBytes($file.FullName)
+ if ($bytes.Length -lt 60 -or [Text.Encoding]::ASCII.GetString($bytes, 0, 8) -ne 'YMHUTUI1' -or $bytes[8] -ne 2) {
+ throw "Tool UI definition has an invalid header: $($file.FullName)"
+ }
+ }
+}
+
function Copy-PublishToLatest {
Stop-ProcessesFromDirectory $LatestRoot
Reset-DirectoryInsideRepo $LatestRoot
Copy-Item -Path (Join-Path $PublishRoot '*') -Destination $LatestRoot -Recurse -Force
Normalize-PublishLayout $LatestRoot
+ Assert-MusicApiPayload $LatestRoot
+ Assert-ToolUiPayload $LatestRoot
Write-LayoutManifests $LatestRoot
}
@@ -1245,6 +1312,9 @@ function Publish-UnpackagedApp([object] $VersionInfo) {
'-o', $PublishRoot
)
Normalize-PublishLayout $PublishRoot
+ Assert-MusicApiPayload $PublishRoot
+ Assert-ToolUiPayload $PublishRoot
+ Assert-PerMonitorV2Manifest (Join-Path $PublishRoot 'YMhutBox.exe')
Write-LayoutManifests $PublishRoot
Copy-PublishToLatest
}
@@ -1311,6 +1381,7 @@ function Build-InstallerBootstrap([object] $VersionInfo, [string] $SignTool) {
if ($SignTool) {
Sign-Artifact $SignTool $setupPath
}
+ Assert-PerMonitorV2Manifest $setupPath
Invoke-ToolQuiet $setupPath @('/SELFTEST') 'WinUI installer bootstrap self-test failed' " Installer self-test: $setupPath"
Invoke-ToolQuiet $setupPath @('/WINDOWSELFTEST') 'WinUI installer window navigation self-test failed' " Installer window self-test: $setupPath"
@@ -1430,6 +1501,9 @@ function Build-MsixPackage([object] $VersionInfo, [string] $MakeAppx, [string] $
Reset-DirectoryInsideRepo $MsixStageRoot
Copy-Item -Path (Join-Path $PublishRoot '*') -Destination $MsixStageRoot -Recurse -Force
+ Assert-MusicApiPayload $MsixStageRoot
+ Assert-ToolUiPayload $MsixStageRoot
+ Assert-PerMonitorV2Manifest (Join-Path $MsixStageRoot 'YMhutBox.exe')
Expand-SatelliteResourcePackages $MsixStageRoot -RemovePackages
Restore-LangResourceLayoutForMsix $MsixStageRoot
Write-AppxManifest $MsixStageRoot $VersionInfo.PackageVersion
@@ -1442,7 +1516,7 @@ function Build-MsixPackage([object] $VersionInfo, [string] $MakeAppx, [string] $
$stageAssets = Join-Path $MsixStageRoot 'Assets'
New-Directory $stageAssets
- Copy-Item -LiteralPath (Join-Path $ProjectAssets '*') -Destination $stageAssets -Recurse -Force
+ Copy-Item -Path (Join-Path $ProjectAssets '*') -Destination $stageAssets -Recurse -Force
Repair-MsixStageThirdPartyPayloads
Invoke-ToolQuiet $MakeAppx @('pack', '/d', $MsixStageRoot, '/p', $msixPath, '/o') 'MakeAppx packaging failed' " MSIX package: $msixPath"
diff --git a/scripts/generate-tool-ui-dat.ps1 b/scripts/generate-tool-ui-dat.ps1
new file mode 100644
index 0000000..5a34285
--- /dev/null
+++ b/scripts/generate-tool-ui-dat.ps1
@@ -0,0 +1,221 @@
+param(
+ [string]$Root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
+)
+
+$ErrorActionPreference = 'Stop'
+
+$catalogPath = Join-Path $Root 'src\YMhut.Box.Core\Tools\ToolCatalog.cs'
+$nexPath = Join-Path $Root 'src\YMhut.Box.Core\Tools\NexNativeTools.cs'
+$outputRoot = Join-Path $Root 'assets\data\tool-ui'
+New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
+
+$catalogText = Get-Content -LiteralPath $catalogPath -Raw -Encoding UTF8
+$rawMatch = [regex]::Match($catalogText, 'private const string RawToolData = """(?.*?)""";', [Text.RegularExpressions.RegexOptions]::Singleline)
+if (-not $rawMatch.Success) { throw 'RawToolData was not found.' }
+
+$ids = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
+$metadata = @{}
+foreach ($line in ($rawMatch.Groups['data'].Value -split "`r?`n")) {
+ if ([string]::IsNullOrWhiteSpace($line)) { continue }
+ $parts = $line -split "`t"
+ if ($parts.Count -lt 4) { continue }
+ $id = $parts[0].Trim()
+ if ([string]::IsNullOrWhiteSpace($id)) { continue }
+ [void]$ids.Add($id)
+ $metadata[$id] = [ordered]@{
+ zh = $parts[1].Trim()
+ en = $parts[1].Trim()
+ descriptionZh = $parts[2].Trim()
+ descriptionEn = $parts[2].Trim()
+ category = $parts[3].Trim()
+ offline = ($parts.Count -gt 4 -and $parts[4].Trim() -eq 'true')
+ }
+}
+
+foreach ($match in [regex]::Matches((Get-Content -LiteralPath $nexPath -Raw -Encoding UTF8), 'Tool\("(?[^"]+)"')) {
+ $id = $match.Groups['id'].Value
+ [void]$ids.Add($id)
+ if (-not $metadata.ContainsKey($id)) {
+ $metadata[$id] = [ordered]@{ zh = $id; en = $id; descriptionZh = ''; descriptionEn = ''; category = 'system'; offline = $true }
+ }
+}
+[void]$ids.Add('dev_environment_config')
+if (-not $metadata.ContainsKey('dev_environment_config')) {
+ $metadata['dev_environment_config'] = [ordered]@{ zh = 'Development environment'; en = 'Development environment'; descriptionZh = 'Inspect and configure the local development environment.'; descriptionEn = 'Inspect and configure the local development environment.'; category = 'dev'; offline = $false }
+}
+[void]$ids.Add('plugin-host')
+$metadata['plugin-host'] = [ordered]@{ zh = '插件安全宿主'; en = 'Plugin security host'; descriptionZh = '隔离第三方 WebView 内容、权限和运行状态。'; descriptionEn = 'Isolates third-party WebView content, permissions, and runtime status.'; category = 'plugin'; offline = $false }
+[void]$ids.Add('external-tool-host')
+$metadata['external-tool-host'] = [ordered]@{ zh = '外部工具宿主'; en = 'External tool host'; descriptionZh = '显示外部程序来源、风险、启动选项和运行结果。'; descriptionEn = 'Shows source, risk, launch options, and results for external programs.'; category = 'external'; offline = $true }
+
+$sha = [Security.Cryptography.SHA256]::Create()
+try { $keyBytes = $sha.ComputeHash([Text.Encoding]::UTF8.GetBytes('YMhut.Box.ToolUiDefinition.v1')) } finally { $sha.Dispose() }
+$aes = [Security.Cryptography.Aes]::Create()
+try {
+ $aes.Key = $keyBytes
+ $aes.Mode = [Security.Cryptography.CipherMode]::CBC
+ $aes.Padding = [Security.Cryptography.PaddingMode]::PKCS7
+ foreach ($id in ($ids | Sort-Object)) {
+ $meta = $metadata[$id]
+ $category = [string]$meta.category
+ $risk = if ($category -in @('security', 'system', 'plugin', 'external')) { 'high' } else { 'normal' }
+ $primaryKind = switch ($category) {
+ 'image' { 'FilePicker'; break }
+ 'calculator' { 'Number'; break }
+ 'network' { 'Query'; break }
+ 'security' { 'MultilineText'; break }
+ 'text' { 'MultilineText'; break }
+ 'dev' { 'MultilineText'; break }
+ 'plugin' { 'WebView'; break }
+ 'external' { 'ExternalSurface'; break }
+ default { 'Text' }
+ }
+
+ $maxLength = 4096
+ if ($category -eq 'text' -or $category -eq 'dev' -or $category -eq 'security') { $maxLength = 200000 }
+ $placeholder = '输入此工具所需的值'
+ if ($category -eq 'network') { $placeholder = '关键词、URL、域名或 IP' }
+ $primaryControls = [Collections.Generic.List[object]]::new()
+ $primaryControls.Add([ordered]@{
+ id = 'primary'
+ kind = $primaryKind
+ label = [ordered]@{ zh = '主要输入'; en = 'Primary input' }
+ placeholder = $placeholder
+ defaultValue = ''
+ required = $false
+ isReadOnly = $false
+ validation = [ordered]@{ maxLength = $maxLength }
+ })
+
+ $sections = [Collections.Generic.List[object]]::new()
+ $primaryTitle = switch ($category) {
+ 'calculator' { [ordered]@{ zh = '计算参数'; en = 'Calculation parameters' } }
+ 'network' { [ordered]@{ zh = '查询条件'; en = 'Query criteria' } }
+ 'image' { [ordered]@{ zh = '文件与媒体'; en = 'Files and media' } }
+ 'security' { [ordered]@{ zh = '安全输入'; en = 'Security input' } }
+ 'dev' { [ordered]@{ zh = '代码与数据'; en = 'Code and data' } }
+ 'text' { [ordered]@{ zh = '文本工作区'; en = 'Text workspace' } }
+ 'system' { [ordered]@{ zh = '系统入口'; en = 'System entry' } }
+ 'plugin' { [ordered]@{ zh = '插件安全容器'; en = 'Plugin security container' } }
+ 'external' { [ordered]@{ zh = '外部程序运行'; en = 'External program launch' } }
+ default { [ordered]@{ zh = '主要操作'; en = 'Primary operation' } }
+ }
+
+ if ($category -eq 'calculator') {
+ $primaryControls.Clear()
+ foreach ($numberId in @('number-primary', 'number-secondary', 'number-third', 'number-fourth')) {
+ $primaryControls.Add([ordered]@{ id = $numberId; kind = 'Number'; label = [ordered]@{ zh = '数值参数'; en = 'Numeric parameter' }; defaultValue = '1'; required = $true; isReadOnly = $false; validation = [ordered]@{ minimum = -100000000; maximum = 100000000 } })
+ }
+ } elseif ($category -eq 'network') {
+ $primaryControls.Add([ordered]@{ id = 'parameter'; kind = 'ComboBox'; label = [ordered]@{ zh = '来源或分类'; en = 'Source or category' }; defaultValue = ''; required = $false; isReadOnly = $false })
+ } elseif ($category -eq 'image') {
+ $primaryControls.Add([ordered]@{ id = 'file'; kind = 'FilePicker'; label = [ordered]@{ zh = '文件'; en = 'File' }; placeholder = '选择本地文件'; defaultValue = ''; required = $false; isReadOnly = $false })
+ }
+
+ $sections.Add([ordered]@{
+ id = 'primary'
+ title = $primaryTitle
+ description = [ordered]@{ zh = [string]$meta.descriptionZh; en = [string]$meta.descriptionEn }
+ controls = @($primaryControls)
+ isAdvanced = $false
+ layout = $(if ($category -eq 'calculator') { 'grid' } else { 'stack' })
+ })
+
+ if ($category -in @('text', 'dev', 'security', 'network', 'design')) {
+ $advancedControls = [Collections.Generic.List[object]]::new()
+ if ($category -in @('text', 'dev', 'security')) {
+ $advancedControls.Add([ordered]@{ id = 'rules'; kind = 'ComboBox'; label = [ordered]@{ zh = '处理规则'; en = 'Processing rules' }; defaultValue = ''; required = $false; isReadOnly = $false })
+ $advancedControls.Add([ordered]@{ id = 'auxiliary'; kind = 'Text'; label = [ordered]@{ zh = '附加参数'; en = 'Additional parameter' }; defaultValue = ''; required = $false; isReadOnly = $false })
+ } elseif ($category -eq 'network') {
+ $advancedControls.Add([ordered]@{ id = 'rules'; kind = 'ComboBox'; label = [ordered]@{ zh = '筛选与显示'; en = 'Filtering and display' }; defaultValue = ''; required = $false; isReadOnly = $false })
+ } else {
+ $advancedControls.Add([ordered]@{ id = 'rules'; kind = 'ComboBox'; label = [ordered]@{ zh = '预览设置'; en = 'Preview settings' }; defaultValue = ''; required = $false; isReadOnly = $false })
+ }
+ $advancedControls.Add([ordered]@{ id = 'advanced-toggle'; kind = 'Toggle'; label = [ordered]@{ zh = '启用高级选项'; en = 'Enable advanced options' }; defaultValue = 'false'; required = $false; isReadOnly = $false })
+ $sections.Add([ordered]@{
+ id = 'advanced'
+ title = [ordered]@{ zh = '高级选项'; en = 'Advanced options' }
+ description = [ordered]@{ zh = '按需展开,不改变默认安全行为。'; en = 'Expand when needed without changing the safe defaults.' }
+ controls = @($advancedControls)
+ isAdvanced = $true
+ layout = 'stack'
+ })
+ }
+
+ $resultKind = switch ($category) {
+ 'calculator' { 'calculator-table' }
+ 'image' { 'image-preview' }
+ 'data' { 'ranked-list' }
+ 'network' { 'link-cards' }
+ 'dev' { if ($id -like '*json*') { 'json-tree' } else { 'code-preview' } }
+ 'security' { 'status-list' }
+ 'system' { 'system-cards' }
+ 'plugin' { 'status-list' }
+ 'external' { 'status-list' }
+ default { 'text' }
+ }
+ $actions = [Collections.Generic.List[object]]::new()
+ $actionLabel = switch ($category) {
+ 'calculator' { [ordered]@{ zh = '计算'; en = 'Calculate' } }
+ 'network' { [ordered]@{ zh = '查询'; en = 'Query' } }
+ 'image' { [ordered]@{ zh = '处理'; en = 'Process' } }
+ 'security' { [ordered]@{ zh = '分析'; en = 'Analyze' } }
+ 'dev' { [ordered]@{ zh = '运行'; en = 'Run' } }
+ default { [ordered]@{ zh = '执行'; en = 'Run' } }
+ }
+ if ($category -eq 'plugin') {
+ $actions.Add([ordered]@{ id = 'reload'; label = [ordered]@{ zh = '安全重新加载'; en = 'Safe reload' }; requiresConfirmation = $false; isEnabled = $true })
+ $actions.Add([ordered]@{ id = 'cancel'; label = [ordered]@{ zh = '停用'; en = 'Disable' }; requiresConfirmation = $true; isEnabled = $true })
+ } elseif ($category -eq 'external') {
+ $actions.Add([ordered]@{ id = 'run'; label = [ordered]@{ zh = '启动'; en = 'Launch' }; requiresConfirmation = $true; isEnabled = $true })
+ } else {
+ $actions.Add([ordered]@{ id = 'run'; label = $actionLabel; requiresConfirmation = ($risk -eq 'high'); isEnabled = $true })
+ if ($category -eq 'image') { $actions.Add([ordered]@{ id = 'choose-file'; label = [ordered]@{ zh = '选择文件'; en = 'Choose file' }; requiresConfirmation = $false; isEnabled = $true }) }
+ $actions.Add([ordered]@{ id = 'clear'; label = [ordered]@{ zh = '清空'; en = 'Clear' }; requiresConfirmation = $false; isEnabled = $true })
+ $actions.Add([ordered]@{ id = 'copy-result'; label = [ordered]@{ zh = '复制结果'; en = 'Copy result' }; requiresConfirmation = $false; isEnabled = $true })
+ }
+
+ $definition = [ordered]@{
+ toolId = $id
+ schemaVersion = 1
+ title = [ordered]@{ zh = [string]$meta.zh; en = [string]$meta.en }
+ description = [ordered]@{ zh = [string]$meta.descriptionZh; en = [string]$meta.descriptionEn }
+ riskLevel = $risk
+ capabilities = @($(if ($meta.offline) { 'offline' } else { 'network' }))
+ sections = @($sections)
+ actions = @($actions)
+ result = [ordered]@{ kind = $resultKind; title = [ordered]@{ zh = '结果'; en = 'Result' }; isScrollable = $true; isCopyable = $true }
+ isFallback = $false
+ diagnostic = $null
+ responsiveHint = [ordered]@{ zh = '窄屏自动单列,宽屏采用输入与结果双栏。'; en = 'Narrow layouts stack controls; wide layouts use input and result columns.' }
+ advancedExpandedByDefault = $false
+ }
+ $json = [Text.Encoding]::UTF8.GetBytes(($definition | ConvertTo-Json -Depth 10 -Compress))
+ $toolIdBytes = [Text.Encoding]::UTF8.GetBytes($id)
+ if ($toolIdBytes.Length -eq 0 -or $toolIdBytes.Length -gt 512) { throw "Tool ID length is invalid: $id" }
+ $digest = [Security.Cryptography.SHA256]::Create()
+ try { $digestBytes = $digest.ComputeHash($json) } finally { $digest.Dispose() }
+ $aes.GenerateIV()
+ $encryptor = $aes.CreateEncryptor()
+ try { $cipher = $encryptor.TransformFinalBlock($json, 0, $json.Length) } finally { $encryptor.Dispose() }
+ $magic = [Text.Encoding]::ASCII.GetBytes('YMHUTUI1')
+ $output = [byte[]]::new($magic.Length + 1 + 2 + $toolIdBytes.Length + $digestBytes.Length + $aes.IV.Length + $cipher.Length)
+ [Array]::Copy($magic, 0, $output, 0, $magic.Length)
+ $output[$magic.Length] = 2
+ $lengthBytes = [BitConverter]::GetBytes([uint16]$toolIdBytes.Length)
+ [Array]::Copy($lengthBytes, 0, $output, $magic.Length + 1, 2)
+ $offset = $magic.Length + 3
+ [Array]::Copy($toolIdBytes, 0, $output, $offset, $toolIdBytes.Length)
+ $offset += $toolIdBytes.Length
+ [Array]::Copy($digestBytes, 0, $output, $offset, $digestBytes.Length)
+ $offset += $digestBytes.Length
+ [Array]::Copy($aes.IV, 0, $output, $offset, $aes.IV.Length)
+ $offset += $aes.IV.Length
+ [Array]::Copy($cipher, 0, $output, $offset, $cipher.Length)
+ [IO.File]::WriteAllBytes((Join-Path $outputRoot "$id.dat"), $output)
+ }
+} finally {
+ $aes.Dispose()
+}
+
+Write-Host "Generated $($ids.Count) tool UI definitions in $outputRoot"
diff --git a/scripts/prepare-music-api.ps1 b/scripts/prepare-music-api.ps1
new file mode 100644
index 0000000..d3781ae
--- /dev/null
+++ b/scripts/prepare-music-api.ps1
@@ -0,0 +1,130 @@
+param(
+ [string]$Destination,
+ [string]$NodeVersion = '22.18.0',
+ [switch]$Force
+)
+
+$ErrorActionPreference = 'Stop'
+function Get-Sha256Hex([string]$Path) {
+ $stream = [IO.File]::OpenRead($Path)
+ $algorithm = [Security.Cryptography.SHA256]::Create()
+ try {
+ return ([BitConverter]::ToString($algorithm.ComputeHash($stream))).Replace('-', '')
+ } finally {
+ $algorithm.Dispose()
+ $stream.Dispose()
+ }
+}
+
+$RepoRoot = Split-Path -Parent $PSScriptRoot
+$SourceRoot = Join-Path $RepoRoot 'third_party\MoeKoeMusic'
+$ApiSource = Join-Path $SourceRoot 'api'
+if ([string]::IsNullOrWhiteSpace($Destination)) {
+ $Destination = Join-Path $RepoRoot 'build\music-api'
+}
+$Destination = [IO.Path]::GetFullPath($Destination)
+$BuildRoot = [IO.Path]::GetFullPath((Join-Path $RepoRoot 'build'))
+if (-not $Destination.StartsWith($BuildRoot + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) {
+ throw "Music API destination must be inside $BuildRoot"
+}
+
+$requiredSource = @(
+ (Join-Path $SourceRoot 'UPSTREAM_COMMIT'),
+ (Join-Path $SourceRoot 'API_SUBMODULE_COMMIT'),
+ (Join-Path $SourceRoot 'MOEKOE_LICENSE'),
+ (Join-Path $SourceRoot 'MOEKOE_README.md'),
+ (Join-Path $ApiSource 'package.json'),
+ (Join-Path $ApiSource 'pnpm-lock.yaml'),
+ (Join-Path $ApiSource 'LICENSE'),
+ (Join-Path $ApiSource 'README.md'),
+ (Join-Path $ApiSource 'ymhut-sidecar.js')
+)
+foreach ($path in $requiredSource) {
+ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Required music API source is missing: $path" }
+}
+
+$fingerprintLines = Get-ChildItem -LiteralPath $SourceRoot -Recurse -File |
+ Where-Object { $_.FullName -notlike '*\node_modules\*' } |
+ Sort-Object FullName |
+ ForEach-Object {
+ $relative = $_.FullName.Substring($SourceRoot.TrimEnd('\').Length + 1).Replace('\', '/')
+ "$relative|$(Get-Sha256Hex $_.FullName)"
+ }
+$fingerprintBytes = [Text.Encoding]::UTF8.GetBytes(($fingerprintLines -join "`n"))
+$sha256 = [Security.Cryptography.SHA256]::Create()
+try {
+ $fingerprint = ([BitConverter]::ToString($sha256.ComputeHash($fingerprintBytes))).Replace('-', '')
+} finally {
+ $sha256.Dispose()
+}
+$markerPath = Join-Path $Destination 'payload.json'
+if (-not $Force -and (Test-Path -LiteralPath $markerPath)) {
+ try {
+ $marker = Get-Content -LiteralPath $markerPath -Raw | ConvertFrom-Json
+ if ($marker.nodeVersion -eq $NodeVersion -and $marker.sourceFingerprint -eq $fingerprint -and
+ (Test-Path -LiteralPath (Join-Path $Destination 'node.exe')) -and
+ (Test-Path -LiteralPath (Join-Path $Destination 'api\node_modules\express\package.json') -PathType Leaf) -and
+ (Test-Path -LiteralPath (Join-Path $Destination 'api\pnpm-lock.yaml')) -and
+ (Test-Path -LiteralPath (Join-Path $Destination 'api\LICENSE'))) {
+ return
+ }
+ } catch {
+ }
+}
+
+$CacheRoot = Join-Path $RepoRoot '.cache\node-runtime'
+New-Item -ItemType Directory -Force -Path $CacheRoot | Out-Null
+$archiveName = "node-v$NodeVersion-win-x64.zip"
+$archivePath = Join-Path $CacheRoot $archiveName
+$checksumsPath = Join-Path $CacheRoot "SHASUMS256-$NodeVersion.txt"
+$distRoot = "https://nodejs.org/dist/v$NodeVersion"
+if (-not (Test-Path -LiteralPath $archivePath)) {
+ Invoke-WebRequest -UseBasicParsing "$distRoot/$archiveName" -OutFile $archivePath
+}
+if (-not (Test-Path -LiteralPath $checksumsPath)) {
+ Invoke-WebRequest -UseBasicParsing "$distRoot/SHASUMS256.txt" -OutFile $checksumsPath
+}
+$checksumLine = Get-Content -LiteralPath $checksumsPath | Where-Object { $_ -match "\s$([regex]::Escape($archiveName))$" } | Select-Object -First 1
+if (-not $checksumLine) { throw "Node checksum was not found for $archiveName" }
+$expectedHash = ($checksumLine -split '\s+')[0].ToUpperInvariant()
+$actualHash = (Get-Sha256Hex $archivePath).ToUpperInvariant()
+if ($actualHash -ne $expectedHash) { throw "Node runtime checksum mismatch for $archiveName" }
+
+$staging = Join-Path $BuildRoot ("music-api.staging." + $PID)
+if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force }
+New-Item -ItemType Directory -Force -Path $staging | Out-Null
+$extractRoot = Join-Path $staging '_node'
+Expand-Archive -LiteralPath $archivePath -DestinationPath $extractRoot -Force
+$nodeRoot = Join-Path $extractRoot "node-v$NodeVersion-win-x64"
+Copy-Item -LiteralPath (Join-Path $nodeRoot 'node.exe') -Destination (Join-Path $staging 'node.exe')
+Copy-Item -LiteralPath (Join-Path $nodeRoot 'LICENSE') -Destination (Join-Path $staging 'NODE_LICENSE')
+Copy-Item -LiteralPath (Join-Path $nodeRoot 'README.md') -Destination (Join-Path $staging 'NODE_README.md')
+Remove-Item -LiteralPath $extractRoot -Recurse -Force
+
+$apiDestination = Join-Path $staging 'api'
+robocopy $ApiSource $apiDestination /E /XD node_modules .git /NFL /NDL /NJH /NJS /NP | Out-Null
+if ($LASTEXITCODE -ge 8) { throw "Failed to copy music API source (robocopy exit $LASTEXITCODE)." }
+Copy-Item -LiteralPath (Join-Path $SourceRoot 'MOEKOE_LICENSE') -Destination (Join-Path $staging 'MOEKOE_LICENSE')
+Copy-Item -LiteralPath (Join-Path $SourceRoot 'MOEKOE_README.md') -Destination (Join-Path $staging 'MOEKOE_README.md')
+Copy-Item -LiteralPath (Join-Path $SourceRoot 'UPSTREAM_COMMIT') -Destination (Join-Path $staging 'UPSTREAM_COMMIT')
+Copy-Item -LiteralPath (Join-Path $SourceRoot 'API_SUBMODULE_COMMIT') -Destination (Join-Path $staging 'API_SUBMODULE_COMMIT')
+Copy-Item -LiteralPath (Join-Path $SourceRoot 'YMhut_PATCHES.md') -Destination (Join-Path $staging 'YMhut_PATCHES.md')
+
+Push-Location $apiDestination
+try {
+ & corepack pnpm install --prod --frozen-lockfile --ignore-scripts --package-import-method=copy --config.node-linker=hoisted --store-dir (Join-Path $RepoRoot '.cache\pnpm-store-music')
+ if ($LASTEXITCODE -ne 0) { throw "pnpm failed to install the locked music API dependencies." }
+} finally {
+ Pop-Location
+}
+
+[ordered]@{
+ protocolVersion = 1
+ moekoeCommit = '4581660209c9cdd07669e8bd5bcbe136b7d91e02'
+ apiCommit = '06560e3e053bda1ab830750db6f645bab703f824'
+ nodeVersion = $NodeVersion
+ sourceFingerprint = $fingerprint
+} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $staging 'payload.json') -Encoding utf8
+
+if (Test-Path -LiteralPath $Destination) { Remove-Item -LiteralPath $Destination -Recurse -Force }
+Move-Item -LiteralPath $staging -Destination $Destination
diff --git a/src/YMhut.Box.Core/App/OpenSourceReferenceService.cs b/src/YMhut.Box.Core/App/OpenSourceReferenceService.cs
index e513607..1a646f2 100644
--- a/src/YMhut.Box.Core/App/OpenSourceReferenceService.cs
+++ b/src/YMhut.Box.Core/App/OpenSourceReferenceService.cs
@@ -9,7 +9,9 @@ public sealed record OpenSourceReferenceItem(
string Version,
string Usage,
string Attribution,
- string Path = "");
+ string Path = "",
+ string SourceUrl = "",
+ string LicensePath = "");
public interface IOpenSourceReferenceService
{
@@ -50,6 +52,8 @@ public sealed class OpenSourceReferenceService(AppPaths paths, ILogService? logS
"Microsoft and .NET contributors retain their original rights.")
};
+ AddMusicApiReferences(references);
+
await AddPackageReferencesSafelyAsync(references, cancellationToken).ConfigureAwait(false);
await AddBundledToolNoticesSafelyAsync(references, cancellationToken).ConfigureAwait(false);
@@ -67,6 +71,75 @@ public sealed class OpenSourceReferenceService(AppPaths paths, ILogService? logS
.ToArray();
}
+ private void AddMusicApiReferences(List references)
+ {
+ var moekoeLicense = FindReferenceFile("music-api/MOEKOE_LICENSE", "third_party/MoeKoeMusic/MOEKOE_LICENSE");
+ var moekoeReadme = FindReferenceFile("music-api/MOEKOE_README.md", "third_party/MoeKoeMusic/MOEKOE_README.md");
+ var apiLicense = FindReferenceFile("music-api/api/LICENSE", "third_party/MoeKoeMusic/api/LICENSE");
+ var apiReadme = FindReferenceFile("music-api/api/README.md", "third_party/MoeKoeMusic/api/README.md");
+ var nodeLicense = FindReferenceFile("music-api/NODE_LICENSE");
+ var packageJson = FindReferenceFile("music-api/api/package.json", "third_party/MoeKoeMusic/api/package.json");
+ var thirdPartyNotices = FindReferenceFile("THIRD_PARTY_NOTICES.md");
+
+ references.Add(new OpenSourceReferenceItem(
+ "MoeKoeMusic",
+ "Bundled music API source",
+ "4581660209c9cdd07669e8bd5bcbe136b7d91e02 · GPL-2.0",
+ "Pinned upstream project and original disclaimer retained with the bundled local KuGou API integration.",
+ "MoeKoeMusic contributors retain copyright, license, project name, and original statements.",
+ moekoeReadme,
+ "https://github.com/MoeKoeMusic/MoeKoeMusic/tree/4581660209c9cdd07669e8bd5bcbe136b7d91e02",
+ moekoeLicense));
+ references.Add(new OpenSourceReferenceItem(
+ "KuGouMusicApi",
+ "Bundled modified component",
+ "1.6.0 · 06560e3e053bda1ab830750db6f645bab703f824 · MIT",
+ "Local random-port API, KuGou protocol modules, KRC decoding, and the two-minute public GET cache base.",
+ "Original MIT notice and disclaimer are distributed beside the modified source; YMhut changes are documented in YMhut_PATCHES.md.",
+ apiReadme,
+ "https://github.com/MakcRe/KuGouMusicApi/tree/06560e3e053bda1ab830750db6f645bab703f824",
+ apiLicense));
+ references.Add(new OpenSourceReferenceItem(
+ "Node.js",
+ "Bundled runtime",
+ "22.18.0",
+ "Runs the loopback-only music API child process without requiring a system Node installation.",
+ "Node.js contributors retain their original rights and license.",
+ nodeLicense,
+ "https://github.com/nodejs/node/tree/v22.18.0",
+ nodeLicense));
+ references.Add(new OpenSourceReferenceItem(
+ "Binaryify/NeteaseCloudMusicApi",
+ "Cache mechanism attribution",
+ "upstream attribution",
+ "The KuGouMusicApi response cache mechanism identifies this project as its source; YMhut limits it to an explicit public GET allowlist.",
+ "Original project authors and contributors retain their rights.",
+ thirdPartyNotices,
+ "https://github.com/Binaryify/NeteaseCloudMusicApi"));
+ references.Add(new OpenSourceReferenceItem(
+ "MoeKoeMusic API npm dependencies",
+ "Bundled npm dependencies",
+ "pnpm lockfile",
+ "axios, express, pako, qrcode, crypto-js, node-forge, big-integer, dotenv, safe-decode-uri-component, and url production dependencies.",
+ "Exact versions and transitive packages are recorded in the bundled pnpm-lock.yaml; each package keeps its upstream license.",
+ packageJson,
+ "https://github.com/MakcRe/KuGouMusicApi/blob/06560e3e053bda1ab830750db6f645bab703f824/package.json",
+ apiLicense));
+ }
+
+ private string FindReferenceFile(params string[] relativePaths)
+ {
+ foreach (var root in CandidateRoots().Distinct(PathComparer))
+ {
+ foreach (var relative in relativePaths)
+ {
+ var path = Path.Combine(root, relative.Replace('/', Path.DirectorySeparatorChar));
+ if (File.Exists(path)) return path;
+ }
+ }
+ return string.Empty;
+ }
+
private async Task AddPackageReferencesSafelyAsync(List references, CancellationToken cancellationToken)
{
try
@@ -344,9 +417,14 @@ public sealed class OpenSourceReferenceService(AppPaths paths, ILogService? logS
{
"Application" => 0,
"Reference project attribution" => 1,
- "Runtime" => 2,
- "NuGet package" => 3,
- _ => 4
+ "Bundled music API source" => 2,
+ "Bundled modified component" => 3,
+ "Cache mechanism attribution" => 4,
+ "Runtime" => 5,
+ "Bundled npm dependencies" => 6,
+ "NuGet package" => 7,
+ "Third-party tool notice" => 8,
+ _ => 9
};
}
diff --git a/src/YMhut.Box.Core/DevEnvironments/DevTerminalSetupService.cs b/src/YMhut.Box.Core/DevEnvironments/DevTerminalSetupService.cs
index 37b87ea..7453fb6 100644
--- a/src/YMhut.Box.Core/DevEnvironments/DevTerminalSetupService.cs
+++ b/src/YMhut.Box.Core/DevEnvironments/DevTerminalSetupService.cs
@@ -62,6 +62,8 @@ public interface IDevTerminalPlatform
Task ResolveCommandAsync(string command, CancellationToken cancellationToken = default);
+ Task GetWindowsTerminalVersionAsync(CancellationToken cancellationToken = default);
+
Task RunAsync(string fileName, string arguments, CancellationToken cancellationToken = default);
Task InstallWindowsTerminalFallbackAsync(CancellationToken cancellationToken = default);
@@ -98,14 +100,15 @@ public sealed class DevTerminalSetupService(
{
var wingetPathTask = platform.ResolveCommandAsync("winget.exe", cancellationToken);
var terminalPathTask = platform.ResolveCommandAsync("wt.exe", cancellationToken);
+ var terminalVersionTask = platform.GetWindowsTerminalVersionAsync(cancellationToken);
var powerShellPathTask = platform.ResolveCommandAsync("pwsh.exe", cancellationToken);
- await Task.WhenAll(wingetPathTask, terminalPathTask, powerShellPathTask).ConfigureAwait(false);
+ await Task.WhenAll(wingetPathTask, terminalPathTask, terminalVersionTask, powerShellPathTask).ConfigureAwait(false);
var terminalPath = await terminalPathTask.ConfigureAwait(false);
var powerShellPath = await powerShellPathTask.ConfigureAwait(false);
var terminalVersion = string.IsNullOrWhiteSpace(terminalPath)
? string.Empty
- : await ReadVersionAsync(terminalPath, "--version", cancellationToken).ConfigureAwait(false);
+ : NormalizeTerminalVersion(await terminalVersionTask.ConfigureAwait(false));
var powerShellVersion = string.IsNullOrWhiteSpace(powerShellPath)
? string.Empty
: await ReadVersionAsync(powerShellPath, "-NoLogo -NoProfile -Command \"$PSVersionTable.PSVersion.ToString()\"", cancellationToken).ConfigureAwait(false);
@@ -254,6 +257,15 @@ public sealed class DevTerminalSetupService(
: string.Empty;
}
+ private static string NormalizeTerminalVersion(string value)
+ {
+ var normalized = (value ?? string.Empty).Trim();
+ return string.IsNullOrWhiteSpace(normalized) ||
+ normalized.StartsWith("Windows Terminal", StringComparison.OrdinalIgnoreCase)
+ ? normalized
+ : $"Windows Terminal {normalized}";
+ }
+
private static bool ContainsNoUpgrade(DevTerminalCommandResult result)
{
var message = result.Output + Environment.NewLine + result.Error;
@@ -341,6 +353,47 @@ public sealed class WindowsDevTerminalPlatform : IDevTerminalPlatform, IDisposab
return KnownCommandPaths(command).FirstOrDefault(File.Exists) ?? string.Empty;
}
+ public async Task GetWindowsTerminalVersionAsync(CancellationToken cancellationToken = default)
+ {
+ const string query = "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"$package = Get-AppxPackage -Name Microsoft.WindowsTerminal -ErrorAction SilentlyContinue | Sort-Object Version -Descending | Select-Object -First 1; if ($null -ne $package) { $package.Version.ToString() }\"";
+ var packageResult = await RunAsync("powershell.exe", query, cancellationToken).ConfigureAwait(false);
+ var packageVersion = FirstVersionLine(packageResult.Output);
+ if (packageResult.Succeeded && !string.IsNullOrWhiteSpace(packageVersion))
+ {
+ return packageVersion;
+ }
+
+ var command = await ResolveCommandAsync("wt.exe", cancellationToken).ConfigureAwait(false);
+ if (string.IsNullOrWhiteSpace(command))
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ var info = FileVersionInfo.GetVersionInfo(command);
+ return FirstVersionLine(info.ProductVersion) ?? FirstVersionLine(info.FileVersion) ?? string.Empty;
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+
+ private static string? FirstVersionLine(string? value)
+ {
+ var line = (value ?? string.Empty)
+ .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .FirstOrDefault();
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ return null;
+ }
+
+ var match = Regex.Match(line, @"\d+(?:\.\d+)+", RegexOptions.CultureInvariant);
+ return match.Success ? match.Value : line;
+ }
+
private static IEnumerable KnownCommandPaths(string command)
{
var local = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
@@ -478,10 +531,10 @@ public sealed class WindowsDevTerminalPlatform : IDevTerminalPlatform, IDisposab
var configured = false;
var terminalPath = await ResolveCommandAsync("wt.exe", cancellationToken).ConfigureAwait(false);
var terminalVersion = string.IsNullOrWhiteSpace(terminalPath)
- ? new DevTerminalCommandResult(-1, string.Empty, string.Empty)
- : await RunAsync(terminalPath, "--version", cancellationToken).ConfigureAwait(false);
+ ? string.Empty
+ : await GetWindowsTerminalVersionAsync(cancellationToken).ConfigureAwait(false);
if (DevTerminalSetupService.SupportsDefaultTerminal(WindowsBuild, WindowsRevision) &&
- DevTerminalSetupService.SupportsWindowsTerminalVersion(terminalVersion.Output))
+ DevTerminalSetupService.SupportsWindowsTerminalVersion(terminalVersion))
{
BackupDefaultTerminalRegistry();
using var key = Registry.CurrentUser.CreateSubKey(ConsoleStartupKey, writable: true);
@@ -534,25 +587,34 @@ public sealed class WindowsDevTerminalPlatform : IDevTerminalPlatform, IDisposab
public async Task OpenTerminalAsync(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
- var command = await ResolveCommandAsync("wt.exe", cancellationToken).ConfigureAwait(false);
- if (string.IsNullOrWhiteSpace(command))
+ var candidates = new[]
{
- command = await ResolveCommandAsync("pwsh.exe", cancellationToken).ConfigureAwait(false);
- }
- if (string.IsNullOrWhiteSpace(command))
+ await ResolveCommandAsync("wt.exe", cancellationToken).ConfigureAwait(false),
+ await ResolveCommandAsync("pwsh.exe", cancellationToken).ConfigureAwait(false),
+ "powershell.exe"
+ };
+ foreach (var command in candidates.Where(value => !string.IsNullOrWhiteSpace(value)).Distinct(StringComparer.OrdinalIgnoreCase))
{
- command = "powershell.exe";
+ try
+ {
+ var process = Process.Start(new ProcessStartInfo
+ {
+ FileName = command,
+ UseShellExecute = false,
+ CreateNoWindow = false,
+ ErrorDialog = false
+ });
+ if (process is not null)
+ {
+ return true;
+ }
+ }
+ catch
+ {
+ }
}
- try
- {
- _ = Process.Start(new ProcessStartInfo { FileName = command, UseShellExecute = true });
- return true;
- }
- catch
- {
- return false;
- }
+ return false;
}
public void Dispose()
diff --git a/src/YMhut.Box.Core/Music/DpapiMusicCredentialStore.cs b/src/YMhut.Box.Core/Music/DpapiMusicCredentialStore.cs
index 735a676..84f4794 100644
--- a/src/YMhut.Box.Core/Music/DpapiMusicCredentialStore.cs
+++ b/src/YMhut.Box.Core/Music/DpapiMusicCredentialStore.cs
@@ -53,9 +53,9 @@ public sealed class DpapiMusicCredentialStore(AppPaths paths) : IMusicCredential
return Path.Combine(Root, string.IsNullOrWhiteSpace(safe) ? "provider.bin" : safe + ".bin");
}
- private static byte[] Protect(byte[] input) => Transform(input, encrypt: true);
+ internal static byte[] Protect(byte[] input) => Transform(input, encrypt: true);
- private static byte[] Unprotect(byte[] input) => Transform(input, encrypt: false);
+ internal static byte[] Unprotect(byte[] input) => Transform(input, encrypt: false);
private static byte[] Transform(byte[] input, bool encrypt)
{
diff --git a/src/YMhut.Box.Core/Music/KugouLocalApiProxyHandler.cs b/src/YMhut.Box.Core/Music/KugouLocalApiProxyHandler.cs
new file mode 100644
index 0000000..ebdb493
--- /dev/null
+++ b/src/YMhut.Box.Core/Music/KugouLocalApiProxyHandler.cs
@@ -0,0 +1,172 @@
+using System.Globalization;
+using System.Net;
+using System.Net.Http.Json;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace YMhut.Box.Core.Music;
+
+public sealed record LocalMusicApiEndpoint(Uri BaseAddress, string Token, int ProtocolVersion, string Commit);
+
+public interface ILocalMusicApiEndpoint
+{
+ bool IsAvailable { get; }
+
+ Task GetEndpointAsync(CancellationToken cancellationToken = default);
+}
+
+public sealed class LocalMusicApiUnavailableException(string message, Exception? innerException = null)
+ : InvalidOperationException(message, innerException);
+
+public sealed class KugouLocalApiProxyHandler : HttpMessageHandler
+{
+ private static long _lastTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
+ private readonly ILocalMusicApiEndpoint _endpoint;
+ private readonly HttpClient _loopbackClient;
+ private readonly CookieContainer _providerCookies = new();
+
+ public KugouLocalApiProxyHandler(ILocalMusicApiEndpoint endpoint)
+ {
+ _endpoint = endpoint;
+ _loopbackClient = new HttpClient(new HttpClientHandler
+ {
+ AllowAutoRedirect = false,
+ UseCookies = true,
+ CookieContainer = new CookieContainer(),
+ UseProxy = false
+ })
+ {
+ Timeout = TimeSpan.FromSeconds(20)
+ };
+ }
+
+ public async Task SendApiAsync(
+ string path,
+ IReadOnlyDictionary? query = null,
+ bool forceFresh = false,
+ HttpMethod? method = null,
+ JsonNode? body = null,
+ CancellationToken cancellationToken = default)
+ {
+ var endpoint = await _endpoint.GetEndpointAsync(cancellationToken).ConfigureAwait(false);
+ method ??= HttpMethod.Get;
+ var values = new List>();
+ if (query is not null) values.AddRange(query);
+ if (method == HttpMethod.Post || forceFresh)
+ {
+ values.Add(new("timestamp", NextTimestamp().ToString(CultureInfo.InvariantCulture)));
+ }
+ var queryText = string.Join("&", values.Select(pair =>
+ $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}"));
+ var relative = path.TrimStart('/') + (queryText.Length == 0 ? string.Empty : "?" + queryText);
+ using var request = new HttpRequestMessage(method, new Uri(endpoint.BaseAddress, relative));
+ request.Headers.TryAddWithoutValidation("x-ymhut-internal-token", endpoint.Token);
+ if (body is not null) request.Content = JsonContent.Create(body, options: JsonOptions);
+ using var response = await _loopbackClient.SendAsync(request, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false);
+ var text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new LocalMusicApiUnavailableException($"本地酷狗 API 请求失败(HTTP {(int)response.StatusCode})。");
+ }
+ return JsonNode.Parse(text) as JsonObject
+ ?? throw new LocalMusicApiUnavailableException("本地酷狗 API 返回了无效数据。");
+ }
+
+ internal static long NextTimestamp()
+ {
+ while (true)
+ {
+ var current = Volatile.Read(ref _lastTimestamp);
+ var next = Math.Max(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), current + 1);
+ if (Interlocked.CompareExchange(ref _lastTimestamp, next, current) == current) return next;
+ }
+ }
+
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ var target = request.RequestUri ?? throw new InvalidOperationException("KuGou request has no target URI.");
+ if (target.Scheme != Uri.UriSchemeHttps ||
+ !(target.Host.Equals("kugou.com", StringComparison.OrdinalIgnoreCase) || target.Host.EndsWith(".kugou.com", StringComparison.OrdinalIgnoreCase)))
+ {
+ throw new InvalidOperationException("Only HTTPS KuGou endpoints can use the local API bridge.");
+ }
+
+ var endpoint = await _endpoint.GetEndpointAsync(cancellationToken).ConfigureAwait(false);
+ var body = request.Content is null
+ ? null
+ : Convert.ToBase64String(await request.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false));
+ var headers = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var header in request.Headers) headers[header.Key] = string.Join(", ", header.Value);
+ if (request.Content is not null)
+ {
+ foreach (var header in request.Content.Headers) headers[header.Key] = string.Join(", ", header.Value);
+ }
+ var cookie = _providerCookies.GetCookieHeader(target);
+ if (!string.IsNullOrWhiteSpace(cookie)) headers["Cookie"] = cookie;
+
+ var payload = new
+ {
+ method = request.Method.Method,
+ url = target.AbsoluteUri,
+ headers,
+ bodyBase64 = body
+ };
+ var timestamp = NextTimestamp().ToString(CultureInfo.InvariantCulture);
+ using var bridgeRequest = new HttpRequestMessage(
+ HttpMethod.Post,
+ new Uri(endpoint.BaseAddress, $"ymhut/proxy?timestamp={timestamp}"))
+ {
+ Content = JsonContent.Create(payload, options: JsonOptions)
+ };
+ bridgeRequest.Headers.TryAddWithoutValidation("x-ymhut-internal-token", endpoint.Token);
+
+ using var bridgeResponse = await _loopbackClient.SendAsync(
+ bridgeRequest,
+ HttpCompletionOption.ResponseContentRead,
+ cancellationToken).ConfigureAwait(false);
+ var responseJson = await bridgeResponse.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ if (!bridgeResponse.IsSuccessStatusCode)
+ {
+ throw new LocalMusicApiUnavailableException($"本地酷狗 API 请求失败(HTTP {(int)bridgeResponse.StatusCode})。");
+ }
+
+ var envelope = JsonNode.Parse(responseJson) as JsonObject
+ ?? throw new LocalMusicApiUnavailableException("本地酷狗 API 返回了无效响应。");
+ var status = envelope["status"]?.GetValue() ?? 502;
+ var responseBody = envelope["bodyBase64"]?.GetValue() ?? string.Empty;
+ var response = new HttpResponseMessage((HttpStatusCode)status)
+ {
+ RequestMessage = request,
+ Content = new ByteArrayContent(string.IsNullOrEmpty(responseBody) ? [] : Convert.FromBase64String(responseBody))
+ };
+
+ if (envelope["headers"] is JsonObject responseHeaders)
+ {
+ foreach (var pair in responseHeaders)
+ {
+ var values = pair.Value is JsonArray array
+ ? array.Select(item => item?.GetValue() ?? string.Empty).Where(value => value.Length > 0).ToArray()
+ : [pair.Value?.GetValue() ?? string.Empty];
+ if (pair.Key.Equals("set-cookie", StringComparison.OrdinalIgnoreCase))
+ {
+ foreach (var value in values)
+ {
+ try { _providerCookies.SetCookies(target, value); } catch (CookieException) { }
+ }
+ }
+ if (!response.Headers.TryAddWithoutValidation(pair.Key, values))
+ {
+ response.Content.Headers.TryAddWithoutValidation(pair.Key, values);
+ }
+ }
+ }
+ return response;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing) _loopbackClient.Dispose();
+ base.Dispose(disposing);
+ }
+}
diff --git a/src/YMhut.Box.Core/Music/KugouMusicProvider.cs b/src/YMhut.Box.Core/Music/KugouMusicProvider.cs
index 447e426..1b5cc74 100644
--- a/src/YMhut.Box.Core/Music/KugouMusicProvider.cs
+++ b/src/YMhut.Box.Core/Music/KugouMusicProvider.cs
@@ -37,8 +37,10 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
];
private readonly IMusicCredentialStore _credentials;
+ private readonly IMusicAccountSessionStore? _sessions;
private readonly HttpClient _publicClient;
private readonly KugouApiClient _api;
+ private readonly KugouLocalApiProxyHandler? _localApi;
private readonly SemaphoreSlim _initialization = new(1, 1);
private readonly SemaphoreSlim _accountCacheGate = new(1, 1);
private readonly Dictionary _playlistData = new(StringComparer.OrdinalIgnoreCase);
@@ -51,19 +53,33 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
private bool _accountCacheLoaded;
private bool _favoriteCacheLoaded;
private bool _initialized;
+ private readonly object _refreshSync = new();
+ private Task? _refreshTask;
+ private DateTimeOffset? _lastRefreshFailure;
public KugouMusicProvider(IMusicCredentialStore credentials, HttpMessageHandler? handler = null)
- : this(credentials, handler, null)
+ : this(credentials, handler, null, null)
+ {
+ }
+
+ public KugouMusicProvider(
+ IMusicCredentialStore credentials,
+ HttpMessageHandler handler,
+ IMusicAccountSessionStore sessions)
+ : this(credentials, handler, null, sessions)
{
}
internal KugouMusicProvider(
IMusicCredentialStore credentials,
HttpMessageHandler? handler,
- Func? playlistKeyFactory)
+ Func? playlistKeyFactory,
+ IMusicAccountSessionStore? sessions = null)
{
_credentials = credentials;
+ _sessions = sessions;
_api = new KugouApiClient(handler, playlistKeyFactory);
+ _localApi = handler as KugouLocalApiProxyHandler;
_publicClient = handler is null ? new HttpClient() : new HttpClient(handler, disposeHandler: false);
_publicClient.Timeout = TimeSpan.FromSeconds(12);
_publicClient.DefaultRequestHeaders.UserAgent.ParseAdd(WebUserAgent);
@@ -76,6 +92,8 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
public MusicLoginState LoginState { get; private set; } = new(false);
+ public IReadOnlyList QualityOptions => BuildQualityOptions(LoginState);
+
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
if (_initialized) return;
@@ -85,6 +103,24 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
if (_initialized) return;
await EnsureDeviceCoreAsync(cancellationToken).ConfigureAwait(false);
var secret = await _credentials.LoadAsync(Id, cancellationToken).ConfigureAwait(false);
+ if (_sessions is not null)
+ {
+ var session = await _sessions.LoadAsync(Id, cancellationToken).ConfigureAwait(false);
+ var cachedAccount = session is null ? null : ParseStoredAccount(session.Credential, out _);
+ if (session is not null && cachedAccount is not null && session.LoginState.LoggedIn)
+ {
+ _account = cachedAccount;
+ var refreshedAt = session.LoginState.RefreshedAt ?? session.SavedAt;
+ LoginState = session.LoginState with
+ {
+ RefreshedAt = refreshedAt,
+ IsCached = true,
+ IsStale = DateTimeOffset.UtcNow - refreshedAt >= TimeSpan.FromHours(12)
+ };
+ _initialized = true;
+ return;
+ }
+ }
if (!string.IsNullOrWhiteSpace(secret))
{
var account = ParseStoredAccount(secret, out var migrated);
@@ -113,6 +149,54 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
}
}
+ public Task RefreshLoginStateAsync(bool force = false, CancellationToken cancellationToken = default)
+ {
+ lock (_refreshSync)
+ {
+ if (!force && LoginState.LoggedIn && !LoginState.IsStale) return Task.FromResult(LoginState);
+ if (!force && _lastRefreshFailure is { } failedAt && DateTimeOffset.UtcNow - failedAt < TimeSpan.FromMinutes(2))
+ {
+ return Task.FromResult(LoginState with { IsCached = true, IsStale = true });
+ }
+ return _refreshTask ??= RefreshLoginStateCoreAsync(cancellationToken);
+ }
+ }
+
+ private async Task RefreshLoginStateCoreAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ await InitializeAsync(cancellationToken).ConfigureAwait(false);
+ if (_account is null) return LoginState;
+ await ValidateAccountAsync(_account, cancellationToken).ConfigureAwait(false);
+ LoginState = EnrichLoginState(ToLoginState(_account));
+ var entitlements = await LoadEntitlementsAsync(_account, cancellationToken).ConfigureAwait(false);
+ if (entitlements.Count > 0) LoginState = LoginState with { Entitlements = entitlements };
+ _lastRefreshFailure = null;
+ await SaveSessionAsync(_account, LoginState, cancellationToken).ConfigureAwait(false);
+ return LoginState;
+ }
+ catch (KugouApiException exception) when (exception.AuthenticationFailure)
+ {
+ await ClearAccountAsync(cancellationToken).ConfigureAwait(false);
+ return LoginState;
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
+ catch
+ {
+ _lastRefreshFailure = DateTimeOffset.UtcNow;
+ LoginState = LoginState with { IsCached = true, IsStale = true };
+ return LoginState;
+ }
+ finally
+ {
+ lock (_refreshSync) _refreshTask = null;
+ }
+ }
+
public async Task LoginWithCookieAsync(string cookie, CancellationToken cancellationToken = default)
{
await EnsureDeviceAsync(cancellationToken).ConfigureAwait(false);
@@ -358,66 +442,71 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
string? lastReason = null;
foreach (var candidate in candidates)
{
- try
+ var freePartModes = _account is null ? new[] { true } : new[] { false, true };
+ foreach (var freePart in freePartModes)
{
- var response = await _api.SendAndroidAsync(
- HttpMethod.Get,
- "/v5/url",
- new Dictionary
- {
- ["album_id"] = identity.AlbumId,
- ["area_code"] = "1",
- ["hash"] = candidate.Hash.ToLowerInvariant(),
- ["ssa_flag"] = "is_fromtrack",
- ["version"] = "11430",
- ["page_id"] = "151369488",
- ["quality"] = candidate.Value,
- ["album_audio_id"] = metadata?.MixSongId.ToString(CultureInfo.InvariantCulture) ?? "0",
- ["behavior"] = "play",
- ["pid"] = "2",
- ["cmd"] = "26",
- ["pidversion"] = "3001",
- ["IsFreePart"] = "0",
- ["ppage_id"] = "463467626,350369493,788954147",
- ["cdnBackup"] = "1",
- ["module"] = "",
- ["clientver"] = "11430"
- },
- null,
- _account,
- "trackercdn.kugou.com",
- cancellationToken,
- addTrackKey: true).ConfigureAwait(false);
-
- foreach (var data in PlaybackResponseNodes(response.Json))
+ try
{
- var extension = KugouApiClient.Text(data, "extName") ?? KugouApiClient.Text(data, "ext_name");
- if (string.Equals(extension, "mp4", StringComparison.OrdinalIgnoreCase))
- {
- lastReason = "当前音质返回了视频容器,已尝试较低音质。";
- continue;
- }
- if (!TryGetHttpsPlaybackUri(data, out var streamUri)) continue;
+ var response = await _api.SendAndroidAsync(
+ HttpMethod.Get,
+ "/v5/url",
+ new Dictionary
+ {
+ ["album_id"] = identity.AlbumId,
+ ["area_code"] = "1",
+ ["hash"] = candidate.Hash.ToLowerInvariant(),
+ ["ssa_flag"] = "is_fromtrack",
+ ["version"] = "11430",
+ ["page_id"] = "151369488",
+ ["quality"] = candidate.Value,
+ ["album_audio_id"] = metadata?.MixSongId.ToString(CultureInfo.InvariantCulture) ?? "0",
+ ["behavior"] = "play",
+ ["pid"] = "2",
+ ["cmd"] = "26",
+ ["pidversion"] = "3001",
+ ["IsFreePart"] = freePart ? "1" : "0",
+ ["ppage_id"] = "463467626,350369493,788954147",
+ ["cdnBackup"] = "1",
+ ["module"] = "",
+ ["clientver"] = "11430"
+ },
+ null,
+ _account,
+ "trackercdn.kugou.com",
+ cancellationToken,
+ addTrackKey: true).ConfigureAwait(false);
- var bitrate = FirstInteger(data, "bitRate", "bitrate", "bit_rate");
- if (bitrate <= 0) bitrate = candidate.Bitrate;
- var trial = FirstInteger(data, "is_free_part", "isFreePart") == 1;
- return new MusicStreamResult(
- streamUri,
- true,
- trial,
- candidate.Quality,
- bitrate * 1000L,
- trial ? "提供方仅允许试听片段。" : candidate.Quality == quality ? null : $"已回退到 {candidate.Quality} 音质。");
+ foreach (var data in PlaybackResponseNodes(response.Json))
+ {
+ var extension = KugouApiClient.Text(data, "extName") ?? KugouApiClient.Text(data, "ext_name");
+ if (string.Equals(extension, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ lastReason = "当前音质返回了视频容器,已尝试较低音质。";
+ continue;
+ }
+ if (!TryGetHttpsPlaybackUri(data, out var streamUri)) continue;
+
+ var bitrate = FirstInteger(data, "bitRate", "bitrate", "bit_rate");
+ if (bitrate <= 0) bitrate = candidate.Bitrate;
+ var fullAudio = FirstInteger(data, "is_full_audio", "isFullAudio") == 1;
+ var trial = FirstInteger(data, "is_free_part", "isFreePart") == 1 || (freePart && !fullAudio);
+ return new MusicStreamResult(
+ streamUri,
+ true,
+ trial,
+ candidate.Quality,
+ bitrate * 1000L,
+ trial ? "提供方仅允许试听片段。" : candidate.Quality == quality ? null : $"已回退到 {candidate.Quality} 音质。");
+ }
+ lastReason = KugouApiClient.Text(response.Json, "error") ??
+ KugouApiClient.Text(response.Json, "msg") ??
+ lastReason ??
+ "提供方未返回可用的 HTTPS 播放地址。";
+ }
+ catch (KugouApiException exception) when (!exception.AuthenticationFailure)
+ {
+ lastReason = exception.Message;
}
- lastReason = KugouApiClient.Text(response.Json, "error") ??
- KugouApiClient.Text(response.Json, "msg") ??
- lastReason ??
- "提供方未返回可用的 HTTPS 播放地址。";
- }
- catch (KugouApiException exception) when (!exception.AuthenticationFailure)
- {
- lastReason = exception.Message;
}
}
return new MusicStreamResult(null, false, false, quality, 0,
@@ -514,21 +603,32 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
private static bool TryGetHttpsPlaybackUri(JsonNode? node, out Uri? result)
{
- foreach (var property in new[] { "url", "play_url", "backup_url", "backupurl" })
+ foreach (var property in new[] { "url", "play_url", "backupUrl", "backup_url", "backupurl" })
{
foreach (var address in TextValues(node?[property]))
{
- if (Uri.TryCreate(address, UriKind.Absolute, out var uri) && uri.Scheme == Uri.UriSchemeHttps)
+ if (!Uri.TryCreate(address, UriKind.Absolute, out var uri)) continue;
+ if (uri.Scheme == Uri.UriSchemeHttps)
{
result = uri;
return true;
}
+ if (uri.Scheme == Uri.UriSchemeHttp && uri.IsDefaultPort && IsKugouMediaHost(uri.Host))
+ {
+ var secure = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttps, Port = -1 }.Uri;
+ result = secure;
+ return true;
+ }
}
}
result = null;
return false;
}
+ private static bool IsKugouMediaHost(string host)
+ => host.EndsWith(".kugou.com", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(host, "kugou.com", StringComparison.OrdinalIgnoreCase);
+
private static IEnumerable TextValues(JsonNode? node)
{
if (node is JsonValue value && value.TryGetValue(out var valueText) && !string.IsNullOrWhiteSpace(valueText))
@@ -576,6 +676,40 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
public async Task GetLyricsAsync(string songId, CancellationToken cancellationToken = default)
{
var identity = SplitSongId(songId);
+ if (_localApi is not null)
+ {
+ try
+ {
+ var searchResult = await _localApi.SendApiAsync("/search/lyric", new Dictionary
+ {
+ ["hash"] = identity.Hash,
+ ["man"] = "yes"
+ }, cancellationToken: cancellationToken).ConfigureAwait(false);
+ var localCandidate = KugouApiClient.Array(searchResult["candidates"] ?? searchResult["data"]?["candidates"]).FirstOrDefault();
+ var localLyricId = KugouApiClient.Text(localCandidate, "id");
+ var localAccessKey = KugouApiClient.Text(localCandidate, "accesskey");
+ if (!string.IsNullOrWhiteSpace(localLyricId) && !string.IsNullOrWhiteSpace(localAccessKey))
+ {
+ var krc = await _localApi.SendApiAsync("/lyric", new Dictionary
+ {
+ ["id"] = localLyricId,
+ ["accesskey"] = localAccessKey,
+ ["fmt"] = "krc",
+ ["decode"] = "true"
+ }, cancellationToken: cancellationToken).ConfigureAwait(false);
+ var decoded = KugouApiClient.Text(krc, "decodeContent") ?? KugouApiClient.Text(krc["data"], "decodeContent");
+ if (!string.IsNullOrWhiteSpace(decoded))
+ {
+ var parsed = MusicLyricsParser.ParseKrc(decoded);
+ if (parsed.Lines.Count > 0) return parsed;
+ }
+ }
+ }
+ catch (Exception exception) when (exception is not OperationCanceledException)
+ {
+ // Fall through to the plain LRC compatibility path.
+ }
+ }
var search = await GetPublicJsonAsync(
"https://lyrics.kugou.com/search?ver=1&man=yes&client=pc&hash=" + Uri.EscapeDataString(identity.Hash),
cancellationToken).ConfigureAwait(false);
@@ -810,24 +944,13 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
device ??= KugouApiClient.CreateDeviceCredential();
_api.Device = device;
await _credentials.SaveAsync(DeviceCredentialKey, JsonSerializer.Serialize(device, CredentialJsonOptions), cancellationToken).ConfigureAwait(false);
- if (string.IsNullOrWhiteSpace(device.Dfid) || device.Dfid == "-")
- {
- try
- {
- device = await _api.RegisterDeviceAsync(cancellationToken).ConfigureAwait(false);
- await _credentials.SaveAsync(DeviceCredentialKey, JsonSerializer.Serialize(device, CredentialJsonOptions), cancellationToken).ConfigureAwait(false);
- }
- catch (KugouApiException)
- {
- _api.Device = device;
- }
- }
_deviceInitialized = true;
}
private async Task RequireAccountAsync(CancellationToken cancellationToken)
{
await InitializeAsync(cancellationToken).ConfigureAwait(false);
+ if (LoginState.IsStale) await RefreshLoginStateAsync(false, cancellationToken).ConfigureAwait(false);
return _account ?? throw new InvalidOperationException("请先登录酷狗音乐账户。");
}
@@ -863,12 +986,15 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
private async Task CommitAccountAsync(KugouAccountCredential account, bool save, CancellationToken cancellationToken)
{
_account = account with { Version = 2 };
- LoginState = ToLoginState(_account);
+ LoginState = EnrichLoginState(ToLoginState(_account));
+ var entitlements = await LoadEntitlementsAsync(_account, cancellationToken).ConfigureAwait(false);
+ if (entitlements.Count > 0) LoginState = LoginState with { Entitlements = entitlements };
InvalidateAccountCaches();
if (save)
{
await _credentials.SaveAsync(Id, JsonSerializer.Serialize(_account, CredentialJsonOptions), cancellationToken).ConfigureAwait(false);
}
+ await SaveSessionAsync(_account, LoginState, cancellationToken).ConfigureAwait(false);
}
private async Task ClearAccountAsync(CancellationToken cancellationToken)
@@ -877,6 +1003,7 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
LoginState = new MusicLoginState(false);
InvalidateAccountCaches();
await _credentials.ClearAsync(Id, cancellationToken).ConfigureAwait(false);
+ if (_sessions is not null) await _sessions.ClearAsync(Id, cancellationToken).ConfigureAwait(false);
}
private async Task ExpireAccountAsync(CancellationToken cancellationToken)
@@ -1324,6 +1451,102 @@ public sealed class KugouMusicProvider : IMusicProvider, IDisposable
private static MusicLoginState ToLoginState(KugouAccountCredential account)
=> new(true, account.UserId, account.Nickname, account.AvatarUrl, account.VipLevel);
+ private async Task SaveSessionAsync(KugouAccountCredential account, MusicLoginState state, CancellationToken cancellationToken)
+ {
+ if (_sessions is null || !state.LoggedIn) return;
+ await _sessions.SaveAsync(new MusicAccountSession(
+ DpapiMusicAccountSessionStore.CurrentVersion,
+ Id,
+ JsonSerializer.Serialize(account, CredentialJsonOptions),
+ state,
+ DateTimeOffset.UtcNow), cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task> LoadEntitlementsAsync(
+ KugouAccountCredential account,
+ CancellationToken cancellationToken)
+ {
+ if (_localApi is null) return [];
+ try
+ {
+ var cookie = $"token={account.Token};userid={account.UserId}";
+ if (!string.IsNullOrWhiteSpace(account.VipToken)) cookie += $";vip_token={account.VipToken}";
+ if (!string.IsNullOrWhiteSpace(account.T1)) cookie += $";t1={account.T1}";
+ var response = await _localApi.SendApiAsync(
+ "/user/vip/detail",
+ method: HttpMethod.Post,
+ body: new JsonObject { ["cookie"] = cookie },
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+ var grants = new List();
+ var index = 0;
+ foreach (var node in KugouApiClient.Array(response["data"]?["busi_vip"] ?? response["busi_vip"]))
+ {
+ var product = KugouApiClient.Text(node, "product_type") ?? KugouApiClient.Text(node, "vip_type") ?? $"group-{index}";
+ var groupCode = KugouApiClient.Text(node, "busi_type") ?? $"group-{index}";
+ var groupName = index switch
+ {
+ 0 => "概念版",
+ 1 => "畅听版",
+ _ => groupCode
+ };
+ var enabled = KugouApiClient.Integer(node, "is_vip") == 1;
+ var expiryText = KugouApiClient.Text(node, "vip_end_time") ?? KugouApiClient.Text(node, "end_time");
+ DateTimeOffset? expiresAt = DateTimeOffset.TryParse(expiryText, out var parsedExpiry) ? parsedExpiry : null;
+ grants.Add(new MusicEntitlementGrant(
+ product,
+ $"{groupName} {product}",
+ groupCode,
+ enabled,
+ expiresAt));
+ index++;
+ }
+ return grants;
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
+ catch
+ {
+ return [];
+ }
+ }
+
+ private static MusicLoginState EnrichLoginState(MusicLoginState state)
+ {
+ if (!state.LoggedIn) return state;
+ var raw = string.IsNullOrWhiteSpace(state.VipLevel) ? "none" : state.VipLevel.Trim();
+ var known = raw.ToLowerInvariant() switch
+ {
+ "1" or "vip" => new MusicEntitlementGrant(raw, "酷狗 VIP", "membership", true),
+ "2" or "svip" => new MusicEntitlementGrant(raw, "酷狗 SVIP", "membership", true),
+ "music" or "musicpack" => new MusicEntitlementGrant(raw, "音乐包", "music-package", true),
+ "none" or "0" => null,
+ _ => new MusicEntitlementGrant(raw, raw, "unknown", true)
+ };
+ return state with
+ {
+ Entitlements = known is null ? [] : [known],
+ RefreshedAt = DateTimeOffset.UtcNow,
+ IsCached = false,
+ IsStale = false
+ };
+ }
+
+ private static IReadOnlyList BuildQualityOptions(MusicLoginState state)
+ {
+ var vip = state.Entitlements.Any(item => item.Enabled &&
+ (item.Group == "membership" || item.PlatformCode.Contains("vip", StringComparison.OrdinalIgnoreCase)));
+ return
+ [
+ new(MusicPlaybackQuality.Master, "蝰蛇母带", "FLAC", 1000, "酷狗 SVIP", vip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, vip ? null : "需要酷狗会员权益"),
+ new(MusicPlaybackQuality.HiRes, "Hi-Res", "FLAC", 900, "酷狗 VIP", vip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, vip ? null : "需要酷狗会员权益"),
+ new(MusicPlaybackQuality.Lossless, "无损音质", "FLAC", 800, "酷狗 VIP", vip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, vip ? null : "需要酷狗会员权益"),
+ new(MusicPlaybackQuality.High, "高品音质", "MP3", 320, "", MusicQualityAvailability.Available),
+ new(MusicPlaybackQuality.Standard, "标准音质", "MP3", 128, "", MusicQualityAvailability.Available)
+ ];
+ }
+
private sealed record KugouSongData(string Hash, long AlbumId, long MixSongId, long FileId);
private sealed record KugouPlaybackCandidate(MusicPlaybackQuality Quality, string Value, string Hash, int Bitrate);
diff --git a/src/YMhut.Box.Core/Music/MusicAccountSessionStore.cs b/src/YMhut.Box.Core/Music/MusicAccountSessionStore.cs
new file mode 100644
index 0000000..7025d7e
--- /dev/null
+++ b/src/YMhut.Box.Core/Music/MusicAccountSessionStore.cs
@@ -0,0 +1,82 @@
+using System.Text;
+using System.Text.Json;
+using YMhut.Box.Core.App;
+
+namespace YMhut.Box.Core.Music;
+
+public sealed record MusicAccountSession(
+ int Version,
+ string Provider,
+ string Credential,
+ MusicLoginState LoginState,
+ DateTimeOffset SavedAt);
+
+public interface IMusicAccountSessionStore
+{
+ Task LoadAsync(string provider, CancellationToken cancellationToken = default);
+
+ Task SaveAsync(MusicAccountSession session, CancellationToken cancellationToken = default);
+
+ Task ClearAsync(string provider, CancellationToken cancellationToken = default);
+}
+
+public sealed class DpapiMusicAccountSessionStore(AppPaths paths) : IMusicAccountSessionStore
+{
+ public const int CurrentVersion = 1;
+ private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
+ private readonly SemaphoreSlim _writeGate = new(1, 1);
+ private string Root => Path.Combine(paths.Data, "Music", "Sessions");
+
+ public async Task LoadAsync(string provider, CancellationToken cancellationToken = default)
+ {
+ var path = PathFor(provider);
+ if (!OperatingSystem.IsWindows() || !File.Exists(path)) return null;
+ try
+ {
+ var encrypted = await File.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false);
+ var json = Encoding.UTF8.GetString(DpapiMusicCredentialStore.Unprotect(encrypted));
+ var session = JsonSerializer.Deserialize(json, JsonOptions);
+ return session is { Version: CurrentVersion } &&
+ string.Equals(session.Provider, provider, StringComparison.OrdinalIgnoreCase)
+ ? session
+ : null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public async Task SaveAsync(MusicAccountSession session, CancellationToken cancellationToken = default)
+ {
+ if (!OperatingSystem.IsWindows()) throw new PlatformNotSupportedException("Windows DPAPI is required for music sessions.");
+ await _writeGate.WaitAsync(cancellationToken).ConfigureAwait(false);
+ var path = PathFor(session.Provider);
+ var temporary = path + $".{Guid.NewGuid():N}.tmp";
+ try
+ {
+ Directory.CreateDirectory(Root);
+ var normalized = session with { Version = CurrentVersion };
+ var encrypted = DpapiMusicCredentialStore.Protect(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(normalized, JsonOptions)));
+ await File.WriteAllBytesAsync(temporary, encrypted, cancellationToken).ConfigureAwait(false);
+ File.Move(temporary, path, true);
+ }
+ finally
+ {
+ try { if (File.Exists(temporary)) File.Delete(temporary); } catch { }
+ _writeGate.Release();
+ }
+ }
+
+ public Task ClearAsync(string provider, CancellationToken cancellationToken = default)
+ {
+ try { File.Delete(PathFor(provider)); } catch { }
+ return Task.CompletedTask;
+ }
+
+ private string PathFor(string provider)
+ {
+ var safe = string.Concat(provider.Where(character => char.IsLetterOrDigit(character) || character is '-' or '_'));
+ return Path.Combine(Root, (string.IsNullOrWhiteSpace(safe) ? "provider" : safe) + ".bin");
+ }
+}
diff --git a/src/YMhut.Box.Core/Music/MusicModels.cs b/src/YMhut.Box.Core/Music/MusicModels.cs
index 8b7b6e9..a0c705a 100644
--- a/src/YMhut.Box.Core/Music/MusicModels.cs
+++ b/src/YMhut.Box.Core/Music/MusicModels.cs
@@ -19,6 +19,13 @@ public enum MusicPlaybackQuality
Standard
}
+public enum MusicQualityAvailability
+{
+ Available,
+ RequiresEntitlement,
+ Unavailable
+}
+
public enum MusicPlayMode
{
List,
@@ -59,12 +66,58 @@ public sealed record MusicPlaylist(
internal object? ProviderData { get; init; }
}
+public sealed record MusicEntitlementGrant(
+ string PlatformCode,
+ string DisplayName,
+ string Group,
+ bool Enabled,
+ DateTimeOffset? ExpiresAt = null);
+
+///
+/// Shared presentation settings for timed lyrics in both music providers.
+/// Values are persisted by the application settings service, not by a provider.
+///
+public sealed class MusicLyricsSettings
+{
+ public bool WordHighlightEnabled { get; set; } = true;
+
+ public bool ShowTranslation { get; set; } = true;
+
+ public bool ShowRomanized { get; set; }
+
+ public double FontScale { get; set; } = 1.0;
+
+ public double LineSpacing { get; set; } = 12.0;
+
+ public int ActiveWordColor { get; set; } = unchecked((int)0xFF5E6D44);
+
+ public bool DesktopSyncEnabled { get; set; } = true;
+}
+
+public sealed record MusicQualityOption(
+ MusicPlaybackQuality Quality,
+ string DisplayName,
+ string Format,
+ int BitrateKbps,
+ string RequiredEntitlement,
+ MusicQualityAvailability Availability,
+ string? UnavailableReason = null);
+
public sealed record MusicLoginState(
bool LoggedIn,
string UserId = "",
string Nickname = "",
string AvatarUrl = "",
- string VipLevel = "none");
+ string VipLevel = "none")
+{
+ public IReadOnlyList Entitlements { get; init; } = [];
+
+ public DateTimeOffset? RefreshedAt { get; init; }
+
+ public bool IsCached { get; init; }
+
+ public bool IsStale { get; init; }
+}
public sealed record MusicQrSession(string Key, string LoginUrl, DateTimeOffset ExpiresAt)
{
@@ -119,7 +172,12 @@ public sealed record TimedLyricLine(
TimeSpan Duration,
string Text,
string? Translation,
- IReadOnlyList Words);
+ IReadOnlyList Words)
+{
+ public string? Romanized { get; init; }
+}
+
+public sealed record MusicLyricCursor(int LineIndex, int WordIndex, double WordProgress);
public sealed record TimedLyrics(IReadOnlyList Lines, string Raw, string? TranslationRaw, string? RomanizedRaw);
@@ -148,8 +206,12 @@ public interface IMusicProvider
MusicLoginState LoginState { get; }
+ IReadOnlyList QualityOptions { get; }
+
Task InitializeAsync(CancellationToken cancellationToken = default);
+ Task RefreshLoginStateAsync(bool force = false, CancellationToken cancellationToken = default);
+
Task LoginWithCookieAsync(string cookie, CancellationToken cancellationToken = default);
Task CreateQrSessionAsync(CancellationToken cancellationToken = default);
diff --git a/src/YMhut.Box.Core/Music/NeteaseMusicProvider.cs b/src/YMhut.Box.Core/Music/NeteaseMusicProvider.cs
index 8dcbad3..74b5ba1 100644
--- a/src/YMhut.Box.Core/Music/NeteaseMusicProvider.cs
+++ b/src/YMhut.Box.Core/Music/NeteaseMusicProvider.cs
@@ -33,16 +33,25 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
];
private readonly IMusicCredentialStore _credentials;
+ private readonly IMusicAccountSessionStore? _sessions;
private readonly ILogService? _logService;
private readonly HttpClient _client;
private readonly SemaphoreSlim _initialization = new(1, 1);
private readonly string _deviceId = Convert.ToHexStringLower(RandomNumberGenerator.GetBytes(16));
private string _cookie = string.Empty;
private bool _initialized;
+ private readonly object _refreshSync = new();
+ private Task? _refreshTask;
+ private DateTimeOffset? _lastRefreshFailure;
- public NeteaseMusicProvider(IMusicCredentialStore credentials, ILogService? logService = null, HttpMessageHandler? handler = null)
+ public NeteaseMusicProvider(
+ IMusicCredentialStore credentials,
+ ILogService? logService = null,
+ HttpMessageHandler? handler = null,
+ IMusicAccountSessionStore? sessions = null)
{
_credentials = credentials;
+ _sessions = sessions;
_logService = logService;
_client = handler is null ? new HttpClient() : new HttpClient(handler, disposeHandler: false);
_client.Timeout = TimeSpan.FromSeconds(12);
@@ -56,6 +65,8 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
public MusicLoginState LoginState { get; private set; } = new(false);
+ public IReadOnlyList QualityOptions => BuildQualityOptions(LoginState);
+
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
if (_initialized) return;
@@ -64,6 +75,23 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
{
if (_initialized) return;
_cookie = await _credentials.LoadAsync(Id, cancellationToken).ConfigureAwait(false);
+ if (_sessions is not null)
+ {
+ var session = await _sessions.LoadAsync(Id, cancellationToken).ConfigureAwait(false);
+ if (session is not null && !string.IsNullOrWhiteSpace(session.Credential) && session.LoginState.LoggedIn)
+ {
+ _cookie = session.Credential;
+ var refreshedAt = session.LoginState.RefreshedAt ?? session.SavedAt;
+ LoginState = session.LoginState with
+ {
+ RefreshedAt = refreshedAt,
+ IsCached = true,
+ IsStale = DateTimeOffset.UtcNow - refreshedAt >= TimeSpan.FromHours(12)
+ };
+ _initialized = true;
+ return;
+ }
+ }
if (string.IsNullOrWhiteSpace(_cookie))
{
LoginState = new MusicLoginState(false);
@@ -77,6 +105,7 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
await _credentials.ClearAsync(Id, cancellationToken).ConfigureAwait(false);
}
_initialized = true;
+ if (LoginState.LoggedIn) await SaveSessionAsync(_cookie, LoginState, cancellationToken).ConfigureAwait(false);
}
finally
{
@@ -84,6 +113,53 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
}
}
+ public Task RefreshLoginStateAsync(bool force = false, CancellationToken cancellationToken = default)
+ {
+ lock (_refreshSync)
+ {
+ if (!force && LoginState.LoggedIn && !LoginState.IsStale) return Task.FromResult(LoginState);
+ if (!force && _lastRefreshFailure is { } failedAt && DateTimeOffset.UtcNow - failedAt < TimeSpan.FromMinutes(2))
+ {
+ return Task.FromResult(LoginState with { IsCached = true, IsStale = true });
+ }
+ return _refreshTask ??= RefreshLoginStateCoreAsync(cancellationToken);
+ }
+ }
+
+ private async Task RefreshLoginStateCoreAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ await InitializeAsync(cancellationToken).ConfigureAwait(false);
+ if (string.IsNullOrWhiteSpace(_cookie)) return LoginState;
+ var state = await GetLoginStateAsync(_cookie, cancellationToken).ConfigureAwait(false);
+ if (!state.LoggedIn)
+ {
+ await LogoutAsync(cancellationToken).ConfigureAwait(false);
+ return LoginState;
+ }
+ state = EnrichLoginState(state);
+ LoginState = state;
+ _lastRefreshFailure = null;
+ await SaveSessionAsync(_cookie, state, cancellationToken).ConfigureAwait(false);
+ return state;
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
+ catch
+ {
+ _lastRefreshFailure = DateTimeOffset.UtcNow;
+ LoginState = LoginState with { IsCached = true, IsStale = true };
+ return LoginState;
+ }
+ finally
+ {
+ lock (_refreshSync) _refreshTask = null;
+ }
+ }
+
public async Task LoginWithCookieAsync(string cookie, CancellationToken cancellationToken = default)
{
var normalized = NormalizeCookie(cookie);
@@ -196,9 +272,10 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
private async Task CommitLoginAsync(string cookie, MusicLoginState state, CancellationToken cancellationToken)
{
_cookie = cookie;
- LoginState = state;
+ LoginState = EnrichLoginState(state);
_initialized = true;
await _credentials.SaveAsync(Id, cookie, cancellationToken).ConfigureAwait(false);
+ await SaveSessionAsync(cookie, LoginState, cancellationToken).ConfigureAwait(false);
}
public async Task LogoutAsync(CancellationToken cancellationToken = default)
@@ -206,6 +283,7 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
_cookie = string.Empty;
LoginState = new MusicLoginState(false);
await _credentials.ClearAsync(Id, cancellationToken).ConfigureAwait(false);
+ if (_sessions is not null) await _sessions.ClearAsync(Id, cancellationToken).ConfigureAwait(false);
}
public async Task SearchAsync(string keywords, MusicSearchKind kind, int limit = 30, CancellationToken cancellationToken = default)
@@ -230,6 +308,7 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
public async Task> GetUserPlaylistsAsync(CancellationToken cancellationToken = default)
{
await InitializeAsync(cancellationToken).ConfigureAwait(false);
+ if (LoginState.IsStale) await RefreshLoginStateAsync(false, cancellationToken).ConfigureAwait(false);
if (!LoginState.LoggedIn) return [];
var result = await GetAsync("https://music.163.com/api/user/playlist", new Dictionary
{
@@ -267,6 +346,7 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
public async Task> GetDailySongsAsync(CancellationToken cancellationToken = default)
{
await InitializeAsync(cancellationToken).ConfigureAwait(false);
+ if (LoginState.IsStale) await RefreshLoginStateAsync(false, cancellationToken).ConfigureAwait(false);
if (!LoginState.LoggedIn) return [];
var result = await GetAsync("https://music.163.com/api/v3/discovery/recommend/songs", new Dictionary { ["csrf_token"] = CsrfToken() }, cancellationToken).ConfigureAwait(false);
return Array(result["data"]?["dailySongs"]).Select(MapSong).Where(song => !string.IsNullOrWhiteSpace(song.Id)).ToArray();
@@ -426,6 +506,7 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
public async Task SetFavoriteAsync(MusicSong song, bool favorite, CancellationToken cancellationToken = default)
{
+ if (LoginState.IsStale) await RefreshLoginStateAsync(false, cancellationToken).ConfigureAwait(false);
EnsureLoggedIn();
var result = await PostApiAsync("/api/song/like", new Dictionary
{
@@ -438,6 +519,7 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
public async Task SetPlaylistSubscribedAsync(MusicPlaylist playlist, bool subscribed, CancellationToken cancellationToken = default)
{
+ if (LoginState.IsStale) await RefreshLoginStateAsync(false, cancellationToken).ConfigureAwait(false);
EnsureLoggedIn();
var result = await PostApiAsync(subscribed ? "/api/playlist/subscribe" : "/api/playlist/unsubscribe", new Dictionary
{
@@ -463,12 +545,59 @@ public sealed partial class NeteaseMusicProvider : IMusicProvider, IDisposable
var userId = Text(profile, "userId") ?? Text(profile, "id") ?? Text(account, "userId") ?? Text(account, "id") ?? "";
if (string.IsNullOrWhiteSpace(userId)) return new MusicLoginState(false);
var vip = Math.Max(Integer(profile, "vipType"), Integer(account, "vipType"));
- return new MusicLoginState(
+ return EnrichLoginState(new MusicLoginState(
true,
userId,
Text(profile, "nickname") ?? Text(account, "userName") ?? Text(account, "nickname") ?? "网易云用户",
Text(profile, "avatarUrl") ?? Text(account, "avatarUrl") ?? "",
- vip >= 10 ? "svip" : vip >= 1 ? "vip" : "none");
+ vip >= 10 ? "svip" : vip >= 1 ? "vip" : "none"));
+ }
+
+ private async Task SaveSessionAsync(string credential, MusicLoginState state, CancellationToken cancellationToken)
+ {
+ if (_sessions is null || string.IsNullOrWhiteSpace(credential) || !state.LoggedIn) return;
+ await _sessions.SaveAsync(new MusicAccountSession(
+ DpapiMusicAccountSessionStore.CurrentVersion,
+ Id,
+ credential,
+ state,
+ DateTimeOffset.UtcNow), cancellationToken).ConfigureAwait(false);
+ }
+
+ private static MusicLoginState EnrichLoginState(MusicLoginState state)
+ {
+ if (!state.LoggedIn) return state;
+ IReadOnlyList grants = state.VipLevel switch
+ {
+ "svip" =>
+ [
+ new("svip", "黑胶 SVIP", "membership", true),
+ new("vip", "黑胶 VIP", "membership", true)
+ ],
+ "vip" => [new("vip", "黑胶 VIP", "membership", true)],
+ _ => []
+ };
+ return state with
+ {
+ Entitlements = grants,
+ RefreshedAt = DateTimeOffset.UtcNow,
+ IsCached = false,
+ IsStale = false
+ };
+ }
+
+ private static IReadOnlyList BuildQualityOptions(MusicLoginState state)
+ {
+ var vip = state.Entitlements.Any(item => item.Enabled && item.PlatformCode is "vip" or "svip");
+ var svip = state.Entitlements.Any(item => item.Enabled && item.PlatformCode == "svip");
+ return
+ [
+ new(MusicPlaybackQuality.Master, "超清母带", "FLAC", 2000, "黑胶 SVIP", svip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, svip ? null : "需要黑胶 SVIP"),
+ new(MusicPlaybackQuality.HiRes, "Hi-Res", "FLAC", 1200, "黑胶 VIP", vip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, vip ? null : "需要黑胶 VIP"),
+ new(MusicPlaybackQuality.Lossless, "无损", "FLAC", 900, "黑胶 VIP", vip ? MusicQualityAvailability.Available : MusicQualityAvailability.RequiresEntitlement, vip ? null : "需要黑胶 VIP"),
+ new(MusicPlaybackQuality.High, "极高", "MP3", 320, "", MusicQualityAvailability.Available),
+ new(MusicPlaybackQuality.Standard, "标准", "AAC", 128, "", MusicQualityAvailability.Available)
+ ];
}
private async Task GetPlaylistDetailAsync(string id, CancellationToken cancellationToken)
@@ -745,6 +874,7 @@ public static partial class MusicLyricsParser
public static TimedLyrics Parse(string raw, string? translated = null, string? romanized = null, string? yrc = null)
{
var translations = ParseLrc(translated ?? "").ToDictionary(line => line.Start, line => line.Text);
+ var romanizations = ParseLrc(romanized ?? "").ToDictionary(line => line.Start, line => line.Text);
var lines = string.IsNullOrWhiteSpace(yrc) ? ParseLrc(raw) : ParseYrc(yrc);
if (lines.Count == 0 && !string.IsNullOrWhiteSpace(raw)) lines = ParseLrc(raw);
var ordered = lines.OrderBy(line => line.Start).ToArray();
@@ -756,7 +886,8 @@ public static partial class MusicLyricsParser
completed.Add(line with
{
Duration = line.Duration > TimeSpan.Zero ? line.Duration : next - line.Start,
- Translation = translations.GetValueOrDefault(line.Start)
+ Translation = translations.GetValueOrDefault(line.Start),
+ Romanized = romanizations.GetValueOrDefault(line.Start)
});
}
return new TimedLyrics(completed, raw, translated, romanized);
@@ -807,6 +938,67 @@ public static partial class MusicLyricsParser
return lines.Count > 0 ? lines : ParseLrc(raw);
}
+ public static TimedLyrics ParseKrc(string raw)
+ {
+ var translations = Array.Empty();
+ var romanizations = Array.Empty();
+ var language = KrcLanguageRegex().Match(raw);
+ if (language.Success)
+ {
+ try
+ {
+ var json = JsonNode.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(language.Groups[1].Value)));
+ foreach (var block in json?["content"]?.AsArray() ?? [])
+ {
+ var values = block?["lyricContent"]?.AsArray()
+ .Select(item => item?.AsArray().FirstOrDefault()?.GetValue() ?? string.Empty)
+ .ToArray() ?? [];
+ if (block?["type"]?.GetValue() == 1) translations = values;
+ else if (block?["type"]?.GetValue() == 0) romanizations = values;
+ }
+ }
+ catch (Exception exception) when (exception is FormatException or JsonException)
+ {
+ }
+ }
+
+ var parsed = new List();
+ foreach (var rawLine in raw.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
+ {
+ var lineMatch = KrcLineRegex().Match(rawLine);
+ if (!lineMatch.Success) continue;
+ var lineStartMs = long.Parse(lineMatch.Groups[1].Value, global::System.Globalization.CultureInfo.InvariantCulture);
+ var lineDurationMs = long.Parse(lineMatch.Groups[2].Value, global::System.Globalization.CultureInfo.InvariantCulture);
+ var text = new StringBuilder();
+ var words = new List();
+ foreach (Match wordMatch in KrcWordRegex().Matches(lineMatch.Groups[3].Value))
+ {
+ var word = wordMatch.Groups[3].Value;
+ if (word.Length == 0) continue;
+ var characterStart = text.Length;
+ text.Append(word);
+ words.Add(new TimedLyricWord(
+ word,
+ TimeSpan.FromMilliseconds(lineStartMs + long.Parse(wordMatch.Groups[1].Value, global::System.Globalization.CultureInfo.InvariantCulture)),
+ TimeSpan.FromMilliseconds(long.Parse(wordMatch.Groups[2].Value, global::System.Globalization.CultureInfo.InvariantCulture)),
+ characterStart,
+ text.Length));
+ }
+ if (text.Length == 0) continue;
+ var index = parsed.Count;
+ parsed.Add(new TimedLyricLine(
+ TimeSpan.FromMilliseconds(lineStartMs),
+ TimeSpan.FromMilliseconds(lineDurationMs),
+ text.ToString(),
+ index < translations.Length && !string.IsNullOrWhiteSpace(translations[index]) ? translations[index] : null,
+ words)
+ {
+ Romanized = index < romanizations.Length && !string.IsNullOrWhiteSpace(romanizations[index]) ? romanizations[index] : null
+ });
+ }
+ return parsed.Count > 0 ? new TimedLyrics(parsed, raw, null, null) : Parse(raw);
+ }
+
[GeneratedRegex(@"\[(\d{1,3}):([0-5]?\d(?:\.\d{1,3})?)\]")]
private static partial Regex LrcTimeRegex();
@@ -815,4 +1007,13 @@ public static partial class MusicLyricsParser
[GeneratedRegex(@"\((\d+),(\d+),\d+\)([^()]*)")]
private static partial Regex YrcWordRegex();
+
+ [GeneratedRegex(@"\[language:([A-Za-z0-9+/=]+)\]", RegexOptions.CultureInvariant)]
+ private static partial Regex KrcLanguageRegex();
+
+ [GeneratedRegex(@"^\[(\d+),(\d+)\](.*)$", RegexOptions.CultureInvariant)]
+ private static partial Regex KrcLineRegex();
+
+ [GeneratedRegex(@"<(\d+),(\d+),\d+>([^<]*)", RegexOptions.CultureInvariant)]
+ private static partial Regex KrcWordRegex();
}
diff --git a/src/YMhut.Box.Core/Settings/AgreementDocument.cs b/src/YMhut.Box.Core/Settings/AgreementDocument.cs
index 0c5609d..84cb43b 100644
--- a/src/YMhut.Box.Core/Settings/AgreementDocument.cs
+++ b/src/YMhut.Box.Core/Settings/AgreementDocument.cs
@@ -10,9 +10,9 @@ public static class AgreementDocument
public const string MusicProviderDisclaimerId = "music-provider-disclaimer";
- public const string MusicProviderDisclaimerVersion = "1.0";
+ public const string MusicProviderDisclaimerVersion = "2.1";
- public const int MusicProviderDisclaimerRevision = 1;
+ public const int MusicProviderDisclaimerRevision = 3;
public const string CurrentVersion = "2.0.6.2";
public const int CurrentRevision = 1;
diff --git a/src/YMhut.Box.Core/Settings/AppSettings.cs b/src/YMhut.Box.Core/Settings/AppSettings.cs
index 9b1c44c..90537fe 100644
--- a/src/YMhut.Box.Core/Settings/AppSettings.cs
+++ b/src/YMhut.Box.Core/Settings/AppSettings.cs
@@ -1,5 +1,16 @@
+using YMhut.Box.Core.Music;
+
namespace YMhut.Box.Core.Settings;
+public sealed class ToolUiPageState
+{
+ public Dictionary Values { get; set; } = new(StringComparer.OrdinalIgnoreCase);
+
+ public Dictionary ExpandedSections { get; set; } = new(StringComparer.OrdinalIgnoreCase);
+
+ public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
+}
+
public sealed class AppSettings
{
public string Theme { get; set; } = "System";
@@ -46,6 +57,8 @@ public sealed class AppSettings
public List PinnedToolIds { get; set; } = [];
+ public Dictionary ToolUiStates { get; set; } = new(StringComparer.OrdinalIgnoreCase);
+
public double? WindowX { get; set; }
public double? WindowY { get; set; }
@@ -80,6 +93,8 @@ public sealed class AppSettings
public bool RequireRiskConfirmation { get; set; } = true;
+ public MusicLyricsSettings Lyrics { get; set; } = new();
+
public string HomePageMode { get; set; } = "dashboard";
public string CloseBehavior { get; set; } = "ask";
diff --git a/src/YMhut.Box.Core/Settings/AppSettingsService.cs b/src/YMhut.Box.Core/Settings/AppSettingsService.cs
index 47af02f..e43e34e 100644
--- a/src/YMhut.Box.Core/Settings/AppSettingsService.cs
+++ b/src/YMhut.Box.Core/Settings/AppSettingsService.cs
@@ -81,6 +81,38 @@ public sealed class AppSettingsService(AppSettingsStore store) : ISettingsServic
}, cancellationToken).ConfigureAwait(false);
}
+ public ToolUiPageState GetToolUiState(string toolId)
+ {
+ if (string.IsNullOrWhiteSpace(toolId)
+ || _current.ToolUiStates is null
+ || !_current.ToolUiStates.TryGetValue(toolId, out var state))
+ {
+ return new ToolUiPageState();
+ }
+
+ return CloneToolUiState(state);
+ }
+
+ public Task SaveToolUiStateAsync(
+ string toolId,
+ ToolUiPageState state,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(toolId))
+ {
+ throw new ArgumentException("Tool ID is required.", nameof(toolId));
+ }
+
+ ArgumentNullException.ThrowIfNull(state);
+ var snapshot = CloneToolUiState(state);
+ snapshot.UpdatedAt = DateTimeOffset.UtcNow;
+ return UpdateAsync(settings =>
+ {
+ settings.ToolUiStates ??= new Dictionary(StringComparer.OrdinalIgnoreCase);
+ settings.ToolUiStates[toolId] = snapshot;
+ }, cancellationToken);
+ }
+
public Task SaveWindowBoundsAsync(
double x,
double y,
@@ -103,4 +135,14 @@ public sealed class AppSettingsService(AppSettingsStore store) : ISettingsServic
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
+
+ private static ToolUiPageState CloneToolUiState(ToolUiPageState state)
+ {
+ return new ToolUiPageState
+ {
+ Values = new Dictionary(state.Values, StringComparer.OrdinalIgnoreCase),
+ ExpandedSections = new Dictionary(state.ExpandedSections, StringComparer.OrdinalIgnoreCase),
+ UpdatedAt = state.UpdatedAt
+ };
+ }
}
diff --git a/src/YMhut.Box.Core/Settings/AppSettingsStore.cs b/src/YMhut.Box.Core/Settings/AppSettingsStore.cs
index f2c94e0..a288aea 100644
--- a/src/YMhut.Box.Core/Settings/AppSettingsStore.cs
+++ b/src/YMhut.Box.Core/Settings/AppSettingsStore.cs
@@ -70,6 +70,8 @@ public sealed class AppSettingsStore
settings.FeedbackDefaultType = NormalizeFeedbackType(settings.FeedbackDefaultType);
settings.FeedbackDefaultSeverity = NormalizeFeedbackSeverity(settings.FeedbackDefaultSeverity);
settings.GlobalHotkeys = HotkeySettingsDefaults.Normalize(settings.GlobalHotkeys);
+ NormalizeLyrics(settings);
+ NormalizeToolUiStates(settings);
if (!settings.LegacyDatabaseImportCompleted)
{
@@ -100,11 +102,45 @@ public sealed class AppSettingsStore
settings.FeedbackDefaultType = NormalizeFeedbackType(settings.FeedbackDefaultType);
settings.FeedbackDefaultSeverity = NormalizeFeedbackSeverity(settings.FeedbackDefaultSeverity);
settings.GlobalHotkeys = HotkeySettingsDefaults.Normalize(settings.GlobalHotkeys);
+ NormalizeLyrics(settings);
+ NormalizeToolUiStates(settings);
Directory.CreateDirectory(Path.GetDirectoryName(_settingsPath)!);
await using var stream = File.Create(_settingsPath);
await JsonSerializer.SerializeAsync(stream, settings, JsonOptions, cancellationToken);
}
+ private static void NormalizeLyrics(AppSettings settings)
+ {
+ settings.Lyrics ??= new YMhut.Box.Core.Music.MusicLyricsSettings();
+ settings.Lyrics.FontScale = Math.Clamp(settings.Lyrics.FontScale, 0.75, 1.75);
+ settings.Lyrics.LineSpacing = Math.Clamp(settings.Lyrics.LineSpacing, 4, 32);
+ if ((settings.Lyrics.ActiveWordColor & unchecked((int)0xFF000000)) == 0)
+ {
+ settings.Lyrics.ActiveWordColor = unchecked((int)0xFF5E6D44);
+ }
+ }
+
+ private static void NormalizeToolUiStates(AppSettings settings)
+ {
+ var normalized = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var entry in settings.ToolUiStates ?? [])
+ {
+ if (string.IsNullOrWhiteSpace(entry.Key) || entry.Value is null)
+ {
+ continue;
+ }
+
+ normalized[entry.Key] = new ToolUiPageState
+ {
+ Values = new Dictionary(entry.Value.Values ?? [], StringComparer.OrdinalIgnoreCase),
+ ExpandedSections = new Dictionary(entry.Value.ExpandedSections ?? [], StringComparer.OrdinalIgnoreCase),
+ UpdatedAt = entry.Value.UpdatedAt
+ };
+ }
+
+ settings.ToolUiStates = normalized;
+ }
+
public async Task RecordRecentToolAsync(string toolId, CancellationToken cancellationToken = default)
{
var settings = await LoadAsync(cancellationToken);
diff --git a/src/YMhut.Box.Core/Settings/ISettingsService.cs b/src/YMhut.Box.Core/Settings/ISettingsService.cs
index fedf0ee..5c2630f 100644
--- a/src/YMhut.Box.Core/Settings/ISettingsService.cs
+++ b/src/YMhut.Box.Core/Settings/ISettingsService.cs
@@ -16,6 +16,13 @@ public interface ISettingsService : INotifyPropertyChanged
Task RecordRecentToolAsync(string toolId, CancellationToken cancellationToken = default);
+ ToolUiPageState GetToolUiState(string toolId);
+
+ Task SaveToolUiStateAsync(
+ string toolId,
+ ToolUiPageState state,
+ CancellationToken cancellationToken = default);
+
Task SaveWindowBoundsAsync(
double x,
double y,
diff --git a/src/YMhut.Box.Core/Tools/ToolUiDefinition.cs b/src/YMhut.Box.Core/Tools/ToolUiDefinition.cs
new file mode 100644
index 0000000..377bcba
--- /dev/null
+++ b/src/YMhut.Box.Core/Tools/ToolUiDefinition.cs
@@ -0,0 +1,307 @@
+using System.Security.Cryptography;
+using System.Buffers.Binary;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using YMhut.Box.Core.Data;
+using YMhut.Box.Core.Logging;
+
+namespace YMhut.Box.Core.Tools;
+
+public enum ToolUiControlKind
+{
+ Text,
+ MultilineText,
+ Query,
+ ComboBox,
+ Toggle,
+ Number,
+ Date,
+ FilePicker,
+ Button,
+ Output,
+ WebView,
+ ExternalSurface
+}
+
+public sealed record ToolUiText(string Zh, string En)
+{
+ public string ForLanguage(string? language)
+ => string.Equals(language, "en-US", StringComparison.OrdinalIgnoreCase) ? En : Zh;
+}
+
+public sealed record ToolUiValidation(
+ string? Pattern = null,
+ double? Minimum = null,
+ double? Maximum = null,
+ int? MaxLength = null,
+ ToolUiText? Error = null);
+
+public sealed record ToolUiControl(
+ string Id,
+ ToolUiControlKind Kind,
+ ToolUiText Label,
+ string? Placeholder = null,
+ string? DefaultValue = null,
+ bool Required = false,
+ bool IsReadOnly = false,
+ ToolUiValidation? Validation = null);
+
+public sealed record ToolUiSection(
+ string Id,
+ ToolUiText Title,
+ ToolUiText Description,
+ IReadOnlyList Controls,
+ bool IsAdvanced = false,
+ string Layout = "stack");
+
+public sealed record ToolUiAction(
+ string Id,
+ ToolUiText Label,
+ bool RequiresConfirmation = false,
+ bool IsEnabled = true);
+
+public sealed record ToolUiResultDefinition(
+ string Kind,
+ ToolUiText Title,
+ bool IsScrollable = true,
+ bool IsCopyable = true);
+
+public sealed record ToolUiDefinition(
+ string ToolId,
+ int SchemaVersion,
+ ToolUiText Title,
+ ToolUiText Description,
+ string RiskLevel,
+ IReadOnlyList Capabilities,
+ IReadOnlyList Sections,
+ IReadOnlyList Actions,
+ ToolUiResultDefinition Result,
+ bool IsFallback = false,
+ string? Diagnostic = null,
+ ToolUiText? ResponsiveHint = null,
+ bool AdvancedExpandedByDefault = false);
+
+public interface IToolUiDefinitionStore
+{
+ Task GetAsync(IToolModule module, CancellationToken cancellationToken = default);
+
+ ToolUiDefinition GetFallback(IToolModule module, string? diagnostic = null);
+}
+
+///
+/// Loads one immutable UI definition per tool. Definitions are data, while the
+/// WinUI layer only owns safe rendering primitives and tool execution.
+///
+public sealed class ToolUiDefinitionStore(
+ IReferenceDataService referenceDataService,
+ ILogService? logService = null) : IToolUiDefinitionStore
+{
+ private const string Magic = "YMHUTUI1";
+ private static readonly HashSet AllowedActions = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "run", "execute", "query", "format", "convert", "generate", "process",
+ "chooseFile", "choose-file", "clear", "copyResult", "copy-result", "reload", "cancel"
+ };
+ private readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase);
+ private readonly object _sync = new();
+
+ public async Task GetAsync(IToolModule module, CancellationToken cancellationToken = default)
+ {
+ if (module is null) throw new ArgumentNullException(nameof(module));
+ lock (_sync)
+ {
+ if (_cache.TryGetValue(module.Id, out var cached)) return cached;
+ }
+
+ var definitionId = module switch
+ {
+ ExternalToolModule => "external-tool-host",
+ _ when module.Metadata.Category == ToolCategory.Plugin => "plugin-host",
+ _ => module.Id
+ };
+ var path = $"data/tool-ui/{definitionId}.dat";
+ try
+ {
+ var resolution = referenceDataService.Resolve(path);
+ if (!resolution.Exists || string.IsNullOrWhiteSpace(resolution.FullPath) || !File.Exists(resolution.FullPath))
+ {
+ return Cache(module.Id, GetFallback(module, $"Missing UI definition: {path}"));
+ }
+
+ var bytes = await File.ReadAllBytesAsync(resolution.FullPath, cancellationToken).ConfigureAwait(false);
+ var definition = ToolUiDefinitionCodec.Decode(bytes);
+ if (!string.Equals(definitionId, module.Id, StringComparison.OrdinalIgnoreCase))
+ {
+ definition = definition with
+ {
+ ToolId = module.Id,
+ Title = new ToolUiText(module.Metadata.Name, module.Metadata.Name),
+ Description = new ToolUiText(module.Metadata.Description, module.Metadata.Description)
+ };
+ }
+ Validate(module, definition);
+ return Cache(module.Id, definition);
+ }
+ catch (Exception exception) when (exception is IOException or InvalidDataException or JsonException or CryptographicException or InvalidOperationException)
+ {
+ if (logService is not null)
+ {
+ await logService.WriteAsync("Warning", "tool-ui", "Tool UI definition was rejected", $"{module.Id}; {exception.Message}").ConfigureAwait(false);
+ }
+
+ return Cache(module.Id, GetFallback(module, exception.Message));
+ }
+ }
+
+ public ToolUiDefinition GetFallback(IToolModule module, string? diagnostic = null)
+ {
+ var spec = ToolPageSpecCatalog.For(module);
+ var risky = module.Metadata.Category is ToolCategory.Security or ToolCategory.System;
+ var primary = spec.PrimaryInput switch
+ {
+ ToolPrimaryInputKind.MultilineText => ToolUiControlKind.MultilineText,
+ ToolPrimaryInputKind.Query or ToolPrimaryInputKind.Url or ToolPrimaryInputKind.Token => ToolUiControlKind.Query,
+ ToolPrimaryInputKind.Number or ToolPrimaryInputKind.NumberPair or ToolPrimaryInputKind.NumberTriple or ToolPrimaryInputKind.NumberQuad => ToolUiControlKind.Number,
+ ToolPrimaryInputKind.FilePath => ToolUiControlKind.FilePicker,
+ ToolPrimaryInputKind.None => ToolUiControlKind.Output,
+ _ => ToolUiControlKind.Text
+ };
+ var title = new ToolUiText(module.Metadata.Name, module.Metadata.Name);
+ var description = new ToolUiText(module.Metadata.Description, module.Metadata.Description);
+ return new ToolUiDefinition(
+ module.Id,
+ 1,
+ title,
+ description,
+ risky ? "high" : "normal",
+ [module.Metadata.OfflineCapable ? "offline" : "network"],
+ [new ToolUiSection(
+ "safe-input",
+ new ToolUiText("安全输入", "Safe input"),
+ new ToolUiText("页面定义不可用时的最小安全入口。", "Minimal safe entry when the page definition is unavailable."),
+ [new ToolUiControl("primary", primary, new ToolUiText("输入", "Input"), Required: false)])],
+ [new ToolUiAction("run", new ToolUiText("执行", "Run"), risky, IsEnabled: string.IsNullOrWhiteSpace(diagnostic))],
+ new ToolUiResultDefinition("status", new ToolUiText("结果", "Result")),
+ IsFallback: true,
+ Diagnostic: diagnostic);
+ }
+
+ private ToolUiDefinition Cache(string id, ToolUiDefinition definition)
+ {
+ lock (_sync) _cache[id] = definition;
+ return definition;
+ }
+
+ private static void Validate(IToolModule module, ToolUiDefinition definition)
+ {
+ if (!string.Equals(module.Id, definition.ToolId, StringComparison.OrdinalIgnoreCase))
+ throw new InvalidDataException("Tool UI definition ID does not match the requested tool.");
+ if (definition.SchemaVersion != 1)
+ throw new InvalidDataException($"Unsupported tool UI schema version: {definition.SchemaVersion}");
+ if (definition.Sections.Count == 0 || definition.Actions.Count == 0)
+ throw new InvalidDataException("Tool UI definition must contain sections and actions.");
+ if (definition.Sections.SelectMany(section => section.Controls).Any(control => string.IsNullOrWhiteSpace(control.Id)))
+ throw new InvalidDataException("Tool UI controls must have IDs.");
+ if (definition.Sections.SelectMany(section => section.Controls).GroupBy(control => control.Id, StringComparer.OrdinalIgnoreCase).Any(group => group.Count() > 1))
+ throw new InvalidDataException("Tool UI control IDs must be unique within a definition.");
+ if (definition.Sections.Any(section => section.Layout is not ("stack" or "grid" or "columns")))
+ throw new InvalidDataException("Tool UI section layout is not supported.");
+ if (definition.Actions.Any(action => string.IsNullOrWhiteSpace(action.Id)))
+ throw new InvalidDataException("Tool UI actions must have IDs.");
+ if (definition.Actions.Any(action => !AllowedActions.Contains(action.Id)))
+ throw new InvalidDataException("Tool UI definition requests an unsupported action capability.");
+ if (definition.Actions.GroupBy(action => action.Id, StringComparer.OrdinalIgnoreCase).Any(group => group.Count() > 1))
+ throw new InvalidDataException("Tool UI action IDs must be unique within a definition.");
+ }
+}
+
+public static class ToolUiDefinitionCodec
+{
+ private const string Magic = "YMHUTUI1";
+ private static readonly byte[] Key = SHA256.HashData(Encoding.UTF8.GetBytes("YMhut.Box.ToolUiDefinition.v1"));
+ private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();
+
+ private static JsonSerializerOptions CreateJsonOptions()
+ {
+ var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
+ options.Converters.Add(new JsonStringEnumConverter());
+ return options;
+ }
+
+ public static byte[] Encode(ToolUiDefinition definition, bool encrypt = true)
+ {
+ var json = JsonSerializer.SerializeToUtf8Bytes(definition, JsonOptions);
+ if (!encrypt) return json;
+
+ using var aes = Aes.Create();
+ aes.Key = Key;
+ aes.GenerateIV();
+ aes.Mode = CipherMode.CBC;
+ aes.Padding = PaddingMode.PKCS7;
+ using var encryptor = aes.CreateEncryptor();
+ var cipher = encryptor.TransformFinalBlock(json, 0, json.Length);
+ var toolId = Encoding.UTF8.GetBytes(definition.ToolId);
+ if (toolId.Length is 0 or > 512) throw new InvalidDataException("Tool UI definition ID length is invalid.");
+ var digest = SHA256.HashData(json);
+ var output = new byte[Magic.Length + 1 + sizeof(ushort) + toolId.Length + digest.Length + aes.IV.Length + cipher.Length];
+ Encoding.ASCII.GetBytes(Magic).CopyTo(output, 0);
+ output[Magic.Length] = 2;
+ BinaryPrimitives.WriteUInt16LittleEndian(output.AsSpan(Magic.Length + 1, sizeof(ushort)), checked((ushort)toolId.Length));
+ var offset = Magic.Length + 1 + sizeof(ushort);
+ toolId.CopyTo(output, offset);
+ offset += toolId.Length;
+ digest.CopyTo(output, offset);
+ offset += digest.Length;
+ aes.IV.CopyTo(output, offset);
+ offset += aes.IV.Length;
+ cipher.CopyTo(output, offset);
+ return output;
+ }
+
+ public static ToolUiDefinition Decode(byte[] bytes)
+ {
+ if (bytes is null || bytes.Length == 0) throw new InvalidDataException("Tool UI definition is empty.");
+ ReadOnlySpan payload = bytes;
+ string? envelopeToolId = null;
+ byte[]? expectedDigest = null;
+ if (bytes.Length >= Magic.Length + 1 + 16 && Encoding.ASCII.GetString(bytes, 0, Magic.Length) == Magic)
+ {
+ var version = bytes[Magic.Length];
+ var offset = Magic.Length + 1;
+ if (version == 2)
+ {
+ if (bytes.Length < offset + sizeof(ushort)) throw new InvalidDataException("Tool UI v2 envelope is truncated.");
+ var toolIdLength = BinaryPrimitives.ReadUInt16LittleEndian(bytes.AsSpan(offset, sizeof(ushort)));
+ offset += sizeof(ushort);
+ if (toolIdLength is 0 or > 512 || bytes.Length < offset + toolIdLength + 32 + 16 + 1)
+ throw new InvalidDataException("Tool UI v2 envelope has invalid lengths.");
+ envelopeToolId = Encoding.UTF8.GetString(bytes, offset, toolIdLength);
+ offset += toolIdLength;
+ expectedDigest = bytes.AsSpan(offset, 32).ToArray();
+ offset += 32;
+ }
+ else if (version != 1)
+ {
+ throw new InvalidDataException("Unsupported tool UI payload version.");
+ }
+
+ using var aes = Aes.Create();
+ aes.Key = Key;
+ aes.IV = bytes.AsSpan(offset, 16).ToArray();
+ aes.Mode = CipherMode.CBC;
+ aes.Padding = PaddingMode.PKCS7;
+ using var decryptor = aes.CreateDecryptor();
+ offset += 16;
+ payload = decryptor.TransformFinalBlock(bytes, offset, bytes.Length - offset);
+ if (expectedDigest is not null && !CryptographicOperations.FixedTimeEquals(expectedDigest, SHA256.HashData(payload)))
+ throw new InvalidDataException("Tool UI definition digest does not match the encrypted payload.");
+ }
+
+ var definition = JsonSerializer.Deserialize(payload, JsonOptions);
+ if (definition is null) throw new InvalidDataException("Tool UI definition JSON is invalid.");
+ if (envelopeToolId is not null && !string.Equals(envelopeToolId, definition.ToolId, StringComparison.OrdinalIgnoreCase))
+ throw new InvalidDataException("Tool UI envelope ID does not match its definition.");
+ return definition;
+ }
+}
diff --git a/src/YMhut.Box.InstallerBootstrap/InstallerWindow.cs b/src/YMhut.Box.InstallerBootstrap/InstallerWindow.cs
index 1a6b367..6a3a30f 100644
--- a/src/YMhut.Box.InstallerBootstrap/InstallerWindow.cs
+++ b/src/YMhut.Box.InstallerBootstrap/InstallerWindow.cs
@@ -3,6 +3,7 @@ using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
@@ -28,6 +29,11 @@ public sealed class InstallerWindow : Window
private static readonly SolidColorBrush WarningBrush = Brush("#FF9A6700");
private static readonly SolidColorBrush ErrorBrush = Brush("#FFB42318");
private static readonly SolidColorBrush WhiteBrush = Brush("#FFFFFF");
+ private static readonly SolidColorBrush DisabledSurfaceBrush = Brush("#FFE7E4DE");
+ private static readonly SolidColorBrush DisabledTextBrush = Brush("#FFA7A39B");
+ private static readonly SolidColorBrush HoverSurfaceBrush = Brush("#FFECE9E3");
+ private static readonly SolidColorBrush PressedSurfaceBrush = Brush("#FFDDD9D1");
+ private static readonly SolidColorBrush FocusStrokeBrush = Brush("#FF6B675F");
private static readonly FontFamily ContentFont = new("MiSans VF, Microsoft YaHei UI, Segoe UI");
private readonly ContentControl _pageHost = new();
private readonly Border _titleDragArea = new();
@@ -41,7 +47,7 @@ public sealed class InstallerWindow : Window
private readonly Button _cancelButton;
private readonly TextBox _directory = new();
private readonly TextBlock _diskSpace = Text("正在检查磁盘空间", 12, foreground: SecondaryBrush);
- private readonly CheckBox _licenseAccepted = new() { Content = "我已阅读并接受 GPL-3.0 许可条款" };
+ private readonly CheckBox _licenseAccepted = new() { Content = "我已阅读并同意《YMhut Box 软件许可与服务协议》" };
private readonly CheckBox _desktopShortcut = new() { Content = "创建桌面快捷方式", IsChecked = true };
private readonly CheckBox _startMenuShortcut = new() { Content = "创建开始菜单快捷方式", IsChecked = true };
private readonly CheckBox _autoStart = new() { Content = "登录 Windows 后自动启动" };
@@ -363,14 +369,7 @@ public sealed class InstallerWindow : Window
Background = SurfaceBrush,
BorderBrush = StrokeBrush,
BorderThickness = new Thickness(1),
- Child = new ScrollViewer
- {
- VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
- HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
- HorizontalContentAlignment = HorizontalAlignment.Stretch,
- Padding = new Thickness(0, 0, 10, 0),
- Content = document
- }
+ Child = PageScroll(document, new Thickness(0, 0, 10, 0), "许可协议")
};
_licenseAccepted.Content = "我已阅读并同意《YMhut Box 软件许可与服务协议》";
_licenseAccepted.HorizontalAlignment = HorizontalAlignment.Left;
@@ -436,16 +435,7 @@ public sealed class InstallerWindow : Window
private UIElement EnvironmentCheckPage()
{
_preflightPanel.HorizontalAlignment = HorizontalAlignment.Stretch;
- return new ScrollViewer
- {
- HorizontalAlignment = HorizontalAlignment.Stretch,
- VerticalAlignment = VerticalAlignment.Stretch,
- HorizontalContentAlignment = HorizontalAlignment.Stretch,
- VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
- HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
- Padding = new Thickness(0, 0, 8, 0),
- Content = _preflightPanel
- };
+ return PageScroll(_preflightPanel, new Thickness(0, 0, 8, 0), "环境检查");
}
private void BeginEnvironmentCheck()
@@ -601,16 +591,7 @@ public sealed class InstallerWindow : Window
UpdateWorkspaceLayout(800);
workspace.SizeChanged += (_, args) => UpdateWorkspaceLayout(args.NewSize.Width);
- var workspaceScroll = new ScrollViewer
- {
- HorizontalAlignment = HorizontalAlignment.Stretch,
- VerticalAlignment = VerticalAlignment.Stretch,
- HorizontalContentAlignment = HorizontalAlignment.Stretch,
- VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
- HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
- Padding = new Thickness(0, 0, 8, 0),
- Content = workspace
- };
+ var workspaceScroll = PageScroll(workspace, new Thickness(0, 0, 8, 0), "安装进度");
var page = new Grid
{
@@ -760,6 +741,9 @@ public sealed class InstallerWindow : Window
RefreshCompletePage();
}
await Task.Delay(20);
+ var pageScroll = FindDescendant(_pageHost)
+ ?? throw new InvalidOperationException($"Installer page {step} does not expose a scroll surface.");
+ pageScroll.ChangeView(null, Math.Min(24, pageScroll.ScrollableHeight), null, disableAnimation: true);
}
await Task.Delay(100);
return InstallerDiagnostics.ErrorCount == initialErrorCount;
@@ -1176,11 +1160,53 @@ public sealed class InstallerWindow : Window
return new DiskSpaceResult(new DriveInfo(root!).AvailableFreeSpace);
}
- private static StackPanel PageStack(params UIElement[] children)
+ private static ScrollViewer PageStack(params UIElement[] children)
{
var panel = new StackPanel { Spacing = 16, HorizontalAlignment = HorizontalAlignment.Stretch };
foreach (var child in children) panel.Children.Add(child);
- return panel;
+ return PageScroll(panel, new Thickness(0, 0, 8, 0), "安装步骤");
+ }
+
+ private static ScrollViewer PageScroll(UIElement content, Thickness padding, string automationName)
+ {
+ var scroll = new ScrollViewer
+ {
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ VerticalAlignment = VerticalAlignment.Stretch,
+ HorizontalContentAlignment = HorizontalAlignment.Stretch,
+ VerticalContentAlignment = VerticalAlignment.Top,
+ VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
+ HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
+ VerticalScrollMode = ScrollMode.Enabled,
+ HorizontalScrollMode = ScrollMode.Disabled,
+ IsTabStop = true,
+ Padding = padding,
+ Content = content
+ };
+ AutomationProperties.SetName(scroll, automationName);
+ scroll.PointerWheelChanged += (_, args) =>
+ {
+ if (args.Handled || scroll.ScrollableHeight <= 0)
+ {
+ return;
+ }
+
+ var delta = args.GetCurrentPoint(scroll).Properties.MouseWheelDelta;
+ if (delta == 0)
+ {
+ return;
+ }
+
+ var target = Math.Clamp(scroll.VerticalOffset - delta, 0, scroll.ScrollableHeight);
+ if (Math.Abs(target - scroll.VerticalOffset) < 0.5)
+ {
+ return;
+ }
+
+ scroll.ChangeView(null, target, null, disableAnimation: true);
+ args.Handled = true;
+ };
+ return scroll;
}
private static Border PreflightBand(PreflightItem item)
@@ -1263,12 +1289,16 @@ public sealed class InstallerWindow : Window
private Button CommandButton(string title, string glyph, Func action, bool primary = false)
{
+ var text = Text(title, 13, FontWeights.SemiBold, primary ? WhiteBrush : TextBrush);
+ var icon = new FontIcon { Glyph = glyph, FontSize = 13, Foreground = primary ? WhiteBrush : TextBrush };
var button = new Button
{
Padding = new Thickness(14, 8, 14, 8),
CornerRadius = new CornerRadius(6),
MinHeight = 38,
FontFamily = ContentFont,
+ IsTabStop = true,
+ UseSystemFocusVisuals = true,
Background = primary ? AccentBrush : SurfaceBrush,
Foreground = primary ? WhiteBrush : TextBrush,
BorderBrush = primary ? AccentBrush : StrokeBrush,
@@ -1276,9 +1306,23 @@ public sealed class InstallerWindow : Window
{
Orientation = Orientation.Horizontal,
Spacing = 7,
- Children = { Text(title, 13, FontWeights.SemiBold, primary ? WhiteBrush : TextBrush), new FontIcon { Glyph = glyph, FontSize = 13 } }
+ Children = { text, icon }
}
};
+ AutomationProperties.SetName(button, title);
+ button.Resources["ButtonBackground"] = primary ? AccentBrush : SurfaceBrush;
+ button.Resources["ButtonBackgroundPointerOver"] = HoverSurfaceBrush;
+ button.Resources["ButtonBackgroundPressed"] = PressedSurfaceBrush;
+ button.Resources["ButtonBackgroundDisabled"] = DisabledSurfaceBrush;
+ button.Resources["ButtonForegroundDisabled"] = DisabledTextBrush;
+ button.Resources["ButtonBorderBrushFocused"] = FocusStrokeBrush;
+ button.Resources["ButtonBorderBrushDisabled"] = DisabledSurfaceBrush;
+ button.IsEnabledChanged += (_, _) =>
+ {
+ var foreground = button.IsEnabled ? (primary ? WhiteBrush : TextBrush) : DisabledTextBrush;
+ text.Foreground = foreground;
+ icon.Foreground = foreground;
+ };
button.Click += async (_, _) => await ExecuteCommandAsync(action);
return button;
}
diff --git a/src/YMhut.Box.InstallerBootstrap/YMhut.Box.InstallerBootstrap.csproj b/src/YMhut.Box.InstallerBootstrap/YMhut.Box.InstallerBootstrap.csproj
index bd26077..f6d8818 100644
--- a/src/YMhut.Box.InstallerBootstrap/YMhut.Box.InstallerBootstrap.csproj
+++ b/src/YMhut.Box.InstallerBootstrap/YMhut.Box.InstallerBootstrap.csproj
@@ -21,6 +21,7 @@
YMhutBoxSetup
YMhut.Box.InstallerBootstrap
..\box-winUI\Assets\app_icon.ico
+ app.manifest
diff --git a/src/YMhut.Box.InstallerBootstrap/app.manifest b/src/YMhut.Box.InstallerBootstrap/app.manifest
new file mode 100644
index 0000000..fe28872
--- /dev/null
+++ b/src/YMhut.Box.InstallerBootstrap/app.manifest
@@ -0,0 +1,10 @@
+
+
+
+
+
+ true/pm
+ PerMonitorV2
+
+
+
diff --git a/src/YMhut.Box.Tests/AppSettingsStoreTests.cs b/src/YMhut.Box.Tests/AppSettingsStoreTests.cs
index 0b5cb74..a3254c2 100644
--- a/src/YMhut.Box.Tests/AppSettingsStoreTests.cs
+++ b/src/YMhut.Box.Tests/AppSettingsStoreTests.cs
@@ -36,6 +36,35 @@ public sealed class AppSettingsStoreTests
settings.RecentToolIds);
}
+ [TestMethod]
+ public async Task ToolUiStateIsPersistedPerToolWithoutChangingTheDefinitionPayload()
+ {
+ var root = Path.Combine(Path.GetTempPath(), "ymhut-box-tests", Guid.NewGuid().ToString("N"));
+ var service = new AppSettingsService(new AppSettingsStore(root));
+ await service.LoadAsync();
+
+ await service.SaveToolUiStateAsync("json_formatter", new ToolUiPageState
+ {
+ Values = new Dictionary { ["input"] = "{\"value\":1}" },
+ ExpandedSections = new Dictionary { ["advanced"] = true }
+ });
+ await service.SaveToolUiStateAsync("safe_browser", new ToolUiPageState
+ {
+ Values = new Dictionary { ["input"] = "https://example.com" }
+ });
+
+ var reloadedService = new AppSettingsService(new AppSettingsStore(root));
+ await reloadedService.LoadAsync();
+ var formatter = reloadedService.GetToolUiState("json_formatter");
+ var browser = reloadedService.GetToolUiState("safe_browser");
+
+ Assert.AreEqual("{\"value\":1}", formatter.Values["input"]);
+ Assert.IsTrue(formatter.ExpandedSections["advanced"]);
+ Assert.AreEqual("https://example.com", browser.Values["input"]);
+ Assert.IsFalse(browser.Values.ContainsKey("advanced"));
+ Assert.IsFalse(reloadedService.GetToolUiState("missing").Values.Any());
+ }
+
[TestMethod]
public async Task LoadAsyncImportsLegacySettingsOnce()
{
diff --git a/src/YMhut.Box.Tests/DevTerminalAndSerialTests.cs b/src/YMhut.Box.Tests/DevTerminalAndSerialTests.cs
index 60f60bd..d238c53 100644
--- a/src/YMhut.Box.Tests/DevTerminalAndSerialTests.cs
+++ b/src/YMhut.Box.Tests/DevTerminalAndSerialTests.cs
@@ -33,6 +33,7 @@ public sealed class DevTerminalAndSerialTests
Assert.IsTrue(snapshot.IsWindowsTerminalDefault);
Assert.IsTrue(snapshot.IsPowerShellDefaultProfile);
Assert.IsTrue(snapshot.IsAdministrator);
+ Assert.IsFalse(platform.RunCalls.Any(call => call.Contains("wt.exe --version", StringComparison.OrdinalIgnoreCase)));
}
[TestMethod]
@@ -194,6 +195,8 @@ public sealed class DevTerminalAndSerialTests
public bool PowerShellDefaultProfile { get; set; }
+ public string TerminalVersion { get; set; } = "Windows Terminal 1.23.0";
+
public bool TerminalFallbackResult { get; set; } = true;
public bool PowerShellFallbackResult { get; set; } = true;
@@ -210,6 +213,12 @@ public sealed class DevTerminalAndSerialTests
return Task.FromResult(Commands.TryGetValue(command, out var path) ? path : string.Empty);
}
+ public Task GetWindowsTerminalVersionAsync(CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return Task.FromResult(TerminalVersion);
+ }
+
public Task RunAsync(
string fileName,
string arguments,
@@ -235,7 +244,7 @@ public sealed class DevTerminalAndSerialTests
}
if (fileName.EndsWith("wt.exe", StringComparison.OrdinalIgnoreCase))
{
- return Task.FromResult(new DevTerminalCommandResult(0, "Windows Terminal 1.23.0", string.Empty));
+ return Task.FromResult(new DevTerminalCommandResult(0, TerminalVersion, string.Empty));
}
if (fileName.EndsWith("pwsh.exe", StringComparison.OrdinalIgnoreCase))
{
diff --git a/src/YMhut.Box.Tests/KugouMusicProviderTests.cs b/src/YMhut.Box.Tests/KugouMusicProviderTests.cs
index 372b292..fbade58 100644
--- a/src/YMhut.Box.Tests/KugouMusicProviderTests.cs
+++ b/src/YMhut.Box.Tests/KugouMusicProviderTests.cs
@@ -363,8 +363,8 @@ public sealed class KugouMusicProviderTests
Assert.IsTrue(stream.Playable);
Assert.AreEqual(MusicPlaybackQuality.HiRes, stream.Quality);
Assert.AreEqual(new Uri("https://media.test/song.flac"), stream.Uri);
- CollectionAssert.AreEqual(new[] { "viper_tape", "high" }, qualities);
- CollectionAssert.AreEqual(new[] { "master-hash", "hires-hash" }, hashes);
+ CollectionAssert.AreEqual(new[] { "viper_tape", "viper_tape", "high" }, qualities);
+ CollectionAssert.AreEqual(new[] { "master-hash", "master-hash", "hires-hash" }, hashes);
StringAssert.Contains(stream.ProviderReason, "回退");
}
@@ -374,6 +374,7 @@ public sealed class KugouMusicProviderTests
CapturedRequest? privilegeRequest = null;
string? privilegeRouter = null;
string? playbackHash = null;
+ Dictionary? playbackQuery = null;
using var provider = new KugouMusicProvider(AccountStore(), new StubHttpHandler(async (request, _) =>
{
if (request.RequestUri!.AbsolutePath == "/v7/get_all_list") return JsonResponse(UserLists());
@@ -389,7 +390,8 @@ public sealed class KugouMusicProviderTests
}
if (request.RequestUri.AbsolutePath == "/v5/url")
{
- playbackHash = ParseQuery(request.RequestUri)["hash"];
+ playbackQuery = ParseQuery(request.RequestUri);
+ playbackHash = playbackQuery["hash"];
return JsonResponse("""
{"status":1,"data":[{"url":["http://media.test/song.flac","https://media.test/song.flac"],"bitrate":800}]}
""");
@@ -405,6 +407,10 @@ public sealed class KugouMusicProviderTests
StringAssert.Contains(privilegeRequest.Body, "\"qualities\"");
StringAssert.Contains(privilegeRequest.Body, "\"album_id\":17");
Assert.AreEqual("lossless-hash", playbackHash);
+ Assert.IsNotNull(playbackQuery);
+ Assert.AreEqual("1005", playbackQuery["appid"]);
+ Assert.AreEqual("151369488", playbackQuery["page_id"]);
+ Assert.AreEqual("2", playbackQuery["pid"]);
Assert.AreEqual(new Uri("https://media.test/song.flac"), stream.Uri);
Assert.AreEqual(800_000L, stream.Bitrate);
}
@@ -435,6 +441,67 @@ public sealed class KugouMusicProviderTests
Assert.IsTrue(stream.Playable);
}
+ [TestMethod]
+ public async Task PlaybackFallsBackToOfficialTrialWhenFullUrlIsUnavailable()
+ {
+ var freePartValues = new List();
+ using var provider = new KugouMusicProvider(AccountStore(), new StubHttpHandler((request, _) =>
+ {
+ if (request.RequestUri!.AbsolutePath == "/v7/get_all_list")
+ {
+ return Task.FromResult(JsonResponse(UserLists()));
+ }
+ if (request.RequestUri.AbsolutePath == "/v2/get_res_privilege/lite")
+ {
+ return Task.FromResult(JsonResponse("{\"status\":1}"));
+ }
+
+ var freePart = ParseQuery(request.RequestUri)["IsFreePart"];
+ freePartValues.Add(freePart);
+ return Task.FromResult(JsonResponse(freePart == "1"
+ ? "{\"status\":1,\"url\":[\"https://media.test/trial.mp3\"],\"bitRate\":128}"
+ : "{\"status\":1,\"url\":[]}"));
+ }));
+ await provider.InitializeAsync();
+
+ var stream = await provider.ResolveStreamAsync("HASH|1", MusicPlaybackQuality.Standard);
+
+ CollectionAssert.AreEqual(new[] { "0", "1" }, freePartValues);
+ Assert.IsTrue(stream.Playable);
+ Assert.IsTrue(stream.Trial);
+ Assert.AreEqual(new Uri("https://media.test/trial.mp3"), stream.Uri);
+ StringAssert.Contains(stream.ProviderReason, "试听");
+ }
+
+ [TestMethod]
+ public async Task PlaybackUpgradesTrustedKugouMediaAddressToHttps()
+ {
+ using var provider = new KugouMusicProvider(DeviceStore(), new StubHttpHandler((request, _) =>
+ Task.FromResult(JsonResponse(request.RequestUri!.AbsolutePath == "/v2/get_res_privilege/lite"
+ ? "{\"status\":1}"
+ : "{\"status\":1,\"url\":[\"http://fsmobile.kugou.com/audio/song.mp3\"],\"bitRate\":128,\"is_full_audio\":1}"))));
+
+ var stream = await provider.ResolveStreamAsync("HASH|1", MusicPlaybackQuality.Standard);
+
+ Assert.IsTrue(stream.Playable);
+ Assert.IsFalse(stream.Trial);
+ Assert.AreEqual(new Uri("https://fsmobile.kugou.com/audio/song.mp3"), stream.Uri);
+ }
+
+ [TestMethod]
+ public async Task PlaybackRejectsUntrustedHttpMediaAddress()
+ {
+ using var provider = new KugouMusicProvider(DeviceStore(), new StubHttpHandler((request, _) =>
+ Task.FromResult(JsonResponse(request.RequestUri!.AbsolutePath == "/v2/get_res_privilege/lite"
+ ? "{\"status\":1}"
+ : "{\"status\":1,\"url\":[\"http://media.example.test/song.mp3\"]}"))));
+
+ var stream = await provider.ResolveStreamAsync("HASH|1", MusicPlaybackQuality.Standard);
+
+ Assert.IsFalse(stream.Playable);
+ Assert.IsNull(stream.Uri);
+ }
+
[TestMethod]
public async Task FavoriteAndUnfavoriteUseLikedListAndRefreshedFileId()
{
diff --git a/src/YMhut.Box.Tests/MusicAndHotkeyTests.cs b/src/YMhut.Box.Tests/MusicAndHotkeyTests.cs
index 01068f6..7bf6c00 100644
--- a/src/YMhut.Box.Tests/MusicAndHotkeyTests.cs
+++ b/src/YMhut.Box.Tests/MusicAndHotkeyTests.cs
@@ -113,6 +113,45 @@ public sealed class MusicAndHotkeyTests
Assert.AreEqual(TimeSpan.FromMilliseconds(500), lines[0].Words[0].Duration);
}
+ [TestMethod]
+ public void LyricsParserAlignsTranslationAndRomanizationByTimestamp()
+ {
+ var lyrics = MusicLyricsParser.Parse(
+ "[00:01.00]原文",
+ "[00:01.00]translation",
+ "[00:01.00]romanized");
+
+ Assert.HasCount(1, lyrics.Lines);
+ Assert.AreEqual("translation", lyrics.Lines[0].Translation);
+ Assert.AreEqual("romanized", lyrics.Lines[0].Romanized);
+ }
+
+ [TestMethod]
+ public void KrcParserKeepsWordsTranslationAndRomanization()
+ {
+ var language = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(
+ "{\"content\":[{\"type\":1,\"lyricContent\":[[\"translation\"]]},{\"type\":0,\"lyricContent\":[[\"romanized\"]]}]}"));
+ var lyrics = MusicLyricsParser.ParseKrc($"[language:{language}]\n[1000,1200]<0,500,0>逐<500,700,0>字");
+
+ Assert.HasCount(1, lyrics.Lines);
+ Assert.AreEqual("逐字", lyrics.Lines[0].Text);
+ Assert.AreEqual("translation", lyrics.Lines[0].Translation);
+ Assert.AreEqual("romanized", lyrics.Lines[0].Romanized);
+ Assert.HasCount(2, lyrics.Lines[0].Words);
+ Assert.AreEqual(TimeSpan.FromMilliseconds(1500), lyrics.Lines[0].Words[1].Start);
+ }
+
+ [TestMethod]
+ public void LocalApiPostTimestampsAreStrictlyIncreasing()
+ {
+ var first = KugouLocalApiProxyHandler.NextTimestamp();
+ var second = KugouLocalApiProxyHandler.NextTimestamp();
+ var third = KugouLocalApiProxyHandler.NextTimestamp();
+
+ Assert.IsGreaterThan(first, second);
+ Assert.IsGreaterThan(second, third);
+ }
+
[TestMethod]
public void HotkeyNormalizationRestoresDefaultsAndTrimsOverrides()
{
diff --git a/src/YMhut.Box.Tests/MusicSessionStoreTests.cs b/src/YMhut.Box.Tests/MusicSessionStoreTests.cs
index 548f7b3..b8b5e40 100644
--- a/src/YMhut.Box.Tests/MusicSessionStoreTests.cs
+++ b/src/YMhut.Box.Tests/MusicSessionStoreTests.cs
@@ -7,6 +7,35 @@ namespace YMhut.Box.Tests;
[TestClass]
public sealed class MusicSessionStoreTests
{
+ [TestMethod]
+ public async Task AccountSnapshotIsDpapiEncryptedAndRoundTripsEntitlements()
+ {
+ if (!OperatingSystem.IsWindows()) Assert.Inconclusive("DPAPI is Windows-only.");
+ var root = Path.Combine(Path.GetTempPath(), "ymhut-music-account-tests", Guid.NewGuid().ToString("N"));
+ try
+ {
+ var store = new DpapiMusicAccountSessionStore(new AppPaths(root));
+ var login = new MusicLoginState(true, "42", "Listener", VipLevel: "vip")
+ {
+ Entitlements = [new("vip", "VIP", "membership", true, DateTimeOffset.UtcNow.AddDays(10))],
+ RefreshedAt = DateTimeOffset.UtcNow
+ };
+ await store.SaveAsync(new MusicAccountSession(1, "netease", "MUSIC_U=secret-token", login, DateTimeOffset.UtcNow));
+
+ var restored = await store.LoadAsync("netease");
+ var encrypted = await File.ReadAllBytesAsync(Path.Combine(root, "Data", "Music", "Sessions", "netease.bin"));
+
+ Assert.IsNotNull(restored);
+ Assert.AreEqual("MUSIC_U=secret-token", restored.Credential);
+ Assert.HasCount(1, restored.LoginState.Entitlements);
+ Assert.IsFalse(System.Text.Encoding.UTF8.GetString(encrypted).Contains("secret-token", StringComparison.Ordinal));
+ }
+ finally
+ {
+ if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
+ }
+ }
+
[TestMethod]
public async Task ConcurrentSavesAreSerializedAndLeaveNoTemporaryFiles()
{
diff --git a/src/YMhut.Box.Tests/NeteaseMusicProviderTests.cs b/src/YMhut.Box.Tests/NeteaseMusicProviderTests.cs
index 38a2fee..1709dc2 100644
--- a/src/YMhut.Box.Tests/NeteaseMusicProviderTests.cs
+++ b/src/YMhut.Box.Tests/NeteaseMusicProviderTests.cs
@@ -9,6 +9,109 @@ namespace YMhut.Box.Tests;
[TestClass]
public sealed class NeteaseMusicProviderTests
{
+ [TestMethod]
+ public async Task FreshAccountSnapshotInitializesWithoutNetworkRequest()
+ {
+ var requests = 0;
+ var sessions = new MemoryAccountSessionStore(AccountSession(DateTimeOffset.UtcNow));
+ using var provider = new NeteaseMusicProvider(
+ new MemoryCredentialStore(),
+ handler: new StubHttpHandler(_ =>
+ {
+ requests++;
+ throw new InvalidOperationException("A fresh snapshot must not touch the network.");
+ }),
+ sessions: sessions);
+
+ await provider.InitializeAsync();
+
+ Assert.AreEqual(0, requests);
+ Assert.IsTrue(provider.LoginState.LoggedIn);
+ Assert.IsTrue(provider.LoginState.IsCached);
+ Assert.IsFalse(provider.LoginState.IsStale);
+ }
+
+ [TestMethod]
+ public async Task StaleAccountSnapshotRefreshesOnDemandAfterTwelveHours()
+ {
+ var requests = 0;
+ var sessions = new MemoryAccountSessionStore(AccountSession(DateTimeOffset.UtcNow.AddHours(-13)));
+ using var provider = new NeteaseMusicProvider(
+ new MemoryCredentialStore(),
+ handler: new StubHttpHandler(_ =>
+ {
+ requests++;
+ return Task.FromResult(JsonResponse("{\"code\":200,\"account\":{\"id\":42,\"userName\":\"Updated User\",\"vipType\":1},\"profile\":null}"));
+ }),
+ sessions: sessions);
+
+ await provider.InitializeAsync();
+ Assert.AreEqual(0, requests);
+ Assert.IsTrue(provider.LoginState.IsStale);
+
+ var refreshed = await provider.RefreshLoginStateAsync();
+
+ Assert.AreEqual(1, requests);
+ Assert.AreEqual("Updated User", refreshed.Nickname);
+ Assert.IsFalse(refreshed.IsCached);
+ Assert.IsFalse(refreshed.IsStale);
+ Assert.IsNotNull(sessions.Session);
+ Assert.AreEqual("Updated User", sessions.Session.LoginState.Nickname);
+ }
+
+ [TestMethod]
+ public async Task ConcurrentAccountRefreshesShareOneNetworkRequest()
+ {
+ var requests = 0;
+ var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var sessions = new MemoryAccountSessionStore(AccountSession(DateTimeOffset.UtcNow.AddHours(-13)));
+ using var provider = new NeteaseMusicProvider(
+ new MemoryCredentialStore(),
+ handler: new StubHttpHandler(async _ =>
+ {
+ requests++;
+ entered.TrySetResult();
+ await release.Task;
+ return JsonResponse("{\"code\":200,\"account\":{\"id\":42,\"userName\":\"Updated User\",\"vipType\":1},\"profile\":null}");
+ }),
+ sessions: sessions);
+ await provider.InitializeAsync();
+
+ var first = provider.RefreshLoginStateAsync();
+ await entered.Task;
+ var second = provider.RefreshLoginStateAsync();
+ release.TrySetResult();
+ await Task.WhenAll(first, second);
+
+ Assert.AreEqual(1, requests);
+ }
+
+ [TestMethod]
+ public async Task TransientAccountRefreshFailureIsThrottledAndKeepsSnapshot()
+ {
+ var requests = 0;
+ var sessions = new MemoryAccountSessionStore(AccountSession(DateTimeOffset.UtcNow.AddHours(-13)));
+ using var provider = new NeteaseMusicProvider(
+ new MemoryCredentialStore(),
+ handler: new StubHttpHandler(_ =>
+ {
+ requests++;
+ throw new HttpRequestException("offline");
+ }),
+ sessions: sessions);
+ await provider.InitializeAsync();
+
+ var first = await provider.RefreshLoginStateAsync();
+ var second = await provider.RefreshLoginStateAsync();
+
+ Assert.AreEqual(1, requests);
+ Assert.IsTrue(first.LoggedIn);
+ Assert.IsTrue(first.IsCached);
+ Assert.IsTrue(first.IsStale);
+ Assert.AreEqual(first, second);
+ }
+
[TestMethod]
public async Task SearchUsesConsistentDesktopWebHeaders()
{
@@ -300,6 +403,18 @@ public sealed class NeteaseMusicProviderTests
private static HttpResponseMessage JsonResponse(string json)
=> new(HttpStatusCode.OK) { Content = new StringContent(json, Encoding.UTF8, "application/json") };
+ private static MusicAccountSession AccountSession(DateTimeOffset refreshedAt)
+ => new(
+ DpapiMusicAccountSessionStore.CurrentVersion,
+ "netease",
+ "MUSIC_U=cached-token; __csrf=cached-csrf",
+ new MusicLoginState(true, "42", "Cached User", VipLevel: "vip")
+ {
+ RefreshedAt = refreshedAt,
+ Entitlements = [new("vip", "黑胶 VIP", "membership", true)]
+ },
+ refreshedAt);
+
private sealed class StubHttpHandler(Func> responseFactory) : HttpMessageHandler
{
protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
@@ -328,6 +443,26 @@ public sealed class NeteaseMusicProviderTests
}
}
+ private sealed class MemoryAccountSessionStore(MusicAccountSession? session) : IMusicAccountSessionStore
+ {
+ public MusicAccountSession? Session { get; private set; } = session;
+
+ public Task LoadAsync(string provider, CancellationToken cancellationToken = default)
+ => Task.FromResult(Session?.Provider.Equals(provider, StringComparison.OrdinalIgnoreCase) == true ? Session : null);
+
+ public Task SaveAsync(MusicAccountSession updated, CancellationToken cancellationToken = default)
+ {
+ Session = updated;
+ return Task.CompletedTask;
+ }
+
+ public Task ClearAsync(string provider, CancellationToken cancellationToken = default)
+ {
+ if (Session?.Provider.Equals(provider, StringComparison.OrdinalIgnoreCase) == true) Session = null;
+ return Task.CompletedTask;
+ }
+ }
+
private sealed record CapturedRequest(
HttpMethod Method,
Uri Uri,
diff --git a/src/YMhut.Box.Tests/ToolExecutorTests.cs b/src/YMhut.Box.Tests/ToolExecutorTests.cs
index 056e6b0..3fe2db5 100644
--- a/src/YMhut.Box.Tests/ToolExecutorTests.cs
+++ b/src/YMhut.Box.Tests/ToolExecutorTests.cs
@@ -741,6 +741,26 @@ public sealed class ToolExecutorTests
Assert.IsFalse(string.IsNullOrWhiteSpace(latest.GetProperty("release_notes").GetString()));
Assert.AreEqual(latestVersion, totalRoot.GetProperty("versions")[0].GetProperty("version").GetString());
Assert.IsFalse(string.IsNullOrWhiteSpace(totalRoot.GetProperty("versions")[0].GetProperty("summary").GetString()));
+
+ Assert.AreEqual("2.0.7.14", latestVersion);
+ Assert.AreEqual("2026-08-18", noticeRoot.GetProperty("release_date").GetString());
+ Assert.IsTrue(noticeRoot.GetProperty("download_url").GetString()!.Contains("2.0.7.14", StringComparison.Ordinal));
+ Assert.AreEqual("2.0.7.14", totalRoot.GetProperty("versions")[0].GetProperty("version").GetString());
+ Assert.AreEqual("2.0.7.13", totalRoot.GetProperty("versions")[1].GetProperty("version").GetString());
+ Assert.AreEqual("2026-08-17", JsonDocument.Parse(File.ReadAllText(Path.Combine(repoRoot, "update-notice", "2.0.7.13.json"))).RootElement.GetProperty("release_date").GetString());
+ Assert.IsTrue(File.ReadAllText(Path.Combine(repoRoot, "update-notice", "2.0.7.13.json")).Contains("MoeKoeMusic", StringComparison.Ordinal));
+ Assert.IsTrue(File.ReadAllText(Path.Combine(repoRoot, "update-notice", "2.0.7.14.json")).Contains("PerMonitorV2", StringComparison.Ordinal));
+
+ foreach (var manifestPath in new[]
+ {
+ Path.Combine(repoRoot, "src", "box-winUI", "app.manifest"),
+ Path.Combine(repoRoot, "src", "YMhut.Box.InstallerBootstrap", "app.manifest")
+ })
+ {
+ var manifest = File.ReadAllText(manifestPath);
+ StringAssert.Contains(manifest, "PerMonitorV2");
+ StringAssert.Contains(manifest, "true/pm");
+ }
}
[TestMethod]
diff --git a/src/YMhut.Box.Tests/ToolUiDefinitionTests.cs b/src/YMhut.Box.Tests/ToolUiDefinitionTests.cs
new file mode 100644
index 0000000..611882e
--- /dev/null
+++ b/src/YMhut.Box.Tests/ToolUiDefinitionTests.cs
@@ -0,0 +1,237 @@
+using YMhut.Box.Core.Data;
+using YMhut.Box.Core.Plugins;
+using YMhut.Box.Core.Tools;
+
+namespace YMhut.Box.Tests;
+
+[TestClass]
+public sealed class ToolUiDefinitionTests
+{
+ [TestMethod]
+ public void EncryptedPayloadRoundTripsDefinition()
+ {
+ var definition = new ToolUiDefinition(
+ "sample",
+ 1,
+ new ToolUiText("示例", "Sample"),
+ new ToolUiText("描述", "Description"),
+ "normal",
+ ["offline"],
+ [new ToolUiSection(
+ "input",
+ new ToolUiText("输入", "Input"),
+ new ToolUiText("说明", "Description"),
+ [new ToolUiControl("value", ToolUiControlKind.Text, new ToolUiText("值", "Value"))])],
+ [new ToolUiAction("run", new ToolUiText("执行", "Run"))],
+ new ToolUiResultDefinition("status", new ToolUiText("结果", "Result")));
+
+ var decoded = ToolUiDefinitionCodec.Decode(ToolUiDefinitionCodec.Encode(definition));
+
+ Assert.AreEqual(definition.ToolId, decoded.ToolId);
+ Assert.AreEqual(definition.Sections[0].Controls[0].Kind, decoded.Sections[0].Controls[0].Kind);
+ Assert.AreEqual(definition.Actions[0].Id, decoded.Actions[0].Id);
+ }
+
+ [TestMethod]
+ public void CodecAcceptsDevelopmentJsonPayload()
+ {
+ var definition = new ToolUiDefinition(
+ "json_tool",
+ 1,
+ new ToolUiText("JSON", "JSON"),
+ new ToolUiText("描述", "Description"),
+ "normal",
+ [],
+ [new ToolUiSection("input", new ToolUiText("输入", "Input"), new ToolUiText("", ""), [])],
+ [new ToolUiAction("run", new ToolUiText("执行", "Run"))],
+ new ToolUiResultDefinition("status", new ToolUiText("结果", "Result")));
+
+ var json = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(definition);
+ var decoded = ToolUiDefinitionCodec.Decode(json);
+
+ Assert.AreEqual("json_tool", decoded.ToolId);
+ }
+
+ [TestMethod]
+ public void GeneratedToolDefinitionPayloadIsReadable()
+ {
+ var path = Path.Combine(FindRepositoryRoot(), "assets", "data", "tool-ui", "json_formatter.dat");
+ Assert.IsTrue(File.Exists(path), "The generated tool UI payload is missing from the repository.");
+ var definition = ToolUiDefinitionCodec.Decode(File.ReadAllBytes(path));
+
+ Assert.AreEqual("json_formatter", definition.ToolId);
+ Assert.IsGreaterThanOrEqualTo(2, definition.Sections.Count);
+ Assert.IsTrue(definition.Sections.SelectMany(section => section.Controls).Any(control => control.Id == "rules"));
+ Assert.AreEqual("json-tree", definition.Result.Kind);
+ }
+
+ [TestMethod]
+ public void EveryGeneratedToolDefinitionHasAValidV2Envelope()
+ {
+ var folder = Path.Combine(FindRepositoryRoot(), "assets", "data", "tool-ui");
+ var files = Directory.GetFiles(folder, "*.dat", SearchOption.TopDirectoryOnly);
+
+ Assert.IsGreaterThanOrEqualTo(202, files.Length);
+ foreach (var file in files)
+ {
+ var bytes = File.ReadAllBytes(file);
+ Assert.IsGreaterThan(9, bytes.Length, file);
+ Assert.AreEqual(2, bytes[8], file);
+ var definition = ToolUiDefinitionCodec.Decode(bytes);
+ Assert.AreEqual(Path.GetFileNameWithoutExtension(file), definition.ToolId, true, file);
+ Assert.IsNotEmpty(definition.Sections, file);
+ Assert.IsNotEmpty(definition.Actions, file);
+ Assert.HasCount(
+ definition.Sections.SelectMany(section => section.Controls).Select(control => control.Id).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
+ definition.Sections.SelectMany(section => section.Controls),
+ file);
+ }
+ }
+
+ [TestMethod]
+ public void EncryptedPayloadRejectsTampering()
+ {
+ var definition = new ToolUiDefinition(
+ "tamper-test",
+ 1,
+ new ToolUiText("测试", "Test"),
+ new ToolUiText("摘要校验", "Digest validation"),
+ "normal",
+ [],
+ [new ToolUiSection("input", new ToolUiText("输入", "Input"), new ToolUiText("", ""),
+ [new ToolUiControl("primary", ToolUiControlKind.Text, new ToolUiText("值", "Value"))])],
+ [new ToolUiAction("run", new ToolUiText("执行", "Run"))],
+ new ToolUiResultDefinition("text", new ToolUiText("结果", "Result")));
+ var bytes = ToolUiDefinitionCodec.Encode(definition);
+ bytes[11 + "tamper-test".Length] ^= 0x01;
+
+ Assert.ThrowsExactly(() => ToolUiDefinitionCodec.Decode(bytes));
+ }
+
+ [TestMethod]
+ public async Task MissingDefinitionReturnsDisabledSafeFallback()
+ {
+ var module = new ToolModule(new ToolMetadata(
+ "missing-definition",
+ "Missing definition",
+ "Fallback test",
+ ToolCategory.Text,
+ [],
+ true,
+ "\uE8D2"));
+ var store = new ToolUiDefinitionStore(new MissingReferenceDataService());
+
+ var definition = await store.GetAsync(module);
+
+ Assert.IsTrue(definition.IsFallback);
+ Assert.IsNotNull(definition.Diagnostic);
+ Assert.IsFalse(definition.Actions.Single().IsEnabled);
+ }
+
+ [TestMethod]
+ public async Task PluginAndExternalModulesUseHostDefinitionsWithTheirOwnIds()
+ {
+ var root = FindRepositoryRoot();
+ var store = new ToolUiDefinitionStore(new RepositoryReferenceDataService(root));
+ var manifest = new PluginManifest(
+ "demo-plugin",
+ "Demo plugin",
+ "1.0.0",
+ "tester",
+ "Plugin test",
+ "index.html",
+ [],
+ [new PluginSurface(PluginSurfaceKind.ToolboxTool, "tool", "Demo tool", "Tool")],
+ []);
+ var plugin = new LoadedPlugin(
+ manifest,
+ root,
+ new PluginRuntimeState("demo-plugin", true, new HashSet(), new HashSet(), null),
+ []);
+ var pluginDefinition = await store.GetAsync(new PluginToolModule(plugin, manifest.Surfaces[0]));
+
+ var external = new ExternalToolModule(new ExternalToolItem(
+ "external:demo",
+ "Demo external",
+ "External test",
+ "test",
+ ToolCategory.Dev,
+ root,
+ "demo.exe",
+ "demo.exe",
+ ".exe",
+ "tester",
+ "1.0",
+ null,
+ null,
+ "x64",
+ [],
+ [],
+ ReferenceToolRiskLevel.None,
+ false));
+ var externalDefinition = await store.GetAsync(external);
+
+ Assert.IsFalse(pluginDefinition.IsFallback);
+ Assert.AreEqual("plugin:demo-plugin:tool", pluginDefinition.ToolId);
+ Assert.IsFalse(externalDefinition.IsFallback);
+ Assert.AreEqual("external:demo", externalDefinition.ToolId);
+ Assert.AreEqual("Demo external", externalDefinition.Title.Zh);
+ }
+
+ private static string FindRepositoryRoot()
+ {
+ var directory = new DirectoryInfo(AppContext.BaseDirectory);
+ while (directory is not null)
+ {
+ if (File.Exists(Path.Combine(directory.FullName, "YMhut.Box.Native.sln")))
+ {
+ return directory.FullName;
+ }
+
+ directory = directory.Parent;
+ }
+
+ throw new DirectoryNotFoundException("Repository root was not found from the test output directory.");
+ }
+
+ private sealed class MissingReferenceDataService : IReferenceDataService
+ {
+ public Task ReadTextAsync(string relativePath, CancellationToken cancellationToken = default)
+ => Task.FromResult(null);
+
+ public ReferenceDataResolution Resolve(string relativePath)
+ => new(relativePath, null, false, [relativePath]);
+
+ public Task ReadJsonAsync(string relativePath, CancellationToken cancellationToken = default)
+ => Task.FromResult(default(T));
+
+ public bool Exists(string relativePath) => false;
+
+ public IReadOnlyList EnumerateFiles(string relativeDirectory, string searchPattern = "*.*") => [];
+
+ public ReferenceDataValidationResult ValidateRequiredAssets(params string[] relativePaths)
+ => new([], relativePaths);
+ }
+
+ private sealed class RepositoryReferenceDataService(string root) : IReferenceDataService
+ {
+ public Task ReadTextAsync(string relativePath, CancellationToken cancellationToken = default)
+ => Task.FromResult(null);
+
+ public ReferenceDataResolution Resolve(string relativePath)
+ {
+ var fullPath = Path.Combine(root, "assets", relativePath.Replace('/', Path.DirectorySeparatorChar));
+ return new ReferenceDataResolution(relativePath, fullPath, File.Exists(fullPath), [fullPath]);
+ }
+
+ public Task ReadJsonAsync(string relativePath, CancellationToken cancellationToken = default)
+ => Task.FromResult(default(T));
+
+ public bool Exists(string relativePath) => Resolve(relativePath).Exists;
+
+ public IReadOnlyList EnumerateFiles(string relativeDirectory, string searchPattern = "*.*") => [];
+
+ public ReferenceDataValidationResult ValidateRequiredAssets(params string[] relativePaths)
+ => new([], relativePaths.Where(path => !Exists(path)).ToArray());
+ }
+}
diff --git a/src/box-winUI/AgreementWindow.cs b/src/box-winUI/AgreementWindow.cs
index 65aed19..0b58614 100644
--- a/src/box-winUI/AgreementWindow.cs
+++ b/src/box-winUI/AgreementWindow.cs
@@ -39,7 +39,7 @@ public sealed class AgreementWindow : Window
_acceptanceStore = acceptanceStore;
_appVersion = appVersion;
_accepted = accepted;
- _requestedTheme = ResolveElementTheme(settingsService.Current.Theme);
+ _requestedTheme = ThemeService.ResolveElementTheme(settingsService.Current.Theme);
Title = $"YMhut Box 使用声明 · {AgreementDocument.CurrentVersion}";
WindowIconService.ApplyAppIcon(this);
ModernUi.SetPalette(ShouldUseDarkPalette(settingsService.Current.Theme));
@@ -124,6 +124,7 @@ public sealed class AgreementWindow : Window
var root = new Grid
{
Background = ThemeBrush("ApplicationPageBackgroundThemeBrush", ModernUi.AppBackground),
+ RequestedTheme = _requestedTheme,
Padding = new Thickness(24),
RowSpacing = 14,
RowDefinitions =
@@ -388,22 +389,7 @@ public sealed class AgreementWindow : Window
return Math.Max(1, (int)Math.Round(dips * scale));
}
- private Brush ThemeBrush(string resourceKey, Brush fallback)
- {
- return Application.Current.Resources.TryGetValue(resourceKey, out var value) && value is Brush brush
- ? brush
- : fallback;
- }
-
- private static ElementTheme ResolveElementTheme(string? theme)
- {
- return (theme ?? string.Empty).Trim().ToLowerInvariant() switch
- {
- "light" => ElementTheme.Light,
- "dark" => ElementTheme.Dark,
- _ => ElementTheme.Default
- };
- }
+ private static Brush ThemeBrush(string resourceKey, Brush fallback) => fallback;
private static bool ShouldUseDarkPalette(string? theme)
{
diff --git a/src/box-winUI/App.xaml.cs b/src/box-winUI/App.xaml.cs
index cf3568c..0a57940 100644
--- a/src/box-winUI/App.xaml.cs
+++ b/src/box-winUI/App.xaml.cs
@@ -169,6 +169,9 @@ public partial class App : Application
}
Trace($"IndependentWindowHost:before-window:{options.Kind}");
+ ModernUi.SetPalette(
+ ThemeService.ShouldUseDarkPalette(settingsService.Current.Theme),
+ settingsService.Current.SeedColor);
CurrentWindow = options.Kind switch
{
IndependentWindowKind.Browser => new SafeBrowserWindow(options.InitialUrl),
diff --git a/src/box-winUI/Assets/LockScreenLogo.png b/src/box-winUI/Assets/LockScreenLogo.png
index f56993c..c0e57c6 100644
Binary files a/src/box-winUI/Assets/LockScreenLogo.png and b/src/box-winUI/Assets/LockScreenLogo.png differ
diff --git a/src/box-winUI/Assets/Square150x150Logo.png b/src/box-winUI/Assets/Square150x150Logo.png
index 1432f18..f482e4e 100644
Binary files a/src/box-winUI/Assets/Square150x150Logo.png and b/src/box-winUI/Assets/Square150x150Logo.png differ
diff --git a/src/box-winUI/Assets/Square44x44Logo.png b/src/box-winUI/Assets/Square44x44Logo.png
index a9c7f8f..3a37741 100644
Binary files a/src/box-winUI/Assets/Square44x44Logo.png and b/src/box-winUI/Assets/Square44x44Logo.png differ
diff --git a/src/box-winUI/Assets/StoreLogo.png b/src/box-winUI/Assets/StoreLogo.png
index 664681a..f1cdf82 100644
Binary files a/src/box-winUI/Assets/StoreLogo.png and b/src/box-winUI/Assets/StoreLogo.png differ
diff --git a/src/box-winUI/Assets/Wide310x150Logo.png b/src/box-winUI/Assets/Wide310x150Logo.png
index 6897305..6530c38 100644
Binary files a/src/box-winUI/Assets/Wide310x150Logo.png and b/src/box-winUI/Assets/Wide310x150Logo.png differ
diff --git a/src/box-winUI/MainWindow.xaml.cs b/src/box-winUI/MainWindow.xaml.cs
index 781bb3d..defcb6f 100644
--- a/src/box-winUI/MainWindow.xaml.cs
+++ b/src/box-winUI/MainWindow.xaml.cs
@@ -54,6 +54,7 @@ public sealed partial class MainWindow : Window, IShellNavigationHost
private readonly IShellDialogService _shellDialogService = AppServices.GetRequiredService();
private readonly IShellNotificationService _shellNotificationService = AppServices.GetRequiredService();
private readonly WebView2EnvironmentFactory _webViewEnvironmentFactory = AppServices.GetRequiredService();
+ private readonly MusicApiProcessService _musicApiProcessService = AppServices.GetRequiredService();
private readonly IToolPageFactory _toolPageFactory = ToolPageRegistry.Current;
private readonly ToolboxSurfacePageFactory _toolboxSurfacePageFactory = new();
private readonly PropertyChangedEventHandler _settingsChangedHandler;
@@ -121,7 +122,15 @@ public sealed partial class MainWindow : Window, IShellNavigationHost
public MainWindow()
{
StartupTrace.Write("MainWindow.ctor:start");
+ var initialSettings = _settingsService.Current;
+ ModernUi.SetPalette(ThemeService.ShouldUseDarkPalette(initialSettings.Theme), initialSettings.SeedColor);
BuildShellContent();
+ ThemeService.ApplyTheme(
+ this,
+ initialSettings.Theme,
+ initialSettings.HardwareAccelerationEnabled ? initialSettings.WindowBackdrop : "solid",
+ initialSettings.SeedColor);
+ ThemeService.ApplyFontScale(initialSettings.FontSize);
Title = "YMhut Box";
WindowIconService.ApplyAppIcon(this);
ExtendsContentIntoTitleBar = true;
@@ -187,6 +196,7 @@ public sealed partial class MainWindow : Window, IShellNavigationHost
_settingsService.PropertyChanged += _settingsChangedHandler;
RootLayout.Loaded += MainWindow_Loaded;
+ _ = _musicApiProcessService.StartAsync();
Closed += (_, _) =>
{
_isClosed = true;
@@ -200,6 +210,7 @@ public sealed partial class MainWindow : Window, IShellNavigationHost
_movePerformanceHelper?.Dispose();
_moveLightModeScope?.Dispose();
_pluginHost.Dispose();
+ _musicApiProcessService.Dispose();
if (_downloadManager is IDisposable disposableDownloadManager)
{
disposableDownloadManager.Dispose();
diff --git a/src/box-winUI/Services/AppServices.cs b/src/box-winUI/Services/AppServices.cs
index 7538bbf..09923df 100644
--- a/src/box-winUI/Services/AppServices.cs
+++ b/src/box-winUI/Services/AppServices.cs
@@ -95,11 +95,18 @@ public static class AppServices
provider.GetService()));
services.AddSingleton();
services.AddSingleton(provider => new DpapiMusicCredentialStore(provider.GetRequiredService()));
+ services.AddSingleton(provider => new DpapiMusicAccountSessionStore(provider.GetRequiredService()));
+ services.AddSingleton(provider => new MusicApiProcessService(provider.GetService()));
+ services.AddSingleton(provider => provider.GetRequiredService());
+ services.AddSingleton(provider => new KugouLocalApiProxyHandler(provider.GetRequiredService()));
services.AddSingleton(provider => new NeteaseMusicProvider(
provider.GetRequiredService(),
- provider.GetService()));
+ provider.GetService(),
+ sessions: provider.GetRequiredService()));
services.AddSingleton(provider => new KugouMusicProvider(
- provider.GetRequiredService()));
+ provider.GetRequiredService(),
+ provider.GetRequiredService(),
+ provider.GetRequiredService()));
services.AddSingleton(provider => new MusicProviderRegistry(
[
provider.GetRequiredService(),
@@ -117,6 +124,9 @@ public static class AppServices
provider.GetRequiredService(),
provider.GetService()));
services.AddSingleton();
+ services.AddSingleton(provider => new ToolUiDefinitionStore(
+ provider.GetRequiredService(),
+ provider.GetService()));
services.AddSingleton();
services.AddSingleton();
services.AddSingleton(provider => new TitleWeatherService(
diff --git a/src/box-winUI/Services/MusicApiProcessService.cs b/src/box-winUI/Services/MusicApiProcessService.cs
new file mode 100644
index 0000000..8a19b7c
--- /dev/null
+++ b/src/box-winUI/Services/MusicApiProcessService.cs
@@ -0,0 +1,346 @@
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Net.Http.Json;
+using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text.Json;
+using Microsoft.Win32.SafeHandles;
+using YMhut.Box.Core.App;
+using YMhut.Box.Core.Logging;
+using YMhut.Box.Core.Music;
+
+namespace YMhut.Box.WinUI.Services;
+
+public sealed class MusicApiProcessService(ILogService? logService = null) : ILocalMusicApiEndpoint, IDisposable
+{
+ private const int ProtocolVersion = 1;
+ private const string MoeKoeCommit = "4581660209c9cdd07669e8bd5bcbe136b7d91e02";
+ private static readonly TimeSpan StartTimeout = TimeSpan.FromSeconds(12);
+ private readonly SemaphoreSlim _startGate = new(1, 1);
+ private readonly Queue _restartAttempts = new();
+ private Process? _process;
+ private LocalMusicApiEndpoint? _endpoint;
+ private SafeJobHandle? _job;
+ private bool _disposed;
+
+ public bool IsAvailable => _endpoint is not null && _process is { HasExited: false };
+
+ public async Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ _ = await GetEndpointAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception exception)
+ {
+ await WriteLogAsync("Warning", "本地音乐 API 未能启动,酷狗来源暂不可用", exception.Message).ConfigureAwait(false);
+ }
+ }
+
+ public async Task GetEndpointAsync(CancellationToken cancellationToken = default)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ if (_endpoint is not null && _process is { HasExited: false }) return _endpoint;
+
+ await _startGate.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ if (_endpoint is not null && _process is { HasExited: false }) return _endpoint;
+ EnforceRestartLimit();
+ StopProcess(kill: true);
+ return await StartCoreAsync(cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ _startGate.Release();
+ }
+ }
+
+ private async Task StartCoreAsync(CancellationToken cancellationToken)
+ {
+ var payload = ResolvePayload();
+ var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = payload.Node,
+ Arguments = $"\"{payload.EntryPoint}\"",
+ WorkingDirectory = payload.ApiRoot,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ };
+ startInfo.Environment["YMHUT_INTERNAL_TOKEN"] = token;
+ startInfo.Environment["YMHUT_PARENT_PID"] = Environment.ProcessId.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ startInfo.Environment["YMHUT_QUIET"] = "1";
+ startInfo.Environment["HOST"] = "127.0.0.1";
+ startInfo.Environment["PORT"] = "0";
+ startInfo.Environment["platform"] = "lite";
+
+ var process = Process.Start(startInfo)
+ ?? throw new LocalMusicApiUnavailableException("无法创建本地音乐 API 子进程。");
+ _process = process;
+ try
+ {
+ AttachKillOnCloseJob(process);
+ var readyLine = await process.StandardOutput.ReadLineAsync(cancellationToken)
+ .AsTask()
+ .WaitAsync(StartTimeout, cancellationToken)
+ .ConfigureAwait(false);
+ if (string.IsNullOrWhiteSpace(readyLine))
+ {
+ throw new LocalMusicApiUnavailableException("本地音乐 API 未返回就绪消息。");
+ }
+
+ var ready = JsonSerializer.Deserialize(readyLine, new JsonSerializerOptions(JsonSerializerDefaults.Web));
+ if (ready is null || ready.Type != "ready" || ready.ProtocolVersion != ProtocolVersion ||
+ ready.Commit != MoeKoeCommit || ready.Pid != process.Id || ready.Port is <= 0 or > 65535)
+ {
+ throw new LocalMusicApiUnavailableException("本地音乐 API 协议握手失败。");
+ }
+
+ var endpoint = new LocalMusicApiEndpoint(
+ new Uri($"http://127.0.0.1:{ready.Port}/", UriKind.Absolute),
+ token,
+ ready.ProtocolVersion,
+ ready.Commit);
+ await VerifyHealthAsync(endpoint, cancellationToken).ConfigureAwait(false);
+ _endpoint = endpoint;
+ process.EnableRaisingEvents = true;
+ process.Exited += Process_Exited;
+ await WriteLogAsync("Information", "本地音乐 API 已启动", $"protocol={ProtocolVersion}; commit={MoeKoeCommit[..12]}").ConfigureAwait(false);
+ return endpoint;
+ }
+ catch
+ {
+ StopProcess(kill: true);
+ throw;
+ }
+ }
+
+ private static async Task VerifyHealthAsync(LocalMusicApiEndpoint endpoint, CancellationToken cancellationToken)
+ {
+ using var handler = new HttpClientHandler { UseProxy = false, UseCookies = false };
+ using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(3) };
+ using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(endpoint.BaseAddress, "ymhut/health"));
+ request.Headers.TryAddWithoutValidation("x-ymhut-internal-token", endpoint.Token);
+ using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
+ if (!response.IsSuccessStatusCode) throw new LocalMusicApiUnavailableException("本地音乐 API 健康检查失败。");
+ var health = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
+ if (health is null || health.ProtocolVersion != ProtocolVersion || health.Commit != MoeKoeCommit || health.Status != "ready")
+ {
+ throw new LocalMusicApiUnavailableException("本地音乐 API 健康响应不匹配。");
+ }
+ }
+
+ private void Process_Exited(object? sender, EventArgs e)
+ {
+ _endpoint = null;
+ _ = WriteLogAsync("Warning", "本地音乐 API 子进程已退出", "酷狗来源将在下次请求时限频重启");
+ }
+
+ private void EnforceRestartLimit()
+ {
+ var now = DateTimeOffset.UtcNow;
+ while (_restartAttempts.Count > 0 && now - _restartAttempts.Peek() > TimeSpan.FromMinutes(1)) _restartAttempts.Dequeue();
+ if (_restartAttempts.Count >= 3)
+ {
+ throw new LocalMusicApiUnavailableException("本地音乐 API 重启过于频繁,请稍后重试。");
+ }
+ _restartAttempts.Enqueue(now);
+ }
+
+ private static (string Node, string EntryPoint, string ApiRoot) ResolvePayload()
+ {
+ foreach (var root in InstallLayoutPaths.CandidateRoots())
+ {
+ var musicApiRoot = Path.Combine(root, "music-api");
+ var node = Path.Combine(musicApiRoot, "node.exe");
+ var apiRoot = Path.Combine(musicApiRoot, "api");
+ var entry = Path.Combine(apiRoot, "ymhut-sidecar.js");
+ if (File.Exists(node) && File.Exists(entry) && File.Exists(Path.Combine(apiRoot, "pnpm-lock.yaml")) &&
+ File.Exists(Path.Combine(apiRoot, "LICENSE")))
+ {
+ return (node, entry, apiRoot);
+ }
+ }
+
+#if DEBUG
+ var current = new DirectoryInfo(AppContext.BaseDirectory);
+ while (current is not null)
+ {
+ var apiRoot = Path.Combine(current.FullName, "third_party", "MoeKoeMusic", "api");
+ var entry = Path.Combine(apiRoot, "ymhut-sidecar.js");
+ if (File.Exists(entry))
+ {
+ var node = FindOnPath("node.exe");
+ if (node is not null) return (node, entry, apiRoot);
+ }
+ current = current.Parent;
+ }
+#endif
+ throw new LocalMusicApiUnavailableException("随包 music-api 运行时不完整。");
+ }
+
+#if DEBUG
+ private static string? FindOnPath(string name)
+ {
+ foreach (var item in (Environment.GetEnvironmentVariable("PATH") ?? string.Empty).Split(Path.PathSeparator))
+ {
+ try
+ {
+ var candidate = Path.Combine(item.Trim(), name);
+ if (File.Exists(candidate)) return candidate;
+ }
+ catch
+ {
+ }
+ }
+ return null;
+ }
+#endif
+
+ private void AttachKillOnCloseJob(Process process)
+ {
+ _job?.Dispose();
+ var job = CreateJobObject(nint.Zero, null);
+ if (job.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error(), "无法创建音乐 API Job Object。");
+ var information = new JobObjectExtendedLimitInformation
+ {
+ BasicLimitInformation = new JobObjectBasicLimitInformation { LimitFlags = 0x00002000 }
+ };
+ var size = Marshal.SizeOf();
+ var pointer = Marshal.AllocHGlobal(size);
+ try
+ {
+ Marshal.StructureToPtr(information, pointer, false);
+ if (!SetInformationJobObject(job, 9, pointer, (uint)size))
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "无法配置音乐 API Job Object。");
+ if (!AssignProcessToJobObject(job, process.Handle))
+ throw new Win32Exception(Marshal.GetLastWin32Error(), "无法将音乐 API 加入 Job Object。");
+ }
+ catch
+ {
+ job.Dispose();
+ throw;
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(pointer);
+ }
+ _job = job;
+ }
+
+ public async Task StopAsync()
+ {
+ var endpoint = _endpoint;
+ var process = _process;
+ _endpoint = null;
+ if (endpoint is not null && process is { HasExited: false })
+ {
+ try
+ {
+ using var handler = new HttpClientHandler { UseProxy = false, UseCookies = false };
+ using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(2) };
+ var timestamp = KugouLocalApiProxyHandler.NextTimestamp();
+ using var request = new HttpRequestMessage(HttpMethod.Post, new Uri(endpoint.BaseAddress, $"ymhut/shutdown?timestamp={timestamp}"));
+ request.Headers.TryAddWithoutValidation("x-ymhut-internal-token", endpoint.Token);
+ using var response = await client.SendAsync(request).ConfigureAwait(false);
+ await process.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
+ }
+ catch
+ {
+ }
+ }
+ StopProcess(kill: true);
+ }
+
+ private void StopProcess(bool kill)
+ {
+ var process = Interlocked.Exchange(ref _process, null);
+ _endpoint = null;
+ if (process is not null)
+ {
+ try { process.Exited -= Process_Exited; } catch { }
+ try { if (kill && !process.HasExited) process.Kill(entireProcessTree: true); } catch { }
+ process.Dispose();
+ }
+ _job?.Dispose();
+ _job = null;
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ try { StopAsync().GetAwaiter().GetResult(); } catch { StopProcess(kill: true); }
+ _startGate.Dispose();
+ }
+
+ private async Task WriteLogAsync(string level, string message, string? detail)
+ {
+ if (logService is null) return;
+ try { await logService.WriteAsync(level, "music-api", message, detail).ConfigureAwait(false); } catch { }
+ }
+
+ private sealed record ReadyMessage(string Type, int ProtocolVersion, string Commit, int Pid, int Port);
+ private sealed record HealthMessage(int ProtocolVersion, string Commit, int Pid, string Status);
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct JobObjectBasicLimitInformation
+ {
+ public long PerProcessUserTimeLimit;
+ public long PerJobUserTimeLimit;
+ public uint LimitFlags;
+ public nuint MinimumWorkingSetSize;
+ public nuint MaximumWorkingSetSize;
+ public uint ActiveProcessLimit;
+ public nuint Affinity;
+ public uint PriorityClass;
+ public uint SchedulingClass;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct IoCounters
+ {
+ public ulong ReadOperationCount;
+ public ulong WriteOperationCount;
+ public ulong OtherOperationCount;
+ public ulong ReadTransferCount;
+ public ulong WriteTransferCount;
+ public ulong OtherTransferCount;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct JobObjectExtendedLimitInformation
+ {
+ public JobObjectBasicLimitInformation BasicLimitInformation;
+ public IoCounters IoInfo;
+ public nuint ProcessMemoryLimit;
+ public nuint JobMemoryLimit;
+ public nuint PeakProcessMemoryUsed;
+ public nuint PeakJobMemoryUsed;
+ }
+
+ private sealed class SafeJobHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ private SafeJobHandle() : base(true) { }
+
+ protected override bool ReleaseHandle() => CloseHandle(handle);
+ }
+
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ private static extern SafeJobHandle CreateJobObject(nint jobAttributes, string? name);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool SetInformationJobObject(SafeJobHandle job, int informationClass, nint information, uint informationLength);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool AssignProcessToJobObject(SafeJobHandle job, nint process);
+
+ [DllImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool CloseHandle(nint handle);
+}
diff --git a/src/box-winUI/Services/MusicLyricsCoordinator.cs b/src/box-winUI/Services/MusicLyricsCoordinator.cs
index 201ab65..e5cbfd6 100644
--- a/src/box-winUI/Services/MusicLyricsCoordinator.cs
+++ b/src/box-winUI/Services/MusicLyricsCoordinator.cs
@@ -1,4 +1,5 @@
using YMhut.Box.Core.Music;
+using YMhut.Box.Core.Settings;
namespace YMhut.Box.WinUI.Services;
@@ -14,6 +15,8 @@ public interface IMusicLyricsCoordinator : IDisposable
int ActiveWordIndex { get; }
+ MusicLyricCursor Cursor { get; }
+
Task RefreshAsync(CancellationToken cancellationToken = default);
}
@@ -25,22 +28,26 @@ public sealed class MusicLyricsCoordinator : IMusicLyricsCoordinator
private readonly IMusicPlaybackService _playback;
private readonly IMusicProviderRegistry _providers;
private readonly IDesktopOverlayService _overlay;
+ private readonly ISettingsService _settingsService;
private readonly object _sync = new();
private CancellationTokenSource? _loadCts;
private string _songKey = string.Empty;
private TimedLyrics? _lyrics;
private int _activeLineIndex = -1;
private int _activeWordIndex = -1;
+ private double _activeWordProgress;
private bool _disposed;
public MusicLyricsCoordinator(
IMusicPlaybackService playback,
IMusicProviderRegistry providers,
- IDesktopOverlayService overlay)
+ IDesktopOverlayService overlay,
+ ISettingsService settingsService)
{
_playback = playback;
_providers = providers;
_overlay = overlay;
+ _settingsService = settingsService;
_playback.CurrentSongChanged += Playback_CurrentSongChanged;
_playback.StateChanged += Playback_StateChanged;
}
@@ -73,6 +80,14 @@ public sealed class MusicLyricsCoordinator : IMusicLyricsCoordinator
}
}
+ public MusicLyricCursor Cursor
+ {
+ get
+ {
+ lock (_sync) return new MusicLyricCursor(_activeLineIndex, _activeWordIndex, _activeWordProgress);
+ }
+ }
+
public async Task RefreshAsync(CancellationToken cancellationToken = default)
{
var song = _playback.CurrentSong;
@@ -97,6 +112,7 @@ public sealed class MusicLyricsCoordinator : IMusicLyricsCoordinator
_lyrics = null;
_activeLineIndex = -1;
_activeWordIndex = -1;
+ _activeWordProgress = 0;
}
previous?.Cancel();
previous?.Dispose();
@@ -133,6 +149,7 @@ public sealed class MusicLyricsCoordinator : IMusicLyricsCoordinator
{
if (!ReferenceEquals(current, _loadCts)) return;
_lyrics = new TimedLyrics([], string.Empty, null, null);
+ _activeWordProgress = 0;
}
SetOverlayPlaceholder();
LyricsChanged?.Invoke(this, EventArgs.Empty);
@@ -180,6 +197,7 @@ public sealed class MusicLyricsCoordinator : IMusicLyricsCoordinator
if (_activeLineIndex < 0 && _activeWordIndex < 0) return false;
_activeLineIndex = -1;
_activeWordIndex = -1;
+ _activeWordProgress = 0;
SetOverlayPlaceholder();
return true;
}
@@ -191,26 +209,41 @@ public sealed class MusicLyricsCoordinator : IMusicLyricsCoordinator
if (line.Words[index].Start <= position) wordIndex = index;
else break;
}
- if (lineIndex == _activeLineIndex && wordIndex == _activeWordIndex) return false;
+ var wordProgress = 0d;
+ if (wordIndex >= 0 && wordIndex < line.Words.Count)
+ {
+ var word = line.Words[wordIndex];
+ wordProgress = word.Duration <= TimeSpan.Zero
+ ? 1
+ : Math.Clamp((position - word.Start).TotalMilliseconds / word.Duration.TotalMilliseconds, 0, 1);
+ }
+ if (lineIndex == _activeLineIndex && wordIndex == _activeWordIndex && Math.Abs(wordProgress - _activeWordProgress) < 0.025) return false;
_activeLineIndex = lineIndex;
_activeWordIndex = wordIndex;
+ _activeWordProgress = wordProgress;
var song = _playback.CurrentSong;
- _overlay.SetDesktopLyrics(
- song?.Name ?? "YMhut Music",
- song?.Artist ?? string.Empty,
- line.Text,
- line.Translation);
+ if (_settingsService.Current.Lyrics.DesktopSyncEnabled)
+ {
+ _overlay.SetDesktopLyrics(
+ song?.Name ?? "YMhut Music",
+ song?.Artist ?? string.Empty,
+ line.Text,
+ _settingsService.Current.Lyrics.ShowTranslation ? line.Translation : null);
+ }
return true;
}
private void SetOverlayPlaceholder()
{
var song = _playback.CurrentSong;
- _overlay.SetDesktopLyrics(
- song?.Name ?? "YMhut Music",
- song?.Artist ?? string.Empty,
- song is null ? "播放歌曲后显示桌面歌词" : "纯音乐,请欣赏");
+ if (_settingsService.Current.Lyrics.DesktopSyncEnabled)
+ {
+ _overlay.SetDesktopLyrics(
+ song?.Name ?? "YMhut Music",
+ song?.Artist ?? string.Empty,
+ song is null ? "播放歌曲后显示桌面歌词" : "纯音乐,请欣赏");
+ }
}
private static string ToKey(MusicSong song) => $"{song.Provider}:{song.Id}";
diff --git a/src/box-winUI/Services/ShellDialogService.cs b/src/box-winUI/Services/ShellDialogService.cs
index a9b7b87..c7248a4 100644
--- a/src/box-winUI/Services/ShellDialogService.cs
+++ b/src/box-winUI/Services/ShellDialogService.cs
@@ -1,5 +1,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
+using YMhut.Box.Core.Settings;
namespace YMhut.Box.WinUI.Services;
@@ -10,9 +11,10 @@ public interface IShellDialogService
Task ShowAsync(ContentDialog dialog);
}
-public sealed class ShellDialogService : IShellDialogService
+public sealed class ShellDialogService(ISettingsService settingsService) : IShellDialogService
{
private XamlRoot? _xamlRoot;
+ private readonly ISettingsService _settingsService = settingsService;
public void Attach(XamlRoot? xamlRoot)
{
@@ -24,6 +26,7 @@ public sealed class ShellDialogService : IShellDialogService
try
{
dialog.XamlRoot ??= _xamlRoot ?? ToastService.CurrentXamlRoot;
+ ThemeService.PrepareContentDialog(dialog, dialog.XamlRoot, _settingsService.Current.Theme);
return await dialog.ShowAsync();
}
catch (Exception exception)
diff --git a/src/box-winUI/ThemeService.cs b/src/box-winUI/ThemeService.cs
index fb18c5a..1d7ac73 100644
--- a/src/box-winUI/ThemeService.cs
+++ b/src/box-winUI/ThemeService.cs
@@ -38,16 +38,28 @@ public static class ThemeService
return;
}
- root.RequestedTheme = theme switch
- {
- "Light" => ElementTheme.Light,
- "Dark" => ElementTheme.Dark,
- _ => ElementTheme.Default,
- };
+ root.RequestedTheme = ResolveElementTheme(theme);
ApplyWindowBackdrop(window, windowBackdrop);
}
+ public static void PrepareContentDialog(ContentDialog dialog, XamlRoot? xamlRoot, string? theme)
+ {
+ if (xamlRoot is not null)
+ {
+ dialog.XamlRoot ??= xamlRoot;
+ }
+
+ dialog.RequestedTheme = ResolveElementTheme(theme);
+ dialog.Resources["ApplicationPageBackgroundThemeBrush"] = ModernUi.AppBackground;
+ dialog.Resources["CardBackgroundFillColorDefaultBrush"] = ModernUi.Surface;
+ dialog.Resources["ControlFillColorDefaultBrush"] = ModernUi.SurfaceAlt;
+ dialog.Resources["ControlFillColorSecondaryBrush"] = ModernUi.SurfaceAlt;
+ dialog.Resources["TextFillColorPrimaryBrush"] = ModernUi.TextPrimary;
+ dialog.Resources["TextFillColorSecondaryBrush"] = ModernUi.TextSecondary;
+ dialog.Resources["ControlStrokeColorDefaultBrush"] = ModernUi.Stroke;
+ }
+
public static void ApplyWindowBackdrop(Window? window, string windowBackdrop)
{
if (window is null)
@@ -204,6 +216,16 @@ public static class ThemeService
};
}
+ public static ElementTheme ResolveElementTheme(string? theme)
+ {
+ return (theme ?? string.Empty).Trim().ToLowerInvariant() switch
+ {
+ "light" => ElementTheme.Light,
+ "dark" => ElementTheme.Dark,
+ _ => ElementTheme.Default
+ };
+ }
+
private static bool IsSystemDarkTheme()
{
try
diff --git a/src/box-winUI/Views/AboutPage.cs b/src/box-winUI/Views/AboutPage.cs
index 7aabe92..37235ba 100644
--- a/src/box-winUI/Views/AboutPage.cs
+++ b/src/box-winUI/Views/AboutPage.cs
@@ -544,16 +544,38 @@ public sealed class AboutPage : Page
};
row.Children.Add(text);
- if (!string.IsNullOrWhiteSpace(reference.Path) && File.Exists(reference.Path))
+ var actions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 4 };
+ var noticePath = !string.IsNullOrWhiteSpace(reference.LicensePath) ? reference.LicensePath : reference.Path;
+ if (!string.IsNullOrWhiteSpace(noticePath) && File.Exists(noticePath))
{
- var button = ModernUi.IconButton("\uE8A7", AppLocalizer.T("打开原始声明文件", "Open original notice"), () => OpenReferenceFile(reference.Path));
- Grid.SetColumn(button, 1);
- row.Children.Add(button);
+ actions.Children.Add(ModernUi.IconButton("\uE8A7", AppLocalizer.T("打开许可证或原始声明", "Open license or original notice"), () => OpenReferenceFile(noticePath)));
+ }
+ if (Uri.TryCreate(reference.SourceUrl, UriKind.Absolute, out var sourceUri) && sourceUri.Scheme == Uri.UriSchemeHttps)
+ {
+ actions.Children.Add(ModernUi.IconButton("\uE71B", AppLocalizer.T("打开源代码仓库", "Open source repository"), () => OpenReferenceUrl(sourceUri)));
+ }
+ if (actions.Children.Count > 0)
+ {
+ Grid.SetColumn(actions, 1);
+ row.Children.Add(actions);
}
return row;
}
+ private static void OpenReferenceUrl(Uri uri)
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo { FileName = uri.AbsoluteUri, UseShellExecute = true });
+ }
+ catch (Exception exception)
+ {
+ CrashLog.Write(exception);
+ ToastService.Show(AppLocalizer.T("无法打开源代码仓库。", "Unable to open the source repository."), ToastKind.Error);
+ }
+ }
+
private static void OpenReferenceFile(string path)
{
try
@@ -578,7 +600,11 @@ public sealed class AboutPage : Page
{
"Application" => AppLocalizer.T("应用", "Application"),
"Reference project attribution" => AppLocalizer.T("参考项目来源", "Reference project source"),
+ "Bundled music API source" => AppLocalizer.T("随包音乐 API 源码", "Bundled music API source"),
+ "Bundled modified component" => AppLocalizer.T("随包修改组件", "Bundled modified component"),
+ "Cache mechanism attribution" => AppLocalizer.T("缓存机制来源", "Cache mechanism attribution"),
"Runtime" => AppLocalizer.T("运行时", "Runtime"),
+ "Bundled npm dependencies" => AppLocalizer.T("随包 npm 依赖", "Bundled npm dependencies"),
"NuGet package" => AppLocalizer.T("NuGet 包", "NuGet package"),
"Third-party tool notice" => AppLocalizer.T("第三方工具声明", "Third-party tool notice"),
_ => AppLocalizer.T("其他来源", "Other source")
@@ -591,9 +617,14 @@ public sealed class AboutPage : Page
{
"Application" => 0,
"Reference project attribution" => 1,
- "Runtime" => 2,
- "NuGet package" => 3,
- _ => 4
+ "Bundled music API source" => 2,
+ "Bundled modified component" => 3,
+ "Cache mechanism attribution" => 4,
+ "Runtime" => 5,
+ "Bundled npm dependencies" => 6,
+ "NuGet package" => 7,
+ "Third-party tool notice" => 8,
+ _ => 9
};
}
diff --git a/src/box-winUI/Views/ExternalToolDetailPage.cs b/src/box-winUI/Views/ExternalToolDetailPage.cs
index 99f2db9..66dabeb 100644
--- a/src/box-winUI/Views/ExternalToolDetailPage.cs
+++ b/src/box-winUI/Views/ExternalToolDetailPage.cs
@@ -16,6 +16,8 @@ public sealed partial class ExternalToolDetailPage : ToolPageBase
private readonly IRiskConfirmationStore _riskStore = AppServices.GetRequiredService();
private readonly ISettingsService _settingsService = AppServices.GetRequiredService();
private readonly ILogService _logService = AppServices.GetRequiredService();
+ private readonly IToolUiDefinitionStore _toolUiDefinitionStore = AppServices.GetRequiredService();
+ private ToolUiDefinition _uiDefinition;
private readonly ComboBox _variantBox = new() { MinWidth = 220 };
private readonly InfoBar _status = new()
{
@@ -46,7 +48,71 @@ public sealed partial class ExternalToolDetailPage : ToolPageBase
_goBack = goBack;
Background = ModernUi.AppBackground;
BindModule(module);
+ _uiDefinition = _toolUiDefinitionStore.GetFallback(module);
Content = BuildLayout();
+ Loaded += ExternalToolDetailPage_Loaded;
+ Unloaded += ExternalToolDetailPage_Unloaded;
+ }
+
+ public ToolUiDefinition UiDefinition => _uiDefinition;
+
+ private async void ExternalToolDetailPage_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (Module is null)
+ {
+ return;
+ }
+
+ try
+ {
+ _uiDefinition = await _toolUiDefinitionStore.GetAsync(Module);
+ _status.Title = _uiDefinition.IsFallback ? "外部工具 · 安全回退页面" : "外部工具";
+ if (_uiDefinition.IsFallback && !string.IsNullOrWhiteSpace(_uiDefinition.Diagnostic))
+ {
+ _status.Message = $"{_status.Message} 定义状态:{_uiDefinition.Diagnostic}";
+ }
+
+ var state = _settingsService.GetToolUiState(Module.Id);
+ if (state.Values.TryGetValue("variant", out var path))
+ {
+ for (var index = 0; index < _variantBox.Items.Count; index++)
+ {
+ if (_variantBox.Items[index] is VariantChoice choice
+ && string.Equals(choice.Path, path, StringComparison.OrdinalIgnoreCase))
+ {
+ _variantBox.SelectedIndex = index;
+ break;
+ }
+ }
+ }
+ }
+ catch (Exception exception)
+ {
+ await _logService.WriteAsync("Warning", "tool-ui", "External tool definition load failed", $"{Module.Id}; {exception.Message}");
+ }
+ }
+
+ private async void ExternalToolDetailPage_Unloaded(object sender, RoutedEventArgs e)
+ {
+ if (Module is null)
+ {
+ return;
+ }
+
+ try
+ {
+ var state = new ToolUiPageState();
+ if (_variantBox.SelectedItem is VariantChoice choice)
+ {
+ state.Values["variant"] = choice.Path;
+ }
+
+ await _settingsService.SaveToolUiStateAsync(Module.Id, state);
+ }
+ catch (Exception exception)
+ {
+ await _logService.WriteAsync("Warning", "tool-ui", "External tool state could not be saved", $"{Module.Id}; {exception.Message}");
+ }
}
private UIElement BuildLayout()
diff --git a/src/box-winUI/Views/NetworkMusicPage.cs b/src/box-winUI/Views/NetworkMusicPage.cs
index 351e9b9..60f817b 100644
--- a/src/box-winUI/Views/NetworkMusicPage.cs
+++ b/src/box-winUI/Views/NetworkMusicPage.cs
@@ -1,3 +1,4 @@
+using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -9,7 +10,9 @@ using QRCoder;
using Windows.Media.Core;
using Windows.Storage.Streams;
using Windows.System;
+using Windows.UI;
using Windows.UI.Text;
+using Windows.UI.ViewManagement;
using YMhut.Box.Core.Music;
using YMhut.Box.Core.Settings;
using YMhut.Box.WinUI.Services;
@@ -33,6 +36,7 @@ public sealed class NetworkMusicPage : Page
private readonly IAppVersionService _appVersionService = AppServices.GetRequiredService();
private readonly IMusicPlaybackService _playback = AppServices.GetRequiredService();
private readonly IDesktopOverlayService _overlayService = AppServices.GetRequiredService();
+ private readonly ISettingsService _settingsService = AppServices.GetRequiredService();
private readonly AutoSuggestBox _search = new() { PlaceholderText = AppLocalizer.T("搜索歌曲、歌手或歌单", "Search songs, artists, or playlists"), MinWidth = 0 };
private readonly ComboBox _searchKind = new() { Width = 112 };
private readonly ListView _results = new() { SelectionMode = ListViewSelectionMode.None, IsItemClickEnabled = true };
@@ -46,12 +50,33 @@ public sealed class NetworkMusicPage : Page
private readonly Button _modeButton;
private readonly Button _favoriteButton;
private readonly Button _desktopLyricsButton;
+ private readonly Button _immersiveButton;
+ private readonly Button _lyricsSettingsButton;
private readonly Button _mvButton;
private readonly PersonPicture _accountPicture = new() { Width = 30, Height = 30 };
private readonly Button _accountButton;
private readonly Slider _position = new() { Minimum = 0, Maximum = 1, Width = 280 };
private readonly Slider _volume = new() { Minimum = 0, Maximum = 100, Width = 92 };
- private readonly ComboBox _quality = new() { Width = 104 };
+ private readonly Slider _immersivePosition = new() { Minimum = 0, Maximum = 1, MinWidth = 180, HorizontalAlignment = HorizontalAlignment.Stretch };
+ private readonly Slider _immersiveVolume = new() { Minimum = 0, Maximum = 100, Width = 110 };
+ private readonly Image _immersiveCover = new() { Width = 320, Height = 320, Stretch = Stretch.UniformToFill };
+ private readonly TextBlock _immersiveTitle = Text(AppLocalizer.T("未在播放", "Nothing playing"), 24, FontWeights.SemiBold, maxLines: 2);
+ private readonly TextBlock _immersiveArtist = Text(string.Empty, 13, foreground: SecondaryTextBrush, maxLines: 2);
+ private readonly StackPanel _immersiveLyrics = new() { Spacing = 16 };
+ private readonly ScrollViewer _immersiveLyricsScroll = new() { VerticalScrollBarVisibility = ScrollBarVisibility.Auto };
+ private readonly Button _immersivePlayPauseButton;
+ private Button? _immersiveBackButton;
+ private FrameworkElement? _topBarElement;
+ private Grid? _contentElement;
+ private FrameworkElement? _playerElement;
+ private FrameworkElement? _immersiveElement;
+ private readonly Button _qualityButton = new()
+ {
+ MinWidth = 116,
+ Height = 36,
+ Padding = new Thickness(10, 0, 10, 0),
+ CornerRadius = new CornerRadius(5)
+ };
private readonly ProgressRing _busy = new() { Width = 22, Height = 22 };
private readonly InfoBar _status = new() { IsOpen = false, IsClosable = true };
private readonly DispatcherTimer _uiTimer = new() { Interval = TimeSpan.FromMilliseconds(250) };
@@ -64,11 +89,12 @@ public sealed class NetworkMusicPage : Page
private bool _subscribed;
private bool _loaded;
private bool _musicDisclaimerAccepted;
- private bool _qualitySyncing;
private string _displayedPlaybackFailure = string.Empty;
private string _favoriteStateSongKey = string.Empty;
private bool? _favoriteState;
private CancellationTokenSource? _favoriteStateCts;
+ private bool _immersiveOpen;
+ private bool _syncingImmersivePosition;
private IMusicProvider Provider => _providers.Current;
@@ -80,6 +106,9 @@ public sealed class NetworkMusicPage : Page
_favoriteButton = IconButton("\uEB51", AppLocalizer.T("收藏当前歌曲", "Favorite current song"), async () => await FavoriteCurrentAsync());
_favoriteButton.IsEnabled = false;
_desktopLyricsButton = IconButton("\uE8D2", AppLocalizer.T("桌面歌词", "Desktop lyrics"), async () => await ToggleDesktopLyricsAsync());
+ _immersiveButton = IconButton("\uE740", AppLocalizer.T("沉浸歌词", "Immersive lyrics"), OpenImmersiveLyrics);
+ _lyricsSettingsButton = IconButton("\uE713", AppLocalizer.T("歌词设置", "Lyric settings"), async () => await ShowLyricsSettingsAsync());
+ _immersivePlayPauseButton = IconButton("\uE768", AppLocalizer.T("播放或暂停", "Play or pause"), () => _playback.PlayPause());
_mvButton = IconButton("\uE714", AppLocalizer.T("播放当前歌曲 MV", "Play current song MV"), async () => await ShowMvAsync());
_accountButton = new Button
{
@@ -98,6 +127,26 @@ public sealed class NetworkMusicPage : Page
Unloaded += NetworkMusicPage_Unloaded;
_uiTimer.Tick += (_, _) => RefreshPlaybackUi();
_search.QuerySubmitted += async (_, args) => await SearchAsync(args.QueryText);
+ KeyDown += (_, args) =>
+ {
+ if (_immersiveOpen && args.Key == VirtualKey.Escape)
+ {
+ args.Handled = true;
+ CloseImmersiveLyrics();
+ }
+ };
+ var closeImmersiveAccelerator = new KeyboardAccelerator { Key = VirtualKey.Escape };
+ closeImmersiveAccelerator.Invoked += (_, args) =>
+ {
+ if (!_immersiveOpen)
+ {
+ return;
+ }
+
+ args.Handled = true;
+ CloseImmersiveLyrics();
+ };
+ KeyboardAccelerators.Add(closeImmersiveAccelerator);
}
private UIElement BuildContent()
@@ -106,16 +155,9 @@ public sealed class NetworkMusicPage : Page
_searchKind.Items.Add(AppLocalizer.T("歌手", "Artists"));
_searchKind.Items.Add(AppLocalizer.T("歌单", "Playlists"));
_searchKind.SelectedIndex = 0;
- foreach (var quality in new[]
- {
- AppLocalizer.T("超清母带", "Hi-Res master"),
- AppLocalizer.T("高清臻音", "Spatial audio"),
- AppLocalizer.T("无损", "Lossless"),
- AppLocalizer.T("极高", "Very high"),
- AppLocalizer.T("标准", "Standard")
- }) _quality.Items.Add(quality);
- _quality.SelectedIndex = 1;
- _quality.SelectionChanged += async (_, _) => await ChangeQualityAsync();
+ ModernUi.ApplyHoverResources(_qualityButton, SurfaceAltBrush, StrokeBrush, ModernUi.HoverSurface, ModernUi.StrokeStrong);
+ _qualityButton.Click += (_, _) => ShowQualityFlyout();
+ RefreshQualityButton();
_volume.Value = _playback.Volume;
_volume.ValueChanged += (_, args) => _playback.Volume = args.NewValue;
_position.AddHandler(UIElement.PointerPressedEvent, new PointerEventHandler((_, _) =>
@@ -150,9 +192,11 @@ public sealed class NetworkMusicPage : Page
root.RowDefinitions.Add(new RowDefinition());
root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(86) });
var topBar = BuildTopBar();
+ _topBarElement = topBar;
root.Children.Add(topBar);
var content = new Grid();
+ _contentElement = content;
content.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(210) });
content.ColumnDefinitions.Add(new ColumnDefinition());
content.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(330) });
@@ -167,8 +211,12 @@ public sealed class NetworkMusicPage : Page
root.Children.Add(content);
var player = BuildPlayerBar();
+ _playerElement = player;
Grid.SetRow(player, 2);
root.Children.Add(player);
+ _immersiveElement = BuildImmersiveView();
+ Grid.SetRowSpan(_immersiveElement, 3);
+ root.Children.Add(_immersiveElement);
root.SizeChanged += (_, args) =>
{
var narrow = args.NewSize.Width < 980;
@@ -276,6 +324,8 @@ public sealed class NetworkMusicPage : Page
_sectionMeta.Text = SongCount(_playback.Queue.Items.Count);
RenderSongs(_playback.Queue.Items);
}));
+ panel.Children.Add(new Border { Height = 1, Margin = new Thickness(6, 10, 6, 10), Background = StrokeBrush });
+ panel.Children.Add(NavButton("\uE946", AppLocalizer.T("使用须知", "Usage notice"), async () => await ShowMusicUsageNoticeAsync()));
return new Border
{
Background = SurfaceBrush,
@@ -311,6 +361,18 @@ public sealed class NetworkMusicPage : Page
{
_lyrics.Children.Add(Text(AppLocalizer.T("播放歌曲后显示逐字与翻译歌词", "Word-timed and translated lyrics appear during playback"), 13, foreground: SecondaryTextBrush));
_lyricsScroll.Content = _lyrics;
+ var header = new Grid();
+ header.ColumnDefinitions.Add(new ColumnDefinition());
+ header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ header.Children.Add(Text(AppLocalizer.T("歌词", "Lyrics"), 17, FontWeights.SemiBold));
+ var actions = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = 6,
+ Children = { _lyricsSettingsButton, _immersiveButton }
+ };
+ Grid.SetColumn(actions, 1);
+ header.Children.Add(actions);
return new Border
{
Padding = new Thickness(20, 18, 18, 18),
@@ -326,7 +388,7 @@ public sealed class NetworkMusicPage : Page
},
Children =
{
- Text(AppLocalizer.T("歌词", "Lyrics"), 17, FontWeights.SemiBold),
+ header,
LyricScroller()
}
}
@@ -340,6 +402,109 @@ public sealed class NetworkMusicPage : Page
return _lyricsScroll;
}
+ private FrameworkElement BuildImmersiveView()
+ {
+ var root = new Grid
+ {
+ Visibility = Visibility.Collapsed,
+ Background = BackgroundBrush,
+ Padding = new Thickness(24, 18, 24, 18),
+ RowSpacing = 14
+ };
+ root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ root.RowDefinitions.Add(new RowDefinition());
+ root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+
+ var header = new Grid { ColumnSpacing = 12 };
+ header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ header.ColumnDefinitions.Add(new ColumnDefinition());
+ _immersiveBackButton = IconButton("\uE72B", AppLocalizer.T("返回音乐列表", "Back to music list"), CloseImmersiveLyrics);
+ header.Children.Add(_immersiveBackButton);
+ var heading = Text(AppLocalizer.T("沉浸歌词", "Immersive lyrics"), 17, FontWeights.SemiBold);
+ Grid.SetColumn(heading, 1);
+ header.Children.Add(heading);
+ root.Children.Add(header);
+
+ var body = new Grid { ColumnSpacing = 36, RowSpacing = 18 };
+ body.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(360) });
+ body.ColumnDefinitions.Add(new ColumnDefinition());
+ var artwork = new StackPanel
+ {
+ Spacing = 14,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Children = { _immersiveCover, _immersiveTitle, _immersiveArtist }
+ };
+ _immersiveTitle.MaxWidth = 360;
+ _immersiveArtist.MaxWidth = 360;
+ body.Children.Add(artwork);
+ _immersiveLyricsScroll.Content = _immersiveLyrics;
+ _immersiveLyricsScroll.Padding = new Thickness(8, 24, 18, 60);
+ Grid.SetColumn(_immersiveLyricsScroll, 1);
+ body.Children.Add(_immersiveLyricsScroll);
+ Grid.SetRow(body, 1);
+ root.Children.Add(body);
+
+ var controls = new Grid { ColumnSpacing = 12 };
+ controls.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ controls.ColumnDefinitions.Add(new ColumnDefinition());
+ controls.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ var transport = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6 };
+ transport.Children.Add(IconButton("\uE892", AppLocalizer.T("上一首", "Previous"), async () => await RunPlayerActionAsync(() => _playback.PreviousAsync())));
+ transport.Children.Add(_immersivePlayPauseButton);
+ transport.Children.Add(IconButton("\uE893", AppLocalizer.T("下一首", "Next"), async () => await RunPlayerActionAsync(() => _playback.NextAsync())));
+ controls.Children.Add(transport);
+ Grid.SetColumn(_immersivePosition, 1);
+ controls.Children.Add(_immersivePosition);
+ var right = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
+ right.Children.Add(new FontIcon { Glyph = "\uE767", FontSize = 14, VerticalAlignment = VerticalAlignment.Center, Foreground = SecondaryTextBrush });
+ right.Children.Add(_immersiveVolume);
+ var immersiveQuality = DarkButton(AppLocalizer.T("音质", "Quality"), "\uE767", () => { });
+ immersiveQuality.Click += (_, _) => ShowQualityFlyout(immersiveQuality);
+ right.Children.Add(immersiveQuality);
+ Grid.SetColumn(right, 2);
+ controls.Children.Add(right);
+ Grid.SetRow(controls, 2);
+ root.Children.Add(controls);
+
+ _immersivePosition.ValueChanged += (_, args) =>
+ {
+ if (!_syncingImmersivePosition) _playback.Seek(TimeSpan.FromSeconds(args.NewValue));
+ };
+ _immersiveVolume.Value = _playback.Volume;
+ _immersiveVolume.ValueChanged += (_, args) => _playback.Volume = args.NewValue;
+ body.SizeChanged += (_, args) =>
+ {
+ var narrow = args.NewSize.Width < 760;
+ body.ColumnDefinitions.Clear();
+ body.RowDefinitions.Clear();
+ if (narrow)
+ {
+ body.ColumnDefinitions.Add(new ColumnDefinition());
+ body.RowDefinitions.Add(new RowDefinition { Height = new GridLength(250) });
+ body.RowDefinitions.Add(new RowDefinition());
+ _immersiveCover.Width = _immersiveCover.Height = 180;
+ Grid.SetColumn(artwork, 0);
+ Grid.SetRow(artwork, 0);
+ Grid.SetColumn(_immersiveLyricsScroll, 0);
+ Grid.SetRow(_immersiveLyricsScroll, 1);
+ }
+ else
+ {
+ body.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(360) });
+ body.ColumnDefinitions.Add(new ColumnDefinition());
+ body.RowDefinitions.Add(new RowDefinition());
+ _immersiveCover.Width = _immersiveCover.Height = 320;
+ Grid.SetColumn(artwork, 0);
+ Grid.SetRow(artwork, 0);
+ Grid.SetColumn(_immersiveLyricsScroll, 1);
+ Grid.SetRow(_immersiveLyricsScroll, 0);
+ }
+ right.Visibility = args.NewSize.Width < 620 ? Visibility.Collapsed : Visibility.Visible;
+ };
+ return root;
+ }
+
private FrameworkElement BuildPlayerBar()
{
var bar = new Grid
@@ -381,7 +546,7 @@ public sealed class NetworkMusicPage : Page
var volumeIcon = new FontIcon { Glyph = "\uE767", FontSize = 15, Foreground = SecondaryTextBrush };
settings.Children.Add(volumeIcon);
settings.Children.Add(_volume);
- settings.Children.Add(_quality);
+ settings.Children.Add(_qualityButton);
settings.Children.Add(_desktopLyricsButton);
bar.Children.Add(settings);
ArrangePlayerBar(bar, track, transport, settings, volumeIcon, double.PositiveInfinity);
@@ -435,7 +600,7 @@ public sealed class NetworkMusicPage : Page
_favoriteButton.Visibility = Visibility.Visible;
volumeIcon.Visibility = full ? Visibility.Visible : Visibility.Collapsed;
_volume.Visibility = full ? Visibility.Visible : Visibility.Collapsed;
- _quality.Visibility = full ? Visibility.Visible : Visibility.Collapsed;
+ _qualityButton.Visibility = full ? Visibility.Visible : Visibility.Collapsed;
}
private async void NetworkMusicPage_Loaded(object sender, RoutedEventArgs e)
@@ -456,9 +621,7 @@ public sealed class NetworkMusicPage : Page
await Provider.InitializeAsync();
await _playback.RestoreAsync();
_volume.Value = _playback.Volume;
- _qualitySyncing = true;
- _quality.SelectedIndex = IndexFromQuality(_playback.Quality);
- _qualitySyncing = false;
+ RefreshQualityButton();
await LoadRecommendedCoreAsync();
});
}
@@ -535,20 +698,12 @@ public sealed class NetworkMusicPage : Page
{
XamlRoot = XamlRoot,
Title = AppLocalizer.T("网络音乐数据源声明", "Online music source notice"),
- Content = new StackPanel
- {
- Spacing = 10,
- Children =
- {
- Text(AppLocalizer.T("本工具展示、登录和播放的内容均由所选第三方音乐服务提供。", "Search, sign-in, and playback content is supplied by the selected third-party music service."), 14, FontWeights.SemiBold, maxLines: 3),
- Text(AppLocalizer.T("请仅使用本人合法账户和已获得授权的内容。会员、版权、地区和服务限制由服务方决定;本工具不会绕过这些限制。", "Use only your own legitimate account and authorized content. Membership, copyright, regional, and service restrictions are decided by the provider and are not bypassed by this tool."), 12, foreground: SecondaryTextBrush, maxLines: 5),
- Text(AppLocalizer.T("登录凭据仅在本机通过 Windows DPAPI 加密保存,不会写入应用日志。", "Sign-in credentials are stored only on this device with Windows DPAPI and are never written to application logs."), 12, foreground: SecondaryTextBrush, maxLines: 4)
- }
- },
+ Content = BuildMusicNoticeContent(),
PrimaryButtonText = AppLocalizer.T("同意并继续", "Accept and continue"),
CloseButtonText = AppLocalizer.T("暂不使用", "Not now"),
DefaultButton = ContentDialogButton.Primary
};
+ ThemeService.PrepareContentDialog(dialog, XamlRoot, _settingsService.Current.Theme);
if (await dialog.ShowAsync() != ContentDialogResult.Primary)
{
ShowStatus(
@@ -570,11 +725,92 @@ public sealed class NetworkMusicPage : Page
return true;
}
+ private async Task ShowMusicUsageNoticeAsync()
+ {
+ var dialog = new ContentDialog
+ {
+ XamlRoot = XamlRoot,
+ Title = AppLocalizer.T("网络音乐使用须知与免责声明", "Online music usage notice and disclaimer"),
+ Content = BuildMusicNoticeContent(),
+ CloseButtonText = AppLocalizer.T("关闭", "Close"),
+ DefaultButton = ContentDialogButton.Close
+ };
+ ThemeService.PrepareContentDialog(dialog, XamlRoot, _settingsService.Current.Theme);
+ await dialog.ShowAsync();
+ }
+
+ private static UIElement BuildMusicNoticeContent()
+ {
+ var panel = new StackPanel { Spacing = 10, MaxWidth = 620 };
+ panel.Children.Add(Text(AppLocalizer.T(
+ "本工具展示、登录和播放的内容均由所选第三方音乐服务提供。仅供学习与技术研究,请尊重版权,不得用于商业行为、非法用途或破坏版权。",
+ "Search, sign-in, and playback content is supplied by third-party music services. This integration is for learning and technical research only; respect copyright and do not use it commercially, illegally, or to undermine copyright."),
+ 14,
+ FontWeights.SemiBold,
+ maxLines: 6));
+ panel.Children.Add(Text(AppLocalizer.T(
+ "酷狗请求由随包 MoeKoeMusic/KuGouMusicApi 在本机随机回环端口承接,端口不对外公开。公共匿名 GET 最多缓存 2 分钟;登录、二维码、账户、权益、个人数据、写操作和播放地址不缓存。",
+ "Kugou requests are handled by the bundled MoeKoeMusic/KuGouMusicApi service on a random local loopback port that is not exposed externally. Public anonymous GET responses may be cached for up to two minutes; sign-in, QR, account, entitlement, personal, mutation, and stream-resolution requests are not cached."),
+ 12,
+ foreground: SecondaryTextBrush,
+ maxLines: 7));
+ panel.Children.Add(Text(AppLocalizer.T(
+ "登录凭据、账户快照和权益分组仅在本机通过 Windows DPAPI 加密保存。登录态存在时不会重复调用登录接口;超过 12 小时仅在打开账户或使用账户能力时合并刷新。Cookie、令牌、二维码凭据和响应正文不会写入日志。",
+ "Credentials, account snapshots, and entitlement groups are encrypted locally with Windows DPAPI. Sign-in endpoints are not repeated while a session exists; snapshots older than 12 hours refresh only when account UI or account features are used. Cookies, tokens, QR credentials, and response bodies are not logged."),
+ 12,
+ foreground: SecondaryTextBrush,
+ maxLines: 8));
+ panel.Children.Add(Text(AppLocalizer.T(
+ "请仅使用本人合法账户和已获授权的内容。本工具不会绕过会员、地区、版权或平台服务限制。",
+ "Use only your own legitimate account and authorized content. This tool does not bypass membership, regional, copyright, or provider restrictions."),
+ 12,
+ foreground: SecondaryTextBrush,
+ maxLines: 5));
+
+ panel.Children.Add(Text("MoeKoeMusic 固定提交 4581660209c9cdd07669e8bd5bcbe136b7d91e02 原始免责声明:", 12, FontWeights.SemiBold));
+ panel.Children.Add(Text(
+ "0. 本程序是酷狗第三方客户端,并非酷狗官方,需要更完善的功能请下载官方客户端体验.\n" +
+ "1. 本项目仅供学习使用,请尊重版权,请勿利用此项目从事商业行为及非法用途!\n" +
+ "2. 使用本项目的过程中可能会产生版权数据。对于这些版权数据,本项目不拥有它们的所有权。为了避免侵权,使用者务必在 24 小时内清除使用本项目的过程中所产生的版权数据。\n" +
+ "3. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。\n" +
+ "1. 禁止在违反当地法律法规的情况下使用本项目。对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。\n" +
+ "2. 音乐平台不易,请尊重版权,支持正版。\n" +
+ "3. 本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。\n" +
+ "4. 如果官方音乐平台觉得本项目不妥,可联系本项目更改或移除。",
+ 11,
+ foreground: SecondaryTextBrush));
+
+ panel.Children.Add(Text("KuGouMusicApi 固定子模块 06560e3e053bda1ab830750db6f645bab703f824 原始免责声明:", 12, FontWeights.SemiBold));
+ panel.Children.Add(Text(
+ "1. 本项目仅供学习使用,请尊重版权,请勿利用此项目从事商业行为及非法用途!\n" +
+ "2. 使用本项目的过程中可能会产生版权数据。对于这些版权数据,本项目不拥有它们的所有权。为了避免侵权,使用者务必在 24 小时内清除使用本项目的过程中所产生的版权数据。\n" +
+ "3. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。\n" +
+ "4. 禁止在违反当地法律法规的情况下使用本项目。对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。\n" +
+ "5. 音乐平台不易,请尊重版权,支持正版。\n" +
+ "6. 本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。\n" +
+ "7. 如果官方音乐平台觉得本项目不妥,可联系本项目更改或移除。",
+ 11,
+ foreground: SecondaryTextBrush));
+ panel.Children.Add(Text(AppLocalizer.T(
+ "许可证与来源:MoeKoeMusic 固定提交适用 GPL-2.0,KuGouMusicApi 固定子模块适用 MIT;Node.js、Binaryify/NeteaseCloudMusicApi 缓存机制及 npm 依赖均在关于页和随包 THIRD_PARTY_NOTICES.md 列出。",
+ "Licenses and sources: the pinned MoeKoeMusic commit is GPL-2.0 and the pinned KuGouMusicApi submodule is MIT. Node.js, the Binaryify/NeteaseCloudMusicApi cache mechanism, and npm dependencies are listed in About and the bundled THIRD_PARTY_NOTICES.md."),
+ 11,
+ foreground: SecondaryTextBrush));
+
+ return new ScrollViewer
+ {
+ MaxHeight = 460,
+ VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
+ Content = panel
+ };
+ }
+
private async Task SelectProviderAsync(string providerId)
{
if (string.Equals(providerId, Provider.Id, StringComparison.OrdinalIgnoreCase)) return;
_providers.Select(providerId);
await Provider.InitializeAsync();
+ RefreshQualityButton();
_sectionTitle.Text = AppLocalizer.T("发现音乐", "Discover");
_sectionMeta.Text = AppLocalizer.T("精选内容", "Curated content");
_visibleSongs = [];
@@ -672,18 +908,100 @@ public sealed class NetworkMusicPage : Page
UpdateActiveLyric(_playback.Position);
}
- private async Task ChangeQualityAsync()
+ private void ShowQualityFlyout(Button? immersiveQualityAnchor = null)
{
- if (_qualitySyncing || !_loaded || _quality.SelectedIndex < 0) return;
- var requested = QualityFromIndex(_quality.SelectedIndex);
+ var anchor = immersiveQualityAnchor ?? _qualityButton;
+ var panel = new StackPanel { Width = 330, Spacing = 2, Padding = new Thickness(4) };
+ panel.Children.Add(Text(
+ AppLocalizer.T($"{ProviderDisplayName(Provider.Id)}音质", $"{ProviderDisplayName(Provider.Id)} quality"),
+ 13,
+ FontWeights.SemiBold));
+ foreach (var option in Provider.QualityOptions)
+ {
+ var selected = option.Quality == _playback.Quality;
+ var available = option.Availability == MusicQualityAvailability.Available;
+ var row = new Grid { MinHeight = 52, ColumnSpacing = 10 };
+ row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) });
+ row.ColumnDefinitions.Add(new ColumnDefinition());
+ row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ row.Children.Add(new FontIcon
+ {
+ Glyph = selected ? "\uE73E" : available ? "\uE8D6" : "\uE72E",
+ FontSize = 14,
+ Foreground = selected ? AccentBrush : SecondaryTextBrush,
+ VerticalAlignment = VerticalAlignment.Center
+ });
+ var labels = new StackPanel { Spacing = 1, VerticalAlignment = VerticalAlignment.Center };
+ labels.Children.Add(Text(option.DisplayName, 13, selected ? FontWeights.SemiBold : FontWeights.Normal));
+ labels.Children.Add(Text(
+ $"{option.Format} · {option.BitrateKbps} kbps" + (string.IsNullOrWhiteSpace(option.RequiredEntitlement) ? string.Empty : $" · {option.RequiredEntitlement}"),
+ 11,
+ foreground: SecondaryTextBrush,
+ maxLines: 1));
+ Grid.SetColumn(labels, 1);
+ row.Children.Add(labels);
+ if (!available)
+ {
+ var reason = Text(option.UnavailableReason ?? AppLocalizer.T("当前不可用", "Unavailable"), 10.5, foreground: SecondaryTextBrush, maxLines: 2);
+ reason.MaxWidth = 96;
+ reason.TextAlignment = TextAlignment.Right;
+ Grid.SetColumn(reason, 2);
+ row.Children.Add(reason);
+ }
+ var button = new Button
+ {
+ Content = row,
+ HorizontalContentAlignment = HorizontalAlignment.Stretch,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ Background = selected ? SurfaceAltBrush : ModernUi.Transparent,
+ BorderThickness = new Thickness(0),
+ CornerRadius = new CornerRadius(5),
+ Padding = new Thickness(8, 2, 8, 2),
+ IsEnabled = available
+ };
+ ToolTipService.SetToolTip(button, available ? option.DisplayName : option.UnavailableReason);
+ button.Click += async (_, _) =>
+ {
+ if (anchor.Flyout is Flyout flyout) flyout.Hide();
+ await ChangeQualityAsync(option.Quality);
+ };
+ panel.Children.Add(button);
+ }
+ anchor.Flyout = new Flyout
+ {
+ Placement = Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.Top,
+ Content = panel
+ };
+ anchor.Flyout.ShowAt(anchor);
+ }
+
+ private void RefreshQualityButton()
+ {
+ var option = Provider.QualityOptions.FirstOrDefault(item => item.Quality == _playback.Quality);
+ var label = option?.DisplayName ?? _playback.Quality.ToString();
+ _qualityButton.Content = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = 7,
+ Children =
+ {
+ new FontIcon { Glyph = "\uE767", FontSize = 13 },
+ Text(label, 12, FontWeights.SemiBold, maxLines: 1),
+ new FontIcon { Glyph = "\uE70D", FontSize = 10 }
+ }
+ };
+ ToolTipService.SetToolTip(_qualityButton, AppLocalizer.T($"当前音质:{label}", $"Current quality: {label}"));
+ }
+
+ private async Task ChangeQualityAsync(MusicPlaybackQuality requested)
+ {
+ if (!_loaded) return;
var previous = _playback.Quality;
_busy.IsActive = true;
try
{
await _playback.ChangeQualityAsync(requested);
- _qualitySyncing = true;
- _quality.SelectedIndex = IndexFromQuality(_playback.Quality);
- _qualitySyncing = false;
+ RefreshQualityButton();
var degraded = _playback.Quality != requested;
ShowStatus(
degraded ? AppLocalizer.T("已自动降级音质", "Quality was reduced") : AppLocalizer.T("音质已切换", "Quality changed"),
@@ -694,15 +1012,12 @@ public sealed class NetworkMusicPage : Page
}
catch (Exception exception)
{
- _qualitySyncing = true;
- _quality.SelectedIndex = IndexFromQuality(previous);
- _qualitySyncing = false;
+ RefreshQualityButton();
if (_playback.LastFailure is { } failure) ShowPlaybackFailure(failure);
else ShowStatus(AppLocalizer.T("音质切换失败", "Quality change failed"), AppLocalizer.SanitizeSensitiveText(exception.Message, 180), InfoBarSeverity.Error);
}
finally
{
- _qualitySyncing = false;
_busy.IsActive = false;
}
}
@@ -832,25 +1147,109 @@ public sealed class NetworkMusicPage : Page
_lyrics.Children.Clear();
_activeLyricIndex = -1;
_activeLyricWordIndex = -1;
+ ApplyLyricLayoutSettings();
if (lyrics.Lines.Count == 0)
{
_lyrics.Children.Add(Text(AppLocalizer.T("当前歌曲没有可用歌词", "No lyrics are available for this song"), 13, foreground: SecondaryTextBrush));
+ RenderImmersiveLyrics(lyrics);
return;
}
- foreach (var line in lyrics.Lines)
+ for (var index = 0; index < lyrics.Lines.Count; index++)
{
- var panel = new StackPanel { Spacing = 2, Tag = line };
- var lyric = Text(line.Text, 14, FontWeights.Normal, SecondaryTextBrush, maxLines: 3);
- lyric.Tag = lyric.FontSize;
- panel.Children.Add(lyric);
- if (!string.IsNullOrWhiteSpace(line.Translation))
- {
- var translation = Text(line.Translation!, 11.5, foreground: SecondaryTextBrush, maxLines: 2);
- translation.Tag = translation.FontSize;
- panel.Children.Add(translation);
- }
- _lyrics.Children.Add(panel);
+ _lyrics.Children.Add(CreateLyricRow(lyrics.Lines[index], index, immersive: false));
}
+ RenderImmersiveLyrics(lyrics);
+ }
+
+ private Button CreateLyricRow(TimedLyricLine line, int index, bool immersive)
+ {
+ var settings = _settingsService.Current.Lyrics;
+ var scale = settings.FontScale;
+ var panel = new StackPanel { Spacing = immersive ? 5 : 3, Tag = line };
+ var lyric = Text(line.Text, (immersive ? 21 : 14) * scale, FontWeights.Normal, SecondaryTextBrush, maxLines: immersive ? 5 : 3);
+ lyric.Tag = lyric.FontSize;
+ panel.Children.Add(lyric);
+ if (settings.ShowTranslation && !string.IsNullOrWhiteSpace(line.Translation))
+ {
+ var translation = Text(line.Translation!, (immersive ? 13 : 11.5) * scale, foreground: SecondaryTextBrush, maxLines: immersive ? 3 : 2);
+ translation.Tag = translation.FontSize;
+ panel.Children.Add(translation);
+ }
+ if (settings.ShowRomanized && !string.IsNullOrWhiteSpace(line.Romanized))
+ {
+ var romanized = Text(line.Romanized!, (immersive ? 12 : 10.5) * scale, foreground: SecondaryTextBrush, maxLines: immersive ? 3 : 2);
+ romanized.Tag = romanized.FontSize;
+ panel.Children.Add(romanized);
+ }
+ var button = new Button
+ {
+ Content = panel,
+ Tag = index,
+ HorizontalContentAlignment = HorizontalAlignment.Stretch,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ Background = ModernUi.Transparent,
+ BorderThickness = new Thickness(0),
+ CornerRadius = new CornerRadius(4),
+ Padding = new Thickness(immersive ? 10 : 4, immersive ? 7 : 3, 10, immersive ? 7 : 3)
+ };
+ Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(button, AppLocalizer.T($"跳转到歌词:{line.Text}", $"Seek to lyric: {line.Text}"));
+ button.Click += (_, _) =>
+ {
+ _playback.Seek(line.Start);
+ RefreshPlaybackUi();
+ };
+ return button;
+ }
+
+ private void RenderImmersiveLyrics(TimedLyrics lyrics)
+ {
+ _immersiveLyrics.Children.Clear();
+ if (lyrics.Lines.Count == 0)
+ {
+ _immersiveLyrics.Children.Add(Text(AppLocalizer.T("当前歌曲没有可用歌词", "No lyrics are available for this song"), 15, foreground: SecondaryTextBrush));
+ return;
+ }
+ for (var index = 0; index < lyrics.Lines.Count; index++)
+ {
+ _immersiveLyrics.Children.Add(CreateLyricRow(lyrics.Lines[index], index, immersive: true));
+ }
+ }
+
+ private void OpenImmersiveLyrics()
+ {
+ if (_playback.CurrentSong is null)
+ {
+ ShowStatus(AppLocalizer.T("尚未播放歌曲", "Nothing is playing"), AppLocalizer.T("选择歌曲后可进入沉浸歌词。", "Choose a song before opening immersive lyrics."), InfoBarSeverity.Informational);
+ return;
+ }
+ _immersiveOpen = true;
+ if (_topBarElement is not null) _topBarElement.Visibility = Visibility.Collapsed;
+ if (_contentElement is not null) _contentElement.Visibility = Visibility.Collapsed;
+ if (_playerElement is not null) _playerElement.Visibility = Visibility.Collapsed;
+ if (_immersiveElement is not null) _immersiveElement.Visibility = Visibility.Visible;
+ UpdateImmersiveMetadata();
+ if (_currentLyrics is not null) RenderImmersiveLyrics(_currentLyrics);
+ _immersiveBackButton?.Focus(FocusState.Programmatic);
+ }
+
+ private void CloseImmersiveLyrics()
+ {
+ _immersiveOpen = false;
+ if (_topBarElement is not null) _topBarElement.Visibility = Visibility.Visible;
+ if (_contentElement is not null) _contentElement.Visibility = Visibility.Visible;
+ if (_playerElement is not null) _playerElement.Visibility = Visibility.Visible;
+ if (_immersiveElement is not null) _immersiveElement.Visibility = Visibility.Collapsed;
+ _immersiveButton.Focus(FocusState.Programmatic);
+ }
+
+ private void UpdateImmersiveMetadata()
+ {
+ var song = _playback.CurrentSong;
+ _immersiveTitle.Text = song?.Name ?? AppLocalizer.T("未在播放", "Nothing playing");
+ _immersiveArtist.Text = song is null ? string.Empty : $"{song.Artist} · {song.Album}";
+ _immersiveCover.Source = Uri.TryCreate(song?.CoverUrl, UriKind.Absolute, out var cover) && cover.Scheme == Uri.UriSchemeHttps
+ ? new BitmapImage(cover) { DecodePixelWidth = 640, DecodePixelHeight = 640 }
+ : null;
}
private void RefreshPlaybackUi()
@@ -860,6 +1259,8 @@ public sealed class NetworkMusicPage : Page
_trackArtist.Text = song is null ? AppLocalizer.T("从搜索或歌单中选择歌曲", "Choose a song from search or a playlist") : $"{song.Artist} · {song.Album}";
if (_playPauseButton.Content is FontIcon icon) icon.Glyph = _playback.IsPlaying ? "\uE769" : "\uE768";
_mvButton.IsEnabled = song is not null;
+ UpdateImmersiveMetadata();
+ if (_immersivePlayPauseButton.Content is FontIcon immersiveIcon) immersiveIcon.Glyph = _playback.IsPlaying ? "\uE769" : "\uE768";
BeginFavoriteStateRefresh(song);
if (_seekInteraction.ShouldSynchronize)
{
@@ -867,7 +1268,12 @@ public sealed class NetworkMusicPage : Page
_position.Maximum = Math.Max(1, _playback.Duration.TotalSeconds);
_position.Value = Math.Clamp(_playback.Position.TotalSeconds, 0, _position.Maximum);
_syncingPosition = false;
+ _syncingImmersivePosition = true;
+ _immersivePosition.Maximum = Math.Max(1, _playback.Duration.TotalSeconds);
+ _immersivePosition.Value = Math.Clamp(_playback.Position.TotalSeconds, 0, _immersivePosition.Maximum);
+ _syncingImmersivePosition = false;
}
+ _immersiveVolume.Value = _playback.Volume;
UpdateActiveLyric(_playback.Position);
if (_playback.LastFailure is { } failure)
{
@@ -893,8 +1299,9 @@ public sealed class NetworkMusicPage : Page
private void UpdateActiveLyric(TimeSpan position)
{
if (_currentLyrics is null || _currentLyrics.Lines.Count == 0) return;
- var nextIndex = _lyricsCoordinator.ActiveLineIndex;
- var nextWordIndex = _lyricsCoordinator.ActiveWordIndex;
+ var cursor = _lyricsCoordinator.Cursor;
+ var nextIndex = cursor.LineIndex;
+ var nextWordIndex = cursor.WordIndex;
if (nextIndex == _activeLyricIndex && nextWordIndex == _activeLyricWordIndex) return;
if (nextIndex < 0)
{
@@ -905,45 +1312,73 @@ public sealed class NetworkMusicPage : Page
if (lineChanged) ResetActiveLyricStyle();
_activeLyricIndex = nextIndex;
_activeLyricWordIndex = nextWordIndex;
- if (_lyrics.Children[nextIndex] is StackPanel current)
+ ApplyActiveLyricStyle(_lyrics, _lyricsScroll, nextIndex, nextWordIndex, lineChanged, immersive: false);
+ ApplyActiveLyricStyle(_immersiveLyrics, _immersiveLyricsScroll, nextIndex, nextWordIndex, lineChanged, immersive: true);
+ }
+
+ private void ApplyActiveLyricStyle(
+ StackPanel container,
+ ScrollViewer scroll,
+ int lineIndex,
+ int wordIndex,
+ bool lineChanged,
+ bool immersive)
+ {
+ if (lineIndex < 0 || lineIndex >= container.Children.Count || LyricPanel(container.Children[lineIndex]) is not { } current) return;
+ var settings = _settingsService.Current.Lyrics;
+ var activeBrush = LyricActiveBrush(settings.ActiveWordColor);
+ foreach (var block in current.Children.OfType())
{
- foreach (var block in current.Children.OfType())
+ block.Foreground = activeBrush;
+ block.FontWeight = FontWeights.SemiBold;
+ block.FontSize = Math.Max(block.Tag is double size ? size : block.FontSize, (immersive ? 22 : 15) * settings.FontScale);
+ }
+ if (current.Children.FirstOrDefault() is TextBlock lyric)
+ {
+ lyric.TextHighlighters.Clear();
+ var words = _currentLyrics!.Lines[lineIndex].Words;
+ if (settings.WordHighlightEnabled && wordIndex >= 0 && wordIndex < words.Count)
{
- block.Foreground = AccentBrush;
- block.FontWeight = FontWeights.SemiBold;
- block.FontSize = Math.Max(block.Tag is double size ? size : block.FontSize, 15);
- }
- if (current.Children.FirstOrDefault() is TextBlock lyric)
- {
- lyric.TextHighlighters.Clear();
- var words = _currentLyrics.Lines[nextIndex].Words;
- if (nextWordIndex >= 0 && nextWordIndex < words.Count)
+ var word = words[wordIndex];
+ if (word.CharacterStart > 0)
{
- var word = words[nextWordIndex];
- var highlight = new TextHighlighter
- {
- Background = AccentBrush,
- Foreground = ModernUi.AccentForeground
- };
- highlight.Ranges.Add(new TextRange
- {
- StartIndex = word.CharacterStart,
- Length = Math.Max(0, word.CharacterEnd - word.CharacterStart)
- });
- lyric.TextHighlighters.Add(highlight);
+ var completed = new TextHighlighter { Foreground = activeBrush };
+ completed.Ranges.Add(new TextRange { StartIndex = 0, Length = word.CharacterStart });
+ lyric.TextHighlighters.Add(completed);
}
+ var highlight = new TextHighlighter
+ {
+ Background = activeBrush,
+ Foreground = ModernUi.AccentForeground
+ };
+ highlight.Ranges.Add(new TextRange
+ {
+ StartIndex = word.CharacterStart,
+ Length = Math.Max(0, word.CharacterEnd - word.CharacterStart)
+ });
+ lyric.TextHighlighters.Add(highlight);
}
- if (lineChanged)
- {
- var offset = current.TransformToVisual(_lyrics).TransformPoint(new Windows.Foundation.Point()).Y;
- _lyricsScroll.ChangeView(null, Math.Max(0, offset - 110), null, disableAnimation: false);
- }
+ }
+ if (lineChanged)
+ {
+ var offset = current.TransformToVisual(container).TransformPoint(new Windows.Foundation.Point()).Y;
+ var animationsEnabled = true;
+ try { animationsEnabled = new UISettings().AnimationsEnabled; } catch { }
+ scroll.ChangeView(null, Math.Max(0, offset - (immersive ? 190 : 110)), null, disableAnimation: !animationsEnabled);
}
}
- private void ResetActiveLyricStyle()
+ private static StackPanel? LyricPanel(UIElement element)
+ => element switch
+ {
+ Button { Content: StackPanel panel } => panel,
+ StackPanel panel => panel,
+ _ => null
+ };
+
+ private void ResetLyricStyle(StackPanel container, int lineIndex)
{
- if (_activeLyricIndex >= 0 && _activeLyricIndex < _lyrics.Children.Count && _lyrics.Children[_activeLyricIndex] is StackPanel previous)
+ if (lineIndex >= 0 && lineIndex < container.Children.Count && LyricPanel(container.Children[lineIndex]) is { } previous)
{
foreach (var block in previous.Children.OfType())
{
@@ -953,12 +1388,144 @@ public sealed class NetworkMusicPage : Page
block.FontSize = block.Tag is double size ? size : block.FontSize;
}
}
+ }
+
+ private void ResetActiveLyricStyle()
+ {
+ ResetLyricStyle(_lyrics, _activeLyricIndex);
+ ResetLyricStyle(_immersiveLyrics, _activeLyricIndex);
_activeLyricIndex = -1;
_activeLyricWordIndex = -1;
}
+ private void ApplyLyricLayoutSettings()
+ {
+ var spacing = _settingsService.Current.Lyrics.LineSpacing;
+ _lyrics.Spacing = spacing;
+ _immersiveLyrics.Spacing = Math.Clamp(spacing * 1.25, 6, 40);
+ _desktopLyricsButton.IsEnabled = _settingsService.Current.Lyrics.DesktopSyncEnabled;
+ }
+
+ private static SolidColorBrush LyricActiveBrush(int argb)
+ => new(ColorHelper.FromArgb(
+ (byte)((uint)argb >> 24),
+ (byte)((uint)argb >> 16),
+ (byte)((uint)argb >> 8),
+ (byte)argb));
+
+ private static Color ToColor(int argb)
+ => ColorHelper.FromArgb(
+ (byte)((uint)argb >> 24),
+ (byte)((uint)argb >> 16),
+ (byte)((uint)argb >> 8),
+ (byte)argb);
+
+ private static int ToArgb(Color color)
+ => unchecked((int)((uint)color.A << 24 | (uint)color.R << 16 | (uint)color.G << 8 | color.B));
+
+ private async Task ShowLyricsSettingsAsync()
+ {
+ var current = _settingsService.Current.Lyrics;
+ var wordHighlight = new ToggleSwitch
+ {
+ Header = AppLocalizer.T("逐字高亮", "Word highlighting"),
+ IsOn = current.WordHighlightEnabled
+ };
+ var translation = new ToggleSwitch
+ {
+ Header = AppLocalizer.T("显示翻译", "Show translation"),
+ IsOn = current.ShowTranslation
+ };
+ var romanized = new ToggleSwitch
+ {
+ Header = AppLocalizer.T("显示罗马音", "Show romanization"),
+ IsOn = current.ShowRomanized
+ };
+ var desktopSync = new ToggleSwitch
+ {
+ Header = AppLocalizer.T("同步桌面歌词", "Sync desktop lyrics"),
+ IsOn = current.DesktopSyncEnabled
+ };
+ var fontScale = new Slider
+ {
+ Minimum = 0.75,
+ Maximum = 1.75,
+ StepFrequency = 0.05,
+ Value = current.FontScale
+ };
+ var lineSpacing = new Slider
+ {
+ Minimum = 4,
+ Maximum = 32,
+ StepFrequency = 1,
+ Value = current.LineSpacing
+ };
+ var colorPicker = new ColorPicker
+ {
+ Color = ToColor(current.ActiveWordColor),
+ IsAlphaEnabled = false,
+ IsColorChannelTextInputVisible = true,
+ IsHexInputVisible = true,
+ MinHeight = 220
+ };
+ var content = new StackPanel
+ {
+ Spacing = 12,
+ MinWidth = 360,
+ Children =
+ {
+ wordHighlight,
+ translation,
+ romanized,
+ desktopSync,
+ Text(AppLocalizer.T("歌词字号", "Lyric size"), 13, FontWeights.SemiBold),
+ fontScale,
+ Text(AppLocalizer.T("歌词行距", "Line spacing"), 13, FontWeights.SemiBold),
+ lineSpacing,
+ Text(AppLocalizer.T("当前词颜色", "Active word color"), 13, FontWeights.SemiBold),
+ colorPicker
+ }
+ };
+ var dialog = new ContentDialog
+ {
+ Title = AppLocalizer.T("歌词字幕设置", "Lyric settings"),
+ Content = new ScrollViewer { Content = content, MaxHeight = 560 },
+ PrimaryButtonText = AppLocalizer.T("保存", "Save"),
+ CloseButtonText = AppLocalizer.T("取消", "Cancel"),
+ DefaultButton = ContentDialogButton.Primary,
+ XamlRoot = XamlRoot
+ };
+ ThemeService.PrepareContentDialog(dialog, XamlRoot, _settingsService.Current.Theme);
+ if (await dialog.ShowAsync() != ContentDialogResult.Primary) return;
+
+ await _settingsService.UpdateAsync(settings =>
+ {
+ settings.Lyrics.WordHighlightEnabled = wordHighlight.IsOn;
+ settings.Lyrics.ShowTranslation = translation.IsOn;
+ settings.Lyrics.ShowRomanized = romanized.IsOn;
+ settings.Lyrics.DesktopSyncEnabled = desktopSync.IsOn;
+ settings.Lyrics.FontScale = fontScale.Value;
+ settings.Lyrics.LineSpacing = lineSpacing.Value;
+ settings.Lyrics.ActiveWordColor = ToArgb(colorPicker.Color);
+ });
+ ApplyLyricLayoutSettings();
+ if (_currentLyrics is not null)
+ {
+ RenderLyrics(_currentLyrics);
+ UpdateActiveLyric(_playback.Position);
+ }
+ }
+
private async Task ToggleDesktopLyricsAsync()
{
+ if (!_settingsService.Current.Lyrics.DesktopSyncEnabled)
+ {
+ ShowStatus(
+ AppLocalizer.T("桌面歌词已关闭", "Desktop lyrics are disabled"),
+ AppLocalizer.T("可在歌词设置中重新启用桌面歌词同步。", "Enable desktop lyric sync in lyric settings."),
+ InfoBarSeverity.Informational);
+ return;
+ }
var song = _playback.CurrentSong;
var line = _activeLyricIndex >= 0 && _currentLyrics is not null && _activeLyricIndex < _currentLyrics.Lines.Count
? _currentLyrics.Lines[_activeLyricIndex]
@@ -1087,6 +1654,10 @@ public sealed class NetworkMusicPage : Page
try
{
await currentProvider.InitializeAsync();
+ if (currentProvider.LoginState.LoggedIn && currentProvider.LoginState.IsStale)
+ {
+ await currentProvider.RefreshLoginStateAsync();
+ }
RefreshAccountPicture();
}
catch (Exception exception)
@@ -1100,7 +1671,7 @@ public sealed class NetworkMusicPage : Page
var requestedProviderId = string.Empty;
var qrImage = new Image { Width = 190, Height = 190, Stretch = Stretch.Uniform };
- var qrStatus = Text(AppLocalizer.T("正在创建登录二维码…", "Creating sign-in QR code..."), 12, foreground: SecondaryTextBrush, maxLines: 3);
+ var qrStatus = Text(AppLocalizer.T("点击“扫码登录”后才会创建二维码", "A QR code is created only after you choose QR sign-in"), 12, foreground: SecondaryTextBrush, maxLines: 3);
var cookie = new TextBox
{
Header = AppLocalizer.T("Cookie 登录", "Cookie sign-in"),
@@ -1122,7 +1693,21 @@ public sealed class NetworkMusicPage : Page
var sourceId = source.Id;
var option = new RadioButton
{
- Content = ProviderDisplayName(sourceId),
+ Content = new StackPanel
+ {
+ Spacing = 1,
+ Children =
+ {
+ Text(ProviderDisplayName(sourceId), 12.5, FontWeights.SemiBold),
+ Text(
+ source.LoginState.LoggedIn
+ ? $"{source.LoginState.Nickname} · " + AccountCacheLabel(source.LoginState)
+ : AppLocalizer.T("未登录", "Signed out"),
+ 10.5,
+ foreground: SecondaryTextBrush,
+ maxLines: 1)
+ }
+ },
GroupName = sourceGroup,
IsChecked = string.Equals(sourceId, currentProvider.Id, StringComparison.OrdinalIgnoreCase),
MinHeight = 36
@@ -1158,11 +1743,21 @@ public sealed class NetworkMusicPage : Page
content.Children.Add(accountState);
if (signedIn)
{
- content.Children.Add(Text(
- AppLocalizer.T($"账户权益:{currentProvider.LoginState.VipLevel}", $"Account entitlement: {currentProvider.LoginState.VipLevel}"),
+ var entitlementPanel = new StackPanel { Spacing = 4 };
+ entitlementPanel.Children.Add(Text(
+ AppLocalizer.T($"账户权益 · {AccountCacheLabel(currentProvider.LoginState)}", $"Entitlements · {AccountCacheLabel(currentProvider.LoginState)}"),
12,
- foreground: SecondaryTextBrush,
- maxLines: 2));
+ FontWeights.SemiBold));
+ if (currentProvider.LoginState.Entitlements.Count == 0)
+ {
+ entitlementPanel.Children.Add(Text(AppLocalizer.T("未返回平台会员权益", "No membership grant was returned"), 11, foreground: SecondaryTextBrush));
+ }
+ foreach (var grant in currentProvider.LoginState.Entitlements)
+ {
+ var expiry = grant.ExpiresAt is null ? string.Empty : AppLocalizer.T($" · 至 {grant.ExpiresAt:yyyy-MM-dd}", $" · until {grant.ExpiresAt:yyyy-MM-dd}");
+ entitlementPanel.Children.Add(Text($"{grant.DisplayName} · {grant.Group}{expiry}", 11.5, foreground: grant.Enabled ? PrimaryTextBrush : SecondaryTextBrush, maxLines: 2));
+ }
+ content.Children.Add(entitlementPanel);
}
var qrActions = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
@@ -1188,7 +1783,7 @@ public sealed class NetworkMusicPage : Page
{
content.Children.Add(qrStatus);
content.Children.Add(qrImage);
- qrActions.Children.Add(DarkButton(AppLocalizer.T("刷新二维码", "Refresh QR code"), "\uE72C", StartQr));
+ qrActions.Children.Add(DarkButton(AppLocalizer.T("扫码登录", "QR sign-in"), "\uE8D7", StartQr, primary: true));
qrActions.Children.Add(DarkButton(AppLocalizer.T("取消扫码", "Cancel QR sign-in"), "\uE71A", () =>
CancelQr(AppLocalizer.T("已取消扫码登录", "QR sign-in canceled"))));
content.Children.Add(qrActions);
@@ -1235,6 +1830,15 @@ public sealed class NetworkMusicPage : Page
InfoBarSeverity.Error);
}
}));
+ if (signedIn) actions.Children.Add(DarkButton(AppLocalizer.T("更新权益", "Refresh entitlements"), "\uE72C", async () =>
+ {
+ var refreshed = await currentProvider.RefreshLoginStateAsync(force: true);
+ accountState.Text = refreshed.LoggedIn
+ ? AppLocalizer.T($"已登录:{refreshed.Nickname} · {AccountCacheLabel(refreshed)}", $"Signed in: {refreshed.Nickname} · {AccountCacheLabel(refreshed)}")
+ : AppLocalizer.T("登录已失效", "The sign-in has expired");
+ reopen = true;
+ dialog?.Hide();
+ }));
if (actions.Children.Count > 0) content.Children.Add(actions);
content.Children.Add(Text(AppLocalizer.T("登录仅用于当前音乐来源的账户能力;不绕过会员、地区或版权限制。", "Sign-in only enables the selected provider's account features; it does not bypass membership, regional, or copyright restrictions."), 11, foreground: SecondaryTextBrush, maxLines: 3));
dialog = new ContentDialog
@@ -1245,7 +1849,7 @@ public sealed class NetworkMusicPage : Page
CloseButtonText = AppLocalizer.T("关闭", "Close"),
DefaultButton = ContentDialogButton.Close
};
- if (!signedIn) StartQr();
+ ThemeService.PrepareContentDialog(dialog, XamlRoot, _settingsService.Current.Theme);
await dialog.ShowAsync();
CancelQr(AppLocalizer.T("已关闭账户窗口", "Account dialog closed"));
@@ -1305,6 +1909,7 @@ public sealed class NetworkMusicPage : Page
Content = media,
CloseButtonText = AppLocalizer.T("关闭", "Close")
};
+ ThemeService.PrepareContentDialog(dialog, XamlRoot, _settingsService.Current.Theme);
try
{
await dialog.ShowAsync();
@@ -1488,6 +2093,13 @@ public sealed class NetworkMusicPage : Page
_ => providerId
};
+ private static string AccountCacheLabel(MusicLoginState state)
+ => state.IsStale
+ ? AppLocalizer.T("缓存,待更新", "Cached, refresh pending")
+ : state.IsCached
+ ? AppLocalizer.T("已缓存", "Cached")
+ : AppLocalizer.T("已更新", "Up to date");
+
private static Button NavButton(string glyph, string title, Func action)
=> DarkButton(title, glyph, async () => await action());
@@ -1594,21 +2206,4 @@ public sealed class NetworkMusicPage : Page
private static TextBlock Text(string value, double size, FontWeight? weight = null, Brush? foreground = null, int maxLines = 0)
=> ModernUi.Text(value, size, weight, foreground, maxLines);
- private static MusicPlaybackQuality QualityFromIndex(int index) => index switch
- {
- 0 => MusicPlaybackQuality.Master,
- 2 => MusicPlaybackQuality.Lossless,
- 3 => MusicPlaybackQuality.High,
- 4 => MusicPlaybackQuality.Standard,
- _ => MusicPlaybackQuality.HiRes
- };
-
- private static int IndexFromQuality(MusicPlaybackQuality quality) => quality switch
- {
- MusicPlaybackQuality.Master => 0,
- MusicPlaybackQuality.HiRes => 1,
- MusicPlaybackQuality.Lossless => 2,
- MusicPlaybackQuality.High => 3,
- _ => 4
- };
}
diff --git a/src/box-winUI/Views/PluginHostPage.cs b/src/box-winUI/Views/PluginHostPage.cs
index 29dff82..1b43e27 100644
--- a/src/box-winUI/Views/PluginHostPage.cs
+++ b/src/box-winUI/Views/PluginHostPage.cs
@@ -22,6 +22,7 @@ public sealed class PluginHostPage : ToolPageBase
private readonly ILogService _logService = AppServices.GetRequiredService();
private readonly WebView2EnvironmentFactory _webViewEnvironmentFactory = AppServices.GetRequiredService();
private readonly ISettingsService _settingsService = AppServices.GetRequiredService();
+ private readonly IToolUiDefinitionStore _toolUiDefinitionStore = AppServices.GetRequiredService();
private readonly WebView2 _webView = new();
private readonly TextBox _outputBox = new()
{
@@ -35,6 +36,7 @@ public sealed class PluginHostPage : ToolPageBase
private bool _loaded;
private PluginBridge? _bridge;
private string _origin = string.Empty;
+ private ToolUiDefinition _uiDefinition;
public PluginHostPage(LoadedPlugin plugin, PluginSurface surface, Action? goBack = null)
{
@@ -42,11 +44,15 @@ public sealed class PluginHostPage : ToolPageBase
_surface = surface;
_goBack = goBack;
Background = ModernUi.AppBackground;
+ BindModule(new PluginToolModule(plugin, surface));
+ _uiDefinition = _toolUiDefinitionStore.GetFallback(Module!);
Content = BuildContent();
Loaded += PluginHostPage_Loaded;
Unloaded += PluginHostPage_Unloaded;
}
+ public ToolUiDefinition UiDefinition => _uiDefinition;
+
private UIElement BuildContent()
{
var root = new Grid
@@ -108,6 +114,13 @@ public sealed class PluginHostPage : ToolPageBase
}
_loaded = true;
+ if (Module is not null)
+ {
+ _uiDefinition = await _toolUiDefinitionStore.GetAsync(Module);
+ _statusText.Text = _uiDefinition.IsFallback
+ ? $"插件页面使用安全回退定义:{_uiDefinition.Diagnostic}"
+ : "插件页面定义已加载,第三方内容保持隔离。";
+ }
await LoadAsync();
}
diff --git a/src/box-winUI/Views/Tools/AdaptiveToolPage.DefinitionSurface.cs b/src/box-winUI/Views/Tools/AdaptiveToolPage.DefinitionSurface.cs
new file mode 100644
index 0000000..567c740
--- /dev/null
+++ b/src/box-winUI/Views/Tools/AdaptiveToolPage.DefinitionSurface.cs
@@ -0,0 +1,499 @@
+using System.Globalization;
+using Microsoft.UI.Text;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+using YMhut.Box.Core.Settings;
+using YMhut.Box.Core.Tools;
+
+namespace YMhut.Box.WinUI.Views.Tools;
+
+///
+/// Renders the immutable tool definition as a page surface. The execution
+/// model remains in AdaptiveToolPage; this file only maps safe controls and
+/// actions to the existing execution inputs.
+///
+public abstract partial class AdaptiveToolPage
+{
+ private void RebuildDefinitionSurface()
+ {
+ if (_toolGrid.Children.Count < 2 || Module is null)
+ {
+ return;
+ }
+
+ // Detach the old controls before reusing the shared input elements.
+ _toolGrid.Children.RemoveAt(0);
+ _toolGrid.Children.Insert(0, BuildModernGalleryInputCard(Module));
+ if (_toolGrid.Children.Count == 2)
+ {
+ _toolGrid.Children.RemoveAt(1);
+ _toolGrid.Children.Insert(1, BuildGalleryResultCard());
+ }
+ ArrangeGalleryToolGrid();
+ }
+
+ private FrameworkElement BuildDefinitionInputSurface(IToolModule module)
+ {
+ _definitionStateControls.Clear();
+ _definitionExpanders.Clear();
+ var sections = new StackPanel
+ {
+ Spacing = _spec.PageDensity == ToolPageDensity.Compact ? 10 : 14
+ };
+
+ foreach (var section in _uiDefinition.Sections)
+ {
+ var content = new StackPanel
+ {
+ Spacing = 10
+ };
+ foreach (var control in section.Controls)
+ {
+ var element = BuildDefinitionControl(module, section, control);
+ if (element is not null)
+ {
+ content.Children.Add(element);
+ }
+ }
+
+ if (content.Children.Count == 0)
+ {
+ continue;
+ }
+
+ var title = section.Title.ForLanguage(_settingsService.Current.Language);
+ var description = section.Description.ForLanguage(_settingsService.Current.Language);
+ var header = BuildGallerySectionHeader(
+ section.IsAdvanced ? AppLocalizer.T("高级", "Advanced") : AppLocalizer.T("工作区", "Workspace"),
+ title,
+ description,
+ section.IsAdvanced ? "\uE713" : "\uE8D2",
+ compact: true);
+
+ if (section.IsAdvanced)
+ {
+ var expander = new Expander
+ {
+ IsExpanded = _uiDefinition.AdvancedExpandedByDefault,
+ Header = header,
+ Content = content,
+ HorizontalAlignment = HorizontalAlignment.Stretch
+ };
+ _definitionExpanders[section.Id] = expander;
+ sections.Children.Add(expander);
+ }
+ else
+ {
+ sections.Children.Add(new StackPanel
+ {
+ Spacing = 10,
+ Children = { header, content }
+ });
+ }
+ }
+
+ if (sections.Children.Count == 0)
+ {
+ sections.Children.Add(ModernUi.Text(
+ AppLocalizer.T("此工具没有可验证的输入定义。", "This tool has no verified input definition."),
+ 13,
+ foreground: ModernUi.TextSecondary));
+ }
+
+ var actions = BuildDefinitionActions();
+ if (actions.Children.Count > 0)
+ {
+ sections.Children.Add(new ScrollViewer
+ {
+ HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
+ VerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
+ Content = actions
+ });
+ }
+
+ return sections;
+ }
+
+ private UIElement? BuildDefinitionControl(IToolModule module, ToolUiSection section, ToolUiControl control)
+ {
+ var id = control.Id.ToLowerInvariant();
+ UIElement? element = id switch
+ {
+ "primary" or "input" => BuildPrimaryDefinitionControl(control),
+ "query" => _queryBox.Visibility == Visibility.Visible ? _queryBox : null,
+ "parameter" => _parameterBox.Visibility == Visibility.Visible ? _parameterBox : null,
+ "date-range" => _startDatePicker.Visibility == Visibility.Visible ? BuildDatePanel() : null,
+ "date-start" => _startDatePicker.Visibility == Visibility.Visible ? _startDatePicker : null,
+ "date-end" => _endDatePicker.Visibility == Visibility.Visible ? _endDatePicker : null,
+ "number-primary" => _numberBox.Visibility == Visibility.Visible ? _numberBox : null,
+ "number-secondary" => _secondaryNumberBox.Visibility == Visibility.Visible ? _secondaryNumberBox : null,
+ "number-third" => _thirdNumberBox.Visibility == Visibility.Visible ? _thirdNumberBox : null,
+ "number-fourth" => _fourthNumberBox.Visibility == Visibility.Visible ? _fourthNumberBox : null,
+ "numbers" => UsesNumericPrimaryInput() ? BuildNumberInputPanel() : null,
+ "rules" => _ruleControlsPanel.Visibility == Visibility.Visible ? _ruleControlsPanel : null,
+ "auxiliary" => _auxBox.Visibility == Visibility.Visible ? _auxBox : null,
+ "advanced-toggle" => BuildAdvancedToggle(control),
+ "status" => ModernUi.Text(
+ control.Label.ForLanguage(_settingsService.Current.Language),
+ 13,
+ foreground: ModernUi.TextSecondary),
+ _ => BuildUnmappedDefinitionControl(control)
+ };
+
+ if (element is FrameworkElement frameworkElement)
+ {
+ ApplyDefinitionDefaults(frameworkElement, control);
+ if (!control.IsReadOnly)
+ {
+ _definitionStateControls[control.Id] = frameworkElement;
+ }
+ }
+
+ return element;
+ }
+
+ private UIElement? BuildPrimaryDefinitionControl(ToolUiControl control)
+ {
+ if (_queryBox.Visibility == Visibility.Visible)
+ {
+ return _queryBox;
+ }
+
+ if (_inputBox.Visibility == Visibility.Visible)
+ {
+ return _inputBox;
+ }
+
+ if (UsesNumericPrimaryInput())
+ {
+ return BuildNumberInputPanel();
+ }
+
+ if (_parameterBox.Visibility == Visibility.Visible)
+ {
+ return _parameterBox;
+ }
+
+ return null;
+ }
+
+ private ToggleSwitch BuildAdvancedToggle(ToolUiControl control)
+ {
+ var toggle = new ToggleSwitch
+ {
+ Header = control.Label.ForLanguage(_settingsService.Current.Language),
+ IsOn = string.Equals(control.DefaultValue, "true", StringComparison.OrdinalIgnoreCase),
+ HorizontalAlignment = HorizontalAlignment.Stretch
+ };
+ ToolTipService.SetToolTip(toggle, control.Placeholder ?? control.Label.ForLanguage(_settingsService.Current.Language));
+ return toggle;
+ }
+
+ private UIElement BuildUnmappedDefinitionControl(ToolUiControl control)
+ {
+ var label = control.Label.ForLanguage(_settingsService.Current.Language);
+ return new InfoBar
+ {
+ IsOpen = true,
+ Severity = InfoBarSeverity.Informational,
+ Title = label,
+ Message = AppLocalizer.T("该控件由定义声明,但当前客户端版本不支持执行;已保持安全只读状态。", "This declared control is not supported by this client version and remains safely read-only."),
+ IsClosable = false
+ };
+ }
+
+ private void ApplyDefinitionDefaults(FrameworkElement element, ToolUiControl control)
+ {
+ if (element is TextBox textBox)
+ {
+ if (!string.IsNullOrWhiteSpace(control.Placeholder))
+ {
+ textBox.PlaceholderText = control.Placeholder;
+ }
+
+ if (!string.IsNullOrWhiteSpace(control.DefaultValue) && string.IsNullOrEmpty(textBox.Text))
+ {
+ textBox.Text = control.DefaultValue;
+ }
+
+ if (control.Validation?.MaxLength is int maxLength && maxLength > 0)
+ {
+ textBox.MaxLength = maxLength;
+ }
+ }
+
+ if (element is Control controlElement && control.IsReadOnly)
+ {
+ controlElement.IsEnabled = false;
+ }
+ }
+
+ private StackPanel BuildDefinitionActions()
+ {
+ var actions = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = 10
+ };
+
+ foreach (var action in _uiDefinition.Actions)
+ {
+ var id = action.Id.ToLowerInvariant();
+ var label = action.Label.ForLanguage(_settingsService.Current.Language);
+ var button = ModernUi.PillButton(label, ActionGlyph(id), primary: id is "run" or "execute" or "query" or "format");
+ button.IsEnabled = action.IsEnabled && !_uiDefinition.IsFallback;
+ if (_uiDefinition.IsFallback && !string.IsNullOrWhiteSpace(_uiDefinition.Diagnostic))
+ {
+ button.IsEnabled = false;
+ ToolTipService.SetToolTip(button, AppLocalizer.T("页面定义不可用,已禁用未验证动作。", "Unverified actions are disabled because the page definition is unavailable."));
+ }
+
+ switch (id)
+ {
+ case "run":
+ case "execute":
+ case "query":
+ case "format":
+ case "convert":
+ case "generate":
+ case "process":
+ button.Click += async (_, _) => await RunCurrentToolAsync();
+ break;
+ case "choosefile":
+ case "choose-file":
+ button.Click += (_, _) => BrowseInputFile();
+ break;
+ case "clear":
+ button.Click += (_, _) => ClearInput();
+ break;
+ case "copyresult":
+ case "copy-result":
+ button.Click += (_, _) => CopyResult();
+ break;
+ case "reload":
+ button.Click += async (_, _) => await RunCurrentToolAsync();
+ break;
+ case "cancel":
+ button.Click += (_, _) => CancelRunningTool();
+ break;
+ default:
+ button.IsEnabled = false;
+ ToolTipService.SetToolTip(button, AppLocalizer.T("未验证的工具动作已禁用。", "Unverified tool action is disabled."));
+ break;
+ }
+
+ actions.Children.Add(button);
+ }
+
+ return actions;
+ }
+
+ private static string ActionGlyph(string id)
+ {
+ return id switch
+ {
+ "clear" => "\uE74D",
+ "copyresult" or "copy-result" => "\uE8C8",
+ "choosefile" or "choose-file" => "\uE8E5",
+ "reload" => "\uE72C",
+ "cancel" => "\uE711",
+ "preview" => "\uE7B3",
+ _ => "\uE768"
+ };
+ }
+
+ private ToolUiPageState CaptureToolUiState()
+ {
+ var state = new ToolUiPageState();
+ CaptureVisibleValue(state, "input", _inputBox);
+ CaptureVisibleValue(state, "query", _queryBox);
+ CaptureVisibleValue(state, "parameter", _parameterBox);
+ CaptureVisibleValue(state, "auxiliary", _auxBox);
+ CaptureVisibleValue(state, "number-primary", _numberBox);
+ CaptureVisibleValue(state, "number-secondary", _secondaryNumberBox);
+ CaptureVisibleValue(state, "number-third", _thirdNumberBox);
+ CaptureVisibleValue(state, "number-fourth", _fourthNumberBox);
+ CaptureVisibleValue(state, "date-start", _startDatePicker);
+ CaptureVisibleValue(state, "date-end", _endDatePicker);
+
+ foreach (var entry in _selectedRuleValues)
+ {
+ state.Values[$"rule:{entry.Key}"] = entry.Value;
+ }
+
+ foreach (var entry in _definitionStateControls)
+ {
+ if (TryReadControlValue(entry.Value, out var value))
+ {
+ state.Values[$"definition:{entry.Key}"] = value;
+ }
+ }
+
+ foreach (var entry in _definitionExpanders)
+ {
+ state.ExpandedSections[entry.Key] = entry.Value.IsExpanded;
+ }
+
+ return state;
+ }
+
+ private void RestoreToolUiState()
+ {
+ if (Module is null)
+ {
+ return;
+ }
+
+ var state = _settingsService.GetToolUiState(Module.Id);
+ RestoreValue(state, "parameter", _parameterBox);
+ RestoreValue(state, "number-primary", _numberBox);
+ RestoreValue(state, "number-secondary", _secondaryNumberBox);
+ RestoreValue(state, "number-third", _thirdNumberBox);
+ RestoreValue(state, "number-fourth", _fourthNumberBox);
+ RestoreValue(state, "date-start", _startDatePicker);
+ RestoreValue(state, "date-end", _endDatePicker);
+ RestoreValue(state, "auxiliary", _auxBox);
+ RestoreValue(state, "query", _queryBox);
+ RestoreValue(state, "input", _inputBox);
+
+ foreach (var rule in _ruleInputControls)
+ {
+ if (!state.Values.TryGetValue($"rule:{rule.Key}", out var value))
+ {
+ continue;
+ }
+
+ _selectedRuleValues[rule.Key] = value;
+ _viewModel.SelectedRule = value;
+ ApplyRuleControlValue(rule.Value, value);
+ }
+
+ foreach (var entry in _definitionStateControls)
+ {
+ if (state.Values.TryGetValue($"definition:{entry.Key}", out var value))
+ {
+ ApplyControlValue(entry.Value, value);
+ }
+ }
+
+ foreach (var entry in _definitionExpanders)
+ {
+ if (state.ExpandedSections.TryGetValue(entry.Key, out var expanded))
+ {
+ entry.Value.IsExpanded = expanded;
+ }
+ }
+ }
+
+ private static void CaptureVisibleValue(ToolUiPageState state, string key, FrameworkElement control)
+ {
+ if (control.Visibility == Visibility.Visible && TryReadControlValue(control, out var value))
+ {
+ state.Values[key] = value;
+ }
+ }
+
+ private static bool TryReadControlValue(FrameworkElement control, out string value)
+ {
+ switch (control)
+ {
+ case TextBox textBox:
+ value = textBox.Text;
+ return true;
+ case AutoSuggestBox suggestBox:
+ value = suggestBox.Text;
+ return true;
+ case NumberBox numberBox when !double.IsNaN(numberBox.Value):
+ value = numberBox.Value.ToString("R", CultureInfo.InvariantCulture);
+ return true;
+ case ComboBox comboBox when comboBox.SelectedItem is ComboBoxItem item:
+ value = item.Tag as string ?? item.Content?.ToString() ?? string.Empty;
+ return true;
+ case RadioButtons radioButtons when radioButtons.SelectedItem is ComboBoxItem item:
+ value = item.Tag as string ?? item.Content?.ToString() ?? string.Empty;
+ return true;
+ case ToggleSwitch toggleSwitch:
+ value = toggleSwitch.IsOn ? "true" : "false";
+ return true;
+ case CalendarDatePicker datePicker when datePicker.Date is DateTimeOffset date:
+ value = date.ToString("O", CultureInfo.InvariantCulture);
+ return true;
+ default:
+ value = string.Empty;
+ return false;
+ }
+ }
+
+ private static void RestoreValue(ToolUiPageState state, string key, FrameworkElement control)
+ {
+ if (state.Values.TryGetValue(key, out var value))
+ {
+ ApplyControlValue(control, value);
+ }
+ }
+
+ private static void ApplyControlValue(FrameworkElement control, string value)
+ {
+ switch (control)
+ {
+ case TextBox textBox:
+ textBox.Text = value;
+ break;
+ case AutoSuggestBox suggestBox:
+ suggestBox.Text = value;
+ break;
+ case NumberBox numberBox when double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var number):
+ numberBox.Value = Math.Clamp(number, numberBox.Minimum, numberBox.Maximum);
+ break;
+ case ComboBox comboBox:
+ SelectTaggedItem(comboBox.Items, value, index => comboBox.SelectedIndex = index);
+ break;
+ case RadioButtons radioButtons:
+ SelectTaggedItem(radioButtons.Items, value, index => radioButtons.SelectedIndex = index);
+ break;
+ case ToggleSwitch toggleSwitch when bool.TryParse(value, out var enabled):
+ toggleSwitch.IsOn = enabled;
+ break;
+ case CalendarDatePicker datePicker when DateTimeOffset.TryParse(
+ value,
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.RoundtripKind,
+ out var date):
+ datePicker.Date = date;
+ break;
+ }
+ }
+
+ private static void SelectTaggedItem(IList
+
+
+
+## ❤️ 前言
+
+早在10年前后的样子,那会在用网页版QQ的时候我就已经开始使用酷狗音乐了(也是十来年的老粉了),所以这些年收藏的歌曲全部都在上面.后来我也尝试开始使用网易云或QQ音乐,也尝试把酷狗的歌单导入进去,但是效果都不尽人意.我听的大多是日漫OP,好多歌曲都没办法找到.
+
+兜兜转转最后还是回到酷狗,但是在Mac端的酷狗,时常可能会出现不能播放的情况,虽说界面没什么功能,但也挺好的.在网友的安利下,我现在一直是在酷狗的[概念版](https://t1.kugou.com/d2tBza3CSV2)上听歌,并且是市面上为数不多能免费听VIP歌曲的音乐播放软件了,力推.
+
+我在我的个人介绍页面说我特别喜欢听歌,尤其是日漫OP.怎么证明呢?(之前我网页版歌单也年久失修了)那就自己开发一个音乐播放器.
+
+
+## ✨ 特性
+
+- ✅ 使用 Vue.js 全家桶开发
+- 🔴 酷狗账号登录(扫码/手机/账号登录)
+- 📃 支持歌词显示
+- 📻 每日推荐歌曲
+- 🔕 无任何社交功能
+- 🔗 官方服务器直连
+- ✔️ 每日自动领取VIP,登录就是VIP
+- 🎨 主题色切换
+- 👋 启动问候语
+- ⚙️ 多平台支持
+- 🧩 超级插件系统
+- 🧭 布局个性化设置
+- 🥚 隐藏功能与彩蛋系统
+- 🚀 新手引导系统
+- ✨ 更多特性由你来探索
+
+## 📢 Todo List
+- [x] 📺 支持 MV 播放
+- [x] 🌚 Light/Dark Mode 自动切换
+- [x] 👆 支持 Touch Bar
+- [x] 🖥️ 支持 PWA
+- [x] ⌨️ 全局快捷键
+- [x] 🤟 多语言支持
+- [x] 📻 桌面歌词
+- [x] ⚙️ 系统架构优化
+- [x] 🎶 歌曲、歌单/收藏、取消
+
+[查看更新日志](http://music.moekoe.cn/changelog.html)
+
+## 📦️ 安装
+
+### 1. 客户端安装
+
+访问本项目的 [Releases](https://github.com/iAJue/MoeKoeMusic/releases) 页面下载安装包。
+
+### 2. WEB端安装(docker)
+
+* 注意:部署后请开放服务器对应端口才可使用,或者使用反向代理实现域名访问。
+
+ 1. 方式一:快速启动(推荐)
+
+ ```
+ git clone https://github.com/iAJue/MoeKoeMusic.git
+ cd MoeKoeMusic
+ git submodule update --init --recursive
+ docker compose up -d &
+ ```
+
+ 2. ~~方式二:使用docker-compose一键安装 (镜像暂未上传官方)~~
+
+ ```
+ docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest
+ ```
+
+ 3. 方式三:宝塔容器编排
+
+ * 远程镜像,版本可能会落后于官方
+
+ ```
+ version: '3.3'
+
+ services:
+ moekoe-music:
+ # 镜像地址
+ image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest
+ container_name: moekoe-music # 容器名
+ restart: unless-stopped # 自动重启
+ build:
+ context: .
+ dockerfile: Dockerfile
+ environment:
+ - PORT=6521
+ - platform=lite
+ ports: # 端口映射
+ - "8080:8080" # 前端服务
+ - "6521:6521" # 接口服务
+
+ ```
+
+ 复制内容上面的内容,粘贴到宝塔面板的容器编排里面,编排名称为MoeKoeMusic,点击部署即可。
+
+### 3. 一键部署
+[](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)
+
+需在环境变量(VITE_APP_API_URL)中填写自己的API地址
+
+## ⚙️ 开发
+
+1. 克隆本仓库
+
+```sh
+git clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git
+```
+
+2. 进入目录并安装依赖
+
+```sh
+cd MoeKoeMusic
+npm run install-all
+```
+3. 启动开发者模式
+```sh
+npm run dev
+```
+4. 打包项目
+```sh
+npm run build
+```
+5. 编译项目
+ - Windows:
+ ```sh
+ npm run electron:build:win [默认 NSIS 安装包]
+ ```
+ - Linux:
+ ```sh
+ npm run electron:build:linux [默认 AppImage 格式]
+ ```
+ - macOS:
+ ```sh
+ npm run electron:build:macos [默认 macOS 双架构]
+ ```
+
+
+更多命令请查看 `package.json` 文件 `scripts`
+
+## 👷♂️ 编译客户端
+
+如果在 Release 页面没有找到适合你的设备的安装包的话,你可以根据下面的步骤来打包自己的客户端。
+
+1. 安装 [Node.js](https://nodejs.org/en/),并确保 `Node.js` 版本 >= 18.0.0。
+
+2. 使用 `git clone https://github.com/iAJue/MoeKoeMusic.git` 克隆本仓库到本地。
+
+3. 使用 `npm install` 安装项目依赖。
+4. 编译API服务端
+ - Windows:
+ ```sh
+ npm run build:api:win
+ ```
+ - Linux:
+ ```sh
+ npm run build:api:linux
+ ```
+ - macOS:
+ ```sh
+ npm run build:api:macos
+ ```
+
+5. 选择下列的命令来打包适合的你的安装包,打包出来的文件在 `/dist_electron` 目录下。了解更多信息可访问 [electron-builder 文档](https://www.electron.build/cli)
+
+
+#### 1. 打包 macOS 平台
+ - 通用的 macOS 包(Intel 和 Apple Silicon 双架构):
+ ```
+ npm run electron:build -- --mac --universal
+ ```
+ - 仅 Intel 架构:
+ ```
+ npm run electron:build -- --mac --x64
+ ```
+ - 仅 Apple Silicon 架构:
+ ```
+ npm run electron:build -- --mac --arm64
+ ```
+
+
+#### 2. 打包 Windows 平台
+
+ - 默认 NSIS 安装包(适合大多数 Windows 用户):
+ ```
+ npm run electron:build -- --win
+ ```
+ - 为 Windows 创建 EXE 文件和 Squirrel 安装包:
+ ```
+ npm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel
+ ```
+ - --ia32 为 32 位 Windows 架构。
+ - --x64 为 64 位 Windows 架构。
+ - --arm64 为 ARM Windows 架构(Surface 等设备)。
+
+ - 为 Windows 生成便携式的 EXE 文件(免安装):
+ ```
+ npm run electron:build -- --win --portable
+ ```
+#### 3. 打包 Linux 平台
+ - 默认 AppImage 格式(适用于大多数 Linux 发行版):
+
+ ```
+ npm run electron:build -- --linux
+ ```
+ - snap(适用于 Ubuntu 和支持 snap 的发行版):
+ ```
+ npm run electron:build -- --linux --target snap
+ ```
+ - deb(适用于 Debian/Ubuntu 系列):
+ ```
+ npm run electron:build -- --linux --target deb
+ ```
+ - rpm(适用于 Red Hat/Fedora 系列):
+ ```
+ npm run electron:build -- --linux --target rpm
+ ```
+ - ARM64架构(ARM v8+):
+ ```
+ npm run build:api:linux-arm64 //编译API
+ npm run electron:build:linux-arm64 //编译主程序
+ ```
+
+#### 4. 打包所有平台
+
+ 如果需要同时生成 Windows、macOS 和 Linux 的安装包,可以使用以下命令:
+ ```
+ npm run electron:build -- -mwl
+ ```
+
+#### 5. 自定义编译设置
+
+您可以根据需要添加其他选项来进一步自定义打包,例如指定 x64 和 arm64 架构,或选择不同的目标格式。
+
+## ⭐ 支持项目
+
+如果您觉得这个项目对您有帮助,欢迎给我们一个 Star!您的支持是我们持续改进的动力。
+
+[](https://github.com/iAJue/MoeKoeMusic)
+
+## ✅ 反馈
+
+如有任何问题或建议,欢迎提交 issue 或 pull request。
+
+## ⚠️ 免责声明
+0. 本程序是酷狗第三方客户端,并非酷狗官方,需要更完善的功能请下载官方客户端体验.
+1. 本项目仅供学习使用,请尊重版权,请勿利用此项目从事商业行为及非法用途!
+2. 使用本项目的过程中可能会产生版权数据。对于这些版权数据,本项目不拥有它们的所有权。为了避免侵权,使用者务必在 24 小时内清除使用本项目的过程中所产生的版权数据。
+3. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。
+1. 禁止在违反当地法律法规的情况下使用本项目。对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。
+2. 音乐平台不易,请尊重版权,支持正版。
+3. 本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。
+4. 如果官方音乐平台觉得本项目不妥,可联系本项目更改或移除。
+
+
+## 📜 开源许可
+
+本项目仅供个人学习研究使用,禁止用于商业及非法用途。
+
+基于 [GNU General Public License v2.0 (GPL-2.0)](https://github.com/iAJue/MoeKoeMusic/blob/main/LICENSE) 许可进行开源。
+
+## 👍 灵感来源
+
+API 源代码来自 [MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi)
+
+- [Apple Music](https://music.apple.com)
+- [YouTube Music](https://music.youtube.com)
+- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
+- [酷狗音乐](https://kugou.com/)
+
+## 🖼️ 截图
+
+
+
+
+
+
+
+
+
+
+
+
+
+## 🗓️ Star History
+
+[](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)
diff --git a/third_party/MoeKoeMusic/UPSTREAM_COMMIT b/third_party/MoeKoeMusic/UPSTREAM_COMMIT
new file mode 100644
index 0000000..9c12141
--- /dev/null
+++ b/third_party/MoeKoeMusic/UPSTREAM_COMMIT
@@ -0,0 +1 @@
+4581660209c9cdd07669e8bd5bcbe136b7d91e02
diff --git a/third_party/MoeKoeMusic/YMhut_PATCHES.md b/third_party/MoeKoeMusic/YMhut_PATCHES.md
new file mode 100644
index 0000000..3081ff0
--- /dev/null
+++ b/third_party/MoeKoeMusic/YMhut_PATCHES.md
@@ -0,0 +1,20 @@
+# YMhut Box changes
+
+Imported from `MoeKoeMusic/MoeKoeMusic` commit
+`4581660209c9cdd07669e8bd5bcbe136b7d91e02`. The `api` directory is the
+repository's `MakcRe/KuGouMusicApi` submodule at commit
+`06560e3e053bda1ab830750db6f645bab703f824`.
+
+YMhut Box adds an internal Windows sidecar entry point and these local-only
+controls:
+
+- loopback-only random-port startup and a versioned JSON readiness message;
+- a per-process authentication token and parent-process watchdog;
+- authenticated health and graceful shutdown routes;
+- a KuGou-host-only compatibility bridge used by the existing signed C# adapter;
+- a two-minute allowlist cache limited to anonymous, public GET routes;
+- relocatable production dependencies staged with pnpm's hoisted linker;
+- quiet logging that does not print query strings, credentials, or response bodies.
+
+The original API source remains under `api/`. Original project notices are in
+`MOEKOE_README.md`, `MOEKOE_LICENSE`, `api/README.md`, and `api/LICENSE`.
diff --git a/third_party/MoeKoeMusic/api/.dockerignore b/third_party/MoeKoeMusic/api/.dockerignore
new file mode 100644
index 0000000..5840756
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/.dockerignore
@@ -0,0 +1,13 @@
+/**
+!/module
+!/plugins
+!/public
+!/static
+!/util
+!/app.js
+!/server.js
+!/package.json
+!/package-lock.json
+!/pnpm-lock.yaml
+!/index.js
+!/main.js
diff --git a/third_party/MoeKoeMusic/api/.env.example b/third_party/MoeKoeMusic/api/.env.example
new file mode 100644
index 0000000..64e2298
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/.env.example
@@ -0,0 +1,21 @@
+## 配置设备平台
+### lite: 酷狗概念版, 默认为手机版
+platform=''
+
+## 服务端口 (可选)
+PORT=''
+
+## HTTP 代理地址 (可选)
+KUGOU_API_PROXY=''
+
+## 设备id(可选,建议固定 )
+## KUGOU_API_GUID 建议为 uuidv4
+## KUGOU_API_DEV 建议长度为 10 位的字符串
+## MAC地址
+KUGOU_API_GUID=''
+KUGOU_API_DEV=''
+KUGOU_API_MAC=''
+## webglhash 指纹,如:9655615838832711846
+## 如果想使用正式的,可以运行 public/sid_edt_generator.html 来获取固定
+KUGOU_API_WEBGL=''
+
diff --git a/third_party/MoeKoeMusic/api/.github/workflows/build.yml b/third_party/MoeKoeMusic/api/.github/workflows/build.yml
new file mode 100644
index 0000000..5927f3f
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/.github/workflows/build.yml
@@ -0,0 +1,148 @@
+# 发行版本部署
+## 多端部署
+name: Build Release
+
+## 权限配置
+permissions:
+ contents: write
+
+on:
+ push:
+ tags:
+ - v*
+
+jobs:
+ # window 端打包
+ build-windows:
+ name: Build for Windows
+ runs-on: windows-latest
+ timeout-minutes: 30
+ steps:
+ # 检出 Git 仓库
+ - name: Check out Git repository
+ uses: actions/checkout@v4
+ # 安装 Node.js
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18.x'
+ # 安装项目依赖
+ - name: Install Dependencies
+ run: npm install
+ # 构建
+ - name: Build App for Windows
+ run: npm run pkgwin
+ shell: bash
+ # 上传构建产物
+ - name: Upload Windows artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: app-win
+ if-no-files-found: ignore
+ path: |
+ bin/*.exe
+ #创建 GitHub Release
+ - name: Create Release
+ uses: softprops/action-gh-release@v2
+ if: startsWith(github.ref, 'refs/tags/v')
+ with:
+ token: ${{ secrets.GITHUB_TOKEN}}
+ draft: false
+ prerelease: false
+ files: bin/*.exe
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN}}
+
+ # mac 端打包
+ build-masos:
+ name: Build for MacOS
+ runs-on: macos-latest
+ timeout-minutes: 30
+ steps:
+ # 检出 Git 仓库
+ - name: Check out Git repository
+ uses: actions/checkout@v4
+ # 安装 Node.js
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18.x'
+ # 安装项目依赖
+ - name: Install Dependencies
+ run: npm install
+ # 构建
+ - name: Build App for Macos
+ run: npm run pkgmacos
+ shell: bash
+ # 上传构建产物
+ - name: Upload Macos artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: app-macos
+ if-no-files-found: ignore
+ path: |
+ bin/*
+ #创建 GitHub Release
+ - name: Create Release
+ uses: softprops/action-gh-release@v2
+ if: startsWith(github.ref, 'refs/tags/v')
+ with:
+ token: ${{ secrets.GITHUB_TOKEN}}
+ draft: false
+ prerelease: false
+ files: bin/*
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN}}
+
+ # Linux 端打包
+ build-linux:
+ name: Build for Linux
+ runs-on: ubuntu-22.04
+ timeout-minutes: 30
+ steps:
+ # 检出 Git 仓库
+ - name: Check out Git repository
+ uses: actions/checkout@v4
+ # 安装 Node.js
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '18.x'
+ # 更新 Ubuntu 软件源
+ - name: Ubuntu Update with sudo
+ run: sudo apt-get update
+ # 安装 RPM 和 Pacman 插件
+ - name: Install RPM & Pacman (on Ubuntu)
+ run: |
+ sudo apt-get install --no-install-recommends -y rpm &&
+ sudo apt-get install --no-install-recommends -y libarchive-tools &&
+ sudo apt-get install --no-install-recommends -y libopenjp2-tools
+ # 安装项目依赖
+ - name: Install Dependencies
+ run: npm install
+ # 构建
+ - name: Build App for Linux
+ run: npm run pkglinux
+ shell: bash
+ # 上传构建产物
+ - name: Upload Linux artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: app-linux
+ if-no-files-found: ignore
+ path: |
+ bin/*
+ #创建 GitHub Release
+ - name: Create Release
+ uses: softprops/action-gh-release@v2
+ if: startsWith(github.ref, 'refs/tags/v')
+ with:
+ token: ${{ secrets.GITHUB_TOKEN}}
+ draft: false
+ prerelease: false
+ files: bin/*
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN}}
diff --git a/third_party/MoeKoeMusic/api/.gitignore b/third_party/MoeKoeMusic/api/.gitignore
new file mode 100644
index 0000000..8af652e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/.gitignore
@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+test
+bin/*
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+.env
\ No newline at end of file
diff --git a/third_party/MoeKoeMusic/api/.prettierrc.json b/third_party/MoeKoeMusic/api/.prettierrc.json
new file mode 100644
index 0000000..084e99f
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/.prettierrc.json
@@ -0,0 +1,19 @@
+{
+ "arrowParens": "always",
+ "bracketSameLine": false,
+ "bracketSpacing": true,
+ "embeddedLanguageFormatting": "auto",
+ "htmlWhitespaceSensitivity": "css",
+ "insertPragma": false,
+ "jsxSingleQuote": false,
+ "printWidth": 150,
+ "proseWrap": "always",
+ "quoteProps": "preserve",
+ "requirePragma": false,
+ "semi": true,
+ "singleQuote": true,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "useTabs": false,
+ "vueIndentScriptAndStyle": false
+}
diff --git a/third_party/MoeKoeMusic/api/CLAUDE.md b/third_party/MoeKoeMusic/api/CLAUDE.md
new file mode 100644
index 0000000..78b591b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/CLAUDE.md
@@ -0,0 +1,184 @@
+# CLAUDE.md
+
+> KuGouMusicApi 项目指南 — 酷狗音乐 NodeJS 版 API
+
+## 项目概述
+
+本项目是酷狗音乐的非官方 NodeJS API 服务,通过伪造请求头和调用官方 API 实现酷狗音乐的全部功能。支持两种运行模式:HTTP 服务模式(Express)和编程式调用模式(作为库引入)。
+
+- **作者**: Lines(基于 MakcRe 的项目)
+- **版本**: 1.5.1
+- **Node 要求**: >= 12
+- **License**: MIT
+
+## 技术栈
+
+- **运行时**: Node.js
+- **HTTP 框架**: Express 4.x
+- **HTTP 客户端**: Axios
+- **加密库**: crypto-js(AES/MD5)、node-forge(RSA)
+- **压缩库**: pako(zlib 解压,用于 KRC 歌词解码)
+- **大整数库**: big-integer(用于 MID 计算)
+- **开发工具**: nodemon、prettier、esbuild、pkg
+
+## 项目结构
+
+```
+KuGouMusicApi/
+├── app.js # CLI 入口(shebang),调用 server.startService()
+├── index.js # 开发入口,require('./app')
+├── main.js # 编程式 API 入口(作为库使用时)
+├── server.js # Express 服务器核心(CORS、Cookie、路由注册)
+├── module/ # API 模块目录(约 160 个接口)
+│ ├── search.js # 搜索
+│ ├── login.js # 登录
+│ ├── login_cellphone.js # 手机登录
+│ ├── song_url.js # 歌曲 URL
+│ ├── playlist_detail.js # 歌单详情
+│ └── ... # 其他接口
+├── util/ # 工具库
+│ ├── index.js # 统一导出入口
+│ ├── config.json # 平台配置(appid、clientver 等)
+│ ├── crypto.js # 加密函数(AES、RSA、MD5、SHA1)
+│ ├── helper.js # 签名函数(signature、sign、signKey)
+│ ├── request.js # HTTP 请求封装(签名、代理、响应处理)
+│ ├── util.js # 通用工具(随机数、Cookie、歌词解码、MID、WebGL 指纹)
+│ ├── runtime.js # 运行时配置(CLI 参数、代理解析)
+│ ├── generate_simulate.js # 行为指纹模拟生成(AES+RSA 加密)
+│ ├── apicache.js # API 响应缓存中间件
+│ └── memory-cache.js # 内存缓存实现
+├── public/ # 前端页面
+│ ├── index.html # 接口文档首页
+│ ├── login_captcha.html # 登录页(依赖 WASM)
+│ ├── login_captcha_simulate.html # 登录页(纯 JS 模拟,不依赖 WASM)
+│ ├── sid_edt_generator.html # SID/EDT 生成工具页
+│ └── verify-pkg/ # WASM 验证码包
+├── docs/ # 接口文档
+├── .env.example # 环境变量配置示例
+├── Dockerfile # Docker 部署配置
+├── vercel.json # Vercel 部署配置
+└── package.json
+```
+
+## 核心架构
+
+### 请求流程
+
+```
+客户端请求 → Express 中间件(CORS → Cookie 解析 → 平台标识注入 → 缓存)
+ → 路由处理器(参数合并 → Cookie 格式化 → Authorization 头解析)
+ → API 模块函数(构建请求参数 → 生成签名 → 调用 createRequest)
+ → createRequest(注入设备标识 → 签名 → 代理 → 发送请求 → 响应处理)
+ → 酷狗服务器
+```
+
+### 签名机制
+
+所有 API 请求都需要签名验证,支持三种签名类型:
+- **Android 签名**(默认): `MD5(盐值 + 排序参数 + 请求体 + 盐值)`
+- **Web 签名**: `MD5(盐值 + 排序参数 + 盐值)`
+- **Register 签名**: `MD5("1014" + 排序值 + "1014")`
+
+区分标准版和概念版(lite),使用不同的盐值和 appid。
+
+### 行为指纹(SID/EDT)
+
+酷狗服务端会检测请求的行为指纹,需要生成模拟数据:
+- **SID**: RSA-OAEP 加密的 AES 密钥(Base64)
+- **EDT**: AES-128-CBC 加密的行为数据(Base64)
+- 行为数据包括:鼠标轨迹(贝塞尔曲线)、滚动事件、窗口事件、WebGL 指纹
+
+### 双平台支持
+
+通过 `.env` 中的 `platform` 环境变量切换:
+- **标准版**(默认): `appid=1005`, `clientver=20489`
+- **概念版 lite**: `appid=3116`, `clientver=11440`
+
+## 常用命令
+
+```bash
+# 安装依赖
+npm install
+
+# 开发模式(热重载)
+npm run dev
+
+# 生产模式
+npm start
+
+# 自定义端口
+PORT=4000 npm run dev
+
+# 使用代理
+KUGOU_API_PROXY=http://127.0.0.1:7890 npm run dev
+
+# CLI 参数方式设置代理
+node app.js --proxy=http://127.0.0.1:7890 --platform=lite
+```
+
+## 环境变量
+
+| 变量 | 说明 | 默认值 |
+|------|------|--------|
+| `platform` | 平台类型:`lite`(概念版)或空(标准版) | 空 |
+| `PORT` | 服务端口 | 3000 |
+| `HOST` | 监听地址 | 空(所有地址) |
+| `KUGOU_API_PROXY` | HTTP 代理地址 | 空 |
+| `KUGOU_API_GUID` | 设备 GUID(建议 UUID v4) | 自动生成 |
+| `KUGOU_API_DEV` | 开发设备标识(10位大写字符串) | 自动生成 |
+| `KUGOU_API_MAC` | 设备 MAC 地址 | 02:00:00:00:00:00 |
+| `KUGOU_API_WEBGL` | WebGL 指纹哈希 | 自动生成随机值 |
+
+## 开发规范
+
+### 添加新接口
+
+1. 在 `module/` 目录下创建新的 `.js` 文件
+2. 文件名即为路由路径(`_` 替换为 `/`),如 `user_detail.js` → `/user/detail`
+3. 模块导出一个函数:`(params, useAxios) => Promise`
+ - `params`: 合并后的请求参数(含 cookie、query、body)
+ - `useAxios`: 请求工厂函数,调用 `createRequest` 发送请求
+
+```javascript
+// module/example.js
+module.exports = (params, useAxios) => {
+ return new Promise((resolve, reject) => {
+ useAxios({
+ baseURL: 'https://gateway.kugou.com',
+ url: '/v1/example',
+ method: 'GET',
+ params: { keyword: params.keyword },
+ cookie: params.cookie,
+ }).then(resolve).catch(reject);
+ });
+};
+```
+
+### 签名选择
+
+- 大多数接口使用 `android` 签名(默认)
+- Web 相关接口使用 `web` 签名
+- 设备注册接口使用 `register` 签名
+
+### Cookie 传递
+
+Cookie 通过以下方式传递:
+- URL 查询参数: `?cookie=token%3Dxxx%3Buserid%3Dxxx`
+- 请求体: `{ cookie: "token=xxx;userid=xxx" }`
+- Authorization 头: `Authorization: token=xxx;userid=xxx`
+
+服务端会自动将 Cookie 字符串转换为 JSON 对象并合并。
+
+## 代码风格
+
+- 使用 Prettier 格式化(配置见 `.prettierrc.json`)
+- 2 空格缩进
+- 单引号
+- 无分号(Prettier 默认)
+
+## 部署
+
+- **Docker**: 使用项目根目录的 `Dockerfile`
+- **Vercel**: 使用 `vercel.json` 配置
+- **pkg 打包**: `npm run pkgwin` / `npm run pkglinux` / `npm run pkgmacos`
+- **esbuild 打包**: `npm run pkgjs`
diff --git a/third_party/MoeKoeMusic/api/Dockerfile b/third_party/MoeKoeMusic/api/Dockerfile
new file mode 100644
index 0000000..c4ab6eb
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/Dockerfile
@@ -0,0 +1,28 @@
+FROM node:lts-alpine
+
+RUN apk add --no-cache tini
+
+RUN corepack enable
+
+RUN npm config set registry https://registry.npmmirror.com && npm install -g pnpm --force
+
+ENV NODE_ENV=production
+
+WORKDIR /app
+
+RUN chown node:node /app
+
+COPY --chown=node:node package.json pnpm-lock.yaml ./
+
+USER node
+
+#RUN pnpm install --prod --frozen-lockfile --ignore-scripts false
+
+RUN npm install --production --ignore-scripts=false
+
+COPY --chown=node:node . ./
+
+EXPOSE 3000
+
+ENTRYPOINT ["/sbin/tini", "--"]
+CMD ["node", "app.js"]
diff --git a/third_party/MoeKoeMusic/api/LICENSE b/third_party/MoeKoeMusic/api/LICENSE
new file mode 100644
index 0000000..a75e629
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/LICENSE
@@ -0,0 +1,14 @@
+MIT License
+
+Copyright (c) 2023 MakcRe
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/third_party/MoeKoeMusic/api/README.md b/third_party/MoeKoeMusic/api/README.md
new file mode 100644
index 0000000..b5bcb1c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/README.md
@@ -0,0 +1,259 @@
+# KuGouMusic API
+
+酷狗音乐 NodeJS 版 API
+
+[](https://github.com/MakcRe)
+
+
+
+
+
+
+
+
+[//]: # '
'
+
+
+
+
+
+## 灵感来自
+
+[Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
+
+## 环境要求
+
+需要 NodeJS 12+ 环境
+
+## 工作原理
+
+跨站请求伪造 (CSRF), 伪造请求头 , 调用官方 API
+
+## 免责声明
+
+> 1. 本项目仅供学习使用,请尊重版权,请勿利用此项目从事商业行为及非法用途!
+> 2. 使用本项目的过程中可能会产生版权数据。对于这些版权数据,本项目不拥有它们的所有权。为了避免侵权,使用者务必在 24 小时内清除使用本项目的过程中所产
+> 生的版权数据。
+> 3. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停
+> 工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。
+> 4. **禁止在违反当地法律法规的情况下使用本项目。** 对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,本
+> 项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。
+> 5. 音乐平台不易,请尊重版权,支持正版。
+> 6. 本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。
+> 7. 如果官方音乐平台觉得本项目不妥,可联系本项目更改或移除。
+
+### 安装
+
+```shell
+$ git clone git@github.com:MakcRe/KuGouMusicApi.git
+$ cd KuGouMusicApi
+$ npm install
+```
+
+### 使用接口为概念版
+
+```
+$ 复制 .env.example 为 .env,并且把里面的 `platform=''` 改为 `platform=lite`
+$ 注意不同版本的平台的 token 是不通用的。
+```
+
+### 运行
+
+```shell
+$ npm run dev
+```
+
+服务器启动默认端口为 3000, 若不想使用 3000 端口 , 可使用以下命令 : Mac/Linux
+
+```shell
+$ PORT=4000 npm run dev
+```
+
+windows 下使用 git-bash 或者 cmder 等终端执行以下命令 :
+
+```shell
+$ set PORT=4000 && npm run dev
+```
+
+windows 下使用 PowerShell 等终端执行以下命令 :
+
+```shell
+$ $Env:PORT=4000; npm run dev
+```
+
+服务器启动默认 host 为 localhost,如果需要更改, 可使用以下命令 : Mac/Linux
+
+```shell
+$ HOST=127.0.0.1 npm run dev
+```
+
+windows 下使用 git-bash 或者 cmder 等终端执行以下命令 :
+
+```shell
+$ set HOST=127.0.0.1 && npm run dev
+```
+
+windows 下使用 PowerShell 等终端执行以下命令 :
+
+```shell
+$ $Env:HOST=127.0.0.1; npm run dev
+```
+
+### 代理配置
+
+如需通过 HTTP 代理请求酷狗接口,可在启动前设置 `KUGOU_API_PROXY` 环境变量,例如:
+
+```shell
+$ $Env:KUGOU_API_PROXY='http://127.0.0.1:7890'; npm run dev
+```
+
+也可以在启动命令后追加代理参数:
+
+```shell
+$ node app.js --proxy=http://127.0.0.1:7890
+```
+
+## Vercel 部署
+
+### 操作方法
+
+1. fork 此项目
+2. 在 Vercel 官网点击 `New Project`
+3. 点击 `Import Git Repository` 并选择你 fork 的此项目并点击 `import`
+4. 点击 `PERSONAL ACCOUNT` 的 `select`
+5. 直接点 `Continue`
+6. 若需要部署版本为概念版(不需要该步骤可以跳过),在 `Environment Variables` 添加 `key` 为 `platform`,`Value (Will Be Encrypted)` 为 `lite` 然后点击
+ `Add`
+7. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可
+
+## 功能特性
+
+1. 登录
+2. 刷新登录
+3. 发送验证码
+4. dfid 获取
+5. 获取用户额外信息
+6. 获取用户 vip 信息
+7. 获取用户歌单
+8. 获取用户关注歌手
+9. 获取用户听歌历史排行
+10. 获取用户最近听歌历史
+11. 获取继续播放信息(对应手机版首页显示继续播放入口)
+12. 收藏歌单/新建歌单
+13. 取消收藏歌单/删除歌单
+14. 对歌单添加歌曲
+15. 对歌单删除歌曲
+16. 新碟上架
+17. 专辑信息
+18. 专辑详情
+19. 专辑音乐列表
+20. 获取音乐 URL
+21. 获取歌曲高潮部分
+22. 搜索
+23. 默认搜索关键词
+24. 综合搜索
+25. 热搜列表
+26. 搜索建议
+27. 歌词搜索
+28. 获取歌词
+29. 歌单分类
+30. 歌单
+31. 主题歌单
+32. 音效歌单
+33. 获取歌单详情
+34. 获取歌单所有歌曲
+35. 获取歌单所有歌曲(新版)
+36. 相似歌单
+37. 获取主题歌单所有歌曲
+38. 获取主题音乐
+39. 获取主题音乐详情
+40. 歌曲推荐
+41. 获取歌手和专辑图片
+42. 获取歌手图片
+43. 获取音乐相关信息
+44. 获取更多音乐版本
+45. 获取音乐伴奏信息
+46. 获取音乐 k 歌数量
+47. 获取音乐详情
+48. 获取音乐专辑/歌手信息
+49. 私人 FM(对应手机和 pc 端的猜你喜欢)
+50. banner
+51. 乐库 banner
+52. 乐库电台
+53. 乐库
+54. 电台 - 推荐
+55. 电台
+56. 电台 - 图片
+57. 电台 - 音乐列表
+58. 编辑精选
+59. 编辑精选数据
+60. 编辑精选歌单
+61. 编辑精选专区
+62. 编辑精选专区详情
+63. 领取 VIP(需要登陆,该接口为测试接口)
+64. 获取歌手列表
+65. 获取歌手详情
+66. 获取歌手专辑
+67. 获取歌手单曲
+68. 获取歌手 MV
+69. 关注歌手
+70. 取消关注歌手
+71. 获取关注歌手新歌
+72. 获取视频 url
+73. 获取歌曲 MV
+74. 获取视频相关信息
+75. 获取视频详情
+76. 新歌速递
+77. 场景音乐列表
+78. 场景音乐详情
+79. 获取场景音乐讨论区
+80. 获取场景音乐模块 Tag
+81. 获取场景音乐歌单列表
+82. 获取场景音乐视频列表
+83. 获取场景音乐音乐列表
+84. 每日推荐
+85. 历史推荐
+86. 风格推荐
+87. 排行列表
+88. 排行榜推荐列表
+89. 排行榜往期列表
+90. 排行榜信息
+91. 排行榜歌曲列表
+92. 歌曲评论
+93. 歌曲评论-根据分类返回
+94. 歌曲评论-根据热词返回
+95. 楼层评论
+96. 歌单评论
+97. 专辑评论
+98. 歌曲曲谱
+99. 曲谱详情
+100. 推荐曲谱
+101. 曲谱合集
+102. 曲谱合集详情
+103. 提交听歌历史
+104. 获取服务器时间
+105. 刷刷
+106. AI 推荐
+107. 频道 - 获取用户所有频道
+108. 频道 - 详情
+109. 频道 - 频道安利
+110. 频道 - 相似频道
+111. 频道 - 订阅
+112. 频道 - 音乐故事
+113. 频道 - 音乐故事详情
+114. 动态 - 最常访问
+115. 获取用户公开的音乐
+116. 听书 - 每日推荐
+117. 听书 - 排行榜推荐
+118. 听书 - VIP 推荐
+119. 听书 - 每周推荐
+120. 听书 - 专辑详情
+121. 听书 - 专辑音乐列表
+122. 歌曲详情 - 歌曲成绩单
+123. 歌曲详情 - 歌曲成绩单详情
+124. 获取已购单曲
+125. 获取已购专辑
+
+## License
+
+[The MIT License (MIT)](https://github.com/MakcRe/KuGouMusicApi/blob/main/LICENSE)
diff --git a/third_party/MoeKoeMusic/api/app.js b/third_party/MoeKoeMusic/api/app.js
new file mode 100644
index 0000000..cfa367c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/app.js
@@ -0,0 +1,8 @@
+#!/usr/bin/env node
+
+async function start() {
+ require('./util/runtime').applyCliOverrides();
+ await require('./server').startService();
+}
+
+start().catch(console.error);
diff --git a/参考/NexBox-main/installer/src-tauri/.gitkeep b/third_party/MoeKoeMusic/api/docs/.nojekyll
similarity index 100%
rename from 参考/NexBox-main/installer/src-tauri/.gitkeep
rename to third_party/MoeKoeMusic/api/docs/.nojekyll
diff --git a/third_party/MoeKoeMusic/api/docs/README.md b/third_party/MoeKoeMusic/api/docs/README.md
new file mode 100644
index 0000000..930556e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/docs/README.md
@@ -0,0 +1,2577 @@
+# KuGouMusic API
+
+酷狗音乐 NodeJS 版 API
+
+[](https://github.com/MakcRe)
+
+
+
+
+
+
+
+
+[//]: # '
'
+
+
+
+
+
+## 环境要求
+
+需要 NodeJS 12+ 环境
+
+## 工作原理
+
+跨站请求伪造 (CSRF), 伪造请求头 , 调用官方 API
+
+## 功能特性
+
+1. [`登录`](#登录)
+2. [`刷新登录`](#刷新登录)
+3. [`发送验证码`](#发送验证码)
+4. [`dfid 获取`](#dfid-获取)
+5. [`获取用户额外信息`](#获取用户额外信息)
+6. [`获取用户 vip 信息`](#获取用户-vip-信息)
+7. [`获取用户歌单`](#获取用户歌单)
+8. [`获取用户云盘`](#获取用户云盘)
+9. [`获取用户云盘音乐 URL`](#获取用户云盘音乐-url)
+10. [`获取用户收藏的视频`](#获取用户收藏的视频)
+11. [`获取用户喜欢的视频`](#获取用户喜欢的视频)
+12. [`获取用户关注歌手`](#获取用户关注歌手)
+13. [`获取用户听歌历史排行`](#获取用户听歌历史排行)
+14. [`获取用户最近听歌历史`](#获取用户最近听歌历史)
+15. [`获取继续播放信息(对应手机版首页显示继续播放入口)`](#获取继续播放信息(对应手机版首页显示继续播放入口))
+16. [`收藏歌单/新建歌单`](#收藏歌单新建歌单)
+17. [`取消收藏歌单/删除歌单`](#取消收藏歌单删除歌单)
+18. [`对歌单添加歌曲`](#对歌单添加歌曲)
+19. [`对歌单删除歌曲`](#对歌单删除歌曲)
+20. [`新碟上架`](#新碟上架)
+21. [`专辑信息`](#专辑信息)
+22. [`专辑详情`](#专辑详情)
+23. [`专辑音乐列表`](#专辑音乐列表)
+24. [`获取音乐 URL`](#获取音乐)
+25. [`获取音乐 URL(新版)`](#获取音乐-url新版)
+26. [`获取歌曲高潮部分`](#获取歌曲高潮部分)
+27. [`搜索`](#搜索)
+28. [`默认搜索关键词`](#默认搜索关键词)
+29. [`综合搜索`](#综合搜索)
+30. [`热搜列表`](#热搜列表)
+31. [`搜索建议`](#搜索建议)
+32. [`歌词搜索`](#歌词搜索)
+33. [`获取歌词`](#获取歌词)
+34. [`歌单分类`](#歌单分类)
+35. [`歌单`](#歌单)
+36. [`主题歌单`](#主题歌单)
+37. [`音效歌单`](#音效歌单)
+38. [`获取歌单详情`](#获取歌单详情)
+39. [`获取歌单所有歌曲`](#获取歌单所有歌曲)
+40. [`获取歌单所有歌曲(新版)`](#获取歌单所有歌曲新版)
+41. [`相似歌单`](#相似歌单)
+42. [`获取主题歌单所有歌曲`](#获取主题歌单所有歌曲)
+43. [`获取主题音乐`](#获取主题音乐)
+44. [`获取主题音乐详情`](#获取主题音乐详情)
+45. [`歌曲推荐`](#歌曲推荐)
+46. [`获取歌手和专辑图片`](#获取歌手和专辑图片)
+47. [`获取歌手图片`](#获取歌手图片)
+48. [`获取音乐相关信息`](#获取音乐相关信息)
+49. [`获取更多音乐版本`](#获取更多音乐版本)
+50. [`获取音乐伴奏信息`](#获取音乐伴奏信息)
+51. [`获取音乐k歌数量`](#获取音乐-k-歌数量)
+52. [`获取音乐详情`](#获取音乐详情)
+53. [`获取音乐专辑/歌手信息`](#获取音乐专辑/歌手信息)
+54. [`私人 FM(对应手机和 pc 端的猜你喜欢)`](#私人-fm对应手机和-pc-端的猜你喜欢)
+55. [`banner`](#banner)
+56. [`乐库 banner`](#乐库-banner)
+57. [`乐库电台`](#乐库电台)
+58. [`乐库`](#乐库)
+59. [`电台 - 推荐`](#电台-推荐)
+60. [`电台`](#电台)
+61. [`电台 - 图片`](#电台-图片)
+62. [`电台 - 音乐列表`](#电台-音乐列表)
+63. [`编辑精选`](#编辑精选)
+64. [`编辑精选数据`](#编辑精选数据)
+65. [`编辑精选歌单`](#编辑精选歌单)
+66. [`编辑精选专区`](#编辑精选专区)
+67. [`编辑精选专区详情`](#编辑精选专区详情)
+68. [`领取 VIP(需要登陆,该接口为测试接口,仅限概念版使用)`](#领取-vip需要登陆该接口为测试接口仅限概念版使用)
+69. [`领取一天 VIP(需要登陆,该接口为测试接口,仅限概念版使用)`](#领取一天-vip需要登陆该接口为测试接口仅限概念版使用)
+70. [`获取当月已领取 VIP 天数(需要登陆,该接口为测试接口,仅限概念版使用)`](#获取当月已领取-vip-天数需要登陆该接口为测试接口仅限概念版使用)
+71. [`获取已领取 VIP 状态(需要登陆,该接口为测试接口,仅限概念版使用)`](#获取已领取-vip-状态需要登陆该接口为测试接口仅限概念版使用)
+72. [`获取歌手列表`](#获取歌手列表)
+73. [`获取歌手详情`](#获取歌手详情)
+74. [`获取歌手专辑`](#获取歌手专辑)
+75. [`获取歌手单曲`](#获取歌手单曲)
+76. [`获取歌手 MV`](#获取歌手-MV)
+77. [`关注歌手`](#关注歌手)
+78. [`取消关注歌手`](#取消关注歌手)
+79. [`获取关注歌手新歌`](#获取关注歌手新歌)
+80. [`获取视频 url`](#获取视频-ur)
+81. [`获取歌曲 MV`](#获取歌曲-MV)
+82. [`获取视频相关信息`](#获取视频相关信息)
+83. [`获取视频详情`](#获取视频详情)
+84. [`新歌速递`](#新歌速递)
+85. [`场景音乐列表`](#场景音乐列表)
+86. [`场景音乐详情`](#场景音乐详情)
+87. [`获取场景音乐讨论区`](#获取场景音乐讨论区)
+88. [`获取场景音乐模块 Tag`](#获取场景音乐模块)
+89. [`获取场景音乐歌单列表`](#获取场景音乐歌单列表)
+90. [`获取场景音乐视频列表`](#获取场景音乐视频列表)
+91. [`获取场景音乐音乐列表`](#获取场景音乐音乐列表)
+92. [`每日推荐`](#每日推荐)
+93. [`历史推荐`](#历史推荐)
+94. [`风格推荐`](#风格推荐)
+95. [`排行列表`](#排行列表)
+96. [`排行榜推荐列表`](#排行榜推荐列表)
+97. [`排行榜往期列表`](#排行榜往期列表)
+98. [`排行榜信息`](#排行榜信息)
+99. [`排行榜歌曲列表`](#排行榜歌曲列表)
+100. [`歌曲收藏数`](#歌曲收藏数)
+101. [`歌曲评论数`](#歌曲评论数)
+102. [`歌曲评论`](#歌曲评论)
+103. [`歌曲评论-根据分类返回`](#歌曲评论-根据分类返回)
+104. [`歌曲评论-根据热词返回`](#歌曲评论-根据热词返回)
+105. [`楼层评论`](#楼层评论)
+106. [`歌单评论`](#歌单评论)
+107. [`专辑评论`](#专辑评论)
+108. [`提交听歌历史`](#提交听歌历史)
+109. [`获取服务器时间`](#获取服务器时间)
+110. [`刷刷`](#刷刷)
+111. [`AI 推荐`](#ai-推荐)
+112. [`曲谱 - 获取歌曲曲谱`](#曲谱---获取歌曲曲谱)
+113. [`曲谱 - 曲谱排行榜`](#曲谱---曲谱排行榜)
+114. [`曲谱 - 曲谱详情`](#曲谱---曲谱详情)
+115. [`曲谱 - 曲谱广场`](#曲谱---曲谱广场)
+116. [`曲谱 - 曲谱标签`](#曲谱---曲谱标签)
+117. [`频道 - 获取用户所有频道`](#频道-获取用户所有频道)
+118. [`频道 - 详情`](#频道---详情)
+119. [`频道 - 频道安利`](#频道-频道安利)
+120. [`频道 - 相似频道`](#频道-相似频道)
+121. [`频道 - 订阅`](#频道-订阅)
+122. [`频道 - 音乐故事`](#频道-音乐故事)
+123. [`频道 - 音乐故事详情`](#频道-音乐故事详情)
+124. [`动态 - 最常访问`](#动态-最常访问)
+125. [`获取用户公开的音乐`](#获取用户公开的音乐)
+126. [`听书 - 每日推荐`](#听书-每日推荐)
+127. [`听书 - 排行榜推荐`](#听书-排行榜推荐)
+128. [`听书 - VIP 推荐`](#听书-vip-推荐)
+129. [`听书 - 每周推荐`](#听书-每周推荐)
+130. [`听书 - 专辑详情`](#听书-专辑详情)
+131. [`听书 - 专辑音乐列表`](#听书-专辑音乐列表)
+132. [`歌曲详情 - 歌曲成绩单`](#歌曲详情-歌曲成绩单)
+133. [`歌曲详情 - 歌曲成绩单详情`](#歌曲详情---歌曲成绩单详情)
+134. [`听歌识曲`](#听歌识曲)
+135. [`获取已购单曲`](#获取已购单曲)
+136. [`获取已购专辑`](#获取已购专辑)
+137. [`上传音乐到云盘`](#上传音乐到云盘)
+
+### 安装
+
+```shell
+$ git clone git@github.com:MakcRe/KuGouMusicApi.git
+$ cd KuGouMusicApi
+$ npm install
+```
+
+### 使用接口为概念版
+
+```
+$ 复制 .env.example 为 .env,并且把里面的 `platform=''` 改为 `platform=lite`
+$ 注意不同版本的平台的 token 是不通用的。
+```
+
+### 运行
+
+```shell
+$ npm run dev
+```
+
+服务器启动默认端口为 3000, 若不想使用 3000 端口 , 可使用以下命令 : Mac/Linux
+
+```shell
+$ PORT=4000 npm run dev
+```
+
+windows 下使用 git-bash 或者 cmder 等终端执行以下命令 :
+
+```shell
+$ set PORT=4000 && npm run dev
+```
+
+windows 下使用 PowerShell 终端执行一下命令 :
+
+```shell
+$ $Env:PORT=4000; npm run dev
+```
+
+服务器启动默认 host 为 localhost,如果需要更改, 可使用以下命令 : Mac/Linux
+
+```shell
+$ HOST=127.0.0.1 npm run dev
+```
+
+windows 下使用 git-bash 或者 cmder 等终端执行以下命令 :
+
+```shell
+$ set HOST=127.0.0.1 && npm run dev
+```
+
+## 免责声明
+
+!> 1. 本项目仅供学习使用,请尊重版权,请勿利用此项目从事商业行为及非法用途!
+
+!> 2. 使用本项目的过程中可能会产生版权数据。对于这些版权数据,本项目不拥有它们的所有权。为了避免侵权,使用者务必在 24 小时内清除使用本项目的过程中所产
+生的版权数据。
+
+!> 3. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停
+工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。
+
+!> 4. **禁止在违反当地法律法规的情况下使用本项目。** 对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担,
+本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。
+
+!> 5. 音乐平台不易,请尊重版权,支持正版。
+
+!> 6. 本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。
+
+!> 7. 如果官方音乐平台觉得本项目不妥,可联系本项目更改或移除。
+
+## Vercel 部署
+
+### 操作方法
+
+1. fork 此项目
+2. 在 Vercel 官网点击 `New Project`
+3. 点击 `Import Git Repository` 并选择你 fork 的此项目并点击 `import`
+4. 点击 `PERSONAL ACCOUNT` 的 `select`
+5. 直接点 `Continue`
+6. 若需要部署版本为概念版(不需要该步骤可以跳过),在 `Environment Variables` 添加 `key` 为 `platform`,`Value (Will Be Encrypted)` 为 `lite` 然后点击
+ `Add`
+7. `PROJECT NAME`自己填,`FRAMEWORK PRESET` 选 `Other` 然后直接点 `Deploy` 接着等部署完成即可
+
+## 接口文档
+
+### 调用前须知
+
+!> 本项目不提供线上 demo,请不要轻易信任使用他人提供的公开服务,以免发生安全问题,泄露自己的账号和密码
+
+!> 为使用方便,降低门槛, 文档示例接口直接使用了 GET 请求,本项目同时支持 GET/POST 请按实际需求使用 (POST 请求 url 必须添加时间戳,使每次请求 url 不一样,
+不然请求会被缓存)
+
+!> 由于接口做了缓存处理 ( 缓存 2 分钟,), 相同的 url 会在两分钟内只向酷狗服务器发一次请求 , 如果遇到不需要缓 存结果的接口 , 可在请求 url 后面加一个时间
+戳参数使 url 不同 , 例子 : /personal/fm?timestamp=1691256061923 (请按自己需求改造缓存中间件(app.js),源码不复杂) 该缓存机制来源
+于[Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
+
+!> 不要频繁调登录接口,不然可能会被风控,登录状态还存在就不要重复调登录接口
+
+!> 如果是跨域请求 , 请在所有请求带上 xhrFields: { withCredentials: true } (axios 为 withCredentials: true, Fetch API 为 fetch(url, { credentials:
+'include' })), 或直接手动传入 cookie。
+
+!> 本项目仅供学习使用,请尊重版权,请勿利用此项目从事商业行为或进行破坏版权行为
+
+!> 文档可能会有缓存 , 如果文档版本和 github 上的版本不一致,请清除缓存再查看
+
+#### 更新记录
+
+26-08-01:添加 `上传音乐到云盘`、`删除用户云盘音乐` 接口
+
+26-05-20:更新曲谱相关接口
+
+25-08-17: 手机登录添加支持多用户登录
+
+25-04-05: 添加 `用户云盘`,`用户云盘 url`, `获取用户收藏视频`,`获取用户喜欢视频` 等接口
+
+25-03-28:添加 `领取一天 VIP`, `获取当月已领取 VIP 天数`, `领取一天 VIP`,`获取已领取 VIP 状态`,`获取音乐 URL(新版)` 等接口
+
+25-03-07:更新单曲搜索接口,更新歌词搜索接口。
+
+25-01-16:添加 `获取继续播放信息` 接口,修复部分老歌在概念版无法获取问题
+
+25-01-11:添加 `关注歌手` `取消关注歌手` `获取关注歌手新歌` 接口
+
+24-08-09:添加 获取更多音乐版本 接口。
+
+24-07-22:添加 `歌曲详情` 接口.
+
+24-07-17: 添加 搜索建议 接口。
+
+24-07-14: 添加 频道、听书等多个接口。
+
+24-07-14: 添加 刷刷、AI 推荐 接口,更新 提交听歌历史 接口。
+
+24-06-30: 添加 获取歌手列表 接口
+
+24-06-25: 添加 默认搜索关键词、获取歌曲 MV、提交听歌历史等接口
+
+24-05-30: 添加曲谱接口
+
+24-05-19: 修复设备平台判断问题,添加 获取用户关注歌手 接口
+
+24-05-14: 添加 楼层评论、歌单评论、专辑评论、热搜列表接口。修复 歌单 接口
+
+24-05-14: 添加 歌曲评论、歌曲评论-根据分类返回、歌曲评论-根据热词返回 接口
+
+24-05-12: 添加支持酷狗概念版登录
+
+24-05-07: 修复热门歌单接口
+
+24-01-17: 添加:排行榜列表接口、排行榜推荐列表接口、排行榜往期列表接口、排行榜信息接口、排行榜歌曲列表接口
+
+24-01-12: 更新私人 FM 接口
+
+24-01-07: 添加 每日推荐、历史推荐、 风格推荐 API 接口
+
+23-12-19: 添加场景音乐相关接口
+
+23-12-18: 添加 收藏歌单,取消收藏歌单,对歌单添加歌曲,对歌单删除歌曲 接口
+
+23-11-10: 添加歌手相关 API、添加视频相关 API
+
+23-10-01: 添加领取 vip 接口
+
+23-08-27: 更新音乐 url 获取
+
+23-08-11: 添加电台 api
+
+23-08-06: 添加缓存机制
+
+23-07-19: 更新酷狗二维码登录
+
+23-06-29: 添加 新歌速递、乐库等多个接口
+
+23-06-28: 添加获取歌手图片接口
+
+23-06-25: 添加 每日主题音乐 获取每日主题音乐详情 歌单 等接口
+
+23-06-23: 修复搜索接口
+
+23-06-23: 添加 搜索、获取歌词等接口
+
+23-06-23: 添加 获取音乐 URL 接口
+
+23-06-23: 更新文档
+
+23-06-23: 添加每日歌曲推荐、专辑详情、专辑音乐列表、新碟上架
+
+23-06-22: 添加支持微信登录、开放接口登录
+
+23-06-22: 添加支持 vercel
+
+23-06-22: 密码登录、验证码登录、扫码登录
+
+### 登录
+
+说明:登录有五个接口使用 `encodeURIComponent`对密码编码或者使用 `POST`请求,避免某些特殊字符无法解析,如#(#在 url 中会被识别为 hash,而不是 query)
+
+不要频繁调登录接口,不然可能会被风控,登录状态还存在就不要重复调登录接口, **不同版本的 token 是不通用的**
+
+#### 1.手机登录
+
+**必选参数:**
+
+`mobile`: 手机号码
+
+`code`: 验证码,使用 [`/captcha/sent`](#发送验证码)接口传入手机号获取验证码,调用此接口传入验证码,可使用验证码登录
+
+**可选参数**
+
+`userid`: 用户 id,当用户存在多个账户是时,必须加上需要登录的用户 id
+
+**接口地址:** `/login/cellphone`
+
+**调用例子:** `/login/cellphone?mobile=xxx&code=xxx`
+
+#### 2. 用户名登录(该登录可能需要验证,不推荐使用)
+
+**必选参数:**
+
+`username`: 用户名
+
+`password`: 密码
+
+**接口地址:** `/login`
+
+**调用例子:** `/login?username=xxx&password=yyy`
+
+#### 3. 开放接口登录(目前仅支持微信登录)
+
+说明: 该接口为第三方平台登录,目前仅支持微信登录
+
+**必选参数:**
+
+`code`: 由微信扫码成功后生成
+
+**接口地址:** `/login/openplat`
+
+**调用例子:** `/login/openplat?code=xxx`
+
+#### 4. 二维码登录
+
+说明: 二维码登录涉及到 3 个接口,调用务必带上时间戳,防止缓存
+
+##### 1.二维码 key 生成接口
+
+说明: 调用此接口可生成一个 key
+
+**接口地址:** `/login/qr/key`
+
+##### 2.二维码生成接口
+
+说明: 调用此接口传入上一个接口生成的 key 可生成二维码图片的 base64 和二维码信息,可使用 base64 展示图片,或者使用二维码信息内容自行使用第三方二维码生成
+库渲染二维码
+
+**必选参数:**
+
+`key`: ,由第一个接口生成
+
+**可选参数:**
+
+`qrimg`: 传入后会额外返回二维码图片 base64 编码
+
+**接口地址:** `/login/qr/create`
+
+**调用例子:** `/login/qr/create?key=xxx`
+
+##### 2.二维码检测扫码状态接口
+
+说明: 轮询此接口可获取二维码扫码状态,0 为二维码过期,1 为等待扫码,2 为待确认,4 为授权登录成功(4 状态码下会返回 token)
+
+**必选参数:**
+
+`key`: ,由第一个接口生成
+
+**接口地址:** `/login/qr/check`
+
+**调用例子:** `/login/qr/check?key=xxx`
+
+#### 5. 微信登录
+
+说明:微信登录涉及到 2 个接口,调用务必带上时间戳,防止缓存
+
+##### 1. 二维码生成接口
+
+说明:调用此接口可生成微信的 uuid, 包括二维码 Bae64 和 二维码扫描链接, 注: 该接口请求的接口过多, 会出现返回较慢的情况
+
+**接口地址:** `/login/wx/create`
+
+**调用例子:** `/login/wx/create`
+
+##### 2.二维码检测扫码状态接口
+
+说明:轮询此接口可获取二维码扫码状态, 408 为等待扫描,404 为已经扫描,403 为拒绝登录,405 为登录成功,402 为已过期(405 状态下登陆完成口会返回 wx_code,
+用于开放登陆 [`/login/openplat`](#_3-开放接口登录目前仅支持微信登录)), 注:该接口有一定延时,不可访问是可以直接到
+https://long.open.weixin.qq.com/connect/l/qrconnect?f=json&uuid=xxx 该接口直接请求
+
+**必选参数:**
+
+`uuid`: 由第一个接口生成
+
+**可选参数:**
+
+`timestamp`: 建议传递,否则由于缓存会导致延迟
+
+**接口地址:** `/login/wx/check`
+
+**调用例子:** `/login/wx/check?timestamp=1691256061923&uuid=xxxxxxxxx`
+
+### 刷新登录
+
+说明 : 调用此接口,可刷新登录状态,可以延长 `token` 过期时间
+
+**可选参数:**
+
+`token`: 登录后获取的 token
+
+`userid`: 用户 id
+
+**接口地址:** `/login/token`
+
+**调用例子:** `/login/token` `/login/token?token=xxx&userid=xxx`
+
+### 发送验证码
+
+说明: 调用此接口 ,传入手机号码, 可发送验证码
+
+**必选参数:**
+
+`mobile`: 手机号码
+
+**接口地址:** `/captcha/sent`
+
+**调用例子:** `/captcha/sent?mobile=xxx`
+
+### dfid 获取
+
+**接口地址:** `/register/dev`
+
+**调用例子:** `/register/dev`
+
+### 获取用户额外信息
+
+说明:登陆后调用此接口,可以获取用户额外信息
+
+**接口地址:** `/user/detail`
+
+### 获取用户 vip 信息
+
+说明:登陆后调用此接口,可以获取用户 vip 信息
+
+**接口地址:** `/user/vip/detail`
+
+### 获取用户歌单
+
+说明:登录后调用此接口,可以获取用户的所有创建以及收藏的歌单
+
+**可选参数:**
+
+`page`:页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/user/playlist`
+
+**调用例子:** `/user/playlist`
+
+### 获取用户关注歌手
+
+说明:登录后调用此接口,可以获取用户的所有关注的歌手/用户
+
+**接口地址:** `/user/follow`
+
+**调用例子:** `/user/follow`
+
+### 获取关注歌手消息
+
+说明:登录后调用此接口,可以关注的歌手/用户消息
+
+`id`: 需要获取歌手/用户消息的 userid
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/user/follow/message`
+
+**调用例子:** `/user/follow/message`
+
+### 获取用户云盘
+
+说明:登录后调用此接口可以获取用户上传到云盘的音乐(需要登录)
+
+**可选参数**
+
+`page `: 页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/user/cloud`
+
+**调用例子:** `/user/cloud`
+
+### 获取用户云盘音乐 URL
+
+说明:登录后调用此接口可以获取用户上传到云盘的音乐 URL,部分可以直接用 `/song/url` 直接获取 URL(需要登录,目前获取到的文件大小都约为 10M 左右)
+
+**必选参数:**
+
+`hash`: 音乐 hash
+
+**可选参数:**
+
+`album_id`: 专辑 id
+
+`name`: 云盘音乐名称
+
+`album_audio_id`:专辑音频 id
+
+**接口地址:** `/user/cloud/url`
+
+**调用例子:** `/user/cloud/url`
+
+### 删除用户云盘音乐
+
+说明:登录后调用此接口可以删除用户云盘中的音乐(需要登录)
+
+**必选参数:**
+
+`fileid`: 云盘文件 ID(列表接口返回的 `kv_id`),多个可用逗号分隔;也可使用 `fileids` 传数组
+
+**可选参数:**
+
+`clientver`: 覆盖客户端版本号,默认使用当前平台配置
+
+`appid`: 覆盖 appid,默认使用当前平台配置
+
+`kv_id`: `fileid` 别名
+
+`album_audio_id`: 专辑音频 ID(列表接口返回的 `album_audio_id`),多个可用逗号分隔;也可使用 `album_audio_ids` 传数组
+
+**接口地址:** `/user/cloud/del`
+
+**调用例子:** `/user/cloud/del?fileid=1&album_audio_id=123`
+
+### 云盘上传前曲库匹配
+
+说明:登录后调用此接口可以根据文件 hash 匹配曲库信息,用于上传前获取 `hash_std`、`audio_id`、`album_audio_id`
+
+**必选参数:**
+
+`hash`: 文件 hash(即文件 MD5),多个可用逗号分隔;也可通过请求体传入文件二进制数据自动计算
+
+**可选参数:**
+
+`album_audio_id`: 专辑音频 ID,已有时可辅助匹配,多个可用逗号分隔;也可使用 `album_audio_ids` 传数组
+
+`clientver`: 覆盖客户端版本号,默认使用当前平台配置
+
+`appid`: 覆盖 appid,默认使用当前平台配置
+
+**接口地址:** `/user/cloud/match`
+
+**调用例子:** `/user/cloud/match?hash=e1fe087ba28766b8c95239487a0e46dc`
+
+### 上传音乐到云盘
+
+说明:登录后调用此接口可以将音乐文件上传到用户云盘,需通过请求体传入文件二进制数据(`Content-Type: application/octet-stream`)
+默认会先根据文件 hash 调用曲库匹配接口,并将匹配到的 `hash_std`、`audio_id`、`album_audio_id` 写入云盘;手动传入这些参数时会优先使用手动值
+
+**必选参数:**
+
+无
+
+**可选参数:**
+
+`filename`: 文件 hash(即文件 MD5),默认自动计算
+
+`extendname`: 文件扩展名,默认为 `mp3`(自动去掉点号)
+
+`name`: 云盘音乐显示名称,默认格式为 `歌手名 - MD5.扩展名`
+
+`author_name`: 歌手名
+
+`hash_std`: 曲库标准 hash,默认自动匹配
+
+`audio_id`: 音频 id,默认为 0
+
+`album_audio_id`: 专辑音频 id,默认为 0
+
+`auto_match`: 是否自动匹配曲库,默认 `1`;传 `0`、`false` 或 `no` 可关闭
+
+**接口地址:** `/user/cloud/upload`
+
+**调用例子:** `/user/cloud/upload`
+
+### 获取用户收藏的视频
+
+说明:登录后调用此接口可以获取用户收藏的视频(需要登录)
+
+**可选参数**
+
+`page `: 页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/user/video/collect`
+
+**调用例子:** `/user/video/collect`
+
+### 获取用户喜欢的视频
+
+说明:登录后调用此接口可以获取用户喜欢的视频(需要登录)
+
+**可选参数**
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/user/video/love`
+
+**调用例子:** `/user/video/love`
+
+### 获取用户听歌历史排行
+
+说明:登录后调用此接口,可以获取用户听歌历史排行
+
+**可选参数:**
+
+`type`:0 为获取最近一周前 120 首歌曲,1:获取全部累计前 120 首歌曲
+
+**接口地址:** `/user/listen`
+
+**调用例子:** `/user/listen`
+
+### 获取用户最近听歌历史
+
+说明:登录后调用此接口,可以近期的听歌历史记录(需要登陆)
+
+**可选参数:**
+
+`bp`: 可以更加上一次返回值传入
+
+**接口地址:** `/user/history`
+
+**调用例子:** `/user/history`
+
+### 获取继续播放信息(对应手机版首页显示继续播放入口)
+
+说明:登录后调用此接口,可以最后设备播放信息(需要登陆)
+
+**可选参数:**
+
+`pagesize`: 每页页数, 默认为 30
+
+**接口地址:** `/lastest/songs/listen`
+
+**调用例子:** `/lastest/songs/listen`
+
+### 收藏歌单/新建歌单
+
+说明 : 调用此接口, 可收藏歌单/新建歌单( 需要登录 ), 收藏成功后建议使用 [`/playlist/tracks/add`](#对歌单添加歌曲) 把原歌单下的歌曲添加到新的歌单
+
+**必选参数:**
+
+name: 歌单名称
+
+list_create_userid: 歌单 list_create_userid
+
+list_create_listid: 歌单 list_create_listid
+
+**可选参数**
+
+`is_pri`: 是否设为隐私,0:公开,1:隐私,仅支持创建歌单时传入
+
+`type`: 1:为收藏歌单,0:创建歌单, 默认为 0
+
+`list_create_gid`:歌单 list_create_gid
+
+**接口地址:** `/playlist/add`
+
+**调用例子:** `/playlist/add?source=1&name=音乐一响%20纯爱登场.&list_create_userid=1782943844&list_create_listid=87`
+
+### 取消收藏歌单/删除歌单
+
+说明 : 调用此接口 , 取消收藏歌单( 需要登录 )
+
+**必选参数:**
+
+listid: 用户歌单 listid
+
+**接口地址:** `/playlist/del`
+
+**接口地址:** `/playlist/del?listid=xxx`
+
+### 对歌单添加歌曲
+
+说明 : 调用此接口 , 可以添加歌曲到歌单 ( 需要登录 )
+
+**必选参数:**
+
+listid: 用户歌单 listid
+
+data: 歌曲数据, 格式为 歌曲名称|歌曲 hash|专辑 id|(mixsongid/album_audio_id),最少需要 歌曲名称以及歌曲 hash(若返回错误则需要全部参数), 支持多个,每
+个以逗号分隔
+
+**接口地址:** `/playlist/tracks/add`
+
+**调用例子:** `/playlist/tracks/add?listid=1&data=我们应该算爱过吧|8E10D8825DDE03BCABBDE13E5A4150D2`
+`/playlist/tracks/add?listid=1&data=我们应该算爱过吧|8E10D8825DDE03BCABBDE13E5A4150D2|67026620|477417208`
+`/playlist/tracks/add?listid=1&data=我们应该算爱过吧|8E10D8825DDE03BCABBDE13E5A4150D2,我们应该算爱过吧|5015FC3FAB5B0C245556A1CC3C4DE355`
+
+### 对歌单删除歌曲
+
+说明 : 调用此接口 , 可以删除歌单某首歌曲 ( 需要登录 )
+
+**必选参数:**
+
+listid: 用户歌单 listid
+
+fileids: 歌单中歌曲的 fileid,可多个,用逗号隔开
+
+**接口地址:** `/playlist/tracks/del`
+
+**调用例子:** `/playlist/tracks/del?listid=1&fileids=xx` `/playlist/tracks/del?listid=1&fileids=xx,xx`
+
+### 新碟上架
+
+说明: 调用此接口 , 可获取新碟上架列表, 如需要专辑详细信息需要调用[`album/detail`](#专辑详情), 如需要获取专辑音乐列表需调
+用[`album/songs`](#专辑音乐列表)
+
+**可选参数:**
+
+`type `: 1:华语;2:欧美;3:日本;4:韩国;推荐为空,默认为空
+
+`page `: 页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/top/album`
+
+**调用例子:** `/top/album`
+
+### 专辑信息
+
+说明: 调用此接口 ,传入专辑 id 可获取专辑相关信息
+
+**必选参数:**
+
+`album_id`: 专辑 id,可以传多个,以逗号分割
+
+**可选参数:**
+
+`fields`: 需要返回的信息,可以传多个,以逗号分割,支持的值有 `trans_param` `special_tag` `authors` `album_name` `publish_date` `cover` `intro`
+`publish_company` `type` `album_id` `language_id` `is_publish` `heat` `grade` `quality` `exclusive` `grade_count` `author_name` `sizable_cover`
+`language` `category`
+
+**接口地址:** `/album`
+
+**调用例子:** `/album?album_id=xxx`, `/album?album_id=xxx,xxx`, `/album?album_id=xxx&fields=language,authors`
+
+### 专辑详情
+
+说明: 调用此接口 ,传入专辑 id 可获取专辑详情
+
+**必选参数:**
+
+`id`: 专辑 id
+
+**接口地址:** `/album/detail`
+
+**调用例子:** `/album/detail?id=10729818`
+
+### 专辑音乐列表
+
+说明: 调用此接口 ,传入专辑 id 可获取专辑音乐列表
+
+**必选参数:**
+
+`id`: 专辑 id
+
+**可选参数:**
+
+`page `: 页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/album/songs`
+
+**调用例子:** `/album/songs?id=10729818`
+
+### 获取音乐 URL
+
+说明: 调用此接口, 传入的音乐 hash, 可以获取对应的音乐的 url, 未登录状态或者非会员可能会返回为空。
+
+⚠️ 注意:因接口问题,目前获取 url 接口数据需要先调用 `/register/dev` 接口获取 dfid,否则会提示 `本次请求需要验证`
+
+**必选参数:**
+
+`hash`: 音乐 hash
+
+**可选参数:**
+
+`album_id`: 专辑 id
+
+`free_part`: 是否返回试听部分(仅部分歌曲)
+
+`album_audio_id`:专辑音频 id
+
+`quality`:获取不同音质的 url
+
+**quality 支持的参数**
+
+`piano`:对应手机端魔法音乐 钢琴,仅部分音乐支持
+
+`acappella`:对应手机端魔法音乐 人声 伴奏,仅部分音乐支持,该模式下返回的音频后缀为 mkv 格式,该文加存在 人声 和 伴奏 两个音轨
+
+`subwoofer`:对应手机端魔法音乐 骨笛,仅部分音乐支持
+
+`ancient`:对应手机端魔法音乐 尤克里里,仅部分音乐支持
+
+`surnay`:对应手机端魔法音乐 唢呐,仅部分音乐支持
+
+`dj`:对应手机端魔法音乐 DJ,仅部分音乐支持
+
+`128`:返回 128 码率 mp3 格式
+
+`320`:返回 320 码率 mp3 格式
+
+`flac`:返回 flac 格式音频
+
+`high`:返回无损格式音频
+
+`viper_atmos`:蝰蛇全景声,仅部分音乐支持
+
+`viper_clear`:蝰蛇超清音质
+
+`viper_tape`:蝰蛇母带,仅部分音乐支持, 该音质需要转码,关于转码相关的技术还不会
+
+`super`:返回 DSD 格式音频,支持的音频少的可伶
+
+**接口地址:** `/song/url`
+
+**调用例子:** `/song/url?hash=xxx`
+
+### 获取音乐 URL(新版)
+
+说明: 调用此接口, 传入的音乐 hash, 可以获取对应的音乐的 url, 未登录状态或者非会员可能会返回为空,该接口会一次性返回支持的音质的音频 url, **但该接口存
+在音频加密(目前无法解码),请谨慎使用**
+
+⚠️ 注意:因接口问题,目前获取 url 接口数据需要先调用 `/register/dev` 接口获取 dfid,否则会提示 `本次请求需要验证`
+
+**必选参数:**
+
+`hash`: 音乐 hash
+
+**可选参数:**
+
+`album_audio_id`:专辑音频 id
+
+`free_part`: 是否返回试听部分(仅部分歌曲)
+
+`album_audio_id`:专辑音频 id
+
+**接口地址:** `/song/url/new`
+
+**调用例子:** `/song/url/new?hash=xxx`
+
+### 获取歌曲高潮部分
+
+说明: 调用此接口, 传入的音乐 hash, 可以获取对应的音乐的高潮时间
+
+**必选参数:**
+
+`hash`: 音乐 hash, 可以传多个,以逗号分割
+
+**接口地址:** `/song/climax`
+
+**调用例子:** `/song/climax?hash=xxx`
+
+### 搜索
+
+说明: 调用此接口 , 传入搜索关键词可以搜索该音乐 / mv / 歌单 / 歌词 / 专辑 / 歌手
+
+**必选参数:**
+
+`keywords`: 关键词
+
+**可选参数:**
+
+`page `: 页数
+
+`pagesize `: 每页页数, 默认为 30
+
+`type`: 搜索类型;默认为单曲,special:歌单,lyric:歌词,song:单曲,album:专辑,author:歌手,mv:mv
+
+⚠️ 注意:因接口问题,获取搜索结果需要在 url 后添加`cookie`认证信息或者`Set-cookie`,否则会返回 `error_code: 152`
+
+⚠️ 注意:建议请求在所有搜索接口时添加认证信息防止调用失败!
+
+**接口地址:** `/search`
+
+**调用例子:** `/search?keywords=海阔天空`(错误示例),`/search?keywords=周杰伦&cookie=token=xxxx;userid=xxxx;dfid=xxxx`(必须携带认证信息)
+
+### 默认搜索关键词
+
+说明 : 调用此接口 , 可获取默认搜索关键词
+
+**接口地址:** `/search/default`
+
+### 综合搜索
+
+说明: 调用此接口, 传入搜索关键词可以获得综合搜索,搜索结果同时包含单曲 , 歌手 , 歌单等信息
+
+**必选参数:**
+
+`keywords`: 关键词
+
+**可选参数:**
+
+`page `: 页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/search/complex`
+
+**调用例子:** `/search/complex?keywords=海阔天空`
+
+### 热搜列表
+
+说明 : 调用此接口,可获取热门搜索列表
+
+**接口地址:** `/search/hot`
+
+**调用例子:** `/search/hot`
+
+### 搜索建议
+
+说明 : 调用此接口 , 传入搜索关键词可获得搜索建议 , 搜索结果同时包含单曲 , 歌手 , 歌单信息
+
+**可选参数:**
+
+`albumTipCount `: 专辑返回数量
+
+`correctTipCount `: 目前未知,可能是歌单
+
+`mvTipCount `: MV 返回数量
+
+`musicTipCount `: 音乐返回数量
+
+**接口地址:** `/search/suggest`
+
+**调用例子:** `/search/suggest?keywords=海阔天空`
+
+### 歌词搜索
+
+说明: 调用此接口, 可以搜索歌词,该接口需配合 [`/lyric`](#获取歌词) 使用。
+
+**必选参数:**
+
+`keywords`: 关键词,与 hash 二选一
+
+`hash`: 歌曲 hash,与 keyword 二选一
+
+**可选参数:**
+
+`album_audio_id`: 专辑音乐 id,
+
+`duration`:歌曲时间
+
+`man`: 是否返回多个歌词,`yes`:返回多个, `no`:返回一个。 默认为`no`
+
+**接口地址:** `/search/lyric`
+
+**调用例子:** `/search/lyric?keywords=xxx` `/search/lyric?keywords=xxx&hash=xxx`
+
+### 获取歌词
+
+说明 : 调用此接口,可以获取歌词,调用该接口前则需要调用[`/search/lyric`](#歌词搜索) 获取完整参数
+
+**必选参数:**
+
+`id`: 歌词 id, 可以从 [`/search/lyric`](#歌词搜搜) 接口中获取
+
+`accesskey`: 歌词 accesskey, 可以从 [`/search/lyric`](#歌词搜搜) 接口中获取
+
+**可选参数:**
+
+`fmt`: 歌词类型,lrc 为普通歌词,krc 为逐字歌词
+
+`decode`: 是否解码,传入该参数这返回解码后的歌词
+
+**接口地址:** `/lyric`
+
+**调用例子:** `/lyric?id=xxx&accesskey=xxx` `/lyric?id=xxx&accesskey=xxx&fmt=lrc` `/lyric?id=xxx&accesskey=xxx&decode=true`
+
+### 歌单分类
+
+说明 : 调用此接口,可获取歌单分类,包含 category 信息
+
+**接口地址:** `/playlist/tags`
+
+**调用例子:** `/playlist/tags`
+
+### 歌单
+
+说明 : 调用此接口 , 可获取歌单
+
+**必选参数:**
+
+`category_id`: tag,0:推荐,11292:HI-RES,其他可以从 [`/playlist/tags`](#歌单分类) 接口中获取(接口下的 `tag_id` 为 `category_id`的值)
+
+**可选参数:**
+
+`withsong`: 是否返回歌曲列表(不全),0:不返回,1:返回
+
+`withtag`: 是否返回歌单分类,0:不返回,1:返回
+
+**接口地址:** `/top/playlist`
+
+**调用例子:** `/top/playlist?category_id=0`
+
+### 主题歌单
+
+说明 : 调用此接口 , 可获取主题歌单, 通过 [`/theme/playlist/track`](#获取主题歌单所有歌曲) 可以获取主题个单下的歌曲
+
+**接口地址:** `/theme/playlist`
+
+**调用例子:** `/theme/playlist`
+
+### 音效歌单
+
+说明 : 调用此接口 , 可获取音效歌单
+
+**可选参数:**
+
+`page `: 页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/playlist/effect`
+
+**调用例子:** `/playlist/effect`
+
+### 获取歌单详情
+
+说明: 调用此接口 , 可获取歌单详细信息
+
+**必选参数:**
+
+`ids`: 歌单中的 `global_collection_id`,可以传多个,用逗号分隔
+
+**接口地址:** `/playlist/detail`
+
+**调用例子:** `/playlist/detail?ids=collection_3_1863870844_4_0` `/playlist/detail?ids=collection_3_1863870844_4_0,collection_3_2093906551_8_0`
+
+### 获取歌单所有歌曲
+
+说明 : 调用此接口,传入对应的歌单 global_collection_id,即可获得对应的所有歌曲
+
+**必选参数:**
+
+`id`: 歌单中的 `global_collection_id`
+
+**可选参数:**
+
+`page `: 页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/playlist/track/all`
+
+**调用例子:** `/playlist/track/all?id=collection_3_1863870844_4_0`
+
+### 获取歌单所有歌曲(新版)
+
+说明 : 调用此接口,传入对应的歌单 listid,即可获得对应的所有歌曲, 目前该接口仅支持 用户所创建及收藏的歌单
+
+**必选参数:**
+
+`lisdid`: 歌单中的 `listid`
+
+**可选参数:**
+
+`page `: 页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/playlist/track/all/new`
+
+**调用例子:** `/playlist/track/all/new?listid=xxx`
+
+### 相似歌单
+
+说明 : 调用此接口,根据歌单 id 获取相似歌单
+
+**必选参数:**
+
+`ids`:歌单 global_collection_id,支持多个,每个以逗号分隔
+
+**接口地址:** `/playlist/similar`
+
+**调用例子:** `/playlist/similar?ids=collection_1_1341266283_964007_0`
+`/playlist/similar?ids=collection_1_1341266283_964007_0,collection_3_1041185112_11_0`
+
+### 获取主题歌单所有歌曲
+
+**必选参数:**
+
+`theme_id`: 主题歌单 id
+
+**接口地址:** `/theme/playlist/track`
+
+**调用例子:** `/theme/playlist/track?theme_id=18`
+
+### 获取主题音乐
+
+说明 : 调用此接口,可以获取主题音乐,调用 [`/theme/music/detail`](#) 可以获取主题音乐详情
+
+**接口地址:** `/theme/music`
+
+**调用例子:** `/theme/music`
+
+### 获取主题音乐详情
+
+说明 : 调用此接口,传入对应的主题 id 可以获取主题音乐详情.
+
+**必选参数:**
+
+`id`: 主题音乐 id
+
+**接口地址:** `/theme/music/detail`
+
+**调用例子:** `/theme/music/detail?id=1002`
+
+### 歌曲推荐
+
+说明 : 调用此接口,可以获取歌曲推荐.
+
+**必选参数:**
+
+`card_id`: 1:对应安卓 精选好歌随心听 || 私人专属好歌,2:对应安卓 经典怀旧金曲,3:对应安卓 热门好歌精选,4:对应安卓 小众宝藏佳作,5:未知,6:对应
+vip 专属推荐
+
+**接口地址:** `/top/card`
+
+**调用例子:** `/top/card?card_id=1`
+
+### 歌曲推荐(概念版)
+
+说明 : 调用此接口,可以获取歌曲推荐
+
+**必选参数:**
+
+`card_id`: 3006: VIP 专属推荐,3001: 私人专属好歌,3004: 小众宝藏佳作,3014: 喜欢这首歌的 TA 也喜欢,3101: 概念 er 新推,3005: 潮流尝鲜
+
+**可选参数**
+
+`pagesize`: 每页页数, 默认为 30
+
+**接口地址:** `/top/card`
+
+**调用例子:** `/top/card/youth?card_id=3006`
+
+### 获取歌手和专辑图片
+
+说明 : 调用此接口,可以获取歌手和专辑图片.
+
+**必选参数:**
+
+`hash`: 歌曲 hash, 可以传多个,每个以逗号分开
+
+**可选参数:**
+
+`album_id`: 专辑 id, 可以传多个,每个以逗号分开
+
+`album_audio_id`: 专辑音乐 id, 可以传多个,每个以逗号分开
+
+`count`: 最多返回多少张图片,默认为 5
+
+**接口地址:** `/images`
+
+**调用例子:** `/images?hash=B04ED0F01ABBB62B9D22EC4616ED8AFE`
+`/image?hash=B04ED0F01ABBB62B9D22EC4616ED8AFE,55603312694BF99AD6000C2D0D72D368&album_id=,75013431`
+
+### 获取歌手图片
+
+说明 : 调用此接口,可以获取歌手图片.
+
+**必选参数:**
+
+`hash`: 歌曲 hash, 可以传多个,每个以逗号分开
+
+**可选参数:**
+
+`audio_id`: 音乐 id, 可以传多个,每个以逗号分开
+
+`album_audio_id`: 专辑音乐 id, 可以传多个,每个以逗号分开
+
+`filename`: 音乐文件名称, 可以传多个,每个以逗号分开
+
+`count`: 最多返回多少张图片,默认为 5
+
+**接口地址:** `/images/audio`
+
+**调用例子:** `/images/audio?hash=B04ED0F01ABBB62B9D22EC4616ED8AFE`
+`/image/audio?hash=B04ED0F01ABBB62B9D22EC4616ED8AFE,55603312694BF99AD6000C2D0D72D368`
+
+### 获取音乐相关信息
+
+说明:调用此接口,可以获取音乐相关信息
+
+**必选参数:**
+
+`hash`: 歌曲 hash, 可以传多个,每个以逗号分开
+
+**接口地址:** `/audio`
+
+**调用例子:** `/audio?hash=B04ED0F01ABBB62B9D22EC4616ED8AFE` `/audio?hash=B04ED0F01ABBB62B9D22EC4616ED8AFE,55603312694BF99AD6000C2D0D72D368`
+
+### 获取更多音乐版本
+
+说明:调用此接口,可以获取更多版本音乐
+
+**必选参数:**
+
+`album_audio_id`:音乐的 mixsongid/album_audio_id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+`show_type`:是否返回分类
+
+`sort`:排序,支持 `all`,`hot`,`new`
+
+`type`: 分类
+
+`show_detail`:是否返回详情,否则只返回总数,0:只返回总数,不传或者其他都返回详情
+
+**接口地址:** `/audio/related`
+
+**调用例子:** `/audio/related?album_audio_id=573120919` `/audio/related?album_audio_id=573120919&show_detail=0`
+
+### 获取音乐伴奏信息
+
+说明:调用此接口,可以获取最佳伴奏信息
+
+**必选参数:**
+
+`hash`:音乐 hash
+
+`fileName`: 音乐 fileName
+
+`mixid`: 音乐的 mixsongid/album_audio_id
+
+**接口地址:** `/audio/accompany/matching`
+
+**调用例子:** `/audio/accompany/matching?fileName=希林娜依高 - Shine Brighter (愈加璀璨)&mixId=637735200&hash=6D431B0507587447B3D7345434DC5825`
+
+### 获取音乐 K 歌数量
+
+说明:调用此接口,可以获取音乐 K 歌数量,**参数信息均来自[获取音乐伴奏信息](#获取音乐伴奏信息)**
+
+**必选参数:**
+
+`songId`:音乐 songid, 该字段需要请求 [获取音乐伴奏信息](#获取音乐伴奏信息) 获取
+
+`singerName`:歌手名称,多个以 `、` 隔开,也可以到 [获取音乐伴奏信息](#获取音乐伴奏信息) 中获取
+
+`songHash`:音乐 hash, 该字段需要请求 [获取音乐伴奏信息](#获取音乐伴奏信息) 获取
+
+**接口地址:** `/audio/ktv/total`
+
+**调用例子:** `/audio/ktv/total?songId=43522508&singerName=希林娜依高&songHash=99AE5A7B04FF76550E380C3757D3E273`
+
+### 获取音乐详情
+
+说明:调用此接口,可以获取音乐详情
+
+**必选参数:**
+
+`hash`: 歌曲 hash, 可以传多个,每个以逗号分开
+
+**接口地址:** `/privilege/lite`
+
+**调用例子:** `/privilege/lite?hash=B04ED0F01ABBB62B9D22EC4616ED8AFE`
+`/privilege/lite?hash=B04ED0F01ABBB62B9D22EC4616ED8AFE,55603312694BF99AD6000C2D0D72D368`
+
+### 获取音乐专辑/歌手信息
+
+说明:调用此接口,可以获取音乐专辑/歌手信息
+
+**必选参数:**
+
+`album_audio_id`: 专辑音乐 id (album_audio_id/MixSongID 均可以), 可以传多个,每个以逗号分开
+
+**可选参数**
+
+`fields`: 可以传 `album_info` `authors.base` `base` `audio_info`, `authors.ip`, `extra`, `tags`, `tagmap` 每个 field 以逗号分开
+
+**接口地址:** `/krm/audio`
+
+**调用例子:** `/krm/audio?album_audio_id=32155307` `/krm/audio?album_audio_id=32155307&fields=album_info,base,authors.base`
+
+### 私人 FM(对应手机和 pc 端的猜你喜欢)
+
+说明 : 私人 FM
+
+**可选参数:**
+
+`hash`: 音乐 hash, 建议
+
+`songid`: 音乐 songid, 建议
+
+`playtime`: 已播放时间, 建议
+
+`mode`: 获取模式,默认为 normal, normal:发现,small: 小众,peak:30s
+
+`action`: 默认为 play, garbage: 为不喜欢
+
+`song_pool_id`: 手机版的 AI,0:Alpha 根据口味推荐相似歌曲, 1:Beta 根据风格推荐相似歌曲, 2:Gamma
+
+`is_overplay`: 是否已播放完成
+
+`remain_songcnt`: 剩余未播放歌曲数, 默认为 0,大于 4 不返回推荐歌曲,建议
+
+**接口地址:** `/personal/fm`
+
+**调用例子:** `/personal/fm`
+
+### banner
+
+说明 : 调用此接口 , 可获取 banner( 轮播图 ) 数据
+
+**接口地址:** `/pc/diantai`
+
+**调用例子:** `/pc/diantai`
+
+### 乐库 banner
+
+说明 : 调用此接口 , 可获取 乐库 banner( 轮播图 ) 数据
+
+**接口地址:** `/yueku/banner`
+
+**调用例子:** `/yueku/banner`
+
+### 乐库电台
+
+说明 : 调用此接口 , 可获取乐库电台数据
+
+**接口地址:** `/yueku/fm`
+
+**调用例子:** `/yueku/fm`
+
+### 乐库
+
+说明 : 调用此接口 , 可获取手机端乐库数据
+
+**接口地址:** `/yueku`
+
+**调用例子:** `/yueku`
+
+### 电台
+
+说明 : 调用此接口 , 可获取所有电台数据
+
+**接口地址:** `/fm/class`
+
+**调用例子:** `/fm/class`
+
+### 电台 - 推荐
+
+说明 : 调用此接口 , 可获取推荐电台
+
+**接口地址:** `/fm/recommend`
+
+**调用例子:** `/fm/recommend`
+
+### 电台 - 图片
+
+说明 : 调用此接口 , 可获取对应电台的图片
+
+**必选参数:**
+
+`fmid`: fmid,可以传多个,以逗号分割
+
+**接口地址:** `/fm/image`
+
+**调用例子:** `/fm/image?fmid=693` `/fm/image?fmid=693,37`
+
+### 电台 - 音乐列表
+
+说明 : 调用此接口 , 可获取对应电台的音乐列表
+
+**必选参数:**
+
+`fmid`: fmid,可以传多个,以逗号分割
+
+**可选参数:**
+
+`fmtype`: fmtype, 可以传多个,以逗号分割
+
+`fmoffset`: 歌曲偏移,可以传多个,以逗号分割
+
+`fmsize`: 歌曲列表大小,可以传多个,以逗号分割
+
+**接口地址:** `/fm/songs`
+
+**调用例子:** `/fm/image?fmid=693` `/fm/image?fmid=693,37&fmtype=2,2&fmoffset=,5&fmsize5,3`
+
+### 编辑精选
+
+说明 : 调用此接口 , 可获取编辑精选数据
+
+**接口地址:** `/top/ip`
+
+**调用例子:** `/top/ip`
+
+### 编辑精选数据
+
+说明 : 调用此接口 , 可获取编辑对应数据
+
+**必选参数:**
+
+`id`: ip id
+
+**可选参数:**
+
+`type`: 数据类型,audios: 音乐, albums: 专辑, videos: 视频, author_list: 歌手
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+**接口地址:** `/ip`
+
+**调用例子:** `/ip?id=87473` `ip?id=87473&type=author_list`
+
+### 编辑精选歌单
+
+说明:调用此接口,可获取编辑精选歌单数据
+
+**必选参数:**
+
+`id`: ip id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+**接口地址:** `/ip/playlist`
+
+**调用例子:** `/ip/playlist?id=87473`
+
+### 编辑精选专区
+
+说明:调用此接口,可获取编辑精选专区相关内容,若数据中没有 ip_id,可使用[`/ip/zone/home`](#编辑精选专区详情)来获取数据
+
+**接口地址:** `/ip/zone`
+
+**调用例子:** `/ip/zone`
+
+### 编辑精选专区详情
+
+说明:调用此接口,可获取编辑精选专区详情,若[`/ip/zone`](#编辑精选专区) 数据中没有 ip_id,可使用该接口获取数据
+
+**必选参数:**
+
+`id`: ip id
+
+**接口地址:** `/ip/zone/home`
+
+**调用例子:** `/ip/zone/home?id=329`
+
+### 领取 VIP(需要登陆,该接口为测试接口,仅限概念版使用,该接口目前不可使用)
+
+说明 : 调用此接口 , 每天可领取 1 天 VIP 时长,需要领取 8 次,每次增加 3 小时,该接口来自 KG 概念版,非会员用户需要自行测试是否可用(尽量别频繁调用)
+
+**接口地址:** `/youth/vip`
+
+**调用例子:** `/youth/vip`
+
+### 领取一天 VIP(需要登陆,该接口为测试接口,仅限概念版使用)
+
+说明 : 调用此接口 , 领取概念版 VIP,传入日期可领取改日期一天 VIP,该接口来自 KG 概念版,非会员用户需要自行测试是否可用(尽量别频繁调用)
+
+注意 ⚠️:建议不要领太多天
+
+**必选参数:**
+
+`receive_day`: 领取 VIP 日期,格式为:2026-01-30
+
+**接口地址:** `/youth/day/vip`
+
+**调用例子:** `/youth/day/vip`
+
+### 升级概念版 VIP(需要登录,需要先领取一天 VIP,该接口为测试接口,仅限概念版使用)
+
+说明 : 调用此接口 , 可以升级成畅听 VIP,该接口需要先领取一天 VIP(`/youth/day/vip`),该接口来自 KG 概念版,非会员用户需要自行测试是否可用(尽量别频繁
+调用)
+
+**接口地址:** `/youth/day/vip/upgrade`
+
+**调用例子:** `/youth/day/vip/upgrade`
+
+### 获取当月已领取 VIP 天数(需要登陆,该接口为测试接口,仅限概念版使用)
+
+说明 : 调用此接口 ,获取当月已领取 VIP 天数
+
+**接口地址:** `/youth/month/vip/record`
+
+**调用例子:** `/youth/month/vip/record`
+
+### 获取已领取 VIP 状态(需要登陆,该接口为测试接口,仅限概念版使用)
+
+说明 : 调用此接口 ,获取已领取 VIP 状态
+
+**接口地址:** `/youth/union/vip`
+
+**调用例子:** `/youth/union/vip`
+
+### 获取歌手列表
+
+说明 : 调用此接口,可以获取歌手列表.
+
+**可选参数:**
+
+`sextypes`:性别类型,0:全部,1:男,2:女,3:组合
+
+`type`:类型,0:全部,1:华语,2:欧美,3:日韩,4:其他,5:日本,6:韩国
+
+`musician`:音乐人,3:为音乐人,0:默认
+
+`hotsize`:返回热门数量,默认 30
+
+**接口地址:** `/artist/lists`
+
+**调用例子:** `/artist/lists`
+
+### 获取歌手详情
+
+说明 : 调用此接口 , 传入歌手 id, 可获得歌手信息
+
+**必选参数:**
+
+`id`: 歌手 id
+
+**接口地址:** `/artist/detail`
+
+**调用例子:** `/artist/detail?id=6539`
+
+### 获取歌手专辑
+
+说明 : 调用此接口 , 传入歌手 id, 可获得歌手专辑
+
+**必选参数:**
+
+`id`: 歌手 id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+`sort`: 排序,hot : 热门, new: 最新
+
+**接口地址:** `/artist/albums`
+
+**调用例子:** `/artist/albums?id=6539`
+
+### 获取歌手单曲
+
+说明 : 调用此接口 , 传入歌手 id, 可获得歌手歌曲
+
+**必选参数:**
+
+`id`: 歌手 id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+`sort`: 排序,hot : 热门, new: 最新
+
+**接口地址:** `/artist/audios`
+
+**调用例子:** `/artist/audios?id=6539`
+
+### 获取歌手 MV
+
+说明 : 调用此接口 , 传入歌手 id, 可获得歌手 MV
+
+**必选参数:**
+
+`id`: 歌手 id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+`tag`: official: 官方版本,live:现场版本,fan:饭制版本,artist: 歌手发布, all: 获取全部,默认为获取全部
+
+**接口地址:** `/artist/videos`
+
+**调用例子:** `/artist/videos?id=6539`
+
+### 关注歌手
+
+说明:调用此接口, 传入歌手 id, 可以关注该歌手(需要登录)
+
+**必选参数:**
+
+`id`: 歌手 id
+
+**接口地址:** `/artist/follow`
+
+**调用例子:** `/artist/follow?id=6539`
+
+### 取消关注歌手
+
+说明:调用此接口, 传入歌手 id, 可以取消关注该歌手(需要登录)
+
+**必选参数:**
+
+`id`: 歌手 id
+
+**接口地址:** `/artist/unfollow`
+
+**调用例子:** `/artist/unfollow?id=6539`
+
+### 获取关注歌手新歌
+
+说明:调用此接口, 可以获取用户已关注的歌手新歌(需要登录)
+
+**可选参数:**
+
+`last_album_id`: 最后专辑 id
+
+`pagesize`: 每页页数, 默认为 30,
+
+`opt_sort`: 排序,1:时间,2:亲密度,默认为 1(时间)
+
+**接口地址:** `/artist/follow/newsongs`
+
+**调用例子:** `/artist/follow/newsongs`
+
+### 获取视频 url
+
+说明 : 传入的视频的 hash, 可以获取对应的视频的 url
+
+**必选参数:**
+
+`hash`: 视频 hash
+
+**接口地址:** `/video/url`
+
+**调用例子:** `/video/url?hash=3B5EE16299F703AEB0E5C28CB152EDF0`
+
+### 获取歌曲 MV
+
+说明 : 传入 album_audio_id/MixSongID 获取歌曲 相对应的 mv
+
+**必选参数:**
+
+album_audio_id: 专辑音乐 id (album_audio_id/MixSongID 均可以), 可以传多个,每个以逗号分开,
+
+**可选参数:**
+
+fields: 支持多个,每个以逗号分隔,支持的值有:mkv,tags,h264,h265,authors
+
+**接口地址:** `/kmr/audio/mv`
+
+**调用例子:** `/kmr/audio/mv?album_audio_id=32155307` `/kmr/audio/mv?album_audio_id=32155307&fields=mkv,tags`
+
+### 获取视频相关信息
+
+说明 : 传入的视频的 hash, 可以获取对应的视频的相关信息
+
+**必选参数:**
+
+`hash`: 视频 hash,可以传多个,以逗号隔开
+
+**接口地址:** `/video/privilege`
+
+**调用例子:** `/video/privilege?hash=3B5EE16299F703AEB0E5C28CB152EDF0`
+
+### 获取视频详情
+
+说明:调用此接口,可以获取视频详情,可以获取更高清的视频 hash
+
+**必选参数:**
+
+`id`: 视频 id/video id
+
+**接口地址:** `/video/detail`
+
+**调用例子:** `/video/detail?id=11517822`
+
+### 新歌速递
+
+说明:调用此接口,可以获取新歌速递
+
+**接口地址:** `/top/song`
+
+**调用例子:** `/top/song`
+
+### 场景音乐列表
+
+说明:调用此接口,可以场景音乐列表
+
+**接口地址:** `/scene/lists`
+
+**调用例子:** `/scene/lists`
+
+### 场景音乐详情
+
+说明:调用此接口,可以场景音乐详情
+
+**必选参数:**
+
+`id`: 场景音乐 scene_id
+
+**接口地址:** `/scene/module`
+
+**调用例子:** `/scene/module?id=9`
+
+### 获取场景音乐讨论区
+
+说明:调用此接口,可以获取场景音乐讨论区
+
+**必选参数**
+
+`id`: 场景音乐 scene_id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+`sort`: 排序,rec: 推荐,hot: 热门,new: 最新, 默认为推荐
+
+**接口地址:** `/scene/list/v2`
+
+**调用例子:** `/scene/list/?id=9`
+
+### 获取场景音乐模块 Tag
+
+说明:调用此接口,可以获取场景模块 Tag
+
+**必选参数**
+
+`id`: 场景音乐 scene_id
+
+`module_id`: 场景音乐 module_id
+
+**可选参数:**
+
+**接口地址:** `/scene/module/info`
+
+**调用例子:** `/scene/module/info?id=9&module_id=83`
+
+### 获取场景音乐歌单列表
+
+说明:调用此接口,可以获取场景音乐歌单列表
+
+**必选参数**
+
+`tag_id`: 场景音乐 tag_id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+**接口地址:** `/scene/collection/list`
+
+**调用例子:** `/scene/collection/list?tag_id=42391`
+
+### 获取场景音乐视频列表
+
+说明:调用此接口,可以获取场景音乐视频列表
+
+**必选参数**
+
+`tag_id`: 场景音乐视频 tag_id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+**接口地址:** `/scene/video/list`
+
+**调用例子:** `/scene/video/list?tag_id=42399`
+
+### 获取场景音乐音乐列表
+
+说明:调用此接口,可以获取场景音乐音乐列表
+
+**必选参数**
+
+`id`: 场景音乐 scene_id
+
+`module_id`: 场景音乐 module_id
+
+`tag`: 场景音乐 tag_id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+**接口地址:** `/scene/audio/list`
+
+**调用例子:** `/scene/audio/list?id=9&module_id=173&tag=42391`
+
+### 每日推荐
+
+说明:调用此接口,可以获取每日推荐列表
+
+**可选参数:**
+
+`platform`:设备类型,默认为 ios,支持 android 和 ios
+
+**接口地址:** `/everyday/recommend`
+
+**调用例子:** `/everyday/recommend`
+
+### 历史推荐
+
+说明:调用此接口,可以获取历史推荐
+
+**可选参数:**
+
+`mode`:当 mode 为 list 时,则返回历史推荐列表,当 mode 为 song 时则返回当前歌曲列表,支持参数为:list 和 song,
+
+`history_name`: 当 mode 为 song 该参数为必选参数。
+
+`date`: 当 mode 为 song 该参数为必选参数。
+
+`platform`:设备类型,默认为 ios,支持 android 和 ios
+
+**接口地址:** `/everyday/history`
+
+**调用例子:** `/everyday/history` `/everyday/history?mode=song&history_name=RT_336d5ebc5436534e61d16e63ddfca327_20240106&date=20240106`
+
+### 风格推荐
+
+说明:调用此接口,可以获取风格推荐
+
+**可选参数:**
+
+`platform`:设备类型,默认为 ios,支持 android 和 ios
+
+`tagids`:支持多个,每个以逗号分隔,该接口下可获取 tag 信息
+
+**接口地址:** `/everyday/style/recommend`
+
+**调用例子:** `/everyday/style/recommend` `/everyday/style/recommend?tagids=S14,S15,S16`
+
+### 排行列表
+
+说明:调用此接口,可以获取排行榜列表
+
+**可选参数:**
+
+`withsong`:是否返回歌曲(部分)
+
+**接口地址:** `/rank/list`
+
+**调用例子:** `/rank/list`
+
+### 排行榜推荐列表
+
+说明:调用此接口,可以获取排行榜推荐列表
+
+**接口地址:** `/rank/top`
+
+**调用例子:** `/rank/top`
+
+### 排行榜往期列表
+
+说明:调用此接口,可以获取排行榜往期列表
+
+**必选参数:**
+
+`rankid`:排行榜 id
+
+**可选参数:**
+
+`rank_cid`:排行榜 cid
+
+**接口地址:** `/rank/vol`
+
+**调用例子:** `/rank/vol?rankid=8888`
+
+### 排行榜信息
+
+说明:调用此接口,可以获取排行榜信息
+
+**必选参数:**
+
+`rankid`:排行榜 id
+
+**可选参数:**
+
+`rank_cid`:排行榜 cid
+
+`album_img`:是否返回专辑图片,1:返回,0:不返回,默认返回
+
+`zone`:排行榜 zone
+
+**接口地址:** `/rank/info`
+
+**调用例子:** `/rank/info?rankid=8888`
+
+### 排行榜歌曲列表
+
+说明:调用此接口,可以获排行榜歌曲列表
+
+**必选参数:**
+
+`rankid`:排行榜 id
+
+**可选参数:**
+
+`rank_cid`:若需要返回往期歌曲列表,则该参数为必填,否则默认返回最新一期,[`/rank/vol`](#排行榜往期列表) 返回值中,`volid` 则为该参数
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+**接口地址:** `/rank/audio`
+
+**调用例子:** `/rank/audio?rankid=8888` `/rank/audio?rankid=8888&rank_cid=76442`
+
+### 歌曲收藏数
+
+说明 : 调用此接口 , 传入音乐 mixsongids 参数 , 可获得该音乐的收藏数( 不需要登录 )
+
+**必选参数:**
+
+`mixsongids`:音乐 mixsongid,多个以逗号分隔
+
+**接口地址:** `/favorite/count`
+
+**调用例子:** `/favorite/count?mixsongids=368015985,368015986` `/favorite/count?mixsongids=368015985`
+
+### 歌曲评论数
+
+说明 : 调用此接口 , 传入音乐 hash/special_id 参数 , 可获得该音乐的评论数( 不需要登录 )
+
+**必选参数:**
+
+`hash`:音乐 hash
+
+`special_id`:为 评论下的 special_child_id 字段
+
+**接口地址:** `/comment/count`
+
+**调用例子:** `/comment/count?hash=98eb07ad8eaf74bf56dece55518ad63e` `/comment/count?special_id=20505418`
+
+### 歌曲评论
+
+说明 : 调用此接口 , 传入音乐 mixsongid 参数 , 可获得该音乐的所有评论 ( 不需要登录 )
+
+**必选参数:**
+
+`mixsongid`:音乐 mixsongid
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+`show_classify`: 是否返回分类列表,0 为不返回,1 为返回
+
+`show_hotword_list`:是否返回热词,0 为不返回,1 为返回
+
+**接口地址:** `/comment/music`
+
+**调用例子:** `/comment/music?mixsongid=302362878`
+
+### 歌曲评论-根据分类返回
+
+说明 : 调用此接口 , 传入音乐 mixsongid 和 type_id 参数 , 可获得该音乐的分类评论 ( 不需要登录 )
+
+**必选参数:**
+
+`mixsongid`:音乐 mixsongid
+
+`type_id`:分类 id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+`sort`:排序,1 为正序,2 为倒序
+
+**接口地址:** `/comment/music/classify`
+
+**调用例子:** `/comment/music/classify?mixsongid=302362878&type_id=12`
+
+### 歌曲评论-根据热词返回
+
+说明 : 调用此接口 , 传入音乐 mixsongid 和 hot_word 参数 , 可获得该音乐的热词评论 ( 不需要登录 )
+
+**必选参数:**
+
+`mixsongid`:音乐 mixsongid
+
+`hot_word`:热词
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+**接口地址:** `/comment/music/hotword`
+
+**调用例子:** `/comment/music/hotword?mixsongid=302362878&hot_word=生活`
+
+### 楼层评论
+
+说明 : 调用此接口 , 传入资源 special_id 和资源类型 tid 和资源 mixsongid 参数, 可获得该资源的歌曲楼层评论
+
+**必选参数:**
+
+`special_id`:为 评论下的 special_child_id 字段
+
+`mixsongid`:为 歌曲的 mixsongid
+
+`tid`:评论 id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+**接口地址:** `/comment/floor`
+
+**调用例子:** `/comment/floor?special_id=100285259&mixsongid=302362878&tid=678433417`
+
+### 歌单评论
+
+说明 : 调用此接口 , 传入歌单 id 参数 , 可获得该歌单的所有评论 ( 不需要登录 )
+
+**必选参数:**
+
+`id`:歌单 global_collection_id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+`show_classify`: 是否返回分类列表,0 为不返回,1 为返回
+
+`show_hotword_list`:是否返回热词,0 为不返回,1 为返回
+
+**接口地址:** `/comment/playlist`
+
+**调用例子:** `/comment/playlist?id=collection_3_1373407643_366_0`
+
+### 专辑评论
+
+说明 : 调用此接口 , 传入 专辑 id 参数 , 可获得该专辑的所有评论 ( 不需要登录 )
+
+`id`:专辑 id
+
+**可选参数:**
+
+`page`: 页码
+
+`pagesize`: 每页页数, 默认为 30
+
+`show_classify`: 是否返回分类列表,0 为不返回,1 为返回
+
+`show_hotword_list`:是否返回热词,0 为不返回,1 为返回
+
+**接口地址:** `/comment/album`
+
+**调用例子:** `/comment/album?id=collection_3_1373407643_366_0`
+
+### 提交听歌历史
+
+说明:提交听歌历史后,支持在其他设备上查看听歌历史
+
+**必选参数:**
+
+`mxid`: 专辑音乐 id (album_audio_id/MixSongID 均可以)
+
+**可选参数:**
+
+`ot`:当前时间戳, 秒级,不要传入毫秒级,否者会返回错误,或者从 [`获取服务器时间`](#获取服务器时间) 中获取
+
+`pc`: 当前播放次数,更新播放次数,当服务器的值大于传入值时,将维持服务最大值,否则更新
+
+**接口地址:** `/playhistory/upload`
+
+**调用例子:** `/playhistory/upload?mxid=32155307`
+
+### 获取服务器时间
+
+说明:获取服务器时间,返回服务器时间戳
+
+**接口地址:** `/server/now`
+
+**调用例子:** `/server/now`
+
+### 刷刷
+
+说明:获取刷刷视频
+
+**接口地址:** `/brush`
+
+**调用例子:** `/brush`
+
+### AI 推荐
+
+说明:传入 album_audio_id/MixSongID 获取 AI 推荐歌曲
+
+**必选参数:**
+
+`album_audio_id`: 专辑音乐 id (album_audio_id/MixSongID 均可以), 可以传多个,每个以逗号分开,
+
+**接口地址:** `/ai/recommend`
+
+**调用例子:** `/ai/recommend?album_audio_id=274565080` `/ai/recommend?album_audio_id=274565080,68435124`
+
+### 曲谱 - 获取歌曲曲谱
+
+说明 : 调用此接口,传入歌曲 album_audio_id/MixSongID 可获得该歌曲的曲谱,注意:需要自己解析,别问我,我也看不懂
+
+**必选参数:**
+
+`album_audio_id`:音乐的 mixsongid/album_audio_id
+
+**可选参数:**
+
+`instruments`:曲谱类型 默认为 1,1:吉他, 2:尤克里里,3:钢琴,4:简谱
+
+`level`: 等级/难度 默认为 0, 吉他(1: 进阶, 2:基础,0:中级),尤克里里/钢琴(0:基础,1:进阶),简谱(0:基础)
+
+**接口地址:** `/sheet/song`
+
+**调用例子:** `/sheet/song?album_audio_id=68435136`
+
+### 曲谱 - 曲谱详情
+
+说明 : 调用此接口,传入曲谱 opern_id 可获得该曲谱详情,**注意:需要自己解析,别问我,我也看不懂**
+
+**必选参数:**
+
+`opern_id`:曲谱 id,
+
+**接口地址:** `/sheet/detail`
+
+**调用例子:** `/sheet/detail?id=68435136_1_0`
+
+### 曲谱 - 曲谱排行榜
+
+说明 : 调用此接口,可以获取推荐排行榜
+
+**可选参数:**
+
+`instruments`:曲谱类型 默认为 1,1:吉他, 2:尤克里里,3:钢琴,4:简谱
+
+`level`: 等级/难度 默认为 0, 吉他(1: 进阶, 2:基础,0:中级),尤克里里/钢琴(0:基础,1:进阶),简谱(0:基础)
+
+`page`:页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/sheet/rank`
+
+**调用例子:** `/sheet/rank`
+
+### 曲谱 - 曲谱广场
+
+说明 : 调用此接口,可以获取推荐曲谱
+
+**可选参数:**
+
+`instruments`:曲谱类型 默认为 1,1:吉他, 2:尤克里里,3:钢琴,4:简谱
+
+`level`: 等级/难度 默认为 0, 吉他(1: 进阶, 2:基础,0:中级),尤克里里/钢琴(0:基础,1:进阶),简谱(0:基础)
+
+`page`:页数
+
+`pagesize `: 每页页数, 默认为 30
+
+`tagid`: 标签, 通过 `/sheet/tags` 获取
+
+**接口地址:** `/sheet/explore`
+
+**调用例子:** `/sheet/explore`
+
+### 曲谱 - 曲谱标签
+
+说明 : 调用此接口,可以获取曲谱标签
+
+**接口地址:** `/sheet/tags`
+
+**调用例子:** `/sheet/tags`
+
+### 频道 - 获取用户所有频道
+
+说明:登录后调用此接口,可以获取用户所有订阅的频道
+
+**可选参数:**
+
+`page`:页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/youth/channel/all`
+
+**调用例子:** `/youth/channel/all`
+
+### 频道 - 详情
+
+说明:调用此接口,传入 `global_collection_id / channel_id` 可以获取频道详情
+
+**必选参数:**
+
+`global_collection_id`:频道 id (global_collection_id / channel_id 均可以), 可以传多个,每个以逗号分开,
+
+**接口地址:** `/youth/channel/detail`
+
+**调用例子:** `/youth/channel/detail?global_collection_id=11576464149`
+
+### 频道 - 频道安利
+
+说明:调用此接口,传入 `global_collection_id / channel_id` 可以获取频道安利
+
+**必选参数:**
+
+`global_collection_id`:频道 id (global_collection_id / channel_id 均可以)
+
+**接口地址:** `/youth/channel/amway`
+
+**调用例子:** `/youth/channel/amway?global_collection_id=11576464149`
+
+### 频道 - 相似频道
+
+说明:调用此接口,传入 `global_collection_id / channel_id` 可以获取相似频道
+
+**必选参数:**
+
+`channel_id`:频道 id (global_collection_id / channel_id 均可以)
+
+**接口地址:** `/youth/channel/similar`
+
+**调用例子:** `/youth/channel/similar?channel_id=11576464149`
+
+### 频道 - 订阅
+
+说明:登录后调用此接口, 传入 `global_collection_id / channel_id` 可订阅频道
+
+**必选参数:**
+
+`global_collection_id`:频道 id (global_collection_id / channel_id 均可以)
+
+**可选参数:**
+
+`t`:1 为订阅,0 为取消订阅,不传默认为订阅
+
+**接口地址:** `/youth/channel/sub`
+
+**调用例子:** `/youth/channel/sub?global_collection_id=11576464149` `/youth/channel/sub?global_collection_id=11576464149&t=0`
+
+### 频道 - 音乐故事
+
+说明:调用此接口,传入 `global_collection_id / channel_id` 可以获取音乐故事
+
+**必选参数:**
+
+`global_collection_id`:频道 id (global_collection_id / channel_id 均可以)
+
+**可选参数:**
+
+`page`:页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/youth/channel/song`
+
+**调用例子:** `/youth/channel/song?global_collection_id=11576464149`
+
+### 频道 - 音乐故事详情
+
+说明:调用此接口,传入 `global_collection_id / channel_id` 和 `fileid` 可以获取音乐故事详情
+
+**必选参数:**
+
+`global_collection_id`:频道 id (global_collection_id / channel_id 均可以)
+
+`fileid`: 音乐故事 fileid
+
+**接口地址:** `/youth/channel/song/detail`
+
+**调用例子:** `/youth/channel/song/detail?global_collection_id=11576464149&fileid=1720958083456581`
+
+### 动态 - 最常访问
+
+说明:登录后调用此接口,可以获取经常访问的频道和用户
+
+**接口地址:** `/youth/dynamic/recent`
+
+**调用例子:** `/youth/dynamic/recent`
+
+### 获取用户公开的音乐
+
+说明:调用此接口,可以获取用户公开的音乐
+
+**必选参数:**
+
+`userid`:用户 id
+
+**可选参数:**
+
+`page`:页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/youth/user/song`
+
+**调用例子:** `/youth/user/song?userid=1354894105`
+
+### 听书 - 每日推荐
+
+**可选参数:**
+
+`page`:页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/longaudio/daily/recommend`
+
+**调用例子:** `/longaudio/daily/recommend`
+
+### 听书 - 排行榜推荐
+
+**接口地址:** `/longaudio/rank/recommend`
+
+**调用例子:** `/longaudio/rank/recommend`
+
+### 听书 - VIP 推荐
+
+**接口地址:** `/longaudio/vip/recommend`
+
+**调用例子:** `/longaudio/vip/recommend`
+
+### 听书 - 每周推荐
+
+**接口地址:** `/longaudio/week/recommend`
+
+**调用例子:** `/longaudio/week/recommend`
+
+### 听书 - 专辑详情
+
+**必选参数:**
+
+`album_id`: 专辑 id 可以传多个,每个以逗号分开,
+
+**接口地址:** `/longaudio/album/detail`
+
+**调用例子:** `/longaudio/album/detail?album_id=56655759`
+
+### 听书 - 专辑音乐列表
+
+**必选参数:**
+
+`album_id`: 专辑 id 可以传多个
+
+**接口地址:** `/longaudio/album/audios`
+
+**调用例子:** `/longaudio/album/audios?album_id=56655759`
+
+### 歌曲详情 - 歌曲成绩单
+
+说明:调用此接口,可以获取`歌曲详情`里面的`歌曲成绩单`信息
+
+**必选参数:**
+
+`album_audio_id`: 专辑音乐 id (album_audio_id/MixSongID 均可以),
+
+**接口地址:** `/song/ranking`
+
+**调用例子:** `/song/ranking?album_audio_id=32155307`
+
+### 歌曲详情 - 歌曲成绩单详情
+
+说明:登陆后调用此接口,可以获取更详细的歌曲成绩单信息
+
+**必选参数:**
+
+`album_audio_id`: 专辑音乐 id (album_audio_id/MixSongID 均可以),
+
+**可选参数:**
+
+`page`:页数
+
+`pagesize `: 每页页数, 默认为 30
+
+**接口地址:** `/song/ranking/filter`
+
+**调用例子:** `/song/ranking/filter?album_audio_id=32155307`
+
+### 验证码 - 获取验证码格式
+
+说明:碰到错误码 20028 时,可调用该接口获取验证码的格式。
+
+**必选参数:**
+
+`eventid`:当请求出现错误码为 20028 时,一般会返回
+
+**接口地址:** `/get/verify/info`
+
+**调用例子:** `/get/verify/info?eventid=gz_tx_event_xxx`
+
+### 验证码 - 验证验证码格式
+
+说明:通过验证码后通过该请求验证。(目前仅实现了腾讯验证码,其他的没碰到)
+
+**必选参数:**
+
+`eventid`:当请求出现错误码为 20028 时,一般会返回
+
+`v_type`:`/get/verify/info` 下获取
+
+`verifycode`:通过滑块验证码等获取
+
+`sid`:通过 wasm 获取,具体可查看 `public\login_captcha.html`
+
+`edt`:同上
+
+**接口地址:** `/verify/user/info`
+
+**调用例子:** `/verify/user/info?eventid=gz_tx_event_xxx&v_type=xx&verifycode=xxx&sid=xxx&edt=xxx`
+
+### 听歌识曲
+
+说明:调用此接口,传入 PCM 音频数据(`application/octet-stream`),可识别歌曲信息。**该接口必须使用 POST 请求**
+
+**必选参数:**
+
+`data`:PCM 音频二进制数据(16bit 采样,通过 POST body 以 `application/octet-stream` 格式传入)
+
+**接口地址:** `/audio/match`
+
+**调用例子:** 使用 POST 请求,`Content-Type: application/octet-stream`,body 为 PCM 二进制数据
+
+```javascript
+// 前端示例
+const res = await fetch('/audio/match', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/octet-stream' },
+ body: pcmArrayBuffer,
+});
+```
+
+### 获取已购单曲
+
+说明:登录后调用此接口,可以获取用户已购买的单曲列表,返回歌曲详情及音频信息(需要登录)
+
+**可选参数:**
+
+`page`:页数
+
+`pagesize`:每页页数,默认为 50
+
+**接口地址:** `/user/purchased/songs`
+
+**调用例子:** `/user/purchased/songs`
+
+### 获取已购专辑
+
+说明:登录后调用此接口,可以获取用户已购买的专辑列表(需要登录)
+
+**可选参数:**
+
+`page`:页数
+
+`pagesize`:每页页数,默认为 15
+
+**接口地址:** `/user/purchased/albums`
+
+**调用例子:** `/user/purchased/albums`
+
+## License
+
+[The MIT License (MIT)](https://github.com/MakcRe/KuGouMusicApi/blob/main/LICENSE)
diff --git a/third_party/MoeKoeMusic/api/docs/_coverpage.md b/third_party/MoeKoeMusic/api/docs/_coverpage.md
new file mode 100644
index 0000000..eb9bf4e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/docs/_coverpage.md
@@ -0,0 +1,12 @@
+# 酷狗音乐 API
+
+> 酷狗音乐 NodeJS 版 API
+
+- 全部接口已升级到最新
+- 具备登录接口
+- 更完善的文档
+
+[GitHub](https://github.com/MakcRe/KuGouMusicApi)
+[Get Started](#kugoumusic-api)
+
+
\ No newline at end of file
diff --git a/third_party/MoeKoeMusic/api/docs/index.html b/third_party/MoeKoeMusic/api/docs/index.html
new file mode 100644
index 0000000..0fc14ea
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/docs/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+ 酷狗音乐 NodeJS 版 API
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/third_party/MoeKoeMusic/api/index.js b/third_party/MoeKoeMusic/api/index.js
new file mode 100644
index 0000000..4cd5921
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/index.js
@@ -0,0 +1 @@
+require('./app');
diff --git a/third_party/MoeKoeMusic/api/interface.d.ts b/third_party/MoeKoeMusic/api/interface.d.ts
new file mode 100644
index 0000000..f5314fe
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/interface.d.ts
@@ -0,0 +1,2754 @@
+/**
+ * @fileoverview KuGouMusicApi TypeScript 类型定义
+ *
+ * 基于 module/ 目录下各模块文件自动生成的扁平函数导出结构,
+ * 每个 API 对应一个独立的导出函数,参数类型与返回类型一一对应。
+ *
+ * 导出结构参考 app.js:
+ * module.exports = { ...require('./server'), ...require('./util/request'), ...obj };
+ */
+
+// ============================================================
+// 基础通用类型
+// ============================================================
+
+/** Cookie 键值对映射 */
+export interface CookieMap {
+ [key: string]: string;
+}
+
+/** 所有 API 接口共享的基础请求参数 */
+export interface CommonParams {
+ /**
+ * 用户认证 Cookie
+ * - 支持字符串格式(如 `"token=xxx;userid=xxx;dfid=xxx"`)
+ * - 支持对象格式(如 `{ token: "xxx", userid: "xxx" }`)
+ */
+ cookie?: string | CookieMap;
+ /** 时间戳,用于绕过 2 分钟缓存(使每次请求 URL 不同) */
+ timestamp?: number | string;
+ /** 是否禁止将响应中的 Cookie 写回客户端 */
+ noCookie?: boolean;
+}
+
+/** API 统一响应结构 */
+export interface ApiResponse {
+ /** HTTP 状态码 */
+ status: number;
+ /** 响应体(JSON 格式) */
+ body: T;
+ /** 响应头 */
+ headers: Record;
+ /** 服务端设置的 Cookie 列表 */
+ cookie?: string[];
+}
+
+/** 带分页的通用请求参数 */
+export interface PaginatedParams extends CommonParams {
+ /** 页码 */
+ page?: number;
+ /** 每页条数,默认 30 */
+ pagesize?: number;
+}
+
+/** 通用错误响应体 */
+export interface ErrorBody {
+ /** 错误码 */
+ code: number;
+ /** 数据(通常为 null) */
+ data: null;
+ /** 错误消息 */
+ msg: string;
+}
+
+// ============================================================
+// 枚举 / 联合字面量类型
+// ============================================================
+
+/**
+ * 音乐音质类型
+ *
+ * - `piano`:魔法音乐 - 钢琴
+ * - `acappella`:魔法音乐 - 人声伴奏(返回 mkv 格式,含人声和伴奏两个音轨)
+ * - `subwoofer`:魔法音乐 - 骨笛
+ * - `ancient`:魔法音乐 - 尤克里里
+ * - `surnay`:魔法音乐 - 唢呐
+ * - `dj`:魔法音乐 - DJ
+ * - `128`:128kbps MP3
+ * - `320`:320kbps MP3
+ * - `flac`:FLAC 无损
+ * - `high`:无损格式
+ * - `viper_atmos`:蝰蛇全景声
+ * - `viper_clear`:蝰蛇超清音质
+ * - `viper_tape`:蝰蛇母带(需转码)
+ * - `super`:DSD 格式(支持极少)
+ */
+export type SongQuality =
+ | 'piano'
+ | 'acappella'
+ | 'subwoofer'
+ | 'ancient'
+ | 'surnay'
+ | 'dj'
+ | '128'
+ | '320'
+ | 'flac'
+ | 'high'
+ | 'viper_atmos'
+ | 'viper_clear'
+ | 'viper_tape'
+ | 'super';
+
+/** 搜索类型 */
+export type SearchType = 'song' | 'special' | 'lyric' | 'album' | 'author' | 'mv';
+
+/** 歌词搜索是否返回多个 */
+export type LyricMan = 'yes' | 'no';
+
+/** 评论排序方向 */
+export type CommentSort = 1 | 2;
+
+/** 私人 FM 获取模式 */
+export type FmMode = 'normal' | 'small' | 'peak';
+
+/** 私人 FM 操作类型 */
+export type FmAction = 'play' | 'garbage';
+
+/** 私人 FM AI 推荐池 */
+export type FmSongPoolId = 0 | 1 | 2;
+
+/** 歌曲推荐 card_id(标准版) */
+export type CardId = 1 | 2 | 3 | 4 | 5 | 6;
+
+/** 歌曲推荐 card_id(概念版) */
+export type YouthCardId = 3001 | 3004 | 3005 | 3006 | 3014 | 3101;
+
+/** 新碟上架地区类型 */
+export type AlbumType = 1 | 2 | 3 | 4;
+
+/** 歌手性别类型 */
+export type ArtistSexType = 0 | 1 | 2 | 3;
+
+/** 歌手地区类型 */
+export type ArtistRegionType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
+
+/** 歌手 MV 标签类型 */
+export type ArtistVideoTag = 'official' | 'live' | 'fan' | 'artist' | 'all';
+
+/** 歌手排序方式 */
+export type ArtistSort = 'hot' | 'new';
+
+/** 场景音乐讨论区排序 */
+export type SceneSort = 'rec' | 'hot' | 'new';
+
+/** 音频相关版本排序 */
+export type AudioRelatedSort = 'all' | 'hot' | 'new';
+
+/** 歌单操作类型:0 = 创建歌单,1 = 收藏歌单 */
+export type PlaylistAddType = 0 | 1;
+
+/** 曲谱类型 */
+export type SheetOpernType = 0 | 1 | 2 | 3 | 98 | 99;
+
+/** 曲谱合集 position */
+export type SheetCollectionPosition = 2 | 3 | 4;
+
+/** 平台设备类型 */
+export type Platform = 'ios' | 'android';
+
+/** 编辑精选数据类型 */
+export type IpDataType = 'audios' | 'albums' | 'videos' | 'author_list';
+
+/** 听歌历史类型:0 = 最近一周,1 = 全部累计 */
+export type ListenType = 0 | 1;
+
+/** 历史推荐模式 */
+export type HistoryMode = 'list' | 'song';
+
+/** 专辑信息可选字段 */
+export type AlbumField =
+ | 'trans_param'
+ | 'special_tag'
+ | 'authors'
+ | 'album_name'
+ | 'publish_date'
+ | 'cover'
+ | 'intro'
+ | 'publish_company'
+ | 'type'
+ | 'album_id'
+ | 'language_id'
+ | 'is_publish'
+ | 'heat'
+ | 'grade'
+ | 'quality'
+ | 'exclusive'
+ | 'grade_count'
+ | 'author_name'
+ | 'sizable_cover'
+ | 'language'
+ | 'category';
+
+/** krm/audio 接口可选字段 */
+export type KrmAudioField = 'album_info' | 'authors.base' | 'base' | 'audio_info' | 'authors.ip' | 'extra' | 'tags' | 'tagmap';
+
+/** 关注歌手新歌排序:1 = 时间,2 = 亲密度 */
+export type FollowNewSongsSort = 1 | 2;
+
+/** 获取歌曲 MV 可选字段 */
+export type MvField = 'mkv' | 'tags' | 'h264' | 'h265' | 'authors';
+
+// ============================================================
+// 请求参数类型 —— 登录与认证
+// ============================================================
+
+/** 手机号登录参数 */
+export interface LoginCellphoneParams extends CommonParams {
+ /** 手机号码(必选) */
+ mobile: string;
+ /** 验证码,通过 `/captcha/sent` 获取(必选) */
+ code: string;
+ /** 用户 id,当用户存在多个账户时必须指定 */
+ userid?: string;
+}
+
+/** 用户名登录参数 */
+export interface LoginParams extends CommonParams {
+ /** 用户名(必选) */
+ username: string;
+ /** 密码(必选),建议使用 encodeURIComponent 编码或 POST 请求 */
+ password: string;
+}
+
+/** 开放接口登录参数(目前仅支持微信) */
+export interface LoginOpenplatParams extends CommonParams {
+ /** 微信扫码成功后生成的 code(必选) */
+ code: string;
+}
+
+/** 二维码 key 生成参数 */
+export interface LoginQrKeyParams extends CommonParams {}
+
+/** 二维码生成参数 */
+export interface LoginQrCreateParams extends CommonParams {
+ /** 由 `/login/qr/key` 生成的 key(必选) */
+ key: string;
+ /** 传入后会额外返回二维码图片 base64 编码 */
+ qrimg?: string | boolean;
+}
+
+/** 二维码扫码状态检测参数 */
+export interface LoginQrCheckParams extends CommonParams {
+ /** 由 `/login/qr/key` 生成的 key(必选) */
+ key: string;
+}
+
+/** 微信二维码生成参数 */
+export interface LoginWxCreateParams extends CommonParams {}
+
+/** 微信二维码扫码状态检测参数 */
+export interface LoginWxCheckParams extends CommonParams {
+ /** 由 `/login/wx/create` 生成的 uuid(必选) */
+ uuid: string;
+ /** 建议传递,避免缓存导致延迟 */
+ timestamp?: number | string;
+}
+
+/** 刷新登录参数 */
+export interface LoginTokenParams extends CommonParams {
+ /** 登录后获取的 token */
+ token?: string;
+ /** 用户 id */
+ userid?: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 验证码 & 设备
+// ============================================================
+
+/** 发送验证码参数 */
+export interface CaptchaSentParams extends CommonParams {
+ /** 手机号码(必选) */
+ mobile: string;
+}
+
+/** dfid 获取参数 */
+export interface RegisterDevParams extends CommonParams {}
+
+/** 获取验证码信息参数 */
+export interface GetVerifyInfoParams extends CommonParams {
+ /** 验证事件 ID(必选) */
+ eventid: string;
+ /** 用户 id */
+ userid?: string;
+ /** 平台 id,默认 2 */
+ platid?: number;
+}
+
+/** 提交验证码验证参数 */
+export interface VerifyUserInfoParams extends CommonParams {
+ /** 验证事件 ID(必选) */
+ eventid: string;
+ /** 验证类型:23 = 腾讯验证码,32 = 手机验证码 */
+ v_type?: number;
+ /** 验证码数据(必选) */
+ verifycode: string;
+ /** RSA 加密的 AES 密钥(从 get_verify_info 或 sidedt 获取) */
+ sid?: string;
+ /** AES 加密的行为数据(从 get_verify_info 或 sidedt 获取) */
+ edt?: string;
+ /** 用户 id */
+ userid?: string;
+ /** 平台 id,默认 2 */
+ platid?: number;
+}
+
+/** 获取设备列表参数 */
+export interface LoginDeviceParams extends CommonParams {
+ /** 用户 id */
+ userid?: string;
+}
+
+/** 登出指定设备参数 */
+export interface LoginDeviceKickParams extends CommonParams {
+ /** 用户 token */
+ token?: string;
+ /** 设备 mid */
+ mid?: string;
+}
+
+/** 获取 sid/edt 参数 */
+export interface SidedtParams extends CommonParams {
+ /** 用户 id */
+ userid?: string;
+ /** 设备指纹 ID */
+ dfid?: string;
+ /** 设备 MID */
+ mid?: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 用户信息
+// ============================================================
+
+/** 获取用户额外信息参数 */
+export interface UserDetailParams extends CommonParams {}
+
+/** 获取用户 VIP 信息参数 */
+export interface UserVipDetailParams extends CommonParams {}
+
+/** 获取用户歌单参数 */
+export interface UserPlaylistParams extends PaginatedParams {}
+
+/** 获取用户关注歌手参数 */
+export interface UserFollowParams extends CommonParams {}
+
+/** 获取关注歌手消息参数 */
+export interface UserFollowMessageParams extends CommonParams {
+ /** 歌手/用户的 userid(必选) */
+ id: string;
+ /** 每页页数,默认 30 */
+ pagesize?: number;
+}
+
+/** 获取用户云盘参数 */
+export interface UserCloudParams extends PaginatedParams {}
+
+/** 获取用户云盘音乐 URL 参数 */
+export interface UserCloudUrlParams extends CommonParams {
+ /** 音乐 hash(必选) */
+ hash: string;
+ /** 专辑 id */
+ album_id?: string;
+ /** 云盘音乐名称 */
+ name?: string;
+ /** 专辑音频 id */
+ album_audio_id?: string;
+}
+
+/** 删除用户云盘音乐参数 */
+export interface UserCloudDelParams extends CommonParams {
+ /** 云盘文件 ID,多个可用逗号分隔 */
+ fileid?: number | string;
+ /** 云盘文件 ID 列表 */
+ fileids?: Array | string;
+ /** 云盘文件 ID 别名 */
+ kv_id?: number | string;
+ /** 专辑音频 ID,列表接口返回 album_audio_id */
+ album_audio_id?: number | string;
+ /** 专辑音频 ID 列表 */
+ album_audio_ids?: Array | string;
+ /** album_audio_id 别名 */
+ mix_id?: number | string;
+ /** album_audio_id 别名 */
+ mixid?: number | string;
+ /** 覆盖客户端版本号,默认使用当前平台配置 */
+ clientver?: number | string;
+ /** 覆盖 appid,默认使用当前平台配置 */
+ appid?: number | string;
+}
+
+/** 云盘上传前曲库匹配参数 */
+export interface UserCloudMatchParams extends CommonParams {
+ /** 文件 hash(即文件 MD5),多个可用逗号分隔;不传时可通过请求体二进制自动计算 */
+ hash?: string;
+ /** 文件 hash 别名 */
+ filename?: string;
+ /** 专辑音频 ID,已有时可辅助匹配 */
+ album_audio_id?: number | string;
+ /** 专辑音频 ID 列表 */
+ album_audio_ids?: Array | string;
+ /** album_audio_id 别名 */
+ mix_id?: number | string;
+ /** album_audio_id 别名 */
+ mixid?: number | string;
+ /** 覆盖客户端版本号,默认使用当前平台配置 */
+ clientver?: number | string;
+ /** 覆盖 appid,默认使用当前平台配置 */
+ appid?: number | string;
+}
+
+/** 上传音乐到用户云盘参数 */
+export interface UserCloudUploadParams extends CommonParams {
+ /** 文件 hash(即文件 MD5),默认根据请求体二进制自动计算 */
+ filename?: string;
+ /** 文件扩展名,默认 mp3 */
+ extendname?: string;
+ /** 云盘音乐显示名称 */
+ name?: string;
+ /** 歌曲名,不传 name 时用于拼接显示名称 */
+ track_name?: string;
+ /** 歌曲名别名 */
+ songname?: string;
+ /** 歌手名 */
+ author_name?: string;
+ /** 曲库标准 hash,不传时默认自动匹配 */
+ hash_std?: string;
+ /** 音频 id,不传时默认自动匹配 */
+ audio_id?: number | string;
+ /** 专辑音频 id,不传时默认自动匹配 */
+ album_audio_id?: number | string;
+ /** album_audio_id 别名 */
+ mix_id?: number | string;
+ /** album_audio_id 别名 */
+ mixid?: number | string;
+ /** 是否自动匹配曲库,默认 1;传 0/false/no 关闭 */
+ auto_match?: number | string | boolean;
+ /** 码率类型,默认 4 */
+ bitrate?: number | string;
+ /** 时长,毫秒 */
+ timelen?: number | string;
+ /** 云盘列表版本,默认 0 */
+ list_ver?: number | string;
+ /** 覆盖客户端版本号,默认使用当前平台配置 */
+ clientver?: number | string;
+ /** 覆盖 appid,默认使用当前平台配置 */
+ appid?: number | string;
+}
+
+/** 获取用户收藏的视频参数 */
+export interface UserVideoCollectParams extends PaginatedParams {}
+
+/** 获取用户喜欢的视频参数 */
+export interface UserVideoLoveParams extends CommonParams {
+ /** 每页页数,默认 30 */
+ pagesize?: number;
+}
+
+/** 获取用户听歌历史排行参数 */
+export interface UserListenParams extends CommonParams {
+ /**
+ * 获取类型
+ * - 0:最近一周前 120 首
+ * - 1:全部累计前 120 首
+ */
+ type?: ListenType;
+}
+
+/** 获取用户最近听歌历史参数 */
+export interface UserHistoryParams extends CommonParams {
+ /** 基于上一次返回值传入,用于翻页 */
+ bp?: string;
+}
+
+/** 获取继续播放信息参数 */
+export interface LastestSongsListenParams extends CommonParams {
+ /** 每页页数,默认 30 */
+ pagesize?: number;
+}
+
+/** 获取用户动态参数 */
+export interface YouthDynamicParams extends CommonParams {}
+
+/** 听歌领取 VIP 参数 */
+export interface YouthListenSongParams extends CommonParams {
+ /** 专辑音乐 id (album_audio_id/MixSongID) */
+ mixsongid?: number | string;
+}
+
+// ============================================================
+// 请求参数类型 —— 歌单管理
+// ============================================================
+
+/** 收藏歌单 / 新建歌单参数 */
+export interface PlaylistAddParams extends CommonParams {
+ /** 歌单名称(必选) */
+ name: string;
+ /** 歌单创建者 userid(必选) */
+ list_create_userid: string;
+ /** 歌单 listid(必选) */
+ list_create_listid: string;
+ /** 是否设为隐私:0 = 公开,1 = 隐私(仅创建歌单时有效) */
+ is_pri?: 0 | 1;
+ /**
+ * 操作类型
+ * - 0:创建歌单(默认)
+ * - 1:收藏歌单
+ */
+ type?: PlaylistAddType;
+ /** 歌单 list_create_gid */
+ list_create_gid?: string;
+}
+
+/** 取消收藏歌单 / 删除歌单参数 */
+export interface PlaylistDelParams extends CommonParams {
+ /** 用户歌单 listid(必选) */
+ listid: string;
+}
+
+/** 对歌单添加歌曲参数 */
+export interface PlaylistTracksAddParams extends CommonParams {
+ /** 用户歌单 listid(必选) */
+ listid: string;
+ /**
+ * 歌曲数据(必选)
+ * 格式:`歌曲名称|歌曲hash|专辑id|(mixsongid/album_audio_id)`
+ * 最少需要歌曲名称和歌曲 hash,多个以逗号分隔
+ */
+ data: string;
+}
+
+/** 对歌单删除歌曲参数 */
+export interface PlaylistTracksDelParams extends CommonParams {
+ /** 用户歌单 listid(必选) */
+ listid: string;
+ /** 歌单中歌曲的 fileid,多个以逗号分隔(必选) */
+ fileids: string;
+}
+
+/** 获取歌单分类参数 */
+export interface PlaylistTagsParams extends CommonParams {}
+
+/** 获取歌单列表参数 */
+export interface TopPlaylistParams extends CommonParams {
+ /**
+ * 分类 tag(必选)
+ * - 0:推荐
+ * - 11292:HI-RES
+ * - 其他值从 `/playlist/tags` 接口获取
+ */
+ category_id: number;
+ /** 是否返回歌曲列表(不全):0 = 不返回,1 = 返回 */
+ withsong?: 0 | 1;
+ /** 是否返回歌单分类:0 = 不返回,1 = 返回 */
+ withtag?: 0 | 1;
+}
+
+/** 获取主题歌单参数 */
+export interface ThemePlaylistParams extends CommonParams {}
+
+/** 获取音效歌单参数 */
+export interface PlaylistEffectParams extends PaginatedParams {}
+
+/** 获取歌单详情参数 */
+export interface PlaylistDetailParams extends CommonParams {
+ /** 歌单 global_collection_id,多个以逗号分隔(必选) */
+ ids: string;
+}
+
+/** 获取歌单所有歌曲参数 */
+export interface PlaylistTrackAllParams extends PaginatedParams {
+ /** 歌单 global_collection_id(必选) */
+ id: string;
+}
+
+/** 获取歌单所有歌曲(新版)参数 */
+export interface PlaylistTrackAllNewParams extends PaginatedParams {
+ /** 歌单 listid(必选) */
+ listid: string;
+}
+
+/** 获取相似歌单参数 */
+export interface PlaylistSimilarParams extends CommonParams {
+ /** 歌单 global_collection_id,多个以逗号分隔(必选) */
+ ids: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 专辑
+// ============================================================
+
+/** 新碟上架参数 */
+export interface TopAlbumParams extends PaginatedParams {
+ /** 地区类型:1 = 华语,2 = 欧美,3 = 日本,4 = 韩国,默认为空 */
+ type?: AlbumType;
+}
+
+/** 专辑信息参数 */
+export interface AlbumParams extends CommonParams {
+ /** 专辑 id,多个以逗号分隔(必选) */
+ album_id: string;
+ /** 需要返回的字段,多个以逗号分隔 */
+ fields?: string;
+}
+
+/** 专辑详情参数 */
+export interface AlbumDetailParams extends CommonParams {
+ /** 专辑 id(必选) */
+ id: string;
+}
+
+/** 专辑音乐列表参数 */
+export interface AlbumSongsParams extends PaginatedParams {
+ /** 专辑 id(必选) */
+ id: string;
+}
+
+/** 唱片店参数 */
+export interface AlbumShopParams extends CommonParams {}
+
+// ============================================================
+// 请求参数类型 —— 音乐 / 歌曲
+// ============================================================
+
+/** 获取音乐 URL 参数 */
+export interface SongUrlParams extends CommonParams {
+ /** 音乐 hash(必选) */
+ hash: string;
+ /** 专辑 id */
+ album_id?: string;
+ /** 是否返回试听部分(仅部分歌曲) */
+ free_part?: boolean | string;
+ /** 专辑音频 id */
+ album_audio_id?: string;
+ /** 音质类型 */
+ quality?: SongQuality;
+}
+
+/** 获取音乐 URL(新版)参数 */
+export interface SongUrlNewParams extends CommonParams {
+ /** 音乐 hash(必选) */
+ hash: string;
+ /** 专辑音频 id */
+ album_audio_id?: string;
+ /** 是否返回试听部分(仅部分歌曲) */
+ free_part?: boolean | string;
+}
+
+/** 获取歌曲高潮部分参数 */
+export interface SongClimaxParams extends CommonParams {
+ /** 音乐 hash,多个以逗号分隔(必选) */
+ hash: string;
+}
+
+/** 获取音乐相关信息参数 */
+export interface AudioParams extends CommonParams {
+ /** 歌曲 hash,多个以逗号分隔(必选) */
+ hash: string;
+}
+
+/** 获取更多音乐版本参数 */
+export interface AudioRelatedParams extends CommonParams {
+ /** 音乐的 mixsongid / album_audio_id(必选) */
+ album_audio_id: string;
+ /** 页码 */
+ page?: number;
+ /** 每页条数,默认 30 */
+ pagesize?: number;
+ /** 是否返回分类 */
+ show_type?: boolean | string;
+ /** 排序:all / hot / new */
+ sort?: AudioRelatedSort;
+ /** 分类 */
+ type?: string;
+ /**
+ * 是否返回详情
+ * - 0:只返回总数
+ * - 不传或其他值:返回详情
+ */
+ show_detail?: 0 | 1;
+}
+
+/** 获取音乐伴奏信息参数 */
+export interface AudioAccompanyMatchingParams extends CommonParams {
+ /** 音乐 hash(必选) */
+ hash: string;
+ /** 音乐文件名(必选) */
+ fileName: string;
+ /** 音乐的 mixsongid / album_audio_id(必选) */
+ mixid: string;
+}
+
+/** 获取音乐 K 歌数量参数 */
+export interface AudioKtvTotalParams extends CommonParams {
+ /** 音乐 songid,从 `/audio/accompany/matching` 获取(必选) */
+ songId: string;
+ /** 歌手名称,多个以 `、` 隔开(必选) */
+ singerName: string;
+ /** 音乐 hash,从 `/audio/accompany/matching` 获取(必选) */
+ songHash: string;
+}
+
+/** 获取音乐详情参数 */
+export interface PrivilegeLiteParams extends CommonParams {
+ /** 歌曲 hash,多个以逗号分隔(必选) */
+ hash: string;
+}
+
+/** 获取音乐专辑/歌手信息参数 */
+export interface KrmAudioParams extends CommonParams {
+ /** 专辑音乐 id (album_audio_id/MixSongID),多个以逗号分隔(必选) */
+ album_audio_id: string;
+ /** 返回字段,多个以逗号分隔 */
+ fields?: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 搜索
+// ============================================================
+
+/** 搜索参数 */
+export interface SearchParams extends CommonParams {
+ /** 搜索关键词(必选) */
+ keywords: string;
+ /** 页码 */
+ page?: number;
+ /** 每页条数,默认 30 */
+ pagesize?: number;
+ /** 搜索类型,默认为单曲 */
+ type?: SearchType;
+}
+
+/** 默认搜索关键词参数 */
+export interface SearchDefaultParams extends CommonParams {}
+
+/** 综合搜索参数 */
+export interface SearchComplexParams extends PaginatedParams {
+ /** 搜索关键词(必选) */
+ keywords: string;
+}
+
+/** 热搜列表参数 */
+export interface SearchHotParams extends CommonParams {}
+
+/** 搜索建议参数 */
+export interface SearchSuggestParams extends CommonParams {
+ /** 搜索关键词(必选) */
+ keywords: string;
+ /** 专辑返回数量 */
+ albumTipCount?: number;
+ /** 歌单返回数量(推测) */
+ correctTipCount?: number;
+ /** MV 返回数量 */
+ mvTipCount?: number;
+ /** 音乐返回数量 */
+ musicTipCount?: number;
+}
+
+/** 歌词搜索参数 */
+export interface SearchLyricParams extends CommonParams {
+ /** 搜索关键词,与 hash 二选一(必选) */
+ keywords?: string;
+ /** 歌曲 hash,与 keywords 二选一(必选) */
+ hash?: string;
+ /** 专辑音乐 id */
+ album_audio_id?: string;
+ /** 歌曲时长 */
+ duration?: number | string;
+ /** 是否返回多个歌词:yes = 多个,no = 一个(默认) */
+ man?: LyricMan;
+}
+
+/** 获取歌词参数 */
+export interface LyricParams extends CommonParams {
+ /** 歌词 id,从 `/search/lyric` 接口获取(必选) */
+ id: string;
+ /** 歌词 accesskey,从 `/search/lyric` 接口获取(必选) */
+ accesskey: string;
+ /** 歌词类型:lrc = 普通歌词,krc = 逐字歌词 */
+ fmt?: 'lrc' | 'krc';
+ /** 是否解码,传入后返回解码后的歌词 */
+ decode?: boolean | string;
+}
+
+/** 混合搜索参数 */
+export interface SearchMixedParams extends CommonParams {
+ /** 搜索关键词(必选) */
+ keyword: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 主题音乐
+// ============================================================
+
+/** 获取主题歌单所有歌曲参数 */
+export interface ThemePlaylistTrackParams extends CommonParams {
+ /** 主题歌单 id(必选) */
+ theme_id: string;
+}
+
+/** 获取主题音乐参数 */
+export interface ThemeMusicParams extends CommonParams {}
+
+/** 获取主题音乐详情参数 */
+export interface ThemeMusicDetailParams extends CommonParams {
+ /** 主题音乐 id(必选) */
+ id: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 歌曲推荐
+// ============================================================
+
+/** 歌曲推荐参数(标准版) */
+export interface TopCardParams extends CommonParams {
+ /**
+ * 推荐类型(必选)
+ * - 1:精选好歌随心听 / 私人专属好歌
+ * - 2:经典怀旧金曲
+ * - 3:热门好歌精选
+ * - 4:小众宝藏佳作
+ * - 5:未知
+ * - 6:VIP 专属推荐
+ */
+ card_id: CardId;
+}
+
+/** 歌曲推荐参数(概念版) */
+export interface TopCardYouthParams extends CommonParams {
+ /**
+ * 推荐类型(必选)
+ * - 3001:私人专属好歌
+ * - 3004:小众宝藏佳作
+ * - 3005:潮流尝鲜
+ * - 3006:VIP 专属推荐
+ * - 3014:喜欢这首歌的 TA 也喜欢
+ * - 3101:概念 er 新推
+ */
+ card_id: YouthCardId;
+ /** 每页条数,默认 30 */
+ pagesize?: number;
+}
+
+/** 曲风盲盒参数(概念版) */
+export interface TopTagCardYouthParams extends CommonParams {}
+
+// ============================================================
+// 请求参数类型 —— 图片
+// ============================================================
+
+/** 获取歌手和专辑图片参数 */
+export interface ImagesParams extends CommonParams {
+ /** 歌曲 hash,多个以逗号分隔(必选) */
+ hash: string;
+ /** 专辑 id,多个以逗号分隔 */
+ album_id?: string;
+ /** 专辑音乐 id,多个以逗号分隔 */
+ album_audio_id?: string;
+ /** 最多返回图片数量,默认 5 */
+ count?: number;
+}
+
+/** 获取歌手图片参数 */
+export interface ImagesAudioParams extends CommonParams {
+ /** 歌曲 hash,多个以逗号分隔(必选) */
+ hash: string;
+ /** 音乐 id,多个以逗号分隔 */
+ audio_id?: string;
+ /** 专辑音乐 id,多个以逗号分隔 */
+ album_audio_id?: string;
+ /** 音乐文件名,多个以逗号分隔 */
+ filename?: string;
+ /** 最多返回图片数量,默认 5 */
+ count?: number;
+}
+
+// ============================================================
+// 请求参数类型 —— 私人 FM
+// ============================================================
+
+/** 私人 FM 参数 */
+export interface PersonalFmParams extends CommonParams {
+ /** 音乐 hash(建议传入) */
+ hash?: string;
+ /** 音乐 songid(建议传入) */
+ songid?: string;
+ /** 已播放时间(建议传入) */
+ playtime?: number | string;
+ /**
+ * 获取模式,默认 normal
+ * - normal:发现
+ * - small:小众
+ * - peak:30s
+ */
+ mode?: FmMode;
+ /**
+ * 操作类型,默认 play
+ * - play:播放
+ * - garbage:不喜欢
+ */
+ action?: FmAction;
+ /**
+ * AI 推荐池
+ * - 0:Alpha 根据口味推荐
+ * - 1:Beta 根据风格推荐
+ * - 2:Gamma
+ */
+ song_pool_id?: FmSongPoolId;
+ /** 是否已播放完成 */
+ is_overplay?: boolean | string;
+ /** 剩余未播放歌曲数,默认 0,大于 4 不返回推荐歌曲(建议传入) */
+ remain_songcnt?: number;
+}
+
+// ============================================================
+// 请求参数类型 —— Banner / 乐库
+// ============================================================
+
+/** Banner 轮播图参数 */
+export interface PcDiantaiParams extends CommonParams {}
+
+/** 乐库 Banner 参数 */
+export interface YuekuBannerParams extends CommonParams {}
+
+/** 乐库电台参数 */
+export interface YuekuFmParams extends CommonParams {}
+
+/** 乐库参数 */
+export interface YuekuParams extends CommonParams {}
+
+// ============================================================
+// 请求参数类型 —— 电台
+// ============================================================
+
+/** 电台分类参数 */
+export interface FmClassParams extends CommonParams {}
+
+/** 推荐电台参数 */
+export interface FmRecommendParams extends CommonParams {}
+
+/** 电台图片参数 */
+export interface FmImageParams extends CommonParams {
+ /** fmid,多个以逗号分隔(必选) */
+ fmid: string;
+}
+
+/** 电台音乐列表参数 */
+export interface FmSongsParams extends CommonParams {
+ /** fmid,多个以逗号分隔(必选) */
+ fmid: string;
+ /** fmtype,多个以逗号分隔 */
+ fmtype?: string;
+ /** 歌曲偏移,多个以逗号分隔 */
+ fmoffset?: string;
+ /** 歌曲列表大小,多个以逗号分隔 */
+ fmsize?: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 编辑精选
+// ============================================================
+
+/** 编辑精选参数 */
+export interface TopIpParams extends CommonParams {}
+
+/** 编辑精选数据参数 */
+export interface IpParams extends PaginatedParams {
+ /** ip id(必选) */
+ id: string;
+ /** 数据类型 */
+ type?: IpDataType;
+}
+
+/** 编辑精选详情参数 */
+export interface IpDateilParams extends CommonParams {
+ /** ip id,多个以逗号分隔(必选) */
+ id: string;
+}
+
+/** 编辑精选歌单参数 */
+export interface IpPlaylistParams extends PaginatedParams {
+ /** ip id(必选) */
+ id: string;
+}
+
+/** 编辑精选专区参数 */
+export interface IpZoneParams extends CommonParams {}
+
+/** 编辑精选专区详情参数 */
+export interface IpZoneHomeParams extends CommonParams {
+ /** ip id(必选) */
+ id: string;
+}
+
+// ============================================================
+// 请求参数类型 —— VIP 相关(仅限概念版)
+// ============================================================
+
+/** 领取 VIP 参数 */
+export interface YouthVipParams extends CommonParams {}
+
+/** 领取一天 VIP 参数 */
+export interface YouthDayVipParams extends CommonParams {
+ /** 领取 VIP 日期,格式:YYYY-MM-DD(必选) */
+ receive_day: string;
+}
+
+/** 升级概念版 VIP 参数 */
+export interface YouthDayVipUpgradeParams extends CommonParams {}
+
+/** 获取当月已领取 VIP 天数参数 */
+export interface YouthMonthVipRecordParams extends CommonParams {}
+
+/** 获取已领取 VIP 状态参数 */
+export interface YouthUnionVipParams extends CommonParams {}
+
+// ============================================================
+// 请求参数类型 —— 歌手
+// ============================================================
+
+/** 获取歌手列表参数 */
+export interface ArtistListsParams extends CommonParams {
+ /**
+ * 性别类型
+ * - 0:全部
+ * - 1:男
+ * - 2:女
+ * - 3:组合
+ */
+ sextypes?: ArtistSexType;
+ /**
+ * 地区类型
+ * - 0:全部
+ * - 1:华语
+ * - 2:欧美
+ * - 3:日韩
+ * - 4:其他
+ * - 5:日本
+ * - 6:韩国
+ */
+ type?: ArtistRegionType;
+ /** 3 = 音乐人,0 = 默认 */
+ musician?: 0 | 3;
+ /** 返回热门数量,默认 30 */
+ hotsize?: number;
+}
+
+/** 获取歌手详情参数 */
+export interface ArtistDetailParams extends CommonParams {
+ /** 歌手 id(必选) */
+ id: string;
+}
+
+/** 获取歌手专辑参数 */
+export interface ArtistAlbumsParams extends PaginatedParams {
+ /** 歌手 id(必选) */
+ id: string;
+ /** 排序:hot = 热门,new = 最新 */
+ sort?: ArtistSort;
+}
+
+/** 获取歌手单曲参数 */
+export interface ArtistAudiosParams extends PaginatedParams {
+ /** 歌手 id(必选) */
+ id: string;
+ /** 排序:hot = 热门,new = 最新 */
+ sort?: ArtistSort;
+}
+
+/** 获取歌手 MV 参数 */
+export interface ArtistVideosParams extends PaginatedParams {
+ /** 歌手 id(必选) */
+ id: string;
+ /** MV 标签筛选,默认获取全部 */
+ tag?: ArtistVideoTag;
+}
+
+/** 关注歌手参数 */
+export interface ArtistFollowParams extends CommonParams {
+ /** 歌手 id(必选) */
+ id: string;
+}
+
+/** 取消关注歌手参数 */
+export interface ArtistUnfollowParams extends CommonParams {
+ /** 歌手 id(必选) */
+ id: string;
+}
+
+/** 获取关注歌手新歌参数 */
+export interface ArtistFollowNewsongsParams extends CommonParams {
+ /** 最后专辑 id */
+ last_album_id?: string;
+ /** 每页条数,默认 30 */
+ pagesize?: number;
+ /** 排序:1 = 时间(默认),2 = 亲密度 */
+ opt_sort?: FollowNewSongsSort;
+}
+
+/** 获取歌手荣誉详情参数 */
+export interface ArtistHonourParams extends PaginatedParams {
+ /** 歌手 id(必选) */
+ id: string;
+}
+
+/** 获取歌手列表(新版)参数 */
+export interface SingerListParams extends CommonParams {
+ /** 性别类型:0 = 全部,1 = 男,2 = 女 */
+ sextype?: number;
+ /** 地区类型:0 = 全部,1 = 华语,2 = 欧美,3 = 日韩,4 = 其他 */
+ type?: number;
+ /** 返回热门数量,默认 200 */
+ hotsize?: number;
+}
+
+// ============================================================
+// 请求参数类型 —— 视频
+// ============================================================
+
+/** 获取视频 URL 参数 */
+export interface VideoUrlParams extends CommonParams {
+ /** 视频 hash(必选) */
+ hash: string;
+}
+
+/** 获取歌曲 MV 参数 */
+export interface KmrAudioMvParams extends CommonParams {
+ /** 专辑音乐 id (album_audio_id/MixSongID),多个以逗号分隔(必选) */
+ album_audio_id: string;
+ /** 返回字段,多个以逗号分隔 */
+ fields?: string;
+}
+
+/** 获取视频相关信息参数 */
+export interface VideoPrivilegeParams extends CommonParams {
+ /** 视频 hash,多个以逗号分隔(必选) */
+ hash: string;
+}
+
+/** 获取视频详情参数 */
+export interface VideoDetailParams extends CommonParams {
+ /** 视频 id / video id(必选) */
+ id: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 新歌速递
+// ============================================================
+
+/** 新歌速递参数 */
+export interface TopSongParams extends CommonParams {}
+
+// ============================================================
+// 请求参数类型 —— 场景音乐
+// ============================================================
+
+/** 场景音乐列表参数 */
+export interface SceneListsParams extends CommonParams {}
+
+/** 场景音乐详情参数 */
+export interface SceneModuleParams extends CommonParams {
+ /** 场景音乐 scene_id(必选) */
+ id: string;
+}
+
+/** 场景音乐讨论区参数 */
+export interface SceneListV2Params extends PaginatedParams {
+ /** 场景音乐 scene_id(必选) */
+ id: string;
+ /** 排序:rec = 推荐,hot = 热门,new = 最新(默认推荐) */
+ sort?: SceneSort;
+}
+
+/** 场景音乐模块 Tag 参数 */
+export interface SceneModuleInfoParams extends CommonParams {
+ /** 场景音乐 scene_id(必选) */
+ id: string;
+ /** 场景音乐 module_id(必选) */
+ module_id: string;
+}
+
+/** 场景音乐歌单列表参数 */
+export interface SceneCollectionListParams extends PaginatedParams {
+ /** 场景音乐 tag_id(必选) */
+ tag_id: string;
+}
+
+/** 场景音乐视频列表参数 */
+export interface SceneVideoListParams extends PaginatedParams {
+ /** 场景音乐视频 tag_id(必选) */
+ tag_id: string;
+}
+
+/** 场景音乐音乐列表参数 */
+export interface SceneAudioListParams extends PaginatedParams {
+ /** 场景音乐 scene_id(必选) */
+ id: string;
+ /** 场景音乐 module_id(必选) */
+ module_id: string;
+ /** 场景音乐 tag_id(必选) */
+ tag: string;
+}
+
+/** 场景音乐推荐参数 */
+export interface SceneMusicParams extends PaginatedParams {
+ /** 场景音乐 scene_id(必选) */
+ id: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 推荐
+// ============================================================
+
+/** 每日推荐参数 */
+export interface EverydayRecommendParams extends CommonParams {
+ /** 设备类型,默认 ios */
+ platform?: Platform;
+}
+
+/** 历史推荐参数 */
+export interface EverydayHistoryParams extends CommonParams {
+ /**
+ * 模式
+ * - list:返回历史推荐列表
+ * - song:返回当前歌曲列表
+ */
+ mode?: HistoryMode;
+ /** 当 mode = song 时必选 */
+ history_name?: string;
+ /** 当 mode = song 时必选 */
+ date?: string;
+ /** 设备类型,默认 ios */
+ platform?: Platform;
+}
+
+/** 风格推荐参数 */
+export interface EverydayStyleRecommendParams extends CommonParams {
+ /** 设备类型,默认 ios */
+ platform?: Platform;
+ /** 风格标签 id,多个以逗号分隔 */
+ tagids?: string;
+}
+
+/** 每日推荐歌曲参数 */
+export interface RecommendSongsParams extends CommonParams {
+ /** 设备类型,默认 android */
+ platform?: Platform;
+ /** 用户 id */
+ userid?: string;
+}
+
+/** 好友推荐参数 */
+export interface EverydayFriendParams extends CommonParams {}
+
+// ============================================================
+// 请求参数类型 —— 排行榜
+// ============================================================
+
+/** 排行列表参数 */
+export interface RankListParams extends CommonParams {
+ /** 是否返回歌曲(部分) */
+ withsong?: boolean | string | number;
+}
+
+/** 排行榜推荐列表参数 */
+export interface RankTopParams extends CommonParams {}
+
+/** 排行榜往期列表参数 */
+export interface RankVolParams extends CommonParams {
+ /** 排行榜 id(必选) */
+ rankid: string;
+ /** 排行榜 cid */
+ rank_cid?: string;
+}
+
+/** 排行榜信息参数 */
+export interface RankInfoParams extends CommonParams {
+ /** 排行榜 id(必选) */
+ rankid: string;
+ /** 排行榜 cid */
+ rank_cid?: string;
+ /** 是否返回专辑图片:1 = 返回,0 = 不返回(默认返回) */
+ album_img?: 0 | 1;
+ /** 排行榜 zone */
+ zone?: string;
+}
+
+/** 排行榜歌曲列表参数 */
+export interface RankAudioParams extends PaginatedParams {
+ /** 排行榜 id(必选) */
+ rankid: string;
+ /** 排行榜 cid,需要返回往期歌曲时必填(从 `/rank/vol` 的 volid 获取) */
+ rank_cid?: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 评论
+// ============================================================
+
+/** 歌曲收藏数参数 */
+export interface FavoriteCountParams extends CommonParams {
+ /** 音乐 mixsongid,多个以逗号分隔(必选) */
+ mixsongids: string;
+}
+
+/** 歌曲评论数参数 */
+export interface CommentCountParams extends CommonParams {
+ /** 音乐 hash(与 special_id 二选一) */
+ hash?: string;
+ /** 评论 special_child_id(与 hash 二选一) */
+ special_id?: string;
+}
+
+/** 歌曲评论参数 */
+export interface CommentMusicParams extends PaginatedParams {
+ /** 音乐 mixsongid(必选) */
+ mixsongid: string;
+ /** 是否返回分类列表:0 = 不返回,1 = 返回 */
+ show_classify?: 0 | 1;
+ /** 是否返回热词:0 = 不返回,1 = 返回 */
+ show_hotword_list?: 0 | 1;
+}
+
+/** 歌曲评论 - 根据分类返回参数 */
+export interface CommentMusicClassifyParams extends PaginatedParams {
+ /** 音乐 mixsongid(必选) */
+ mixsongid: string;
+ /** 分类 id(必选) */
+ type_id: string;
+ /** 排序:1 = 正序,2 = 倒序 */
+ sort?: CommentSort;
+}
+
+/** 歌曲评论 - 根据热词返回参数 */
+export interface CommentMusicHotwordParams extends PaginatedParams {
+ /** 音乐 mixsongid(必选) */
+ mixsongid: string;
+ /** 热词(必选) */
+ hot_word: string;
+}
+
+/** 楼层评论参数 */
+export interface CommentFloorParams extends PaginatedParams {
+ /** 评论 special_child_id(必选) */
+ special_id: string;
+ /** 歌曲 mixsongid(必选) */
+ mixsongid: string;
+ /** 评论 id(必选) */
+ tid: string;
+}
+
+/** 歌单评论参数 */
+export interface CommentPlaylistParams extends PaginatedParams {
+ /** 歌单 global_collection_id(必选) */
+ id: string;
+ /** 是否返回分类列表:0 = 不返回,1 = 返回 */
+ show_classify?: 0 | 1;
+ /** 是否返回热词:0 = 不返回,1 = 返回 */
+ show_hotword_list?: 0 | 1;
+}
+
+/** 专辑评论参数 */
+export interface CommentAlbumParams extends PaginatedParams {
+ /** 专辑 id(必选) */
+ id: string;
+ /** 是否返回分类列表:0 = 不返回,1 = 返回 */
+ show_classify?: 0 | 1;
+ /** 是否返回热词:0 = 不返回,1 = 返回 */
+ show_hotword_list?: 0 | 1;
+}
+
+// ============================================================
+// 请求参数类型 —— 曲谱
+// ============================================================
+
+/** 歌曲曲谱参数 */
+export interface SheetListParams extends PaginatedParams {
+ /** 音乐的 mixsongid / album_audio_id(必选) */
+ album_audio_id: string;
+ /**
+ * 曲谱类型
+ * - 0:全部
+ * - 1:钢琴
+ * - 2:吉他
+ * - 3:鼓
+ * - 98:简谱
+ * - 99:其他
+ */
+ opern_type?: SheetOpernType;
+}
+
+/** 曲谱详情参数 */
+export interface SheetDetailParams extends CommonParams {
+ /** 曲谱 id(必选) */
+ id: string;
+ /** 曲谱 source(必选) */
+ source: string;
+}
+
+/** 推荐曲谱参数 */
+export interface SheetHotParams extends CommonParams {
+ /** 曲谱类型 */
+ opern_type?: SheetOpernType;
+}
+
+/** 曲谱合集参数(含合集详情) */
+export interface SheetCollectionParams extends PaginatedParams {
+ /**
+ * position 类型
+ * - 2:精选谱单
+ * - 3:音乐教材
+ * - 4:古典钢琴
+ */
+ position?: SheetCollectionPosition;
+ /** 合集 id(获取合集详情时传入) */
+ collection_id?: string;
+}
+
+/**
+ * 曲谱推荐参数
+ *
+ * instruments 乐器类型:
+ * - 1:吉他(opern_level: 0=中级, 1=进阶, 2=基础)
+ * - 2:尤克里里(opern_level: 0=基础, 1=进阶)
+ * - 3:钢琴(opern_level: 0=基础, 1=进阶)
+ * - 4:简谱(opern_level: 0=基础)
+ */
+export interface SheetExploreParams extends PaginatedParams {
+ /** 乐器类型,默认 1 */
+ instruments?: number;
+ /** 难度等级,默认 0 */
+ level?: number;
+ /** 标签 id */
+ tagid?: number;
+}
+
+/**
+ * 曲谱排行榜参数
+ *
+ * instruments 乐器类型同 SheetExploreParams
+ */
+export interface SheetRankParams extends PaginatedParams {
+ /** 乐器类型,默认 1 */
+ instruments?: number;
+ /** 难度等级,默认 0 */
+ level?: number;
+ /** 标签 id */
+ tagid?: number;
+}
+
+/** 曲谱歌曲详情参数 */
+export interface SheetSongParams extends CommonParams {
+ /** 专辑音乐 id (album_audio_id/MixSongID)(必选) */
+ album_audio_id: string;
+ /** 乐器类型,默认 1 */
+ instruments?: number;
+ /** 难度等级,默认 0 */
+ level?: number;
+}
+
+/** 获取曲谱标签参数 */
+export interface SheetTagsParams extends CommonParams {}
+
+// ============================================================
+// 请求参数类型 —— 听歌历史 & 服务器
+// ============================================================
+
+/** 提交听歌历史参数 */
+export interface PlayhistoryUploadParams extends CommonParams {
+ /** 专辑音乐 id (album_audio_id/MixSongID)(必选) */
+ mxid: string;
+ /** 当前时间戳(秒级,非毫秒),也可从 `/server/now` 获取 */
+ ot?: number | string;
+ /** 当前播放次数,服务器值大于传入值时维持最大值,否则更新 */
+ pc?: number;
+}
+
+/** 获取服务器时间参数 */
+export interface ServerNowParams extends CommonParams {}
+
+// ============================================================
+// 请求参数类型 —— 刷刷 & AI
+// ============================================================
+
+/** 刷刷参数 */
+export interface BrushParams extends CommonParams {}
+
+/** AI 推荐参数 */
+export interface AiRecommendParams extends CommonParams {
+ /** 专辑音乐 id (album_audio_id/MixSongID),多个以逗号分隔(必选) */
+ album_audio_id: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 频道
+// ============================================================
+
+/** 获取用户所有频道参数 */
+export interface YouthChannelAllParams extends PaginatedParams {}
+
+/** 频道详情参数 */
+export interface YouthChannelDetailParams extends CommonParams {
+ /** 频道 id (global_collection_id/channel_id),多个以逗号分隔(必选) */
+ global_collection_id: string;
+}
+
+/** 频道安利参数 */
+export interface YouthChannelAmwayParams extends CommonParams {
+ /** 频道 id (global_collection_id/channel_id)(必选) */
+ global_collection_id: string;
+}
+
+/** 相似频道参数 */
+export interface YouthChannelSimilarParams extends CommonParams {
+ /** 频道 id (global_collection_id/channel_id)(必选) */
+ channel_id: string;
+}
+
+/** 频道订阅参数 */
+export interface YouthChannelSubParams extends CommonParams {
+ /** 频道 id (global_collection_id/channel_id)(必选) */
+ global_collection_id: string;
+ /** 1 = 订阅,0 = 取消订阅,默认订阅 */
+ t?: 0 | 1;
+}
+
+/** 频道音乐故事参数 */
+export interface YouthChannelSongParams extends PaginatedParams {
+ /** 频道 id (global_collection_id/channel_id)(必选) */
+ global_collection_id: string;
+}
+
+/** 频道音乐故事详情参数 */
+export interface YouthChannelSongDetailParams extends CommonParams {
+ /** 频道 id (global_collection_id/channel_id)(必选) */
+ global_collection_id: string;
+ /** 音乐故事 fileid(必选) */
+ fileid: string;
+}
+
+/** 动态 - 最常访问参数 */
+export interface YouthDynamicRecentParams extends CommonParams {}
+
+// ============================================================
+// 请求参数类型 —— 用户公开音乐
+// ============================================================
+
+/** 获取用户公开的音乐参数 */
+export interface YouthUserSongParams extends PaginatedParams {
+ /** 用户 id(必选) */
+ userid: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 听书
+// ============================================================
+
+/** 听书 - 每日推荐参数 */
+export interface LongaudioDailyRecommendParams extends PaginatedParams {}
+
+/** 听书 - 排行榜推荐参数 */
+export interface LongaudioRankRecommendParams extends CommonParams {}
+
+/** 听书 - VIP 推荐参数 */
+export interface LongaudioVipRecommendParams extends CommonParams {}
+
+/** 听书 - 每周推荐参数 */
+export interface LongaudioWeekRecommendParams extends CommonParams {}
+
+/** 听书 - 专辑详情参数 */
+export interface LongaudioAlbumDetailParams extends CommonParams {
+ /** 专辑 id,多个以逗号分隔(必选) */
+ album_id: string;
+}
+
+/** 听书 - 专辑音乐列表参数 */
+export interface LongaudioAlbumAudiosParams extends CommonParams {
+ /** 专辑 id,多个以逗号分隔(必选) */
+ album_id: string;
+}
+
+// ============================================================
+// 请求参数类型 —— 歌曲成绩单
+// ============================================================
+
+/** 歌曲成绩单参数 */
+export interface SongRankingParams extends CommonParams {
+ /** 专辑音乐 id (album_audio_id/MixSongID)(必选) */
+ album_audio_id: string;
+}
+
+/** 歌曲成绩单详情参数 */
+export interface SongRankingFilterParams extends PaginatedParams {
+ /** 专辑音乐 id (album_audio_id/MixSongID)(必选) */
+ album_audio_id: string;
+}
+
+// ============================================================
+// 服务器 & 工具类型
+// ============================================================
+
+/** Express 应用扩展,附带底层 HTTP Server 引用 */
+export interface ServerExtension {
+ /** 底层 HTTP Server 实例 */
+ service?: import('http').Server;
+}
+
+/** 模块定义结构 */
+export interface ModuleDefinition {
+ /** 模块标识符(文件名去 .js 后缀) */
+ identifier?: string;
+ /** Express 路由路径 */
+ route: string;
+ /** 模块导出内容或文件路径 */
+ module: any;
+}
+
+/** 请求配置对象(传递给 createRequest) */
+export interface RequestConfig {
+ /** 客户端 IP */
+ ip?: string;
+ [key: string]: any;
+}
+
+// ============================================================
+// 导出函数 —— 登录与认证
+// ============================================================
+
+/**
+ * 手机号验证码登录
+ * @route /login/cellphone
+ */
+export function login_cellphone(params: LoginCellphoneParams): Promise;
+
+/**
+ * 用户名密码登录(可能需要验证,不推荐使用)
+ * @route /login
+ */
+export function login(params: LoginParams): Promise;
+
+/**
+ * 开放接口登录(目前仅支持微信)
+ * @route /login/openplat
+ */
+export function login_openplat(params: LoginOpenplatParams): Promise;
+
+/**
+ * 二维码登录 - 生成 key
+ * @route /login/qr/key
+ */
+export function login_qr_key(params?: LoginQrKeyParams): Promise;
+
+/**
+ * 二维码登录 - 生成二维码
+ * @route /login/qr/create
+ */
+export function login_qr_create(params: LoginQrCreateParams): Promise;
+
+/**
+ * 二维码登录 - 检测扫码状态
+ * - 0: 二维码过期
+ * - 1: 等待扫码
+ * - 2: 待确认
+ * - 4: 授权登录成功(返回 token)
+ * @route /login/qr/check
+ */
+export function login_qr_check(params: LoginQrCheckParams): Promise;
+
+/**
+ * 微信登录 - 生成二维码
+ * @route /login/wx/create
+ */
+export function login_wx_create(params?: LoginWxCreateParams): Promise;
+
+/**
+ * 微信登录 - 检测扫码状态
+ * - 408: 等待扫描
+ * - 404: 已扫描
+ * - 403: 拒绝登录
+ * - 405: 登录成功(返回 wx_code)
+ * - 402: 已过期
+ * @route /login/wx/check
+ */
+export function login_wx_check(params: LoginWxCheckParams): Promise;
+
+/**
+ * 刷新登录状态,延长 token 过期时间
+ * @route /login/token
+ */
+export function login_token(params?: LoginTokenParams): Promise;
+
+/**
+ * 获取用户设备列表(需登录)
+ * @route /v2/get_dev
+ */
+export function login_device(params?: LoginDeviceParams): Promise;
+
+/**
+ * 登出指定设备(需登录)
+ * @route /loginservice/v1/dev_logout
+ */
+export function login_device_kick(params?: LoginDeviceKickParams): Promise;
+
+// ============================================================
+// 导出函数 —— 验证码 & 设备
+// ============================================================
+
+/**
+ * 发送手机验证码
+ * @route /captcha/sent
+ */
+export function captcha_sent(params: CaptchaSentParams): Promise;
+
+/**
+ * 获取 dfid(设备标识),获取音乐 URL 前需先调用此接口
+ * @route /register/dev
+ */
+export function register_dev(params?: RegisterDevParams): Promise;
+
+/**
+ * 获取验证码信息(登录触发二次验证时调用)
+ * @route /verifyservice/v3/get_verify_info
+ */
+export function get_verify_info(params: GetVerifyInfoParams): Promise;
+
+/**
+ * 提交验证码验证(腾讯验证码/手机验证码)
+ * @route /verifyservice/v4/verify_user_info
+ */
+export function verify_user_info(params: VerifyUserInfoParams): Promise;
+
+/**
+ * 生成 sid/edt 并提交验证(内部调用 generateSimulate + verify_user_info)
+ * @route /verifyservice/v4/verify_user_info
+ */
+export function sidedt(params?: SidedtParams): Promise;
+
+// ============================================================
+// 导出函数 —— 用户信息
+// ============================================================
+
+/**
+ * 获取用户额外信息(需登录)
+ * @route /user/detail
+ */
+export function user_detail(params?: UserDetailParams): Promise;
+
+/**
+ * 获取用户 VIP 信息(需登录)
+ * @route /user/vip/detail
+ */
+export function user_vip_detail(params?: UserVipDetailParams): Promise;
+
+/**
+ * 获取用户所有创建及收藏的歌单(需登录)
+ * @route /user/playlist
+ */
+export function user_playlist(params?: UserPlaylistParams): Promise;
+
+/**
+ * 获取用户所有关注的歌手/用户(需登录)
+ * @route /user/follow
+ */
+export function user_follow(params?: UserFollowParams): Promise;
+
+/**
+ * 获取关注歌手/用户消息(需登录)
+ * @route /user/follow/message
+ */
+export function user_follow_message(params: UserFollowMessageParams): Promise;
+
+/**
+ * 获取用户云盘音乐(需登录)
+ * @route /user/cloud
+ */
+export function user_cloud(params?: UserCloudParams): Promise;
+
+/**
+ * 获取用户云盘音乐 URL(需登录,目前文件大小约 10M)
+ * @route /user/cloud/url
+ */
+export function user_cloud_url(params: UserCloudUrlParams): Promise;
+
+/**
+ * 删除用户云盘音乐(需登录)
+ * @route /user/cloud/del
+ */
+export function user_cloud_del(params: UserCloudDelParams): Promise;
+
+/**
+ * 云盘上传前曲库匹配(需登录)
+ * @route /user/cloud/match
+ */
+export function user_cloud_match(params: UserCloudMatchParams): Promise;
+
+/**
+ * 上传音乐到云盘(需登录)
+ * @route /user/cloud/upload
+ */
+export function user_cloud_upload(params: UserCloudUploadParams): Promise;
+
+/**
+ * 获取用户收藏的视频(需登录)
+ * @route /user/video/collect
+ */
+export function user_video_collect(params?: UserVideoCollectParams): Promise;
+
+/**
+ * 获取用户喜欢的视频(需登录)
+ * @route /user/video/love
+ */
+export function user_video_love(params?: UserVideoLoveParams): Promise;
+
+/**
+ * 获取用户听歌历史排行(需登录)
+ * @route /user/listen
+ */
+export function user_listen(params?: UserListenParams): Promise;
+
+/**
+ * 获取用户最近听歌历史(需登录)
+ * @route /user/history
+ */
+export function user_history(params?: UserHistoryParams): Promise;
+
+/**
+ * 获取继续播放信息,对应手机版首页继续播放入口(需登录)
+ * @route /lastest/songs/listen
+ */
+export function lastest_songs_listen(params?: LastestSongsListenParams): Promise;
+
+// ============================================================
+// 导出函数 —— 歌单管理
+// ============================================================
+
+/**
+ * 收藏歌单 / 新建歌单(需登录)
+ * @route /playlist/add
+ */
+export function playlist_add(params: PlaylistAddParams): Promise;
+
+/**
+ * 取消收藏歌单 / 删除歌单(需登录)
+ * @route /playlist/del
+ */
+export function playlist_del(params: PlaylistDelParams): Promise;
+
+/**
+ * 对歌单添加歌曲(需登录)
+ * @route /playlist/tracks/add
+ */
+export function playlist_tracks_add(params: PlaylistTracksAddParams): Promise;
+
+/**
+ * 对歌单删除歌曲(需登录)
+ * @route /playlist/tracks/del
+ */
+export function playlist_tracks_del(params: PlaylistTracksDelParams): Promise;
+
+/**
+ * 获取歌单分类(含 category 信息)
+ * @route /playlist/tags
+ */
+export function playlist_tags(params?: PlaylistTagsParams): Promise;
+
+/**
+ * 获取歌单列表
+ * @route /top/playlist
+ */
+export function top_playlist(params: TopPlaylistParams): Promise;
+
+/**
+ * 获取主题歌单
+ * @route /theme/playlist
+ */
+export function theme_playlist(params?: ThemePlaylistParams): Promise;
+
+/**
+ * 获取音效歌单
+ * @route /playlist/effect
+ */
+export function playlist_effect(params?: PlaylistEffectParams): Promise;
+
+/**
+ * 获取歌单详情
+ * @route /playlist/detail
+ */
+export function playlist_detail(params: PlaylistDetailParams): Promise;
+
+/**
+ * 获取歌单所有歌曲
+ * @route /playlist/track/all
+ */
+export function playlist_track_all(params: PlaylistTrackAllParams): Promise;
+
+/**
+ * 获取歌单所有歌曲(新版,仅支持用户创建及收藏的歌单)
+ * @route /playlist/track/all/new
+ */
+export function playlist_track_all_new(params: PlaylistTrackAllNewParams): Promise;
+
+/**
+ * 获取相似歌单
+ * @route /playlist/similar
+ */
+export function playlist_similar(params: PlaylistSimilarParams): Promise;
+
+/**
+ * 获取主题歌单所有歌曲
+ * @route /theme/playlist/track
+ */
+export function theme_playlist_track(params: ThemePlaylistTrackParams): Promise;
+
+/**
+ * 获取主题音乐
+ * @route /theme/music
+ */
+export function theme_music(params?: ThemeMusicParams): Promise;
+
+/**
+ * 获取主题音乐详情
+ * @route /theme/music/detail
+ */
+export function theme_music_detail(params: ThemeMusicDetailParams): Promise;
+
+// ============================================================
+// 导出函数 —— 专辑
+// ============================================================
+
+/**
+ * 获取新碟上架列表
+ * @route /top/album
+ */
+export function top_album(params?: TopAlbumParams): Promise;
+
+/**
+ * 获取专辑信息
+ * @route /album
+ */
+export function album(params: AlbumParams): Promise;
+
+/**
+ * 获取专辑详情
+ * @route /album/detail
+ */
+export function album_detail(params: AlbumDetailParams): Promise;
+
+/**
+ * 获取专辑音乐列表
+ * @route /album/songs
+ */
+export function album_songs(params: AlbumSongsParams): Promise;
+
+/**
+ * 获取唱片店分类数据
+ * @route /zhuanjidata/v3/album_shop_v2/get_classify_data
+ */
+export function album_shop(params?: AlbumShopParams): Promise;
+
+// ============================================================
+// 导出函数 —— 音乐 / 歌曲
+// ============================================================
+
+/**
+ * 获取音乐 URL(需先调用 /register/dev 获取 dfid)
+ * @route /song/url
+ */
+export function song_url(params: SongUrlParams): Promise;
+
+/**
+ * 获取音乐 URL(新版,一次性返回所有音质,但存在音频加密)
+ * @route /song/url/new
+ */
+export function song_url_new(params: SongUrlNewParams): Promise;
+
+/**
+ * 获取歌曲高潮部分时间
+ * @route /song/climax
+ */
+export function song_climax(params: SongClimaxParams): Promise;
+
+/**
+ * 获取音乐相关信息
+ * @route /audio
+ */
+export function audio(params: AudioParams): Promise;
+
+/**
+ * 获取更多版本音乐
+ * @route /audio/related
+ */
+export function audio_related(params: AudioRelatedParams): Promise;
+
+/**
+ * 获取最佳伴奏信息
+ * @route /audio/accompany/matching
+ */
+export function audio_accompany_matching(params: AudioAccompanyMatchingParams): Promise;
+
+/**
+ * 获取音乐 K 歌数量(参数来自 /audio/accompany/matching)
+ * @route /audio/ktv/total
+ */
+export function audio_ktv_total(params: AudioKtvTotalParams): Promise;
+
+/**
+ * 获取音乐详情
+ * @route /privilege/lite
+ */
+export function privilege_lite(params: PrivilegeLiteParams): Promise;
+
+/**
+ * 获取音乐专辑/歌手信息
+ * @route /krm/audio
+ */
+export function krm_audio(params: KrmAudioParams): Promise;
+
+/**
+ * 获取歌曲推荐(标准版)
+ * @route /top/card
+ */
+export function top_card(params: TopCardParams): Promise;
+
+/**
+ * 获取歌曲推荐(概念版)
+ * @route /top/card/youth
+ */
+export function top_card_youth(params: TopCardYouthParams): Promise;
+
+/**
+ * 获取曲风盲盒(概念版)
+ * @route /youth/v1/song/tag_card_recommend
+ */
+export function top_tag_card_youth(params?: TopTagCardYouthParams): Promise;
+
+/**
+ * 获取歌手和专辑图片
+ * @route /images
+ */
+export function images(params: ImagesParams): Promise;
+
+/**
+ * 获取歌手图片
+ * @route /images/audio
+ */
+export function images_audio(params: ImagesAudioParams): Promise;
+
+// ============================================================
+// 导出函数 —— 搜索
+// ============================================================
+
+/**
+ * 搜索音乐 / MV / 歌单 / 歌词 / 专辑 / 歌手
+ * ⚠️ 需携带 cookie 认证信息,否则返回 error_code: 152
+ * @route /search
+ */
+export function search(params: SearchParams): Promise;
+
+/**
+ * 获取默认搜索关键词
+ * @route /search/default
+ */
+export function search_default(params?: SearchDefaultParams): Promise;
+
+/**
+ * 综合搜索(结果包含单曲、歌手、歌单等)
+ * @route /search/complex
+ */
+export function search_complex(params: SearchComplexParams): Promise;
+
+/**
+ * 获取热门搜索列表
+ * @route /search/hot
+ */
+export function search_hot(params?: SearchHotParams): Promise;
+
+/**
+ * 获取搜索建议(包含单曲、歌手、歌单信息)
+ * @route /search/suggest
+ */
+export function search_suggest(params: SearchSuggestParams): Promise;
+
+/**
+ * 歌词搜索,需配合 /lyric 使用
+ * @route /search/lyric
+ */
+export function search_lyric(params: SearchLyricParams): Promise;
+
+/**
+ * 混合搜索(iOS 端接口)
+ * @route /v3/search/mixed
+ */
+export function search_mixed(params: SearchMixedParams): Promise;
+
+/**
+ * 获取歌词(需先调用 /search/lyric 获取 id 和 accesskey)
+ * @route /lyric
+ */
+export function lyric(params: LyricParams): Promise;
+
+// ============================================================
+// 导出函数 —— 私人 FM
+// ============================================================
+
+/**
+ * 私人 FM(对应手机和 PC 端的猜你喜欢)
+ * @route /personal/fm
+ */
+export function personal_fm(params?: PersonalFmParams): Promise;
+
+// ============================================================
+// 导出函数 —— Banner / 乐库
+// ============================================================
+
+/**
+ * 获取 Banner 轮播图数据
+ * @route /pc/diantai
+ */
+export function pc_diantai(params?: PcDiantaiParams): Promise;
+
+/**
+ * 获取乐库 Banner 轮播图数据
+ * @route /yueku/banner
+ */
+export function yueku_banner(params?: YuekuBannerParams): Promise;
+
+/**
+ * 获取乐库电台数据
+ * @route /yueku/fm
+ */
+export function yueku_fm(params?: YuekuFmParams): Promise;
+
+/**
+ * 获取手机端乐库数据
+ * @route /yueku
+ */
+export function yueku(params?: YuekuParams): Promise;
+
+// ============================================================
+// 导出函数 —— 电台
+// ============================================================
+
+/**
+ * 获取所有电台分类数据
+ * @route /fm/class
+ */
+export function fm_class(params?: FmClassParams): Promise;
+
+/**
+ * 获取推荐电台
+ * @route /fm/recommend
+ */
+export function fm_recommend(params?: FmRecommendParams): Promise;
+
+/**
+ * 获取电台图片
+ * @route /fm/image
+ */
+export function fm_image(params: FmImageParams): Promise;
+
+/**
+ * 获取电台音乐列表
+ * @route /fm/songs
+ */
+export function fm_songs(params: FmSongsParams): Promise;
+
+// ============================================================
+// 导出函数 —— 编辑精选
+// ============================================================
+
+/**
+ * 获取编辑精选数据
+ * @route /top/ip
+ */
+export function top_ip(params?: TopIpParams): Promise;
+
+/**
+ * 获取编辑精选对应数据
+ * @route /ip
+ */
+export function ip(params: IpParams): Promise;
+
+/**
+ * 获取编辑精选详情(批量)
+ * @route /openapi/v1/ip
+ */
+export function ip_dateil(params: IpDateilParams): Promise;
+
+/**
+ * 获取编辑精选歌单数据
+ * @route /ip/playlist
+ */
+export function ip_playlist(params: IpPlaylistParams): Promise;
+
+/**
+ * 获取编辑精选专区相关内容
+ * @route /ip/zone
+ */
+export function ip_zone(params?: IpZoneParams): Promise;
+
+/**
+ * 获取编辑精选专区详情
+ * @route /ip/zone/home
+ */
+export function ip_zone_home(params: IpZoneHomeParams): Promise;
+
+// ============================================================
+// 导出函数 —— VIP(仅限概念版)
+// ============================================================
+
+/**
+ * 领取 VIP(测试接口,仅限概念版,目前不可使用)
+ * @route /youth/vip
+ */
+export function youth_vip(params?: YouthVipParams): Promise;
+
+/**
+ * 领取一天 VIP(测试接口,仅限概念版)
+ * @route /youth/day/vip
+ */
+export function youth_day_vip(params: YouthDayVipParams): Promise;
+
+/**
+ * 升级概念版 VIP 为畅听 VIP(需先领取一天 VIP)
+ * @route /youth/day/vip/upgrade
+ */
+export function youth_day_vip_upgrade(params?: YouthDayVipUpgradeParams): Promise;
+
+/**
+ * 获取当月已领取 VIP 天数(测试接口,仅限概念版)
+ * @route /youth/month/vip/record
+ */
+export function youth_month_vip_record(params?: YouthMonthVipRecordParams): Promise;
+
+/**
+ * 获取已领取 VIP 状态(测试接口,仅限概念版)
+ * @route /youth/union/vip
+ */
+export function youth_union_vip(params?: YouthUnionVipParams): Promise;
+
+/**
+ * 听歌领取 VIP(需登录)
+ * @route /youth/v2/report/listen_song
+ */
+export function youth_listen_song(params?: YouthListenSongParams): Promise;
+
+// ============================================================
+// 导出函数 —— 歌手
+// ============================================================
+
+/**
+ * 获取歌手列表
+ * @route /artist/lists
+ */
+export function artist_lists(params?: ArtistListsParams): Promise;
+
+/**
+ * 获取歌手详情
+ * @route /artist/detail
+ */
+export function artist_detail(params: ArtistDetailParams): Promise;
+
+/**
+ * 获取歌手专辑
+ * @route /artist/albums
+ */
+export function artist_albums(params: ArtistAlbumsParams): Promise;
+
+/**
+ * 获取歌手单曲
+ * @route /artist/audios
+ */
+export function artist_audios(params: ArtistAudiosParams): Promise;
+
+/**
+ * 获取歌手 MV
+ * @route /artist/videos
+ */
+export function artist_videos(params: ArtistVideosParams): Promise;
+
+/**
+ * 关注歌手(需登录)
+ * @route /artist/follow
+ */
+export function artist_follow(params: ArtistFollowParams): Promise;
+
+/**
+ * 取消关注歌手(需登录)
+ * @route /artist/unfollow
+ */
+export function artist_unfollow(params: ArtistUnfollowParams): Promise;
+
+/**
+ * 获取已关注歌手的新歌(需登录)
+ * @route /artist/follow/newsongs
+ */
+export function artist_follow_newsongs(params?: ArtistFollowNewsongsParams): Promise;
+
+/**
+ * 获取歌手荣誉详情
+ * @route /v1/query_singer_honour_detail
+ */
+export function artist_honour(params: ArtistHonourParams): Promise;
+
+/**
+ * 获取歌手列表(新版)
+ * @route /ocean/v6/singer/list
+ */
+export function singer_list(params?: SingerListParams): Promise;
+
+// ============================================================
+// 导出函数 —— 视频
+// ============================================================
+
+/**
+ * 获取视频 URL
+ * @route /video/url
+ */
+export function video_url(params: VideoUrlParams): Promise;
+
+/**
+ * 获取歌曲对应的 MV
+ * @route /kmr/audio/mv
+ */
+export function kmr_audio_mv(params: KmrAudioMvParams): Promise;
+
+/**
+ * 获取视频相关信息
+ * @route /video/privilege
+ */
+export function video_privilege(params: VideoPrivilegeParams): Promise;
+
+/**
+ * 获取视频详情(可获取更高清的视频 hash)
+ * @route /video/detail
+ */
+export function video_detail(params: VideoDetailParams): Promise;
+
+// ============================================================
+// 导出函数 —— 新歌速递
+// ============================================================
+
+/**
+ * 获取新歌速递
+ * @route /top/song
+ */
+export function top_song(params?: TopSongParams): Promise;
+
+// ============================================================
+// 导出函数 —— 场景音乐
+// ============================================================
+
+/**
+ * 获取场景音乐列表
+ * @route /scene/lists
+ */
+export function scene_lists(params?: SceneListsParams): Promise;
+
+/**
+ * 获取场景音乐详情
+ * @route /scene/module
+ */
+export function scene_module(params: SceneModuleParams): Promise;
+
+/**
+ * 获取场景音乐讨论区
+ * @route /scene/list/v2
+ */
+export function scene_lists_v2(params: SceneListV2Params): Promise;
+
+/**
+ * 获取场景音乐模块 Tag
+ * @route /scene/module/info
+ */
+export function scene_module_info(params: SceneModuleInfoParams): Promise;
+
+/**
+ * 获取场景音乐歌单列表
+ * @route /scene/collection/list
+ */
+export function scene_collection_list(params: SceneCollectionListParams): Promise;
+
+/**
+ * 获取场景音乐视频列表
+ * @route /scene/video/list
+ */
+export function scene_video_list(params: SceneVideoListParams): Promise;
+
+/**
+ * 获取场景音乐音乐列表
+ * @route /scene/audio/list
+ */
+export function scene_audio_list(params: SceneAudioListParams): Promise;
+
+/**
+ * 获取场景音乐推荐
+ * @route /genesisapi/v1/scene_music/rec_music
+ */
+export function scene_music(params: SceneMusicParams): Promise;
+
+// ============================================================
+// 导出函数 —— 推荐
+// ============================================================
+
+/**
+ * 获取每日推荐列表
+ * @route /everyday/recommend
+ */
+export function everyday_recommend(params?: EverydayRecommendParams): Promise;
+
+/**
+ * 获取历史推荐
+ * @route /everyday/history
+ */
+export function everyday_history(params?: EverydayHistoryParams): Promise;
+
+/**
+ * 获取风格推荐
+ * @route /everyday/style/recommend
+ */
+export function everyday_style_recommend(params?: EverydayStyleRecommendParams): Promise;
+
+/**
+ * 获取每日推荐歌曲
+ * @route /everyday_song_recommend
+ */
+export function recommend_songs(params?: RecommendSongsParams): Promise;
+
+/**
+ * 获取好友推荐
+ * @route /sing7/relation/json/v3/friend_rec_by_using_song_list
+ */
+export function everyday_friend(params?: EverydayFriendParams): Promise;
+
+// ============================================================
+// 导出函数 —— 排行榜
+// ============================================================
+
+/**
+ * 获取排行榜列表
+ * @route /rank/list
+ */
+export function rank_list(params?: RankListParams): Promise;
+
+/**
+ * 获取排行榜推荐列表
+ * @route /rank/top
+ */
+export function rank_top(params?: RankTopParams): Promise;
+
+/**
+ * 获取排行榜往期列表
+ * @route /rank/vol
+ */
+export function rank_vol(params: RankVolParams): Promise;
+
+/**
+ * 获取排行榜信息
+ * @route /rank/info
+ */
+export function rank_info(params: RankInfoParams): Promise;
+
+/**
+ * 获取排行榜歌曲列表
+ * @route /rank/audio
+ */
+export function rank_audio(params: RankAudioParams): Promise;
+
+// ============================================================
+// 导出函数 —— 评论
+// ============================================================
+
+/**
+ * 获取歌曲收藏数(无需登录)
+ * @route /favorite/count
+ */
+export function favorite_count(params: FavoriteCountParams): Promise;
+
+/**
+ * 获取歌曲评论数(无需登录)
+ * @route /comment/count
+ */
+export function comment_count(params: CommentCountParams): Promise;
+
+/**
+ * 获取歌曲所有评论(无需登录)
+ * @route /comment/music
+ */
+export function comment_music(params: CommentMusicParams): Promise;
+
+/**
+ * 获取歌曲分类评论(无需登录)
+ * @route /comment/music/classify
+ */
+export function comment_music_classify(params: CommentMusicClassifyParams): Promise;
+
+/**
+ * 获取歌曲热词评论(无需登录)
+ * @route /comment/music/hotword
+ */
+export function comment_music_hotword(params: CommentMusicHotwordParams): Promise;
+
+/**
+ * 获取楼层评论
+ * @route /comment/floor
+ */
+export function comment_floor(params: CommentFloorParams): Promise;
+
+/**
+ * 获取歌单评论(无需登录)
+ * @route /comment/playlist
+ */
+export function comment_playlist(params: CommentPlaylistParams): Promise;
+
+/**
+ * 获取专辑评论(无需登录)
+ * @route /comment/album
+ */
+export function comment_album(params: CommentAlbumParams): Promise;
+
+// ============================================================
+// 导出函数 —— 曲谱
+// ============================================================
+
+/**
+ * 获取歌曲曲谱(AI 曲谱为 XML 文件需自行解析)
+ * @route /sheet/list
+ */
+export function sheet_list(params: SheetListParams): Promise;
+
+/**
+ * 获取曲谱详情(AI 曲谱为 XML 文件需自行解析)
+ * @route /sheet/detail
+ */
+export function sheet_detail(params: SheetDetailParams): Promise;
+
+/**
+ * 获取推荐曲谱
+ * @route /sheet/hot
+ */
+export function sheet_hot(params?: SheetHotParams): Promise;
+
+/**
+ * 获取曲谱合集 / 曲谱合集详情
+ * @route /sheet/collection
+ */
+export function sheet_collection(params?: SheetCollectionParams): Promise;
+
+/**
+ * 获取曲谱推荐
+ * @route /opern/v1/home/get_rec_opern
+ */
+export function sheet_explore(params?: SheetExploreParams): Promise;
+
+/**
+ * 获取曲谱排行榜
+ * @route /opern/v1/home/get_rank_opern
+ */
+export function sheet_rank(params?: SheetRankParams): Promise;
+
+/**
+ * 获取曲谱歌曲详情
+ * @route /opern/v1/detail/song_info
+ */
+export function sheet_song(params: SheetSongParams): Promise;
+
+/**
+ * 获取曲谱标签
+ * @route /opern/v1/home/get_tags
+ */
+export function sheet_tags(params?: SheetTagsParams): Promise;
+
+// ============================================================
+// 导出函数 —— 听歌历史 & 服务器
+// ============================================================
+
+/**
+ * 提交听歌历史(支持跨设备查看)
+ * @route /playhistory/upload
+ */
+export function playhistory_upload(params: PlayhistoryUploadParams): Promise;
+
+/**
+ * 获取服务器时间戳
+ * @route /server/now
+ */
+export function server_now(params?: ServerNowParams): Promise;
+
+// ============================================================
+// 导出函数 —— 刷刷 & AI
+// ============================================================
+
+/**
+ * 获取刷刷视频
+ * @route /brush
+ */
+export function brush(params?: BrushParams): Promise;
+
+/**
+ * 获取 AI 推荐歌曲
+ * @route /ai/recommend
+ */
+export function ai_recommend(params: AiRecommendParams): Promise;
+
+// ============================================================
+// 导出函数 —— 频道
+// ============================================================
+
+/**
+ * 获取用户所有订阅的频道(需登录)
+ * @route /youth/channel/all
+ */
+export function youth_channel_all(params?: YouthChannelAllParams): Promise;
+
+/**
+ * 获取频道详情
+ * @route /youth/channel/detail
+ */
+export function youth_channel_detail(params: YouthChannelDetailParams): Promise;
+
+/**
+ * 获取频道安利
+ * @route /youth/channel/amway
+ */
+export function youth_channel_amway(params: YouthChannelAmwayParams): Promise;
+
+/**
+ * 获取相似频道
+ * @route /youth/channel/similar
+ */
+export function youth_channel_similar(params: YouthChannelSimilarParams): Promise;
+
+/**
+ * 订阅 / 取消订阅频道(需登录)
+ * @route /youth/channel/sub
+ */
+export function youth_channel_sub(params: YouthChannelSubParams): Promise;
+
+/**
+ * 获取频道音乐故事
+ * @route /youth/channel/song
+ */
+export function youth_channel_song(params: YouthChannelSongParams): Promise;
+
+/**
+ * 获取频道音乐故事详情
+ * @route /youth/channel/song/detail
+ */
+export function youth_channel_song_detail(params: YouthChannelSongDetailParams): Promise;
+
+/**
+ * 获取经常访问的频道和用户(需登录)
+ * @route /youth/dynamic/recent
+ */
+export function youth_dynamic_recent(params?: YouthDynamicRecentParams): Promise;
+
+/**
+ * 获取用户动态(需登录)
+ * @route /youth/v3/user/get_dynamic
+ */
+export function youth_dynamic(params?: YouthDynamicParams): Promise;
+
+// ============================================================
+// 导出函数 —— 用户公开音乐
+// ============================================================
+
+/**
+ * 获取用户公开的音乐
+ * @route /youth/user/song
+ */
+export function youth_user_song(params: YouthUserSongParams): Promise;
+
+// ============================================================
+// 导出函数 —— 听书
+// ============================================================
+
+/**
+ * 听书 - 每日推荐
+ * @route /longaudio/daily/recommend
+ */
+export function longaudio_daily_recommend(params?: LongaudioDailyRecommendParams): Promise;
+
+/**
+ * 听书 - 排行榜推荐
+ * @route /longaudio/rank/recommend
+ */
+export function longaudio_rank_recommend(params?: LongaudioRankRecommendParams): Promise;
+
+/**
+ * 听书 - VIP 推荐
+ * @route /longaudio/vip/recommend
+ */
+export function longaudio_vip_recommend(params?: LongaudioVipRecommendParams): Promise;
+
+/**
+ * 听书 - 每周推荐
+ * @route /longaudio/week/recommend
+ */
+export function longaudio_week_recommend(params?: LongaudioWeekRecommendParams): Promise;
+
+/**
+ * 听书 - 专辑详情
+ * @route /longaudio/album/detail
+ */
+export function longaudio_album_detail(params: LongaudioAlbumDetailParams): Promise;
+
+/**
+ * 听书 - 专辑音乐列表
+ * @route /longaudio/album/audios
+ */
+export function longaudio_album_audios(params: LongaudioAlbumAudiosParams): Promise;
+
+// ============================================================
+// 导出函数 —— 歌曲成绩单
+// ============================================================
+
+/**
+ * 获取歌曲成绩单信息
+ * @route /song/ranking
+ */
+export function song_ranking(params: SongRankingParams): Promise;
+
+/**
+ * 获取更详细的歌曲成绩单信息(需登录)
+ * @route /song/ranking/filter
+ */
+export function song_ranking_filter(params: SongRankingFilterParams): Promise;
+
+// ============================================================
+// 导出函数 —— 服务器管理(来自 server 模块)
+// ============================================================
+
+/**
+ * 启动 KuGouMusic API HTTP 服务
+ * @returns Express 应用实例(含 service 属性指向 HTTP Server)
+ */
+export function startService(): Promise;
+
+/**
+ * 动态扫描目录获取所有 API 模块定义
+ * @param modulesPath - 模块目录绝对路径
+ * @param specificRoute - 自定义路由映射
+ * @param doRequire - 是否通过 require 加载模块(默认 true)
+ */
+export function getModulesDefinitions(modulesPath: string, specificRoute: Record, doRequire?: boolean): Promise;
+
+// ============================================================
+// 导出函数 —— 请求工具(来自 util/request 模块)
+// ============================================================
+
+/**
+ * 创建底层 HTTP 请求
+ * @param config - 请求配置
+ */
+export function createRequest(config: RequestConfig): Promise;
diff --git a/third_party/MoeKoeMusic/api/main.js b/third_party/MoeKoeMusic/api/main.js
new file mode 100644
index 0000000..6add2fd
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/main.js
@@ -0,0 +1,126 @@
+/**
+ * @fileoverview KuGouMusicApi 程序化 API 入口模块
+ *
+ * 本模块是项目的编程式调用入口(区别于 index.js 的 HTTP 服务入口),
+ * 负责:
+ * 1. 扫描 `module/` 目录下所有 API 模块文件并动态加载
+ * 2. 为每个模块创建统一的包装函数,自动处理 Cookie 格式转换
+ * 3. 将所有 API 函数与服务器工具、请求工具合并后统一导出
+ *
+ * 导出结构为扁平对象,模块文件名(去 .js 后缀)即为 API 函数名。
+ * 例如:`module/search.js` → `api.search(params)`
+ *
+ * @module app
+ * @requires node:fs
+ * @requires path
+ * @requires ./util - 工具函数(cookieToJson 等)
+ * @requires ./server - 服务器管理(startService、getModulesDefinitions)
+ * @requires ./util/request - 底层 HTTP 请求工具(createRequest)
+ *
+ * @example
+ * // 作为库使用(编程式调用,不启动 HTTP 服务)
+ * const api = require('./app');
+ *
+ * // 手机登录
+ * const loginRes = await api.login_cellphone({
+ * mobile: '13800138000',
+ * code: '123456'
+ * });
+ *
+ * // 搜索音乐(携带认证 Cookie)
+ * const searchRes = await api.search({
+ * keywords: '海阔天空',
+ * cookie: `token=${loginRes.body.token};userid=${loginRes.body.userid}`
+ * });
+ */
+
+const fs = require('node:fs');
+const path = require('path');
+const { cookieToJson } = require('./util');
+
+/**
+ * 动态注册的 API 函数集合
+ *
+ * 键为模块文件名(去 .js 后缀),值为对应的包装函数。
+ * 例如:`{ search: [Function], login_cellphone: [Function], ... }`
+ *
+ * @type {Record) => Promise>}
+ */
+let obj = {};
+
+/**
+ * ============================================================
+ * 动态扫描并加载 module/ 目录下的所有 API 模块
+ * ============================================================
+ *
+ * 扫描流程:
+ * 1. 使用 `fs.readdirSync` 同步读取 `module/` 目录下所有文件
+ * 2. 对文件列表进行倒序排列(与 server.js 中的路由注册顺序保持一致)
+ * 3. 过滤出以 `.js` 结尾的文件
+ * 4. 对每个模块文件:
+ * a. 通过 `require` 加载模块,获取模块处理函数
+ * b. 从文件名提取 API 函数名(去掉 `.js` 后缀,如 `search.js` → `search`)
+ * c. 创建包装函数并注册到 `obj` 对象中
+ *
+ * 每个包装函数的作用:
+ * - 将调用者传入的 cookie 字符串自动转换为 JSON 对象
+ * - 合并调用参数与默认 cookie
+ * - 调用原始模块处理函数,同时注入请求工厂函数(lazy 加载 createRequest)
+ */
+fs.readdirSync(path.join(__dirname, 'module'))
+ .reverse()
+ .forEach((file) => {
+ // 跳过非 .js 文件(如 .json、.map、目录等)
+ if (!file.endsWith('.js')) return;
+
+ // 加载模块,获取其导出的处理函数
+ let fileModule = require(path.join(__dirname, 'module', file));
+
+ // 提取 API 函数名:取文件名中第一个 `.` 之前的部分
+ // 例如:`search.js` → `search`,`login_cellphone.js` → `login_cellphone`
+ let fn = file.split('.').shift() || '';
+
+ /**
+ * 为当前模块创建包装函数
+ *
+ * @param {Record} [data={}] - 调用者传入的请求参数
+ * @returns {Promise} 模块处理函数的返回结果
+ */
+ obj[fn] = (data = {}) => {
+ // 自动将 cookie 字符串转换为 JSON 对象
+ // 调用者可以传入 cookie 字符串(如 "token=xxx;userid=xxx")或对象格式
+ if (typeof data.cookie === 'string') data.cookie = cookieToJson(data.cookie);
+
+ // 调用原始模块处理函数,传入两个参数:
+ // 1. 合并后的参数对象(确保 cookie 始终为对象格式,默认为空对象)
+ // 2. 请求工厂函数:延迟加载 createRequest,避免循环依赖
+ return fileModule({ ...data, cookie: data.cookie ? data.cookie : {} }, (...args) => {
+ // 延迟加载:仅在实际需要发起请求时才 require createRequest
+ // 这样可以避免模块加载阶段的循环依赖问题
+ const { createRequest } = require('./util/request');
+ return createRequest(...args);
+ });
+ };
+ });
+
+/**
+ * ============================================================
+ * 统一导出
+ * ============================================================
+ *
+ * 最终导出一个扁平对象,包含三部分内容:
+ *
+ * 1. `...require('./server')` — 服务器管理工具
+ * - `startService()`: 启动 HTTP 服务
+ * - `getModulesDefinitions()`: 动态扫描模块定义
+ *
+ * 2. `...require('./util/request')` — 底层请求工具
+ * - `createRequest()`: 创建 HTTP 请求
+ *
+ * 3. `...obj` — 所有 API 模块函数
+ * - 每个 module/*.js 文件对应一个同名函数
+ * - 如 `search`、`login_cellphone`、`song_url` 等
+ *
+ * @type {Record & import("./server")}
+ */
+module.exports = { ...require('./server'), ...require('./util/request'), ...obj };
diff --git a/参考/NexBox-main/public/public/sponsor/.gitkeep b/third_party/MoeKoeMusic/api/module/.gitignore
similarity index 100%
rename from 参考/NexBox-main/public/public/sponsor/.gitkeep
rename to third_party/MoeKoeMusic/api/module/.gitignore
diff --git a/third_party/MoeKoeMusic/api/module/ai_recommend.js b/third_party/MoeKoeMusic/api/module/ai_recommend.js
new file mode 100644
index 0000000..dba7a07
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/ai_recommend.js
@@ -0,0 +1,34 @@
+const { appid, clientver, signParamsKey, cryptoMd5 } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const mid = params?.cookie?.KUGOU_API_MID; // 可以自定义
+ const clienttime = Date.now();
+
+ const recommend_source = (params?.album_audio_id || '').split(',').map((s) => ({ ID: Number(s) }));
+
+ const dataMap = {
+ platform: 'ios',
+ clientver,
+ clienttime: clienttime,
+ userid,
+ client_playlist: [],
+ source_type: 2,
+ playlist_ver: 2,
+ area_code: 1,
+ appid,
+ key: signParamsKey(clienttime.toString()),
+ mid,
+ recommend_source,
+ };
+
+ return useAxios({
+ url: '/recommend',
+ data: dataMap,
+ method: 'post',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'songlistairec.kugou.com' },
+ clearDefaultParams: true,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/album.js b/third_party/MoeKoeMusic/api/module/album.js
new file mode 100644
index 0000000..f20fca8
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/album.js
@@ -0,0 +1,34 @@
+const { clientver, appid, cryptoMd5, signParamsKey } = require('../util');
+
+
+module.exports = (params, useAxios) => {
+ const dateTime = Date.now();
+ const data = (params?.album_id || '').split(',').map((s) => ({ album_id: s, album_name: '', author_name: '' }));
+ const dfid = params?.cookie?.dfid || params?.dfid || '-';
+ const userid = params?.cookie?.userid || params?.userid || 0;
+ const token = params?.cookie?.token || params?.token || 0;
+
+ const dataMap = {
+ appid,
+ clienttime: dateTime,
+ clientver,
+ data,
+ dfid,
+ fields: params?.fields || '',
+ key: signParamsKey(dateTime),
+ mid: params?.cookie?.KUGOU_API_MID,
+ };
+
+ if (token) dataMap['token'] = token;
+ if (userid) dataMap['userid'] = userid;
+
+ return useAxios({
+ baseURL: 'http://kmr.service.kugou.com',
+ url: '/v1/album',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'kmr.service.kugou.com', 'Content-Type': 'application/json' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/album_detail.js b/third_party/MoeKoeMusic/api/module/album_detail.js
new file mode 100644
index 0000000..eb16928
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/album_detail.js
@@ -0,0 +1,16 @@
+// 专辑详情
+module.exports = (params, useAxios) => {
+ const data = {
+ data: [{ album_id: params.id }],
+ is_buy: params?.is_buy || 0,
+ fields: 'album_id,album_name,publish_date,sizable_cover,intro,language,is_publish,heat,type,quality,authors,exclusive,author_name,trans_param',
+ };
+ return useAxios({
+ url: '/kmr/v2/albums',
+ method: 'POST',
+ data,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'openapi.kugou.com', 'kg-tid': '255' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/album_shop.js b/third_party/MoeKoeMusic/api/module/album_shop.js
new file mode 100644
index 0000000..e6f84dd
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/album_shop.js
@@ -0,0 +1,9 @@
+// 唱片店
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/zhuanjidata/v3/album_shop_v2/get_classify_data',
+ method: 'GET',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/album_songs.js b/third_party/MoeKoeMusic/api/module/album_songs.js
new file mode 100644
index 0000000..e127b0a
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/album_songs.js
@@ -0,0 +1,18 @@
+// 专辑音乐列表
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ album_id: params.id,
+ is_buy: params?.is_buy || '',
+ page: params?.page || 1,
+ pagesize: params?.pagesize || 30,
+ };
+
+ return useAxios({
+ url: '/v1/album_audio/lite',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'openapi.kugou.com', 'kg-tid': '255' },
+ });
+}
\ No newline at end of file
diff --git a/third_party/MoeKoeMusic/api/module/artist_albums.js b/third_party/MoeKoeMusic/api/module/artist_albums.js
new file mode 100644
index 0000000..6a2473c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/artist_albums.js
@@ -0,0 +1,21 @@
+// 获取歌手专辑
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ author_id: params.id,
+ pagesize: params?.pagesize || 30,
+ page: params?.page || 1,
+ sort: params?.sort === 'hot' ? 3 : 1, // 3:最热,1:最新
+ category: 1,
+ area_code: 'all',
+ };
+
+ return useAxios({
+ url: '/kmr/v1/author/albums',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'openapi.kugou.com', 'kg-tid': 36 },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/artist_audios.js b/third_party/MoeKoeMusic/api/module/artist_audios.js
new file mode 100644
index 0000000..90ac65b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/artist_audios.js
@@ -0,0 +1,29 @@
+const { cryptoMd5, signParamsKey, clientver, appid } = require('../util');
+// 获取歌手单曲
+
+module.exports = (params, useAxios) => {
+ const clienttime = Math.floor(new Date().getTime() / 1000);
+ const mid = params?.cookie?.KUGOU_API_MID;
+ const dataMap = {
+ appid,
+ clientver,
+ mid,
+ clienttime,
+ key: signParamsKey(clienttime),
+ author_id: params.id,
+ pagesize: params?.pagesize || 30,
+ page: params?.page || 1,
+ sort: params?.sort === 'hot' ? 1 : 2, // 1:最热,2:最新
+ area_code: 'all',
+ };
+
+ return useAxios({
+ baseURL: 'https://openapi.kugou.com',
+ url: '/kmr/v1/audio_group/author',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'openapi.kugou.com', 'kg-tid': 220 },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/artist_detail.js b/third_party/MoeKoeMusic/api/module/artist_detail.js
new file mode 100644
index 0000000..20f6db4
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/artist_detail.js
@@ -0,0 +1,11 @@
+// 歌手详情
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/kmr/v3/author',
+ method: 'POST',
+ data: { author_id: params.id },
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'openapi.kugou.com', 'kg-tid': 36 },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/artist_follow.js b/third_party/MoeKoeMusic/api/module/artist_follow.js
new file mode 100644
index 0000000..a18a2a9
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/artist_follow.js
@@ -0,0 +1,31 @@
+const { cryptoAesEncrypt, rsaEncrypt2 } = require('../util');
+// 关注歌手
+module.exports = (params, useAxios) => {
+ const singerid = Number(params.id);
+ const token = params?.token || params.cookie?.token || '';
+ const userid = Number(params?.userid || params?.cookie?.userid || 0);
+ const clienttime = Math.floor(Date.now() / 1000);
+
+ const encrypt = cryptoAesEncrypt({ singerid, token });
+
+
+ const paramsMap = {
+ plat: 0,
+ userid,
+ singerid,
+ source: 7,
+ p: rsaEncrypt2({ clienttime, key: encrypt.key }),
+ params: encrypt.str
+ }
+
+
+
+ return useAxios({
+ url: '/followservice/v3/follow_singer',
+ method: 'post',
+ data: paramsMap,
+ params: {clienttime},
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/artist_follow_newsongs.js b/third_party/MoeKoeMusic/api/module/artist_follow_newsongs.js
new file mode 100644
index 0000000..80393fd
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/artist_follow_newsongs.js
@@ -0,0 +1,21 @@
+// 获取关注歌手新歌
+module.exports = (params, useAxios) => {
+ const last_album_id = params.last_album_id || 0;
+ const page_size = params.pagesize || 30;
+ const opt_sort = params.opt_sort === 2 ? 2 : 1;
+
+ const paramsMap = {
+ last_album_id,
+ page_size,
+ opt_sort,
+ };
+
+ return useAxios({
+ url: '/feed/v1/follow/newsong_album_list',
+ method: 'post',
+ data: { last_album_id },
+ params: paramsMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/artist_honour.js b/third_party/MoeKoeMusic/api/module/artist_honour.js
new file mode 100644
index 0000000..026bc0e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/artist_honour.js
@@ -0,0 +1,11 @@
+// 歌手荣誉详情
+module.exports = (params, useAxios) => {
+ return useAxios({
+ baseURL: 'http://h5activity.kugou.com',
+ url: '/v1/query_singer_honour_detail',
+ method: 'POST',
+ params: { singer_id: params.id, pagesize: params?.pagesize || 30, page: params?.page || 1 },
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/artist_lists.js b/third_party/MoeKoeMusic/api/module/artist_lists.js
new file mode 100644
index 0000000..d8eed45
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/artist_lists.js
@@ -0,0 +1,18 @@
+// 歌手列表
+module.exports = (params, useAxios) => {
+ const paramsMap = {
+ musician: Number(params.musician || 0),
+ sextype: params.sextypes || 0,
+ showtype: 2,
+ type: params.type || 0,
+ hotsize: Number(params.hotsize || 30)
+ };
+
+ return useAxios({
+ url: '/ocean/v6/singer/list',
+ method: 'GET',
+ params: paramsMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/artist_unfollow.js b/third_party/MoeKoeMusic/api/module/artist_unfollow.js
new file mode 100644
index 0000000..f1a22cd
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/artist_unfollow.js
@@ -0,0 +1,28 @@
+const { cryptoAesEncrypt, rsaEncrypt2 } = require('../util');
+// 取消关注歌手
+module.exports = (params, useAxios) => {
+ const singerid = params.id;
+ const token = params?.token || params.cookie?.token || '';
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const clienttime = Math.floor(Date.now() / 1000);
+
+ const encrypt = cryptoAesEncrypt({ singerid, token });
+
+ const paramsMap = {
+ plat: 0,
+ userid,
+ singerid,
+ source: 7,
+ p: rsaEncrypt2({ clienttime, key: encrypt.key }),
+ params: encrypt.str
+ }
+
+
+ return useAxios({
+ url: '/followservice/v3/unfollow_singer',
+ method: 'post',
+ data: paramsMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/artist_videos.js b/third_party/MoeKoeMusic/api/module/artist_videos.js
new file mode 100644
index 0000000..90e439b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/artist_videos.js
@@ -0,0 +1,22 @@
+// 获取歌手mv
+
+const tag_idx = { official: 18, live: 20, fan: 23, artist: 42419, all: '' };
+
+module.exports = (params, useAxios) => {
+ const paramsMap = {
+ author_id: params.id,
+ is_fanmade: '',
+ tag_idx: tag_idx[params?.tag || 'all'] || '', // 18:官方版本,20:现场版本,23:饭制版本,42419:歌手发布
+ pagesize: params.pagesize || 30,
+ page: params.page || 1,
+ };
+
+ return useAxios({
+ baseURL: 'https://openapicdn.kugou.com',
+ url: '/kmr/v1/author/videos',
+ method: 'GET',
+ params: paramsMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/audio.js b/third_party/MoeKoeMusic/api/module/audio.js
new file mode 100644
index 0000000..ea50165
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/audio.js
@@ -0,0 +1,32 @@
+const { signParamsKey, appid, clientver } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const dateTime = Date.now();
+ const data = (params?.hash || '').split(',').map((s) => ({ hash: s, audio_id: 0 }));
+ const dfid = params?.cookie?.dfid || params?.dfid || '-';
+ const userid = params?.cookie?.userid || params?.userid || 0;
+ const token = params?.cookie?.token || params?.token || 0;
+
+ const dataMap = {
+ appid,
+ clienttime: dateTime,
+ clientver,
+ data,
+ dfid,
+ key: signParamsKey(dateTime),
+ mid: params?.cookie?.KUGOU_API_MID,
+ };
+
+ if (token) dataMap['token'] = token;
+ if (userid) dataMap['userid'] = userid;
+
+ return useAxios({
+ baseURL: 'http://kmr.service.kugou.com',
+ url: '/v1/audio/audio',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'kmr.service.kugou.com', 'Content-Type': 'application/json' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/audio_accompany_matching.js b/third_party/MoeKoeMusic/api/module/audio_accompany_matching.js
new file mode 100644
index 0000000..a40a6e3
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/audio_accompany_matching.js
@@ -0,0 +1,34 @@
+const { cryptoMd5, appid } = require('../util');
+
+module.exports = (params, useAxios) => {
+
+ const dataMap = {
+ isteen: 0,
+ mixId: Number(params.mixId) || 0,
+ usemkv: 1,
+ platform: 2,
+ fileName: params.fileName || '',
+ hash: params.hash,
+ version: 12375,
+ appid,
+ }
+
+
+ const str = '*s&iN#G70*';
+ const paramsString = Object.keys(dataMap)
+ .sort()
+ .map((key) => `${key}=${typeof dataMap[key] === 'object' ? JSON.stringify(dataMap[key]) : dataMap[key]}`)
+ .join('&');
+ dataMap['sign'] = cryptoMd5(`${paramsString}${str}`).substring(8, 24);
+
+ return useAxios({
+ baseURL: 'https://nsongacsing.kugou.com',
+ url: '/sing7/accompanywan/json/v2/cdn/optimal_matching_accompany_2_listen.do',
+ params: dataMap,
+ method: 'get',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ clearDefaultParams: true,
+ notSignature: true,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/audio_ktv_total.js b/third_party/MoeKoeMusic/api/module/audio_ktv_total.js
new file mode 100644
index 0000000..83f542e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/audio_ktv_total.js
@@ -0,0 +1,34 @@
+const { cryptoMd5, appid } = require('../util');
+
+module.exports = (params, useAxios) => {
+
+ const dataMap = {
+ isteen: 0,
+ songId: Number(params.songId),
+ usemkv: 1,
+ platform: 2,
+ singerName: params.singerName,
+ songHash: params.songHash,
+ version: 12375,
+ appid,
+ }
+
+
+ const str = '*s&iN#G70*';
+ const paramsString = Object.keys(dataMap)
+ .sort()
+ .map((key) => `${key}=${typeof dataMap[key] === 'object' ? JSON.stringify(dataMap[key]) : dataMap[key]}`)
+ .join('&');
+ dataMap['sign'] = cryptoMd5(`${paramsString}${str}`).substring(8, 24);
+
+ return useAxios({
+ baseURL: 'https://acsing.service.kugou.com',
+ url: '/sing7/listenguide/json/v2/cdn/listenguide/get_total_opus_num_v02.do',
+ params: dataMap,
+ method: 'get',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ clearDefaultParams: true,
+ notSignature: true,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/audio_match.js b/third_party/MoeKoeMusic/api/module/audio_match.js
new file mode 100644
index 0000000..fcc8d07
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/audio_match.js
@@ -0,0 +1,20 @@
+// 听歌识曲
+module.exports = (params, useAxios) => {
+ const paramsMap = {
+ fpid: Date.now(),
+ area_code: 1,
+ include_unpublish: 1,
+ useid: params?.userid || params?.cookie?.userid || 0,
+ multi_result: 1,
+ };
+
+ return useAxios({
+ url: '/fingerprint.service/v1/music_trackid_mulit',
+ encryptType: 'android',
+ method: 'POST',
+ data: params.data,
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ headers: { 'content-type': 'application/octet-stream', 'user-agent': 'KuGou/11490 (Android)' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/audio_related.js b/third_party/MoeKoeMusic/api/module/audio_related.js
new file mode 100644
index 0000000..75c6299
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/audio_related.js
@@ -0,0 +1,46 @@
+const { cryptoMd5 } = require('../util');
+
+const sortType = { all: 1, hot: 2, new: 3 };
+
+// https://listkmrp3cdnretry.kugou.com/v3/album_audio/related
+module.exports = (params, useAxios) => {
+ const show_detail = Number(params.show_detail) === 0;
+
+ let dataMap = {
+ album_audio_id: Number(params.album_audio_id),
+ appid: 1005,
+ area_code: 1,
+ clientver: 12329,
+ };
+
+ if (!show_detail) {
+ dataMap = {
+ ...dataMap,
+ page: params.page || 1,
+ pagesize: params.pagesize || 30,
+ show_input: 1,
+ show_type: params.show_type || 0,
+ sort: sortType[params.sort] || 1,
+ type: params.type || 0,
+ };
+ }
+
+ dataMap['version'] = 1;
+
+ const str = 'OIlwieks28dk2k092lksi2UIkp';
+ const paramsString = Object.keys(dataMap)
+ .sort()
+ .map((key) => `${key}=${typeof dataMap[key] === 'object' ? JSON.stringify(dataMap[key]) : dataMap[key]}`)
+ .join('');
+ dataMap['signature'] = cryptoMd5(`${str}${paramsString}${str}`);
+
+ return useAxios({
+ baseURL: 'https://listkmrp3cdnretry.kugou.com',
+ url: !show_detail ? '/v3/album_audio/related' : '/v2/audio_related/total',
+ params: dataMap,
+ method: 'get',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ clearDefaultParams: true,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/brush.js b/third_party/MoeKoeMusic/api/module/brush.js
new file mode 100644
index 0000000..73d903b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/brush.js
@@ -0,0 +1,49 @@
+const { appid, clientver, signParamsKey } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const userid = params?.cookie?.userid || params?.userid || 0;
+ const vip_type = params?.cookie?.vip_type || params?.vipType || 0;
+ const dateTime = Date.now();
+
+ const personal_recommend = {
+ userid,
+ appid,
+ playlist_ver: 2,
+ clienttime: dateTime,
+ mid: params?.cookie?.KUGOU_API_MID,
+ new_sync_point: dateTime,
+ module_id: 1,
+ action: 'login',
+ vip_type,
+ vip_flags: 3,
+ recommend_source_locked: 0,
+ song_pool_id: Number(params?.song_pool_id || 0),
+ callerid: 0,
+ m_type: 1,
+ kguid: userid,
+ platform: 'ios',
+ area_code: 1,
+ fakem: 'ca981cfc583a4c37f28d2d49000013c16a0a',
+ clientver: 11850,
+ mode: params?.mode || 'normal',
+ active_swtich: 'on',
+ key: signParamsKey(dateTime),
+ };
+
+ const dataMap = {
+ behaviors: [],
+ abtest: {
+ abtest: { shuashua: { commentcard: 2 } },
+ },
+ personal_recommend_params: personal_recommend,
+ };
+
+ return useAxios({
+ url: '/genesisapi/v1/newepoch_song_rec/feed',
+ data: dataMap,
+ params: { sort_type: 1, platform: 'ios', page: 1, content_ver: 4, clientver: 11850 },
+ method: 'post',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/captcha_sent.js b/third_party/MoeKoeMusic/api/module/captcha_sent.js
new file mode 100644
index 0000000..a566a9c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/captcha_sent.js
@@ -0,0 +1,17 @@
+// 手机验证码发送
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ businessid: 5,
+ mobile: `${params?.mobile}`,
+ plat: 3,
+ };
+
+ return useAxios({
+ baseURL: 'http://login.user.kugou.com',
+ url: '/v7/send_mobile_code',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: {mid: params?.cookie?.KUGOU_API_MID},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/comment_album.js b/third_party/MoeKoeMusic/api/module/comment_album.js
new file mode 100644
index 0000000..5127c9e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/comment_album.js
@@ -0,0 +1,21 @@
+// 歌曲评论
+module.exports = (params, useAxios) => {
+
+ const paramsMap = {
+ childrenid: params.id,
+ need_show_image: 1,
+ p: params.page || 1,
+ pagesize: params.pagesize || 30,
+ show_classify: params.show_classify || 1,
+ show_hotword_list: params.show_hotword_list || 1,
+ code: '94f1792ced1df89aa68a7939eaf2efca',
+ }
+
+ return useAxios({
+ url: '/m.comment.service/v1/cmtlist',
+ encryptType: 'android',
+ method: 'POST',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/comment_count.js b/third_party/MoeKoeMusic/api/module/comment_count.js
new file mode 100644
index 0000000..87a269c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/comment_count.js
@@ -0,0 +1,23 @@
+// 歌曲评论数
+module.exports = (params, useAxios) => {
+
+ const paramsMap = {
+ r: 'comments/getcommentsnum',
+ code: 'fc4be23b4e972707f36b8a828a93ba8a',
+ };
+
+ if (params && params.hash) {
+ paramsMap.hash = params.hash;
+ } else if (params && params.special_id) {
+ paramsMap.childrenid = params.special_id;
+ }
+
+ return useAxios({
+ url: '/index.php',
+ encryptType: 'web',
+ method: 'GET',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'sum.comment.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/comment_floor.js b/third_party/MoeKoeMusic/api/module/comment_floor.js
new file mode 100644
index 0000000..e85a052
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/comment_floor.js
@@ -0,0 +1,59 @@
+// 楼层评论
+const songCode = 'fc4be23b4e972707f36b8a828a93ba8a';
+const playlistCode = 'ca53b96fe5a1d9c22d71c8f522ef7c4f';
+const albumCode = '94f1792ced1df89aa68a7939eaf2efca';
+
+const isNonEmpty = (value) => {
+ if (value == null) return false;
+ const text = `${value}`.trim();
+ return text !== '' && text !== 'null' && text !== 'undefined';
+};
+
+const resolveFloorCommentRequestConfig = (params = {}) => {
+ const resourceType = `${params.resource_type || params.resourceType || ''}`.toLowerCase();
+ const code = isNonEmpty(params.code)
+ ? `${params.code}`
+ : resourceType === 'playlist'
+ ? playlistCode
+ : resourceType === 'album'
+ ? albumCode
+ : songCode;
+
+ const useServiceEndpoint =
+ resourceType === 'playlist' ||
+ resourceType === 'album' ||
+ code === playlistCode ||
+ code === albumCode;
+
+ const paramsMap = {
+ childrenid: params.special_id,
+ need_show_image: 1,
+ p: params.page || 1,
+ pagesize: params.pagesize || 30,
+ show_classify: params.show_classify ?? 1,
+ show_hotword_list: params.show_hotword_list ?? 1,
+ code,
+ tid: params.tid,
+ };
+
+ if (isNonEmpty(params.mixsongid)) {
+ paramsMap.mixsongid = params.mixsongid;
+ }
+
+ return {
+ url: useServiceEndpoint
+ ? '/m.comment.service/v1/hot_replylist'
+ : '/mcomment/v1/hot_replylist',
+ encryptType: 'android',
+ method: 'POST',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ };
+};
+
+const commentFloor = (params, useAxios) => {
+ return useAxios(resolveFloorCommentRequestConfig(params));
+};
+
+module.exports = commentFloor;
+module.exports.resolveFloorCommentRequestConfig = resolveFloorCommentRequestConfig;
diff --git a/third_party/MoeKoeMusic/api/module/comment_music.js b/third_party/MoeKoeMusic/api/module/comment_music.js
new file mode 100644
index 0000000..1c34196
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/comment_music.js
@@ -0,0 +1,22 @@
+// 歌曲评论
+module.exports = (params, useAxios) => {
+
+ const paramsMap = {
+ mixsongid: params.mixsongid,
+ need_show_image: 1,
+ p: params.page || 1,
+ pagesize: params.pagesize || 30,
+ show_classify: params.show_classify || 1,
+ show_hotword_list: params.show_hotword_list || 1,
+ extdata: '0',
+ code: 'fc4be23b4e972707f36b8a828a93ba8a'
+ }
+
+ return useAxios({
+ url: '/mcomment/v1/cmtlist',
+ encryptType: 'android',
+ method: 'POST',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/comment_music_classify.js b/third_party/MoeKoeMusic/api/module/comment_music_classify.js
new file mode 100644
index 0000000..451a442
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/comment_music_classify.js
@@ -0,0 +1,22 @@
+// 歌曲评论-根据分类获取评论
+module.exports = (params, useAxios) => {
+
+ const paramsMap = {
+ mixsongid: params.mixsongid,
+ need_show_image: 1,
+ page: params.page || 1,
+ pagesize: params.pagesize || 30,
+ type_id: params.type_id,
+ extdata: '0',
+ code: 'fc4be23b4e972707f36b8a828a93ba8a',
+ sort_method: Number(params.sort) === 2 ? params.sort : 1
+ }
+
+ return useAxios({
+ url: '/mcomment/v1/cmt_classify_list',
+ encryptType: 'android',
+ method: 'POST',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/comment_music_hotword.js b/third_party/MoeKoeMusic/api/module/comment_music_hotword.js
new file mode 100644
index 0000000..d1c89f3
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/comment_music_hotword.js
@@ -0,0 +1,21 @@
+// 歌曲评论-更具热词获取评论
+module.exports = (params, useAxios) => {
+
+ const paramsMap = {
+ mixsongid: params.mixsongid,
+ need_show_image: 1,
+ p: params.page || 1,
+ pagesize: params.pagesize || 30,
+ hot_word: params.hot_word,
+ extdata: '0',
+ code: 'fc4be23b4e972707f36b8a828a93ba8a',
+ }
+
+ return useAxios({
+ url: '/mcomment/v1/get_hot_word',
+ encryptType: 'android',
+ method: 'POST',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/comment_playlist.js b/third_party/MoeKoeMusic/api/module/comment_playlist.js
new file mode 100644
index 0000000..5d4d2b6
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/comment_playlist.js
@@ -0,0 +1,23 @@
+// 歌曲评论
+module.exports = (params, useAxios) => {
+
+ const paramsMap = {
+ childrenid: params.id,
+ need_show_image: 1,
+ p: params.page || 1,
+ pagesize: params.pagesize || 30,
+ show_classify: params.show_classify || 1,
+ show_hotword_list: params.show_hotword_list || 1,
+ code: 'ca53b96fe5a1d9c22d71c8f522ef7c4f',
+ content_type: 0,
+ tag: 5
+ }
+
+ return useAxios({
+ url: '/m.comment.service/v1/cmtlist',
+ encryptType: 'android',
+ method: 'POST',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/everyday_friend.js b/third_party/MoeKoeMusic/api/module/everyday_friend.js
new file mode 100644
index 0000000..ad3b279
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/everyday_friend.js
@@ -0,0 +1,13 @@
+
+module.exports = (params, useAxios) => {
+ return useAxios({
+ baseURL: 'https://acsing.service.kugou.com',
+ url: '/sing7/relation/json/v3/friend_rec_by_using_song_list',
+ encryptType: 'android',
+ method: 'POST',
+ data: {list: [{user_id: 853927886, mixsong_ids: [290083753,251724346,571554587,250126644,208831644,40328518,250504076,581706850,318347675,585258401,288481998,407414475,28239430,280584633,291957521,64556644,243149863,488725103,32114153,39951172,29019580,40397606,327507651,32029382,32218359,340353127,276448762,177071956,100031397,249251602]}]},
+ params: { channel: 130, isteen: 0, platform: 2, usemkv: 1 },
+ cookie: params?.cookie || {},
+ headers: { pid: 126556797 },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/everyday_history.js b/third_party/MoeKoeMusic/api/module/everyday_history.js
new file mode 100644
index 0000000..988158c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/everyday_history.js
@@ -0,0 +1,20 @@
+// mode list ,song
+
+module.exports = (params, useAxios) => {
+ const paramsMap = {
+ mode: params.mode || 'list',
+ platform: params.platform || 'ios'
+ };
+
+ if (params.history_name) paramsMap['history_name'] = params.history_name;
+ if (params.date) paramsMap['date'] = params.date;
+
+ return useAxios({
+ url: '/everyday/api/v1/get_history',
+ encryptType: 'android',
+ method: 'POST',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'everydayrec.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/everyday_recommend.js b/third_party/MoeKoeMusic/api/module/everyday_recommend.js
new file mode 100644
index 0000000..20538fa
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/everyday_recommend.js
@@ -0,0 +1,10 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/everyday_song_recommend',
+ encryptType: 'android',
+ method: 'POST',
+ params: { platform: params.platform || 'ios' },
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'everydayrec.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/everyday_style_recommend.js b/third_party/MoeKoeMusic/api/module/everyday_style_recommend.js
new file mode 100644
index 0000000..e56b695
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/everyday_style_recommend.js
@@ -0,0 +1,14 @@
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ platform: params.platform || 'ios'
+ };
+
+ return useAxios({
+ url: '/everydayrec.service/everyday_style_recommend',
+ encryptType: 'android',
+ method: 'POST',
+ data: {},
+ params: { tagids: params.tagids ?? ''},
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/favorite_count.js b/third_party/MoeKoeMusic/api/module/favorite_count.js
new file mode 100644
index 0000000..f628d89
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/favorite_count.js
@@ -0,0 +1,9 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: `/count/v1/audio/mget_collect`,
+ method: 'GET',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ params: { mixsongids: params.mixsongids },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/fm_class.js b/third_party/MoeKoeMusic/api/module/fm_class.js
new file mode 100644
index 0000000..934c25a
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/fm_class.js
@@ -0,0 +1,25 @@
+const { appid, clientver, cryptoMd5, signParamsKey } = require('../util');
+module.exports = (params, useAxios) => {
+ const dateTime = Date.now();
+ const userid = params?.cookie?.userid || params?.userid || 0;
+ const dataMap = {
+ kguid: userid,
+ clienttime: dateTime,
+ mid: params?.cookie?.KUGOU_API_MID,
+ platform: 'android',
+ clientver,
+ uid: userid,
+ get_tracker: 1,
+ key: signParamsKey(dateTime),
+ appid,
+ };
+
+ return useAxios({
+ url: '/v1/class_fm_song',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'fm.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/fm_image.js b/third_party/MoeKoeMusic/api/module/fm_image.js
new file mode 100644
index 0000000..47681f8
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/fm_image.js
@@ -0,0 +1,30 @@
+const { appid, clientver, signParamsKey } = require('../util');
+module.exports = (params, useAxios) => {
+ const dateTime = Date.now();
+ const dfid = params?.cookie?.dfid || params?.dfid || '-';
+ const userid = params?.cookie?.userid || params?.userid;
+ const token = params?.cookie?.token || params?.token;
+
+ const fmData = (params?.fmid || '').split(',').map((s) => ({ fields: 'imgUrl100,imgUrl50', fmid: s, fmtype: 2 }));
+
+ const dataMap = {
+ appid,
+ clienttime: dateTime,
+ clientver,
+ data: fmData,
+ dfid,
+ key: signParamsKey(dateTime),
+ mid: params?.cookie?.KUGOU_API_MID,
+ };
+ if (userid) dataMap['userid'] = userid;
+ if (token) dataMap['token'] = token;
+
+ return useAxios({
+ url: '/v1/fm_info',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'fm.service.kugou.com', 'Content-Type': 'application/json' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/fm_recommend.js b/third_party/MoeKoeMusic/api/module/fm_recommend.js
new file mode 100644
index 0000000..4411632
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/fm_recommend.js
@@ -0,0 +1,26 @@
+const { appid, clientver, signParamsKey } = require('../util');
+module.exports = (params, useAxios) => {
+ const dateTime = Date.now();
+ const dfid = params?.cookie?.dfid || params?.dfid || '-';
+ const dataMap = {
+ appid,
+ clientver,
+ clienttime: dateTime,
+ mid: params?.cookie?.KUGOU_API_MID,
+ key: signParamsKey(dateTime),
+ rcmdsongcount: 1,
+ level: 0,
+ area_code: 1,
+ get_tracker: 1,
+ uid: 0,
+ };
+
+ return useAxios({
+ url: '/v1/rcmd_list',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'fm.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/fm_songs.js b/third_party/MoeKoeMusic/api/module/fm_songs.js
new file mode 100644
index 0000000..e271c9c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/fm_songs.js
@@ -0,0 +1,41 @@
+const { appid, clientver, signParamsKey } = require('../util');
+module.exports = (params, useAxios) => {
+ const dateTime = Date.now();
+ const userid = params?.cookie?.userid || params?.userid;
+
+ const fmData = (params?.fmid || '').split(',').map((s) => ({
+ fmid: s,
+ fmtype: params?.type || 2,
+ offset: params?.offset || -1,
+ size: params?.size || 20,
+ singername: s.singername || '',
+ }));
+
+ // fmType 生成
+ (params?.fmtype || '').split(',').forEach((s, l) => (fmData[l].fmtype = s || fmData[l].fmtype));
+
+ (params?.fmoffset || '').split(',').forEach((s, l) => (fmData[l].offset = s || fmData[l].offset));
+
+ (params?.fmsize || '').split(',').forEach((s, l) => (fmData[l].size = s || fmData[l].size));
+
+ const dataMap = {
+ appid,
+ area_code: 1,
+ clienttime: dateTime,
+ clientver,
+ data: fmData,
+ get_tracker: 1,
+ key: signParamsKey(dateTime),
+ mid: params?.cookie?.KUGOU_API_MID,
+ uid: userid,
+ };
+
+ return useAxios({
+ url: '/v1/app_song_list_offset',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'fm.service.kugou.com', 'Content-Type': 'application/json' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/get_mode_info.js b/third_party/MoeKoeMusic/api/module/get_mode_info.js
new file mode 100644
index 0000000..1e8c11c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/get_mode_info.js
@@ -0,0 +1,24 @@
+// 获取音效详情
+const { signParamsKey } = require('../util');
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ model_id: Number(params.model_id || 0),
+ req_src: 'collection',
+ earphone_vip: 1,
+ sound_ver: 2,
+ key: signParamsKey(Date.now()),
+ page: params.page || 1,
+ pagesize: params.pagesize || 30,
+ };
+
+
+
+ return useAxios({
+ baseURL: 'http://mobileservice.kugou.com',
+ url: '/api/v5/earphone/get_model_info',
+ encryptType: 'android',
+ method: 'GET',
+ params: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/get_model.js b/third_party/MoeKoeMusic/api/module/get_model.js
new file mode 100644
index 0000000..90cfd60
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/get_model.js
@@ -0,0 +1,26 @@
+// 获取社区音效
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ super_vip: 1,
+ sound_ver: 2,
+ page: params.page || 1,
+ pagesize: params.pagesize || 30,
+ apiver: 3,
+ classify: '2,3',
+ plat: 2,
+ privilege: 1,
+ sort: 2
+ };
+
+
+
+
+ return useAxios({
+ url: '/ocean/v6/sound/list',
+ encryptType: 'android',
+ method: 'GET',
+ params: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/get_verify_info.js b/third_party/MoeKoeMusic/api/module/get_verify_info.js
new file mode 100644
index 0000000..1c822be
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/get_verify_info.js
@@ -0,0 +1,22 @@
+// 获取验证码数据
+module.exports = (params, useAxios) => {
+ const userid = Number(params?.userid || params?.cookie?.userid || '0');
+ const dataMap = {
+ eventid: params?.eventid,
+ userid,
+ platid: params?.platid || 2,
+ rtype: 1,
+ wasm: 1,
+ i: '',
+ sid: '',
+ edt: '',
+ };
+
+ return useAxios({
+ url: '/verifyservice/v3/get_verify_info',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/images.js b/third_party/MoeKoeMusic/api/module/images.js
new file mode 100644
index 0000000..d558b75
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/images.js
@@ -0,0 +1,43 @@
+const { signatureAndroidParams, appid, clientver } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const data = (params?.hash || '').split(',').map((s) => ({ album_id: 0, hash: s, album_audio_id: 0 }));
+ (params?.album_id || '').split(',').forEach((s, index) => {
+ if (index <= data.length - 1) {
+ data[index]['album_id'] = s || 0;
+ }
+ });
+ (params?.album_audio_id || '').split(',').forEach((s, index) => {
+ if (index <= data.length - 1) {
+ data[index]['album_audio_id'] = s || 0;
+ }
+ });
+
+ const paramsMap = {
+ album_image_type: '-3',
+ appid,
+ clientver,
+ author_image_type: '3,4,5',
+ count: params?.count || 5,
+ data,
+ isCdn: 1,
+ publish_time: 1,
+ };
+
+ const query = Object.keys(paramsMap)
+ .sort()
+ .map((s) => `${s}=${encodeURIComponent(typeof paramsMap[s] === 'object' ? JSON.stringify(paramsMap[s]) : paramsMap[s])}`);
+
+ const signature = signatureAndroidParams(paramsMap);
+
+ return useAxios({
+ baseURL: 'https://expendablekmr.kugou.com',
+ url: `/container/v2/image?${query.join('&')}`,
+ method: 'GET',
+ encryptType: 'android',
+ params: { signature },
+ cookie: params?.cookie || {},
+ notSign: true,
+ clearDefaultParams: true,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/images_audio.js b/third_party/MoeKoeMusic/api/module/images_audio.js
new file mode 100644
index 0000000..a3f172c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/images_audio.js
@@ -0,0 +1,47 @@
+const { signatureAndroidParams, appid, clientver } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const data = (params?.hash || '').split(',').map((s) => ({ audio_id: 0, hash: s, album_audio_id: 0, filename: '' }));
+ (params?.audio_id || '').split(',').forEach((s, index) => {
+ if (index <= data.length - 1) {
+ data[index]['audio_id'] = s || 0;
+ }
+ });
+ (params?.album_audio_id || '').split(',').forEach((s, index) => {
+ if (index <= data.length - 1) {
+ data[index]['album_audio_id'] = s || 0;
+ }
+ });
+ (params?.filename || '').split(',').forEach((s, index) => {
+ if (index <= data.length - 1) {
+ data[index]['filename'] = s;
+ }
+ });
+
+ const paramsMap = {
+ appid,
+ clientver,
+ count: params?.count || 5,
+ data,
+ isCdn: 1,
+ publish_time: 1,
+ show_authors: 1,
+ };
+
+ const query = Object.keys(paramsMap)
+ .sort()
+ .map((s) => `${s}=${encodeURIComponent(typeof paramsMap[s] === 'object' ? JSON.stringify(paramsMap[s]) : paramsMap[s])}`);
+
+ const signature = signatureAndroidParams(paramsMap);
+
+ return useAxios({
+ baseURL: 'https://expendablekmr.kugou.com',
+ url: `/v2/author_image/audio?${query.join('&')}`,
+ method: 'GET',
+ encryptType: 'android',
+ params: { signature },
+ cookie: params?.cookie || {},
+ notSign: true,
+ clearDefaultParams: true,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/ip.js b/third_party/MoeKoeMusic/api/module/ip.js
new file mode 100644
index 0000000..2fa6941
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/ip.js
@@ -0,0 +1,21 @@
+// 根据 ip id 获取相对应的 歌曲/专辑/视频/歌手
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ is_publish: 1,
+ ip_id: params?.id,
+ sort: 3,
+ page: params?.page || 1,
+ pagesize: params?.pagesize || 30,
+ query: 1,
+ };
+
+ const type = ['audios', 'albums', 'videos', 'author_list'].includes(params?.type) ? params.type : 'audios';
+
+ return useAxios({
+ url: `/openapi/v1/ip/${type}`,
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/ip_dateil.js b/third_party/MoeKoeMusic/api/module/ip_dateil.js
new file mode 100644
index 0000000..fb528a5
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/ip_dateil.js
@@ -0,0 +1,17 @@
+// 获取ip详情
+module.exports = (params, useAxios) => {
+ const data = (params?.id || '').split(',').map((s) => ({ ip_id: s }));
+
+ const dataMap = {
+ data,
+ is_publish: 1,
+ };
+
+ return useAxios({
+ url: '/openapi/v1/ip',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/ip_playlist.js b/third_party/MoeKoeMusic/api/module/ip_playlist.js
new file mode 100644
index 0000000..30e910c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/ip_playlist.js
@@ -0,0 +1,16 @@
+// 根据 ip 获取相对于歌单
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ ip: params?.id,
+ page: params?.page || 1,
+ pagesize: params?.pagesize || 30,
+ };
+
+ return useAxios({
+ url: '/ocean/v6/pubsongs/list_info_for_ip',
+ encryptType: 'android',
+ method: 'POST',
+ params: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/ip_zone.js b/third_party/MoeKoeMusic/api/module/ip_zone.js
new file mode 100644
index 0000000..430261e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/ip_zone.js
@@ -0,0 +1,31 @@
+// ip 专区
+module.exports = (params, useAxios) => {
+ return new Promise((resolve, reject) => {
+ useAxios({
+ url: `/v1/zone/index`,
+ encryptType: 'android',
+ method: 'GET',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'yuekucategory.kugou.com' },
+ })
+ .then((resp) => {
+ if (resp.body.status == 1) {
+ if (resp.body.data?.list) {
+ const list = Array.isArray(resp.body.data.list) ? resp.body.data?.list : [];
+ for (let index = 0; index < list.length; index += 1) {
+ if (list[index].special_link) {
+ const urls = new URLSearchParams(list[index].special_link);
+ if (urls.has('path')) {
+ const pathUrls = new URLSearchParams(urls.get('path') || '');
+ list[index]['ip_id'] = Number(pathUrls.get('ip_id') || '');
+ }
+ }
+ }
+ resp.body.data.list = list;
+ }
+ }
+ resolve(resp);
+ })
+ .catch((e) => reject(e));
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/ip_zone_home.js b/third_party/MoeKoeMusic/api/module/ip_zone_home.js
new file mode 100644
index 0000000..a289ef3
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/ip_zone_home.js
@@ -0,0 +1,16 @@
+// 获取今日推荐信息,有可能为空
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ id: params?.id,
+ share: 0,
+ };
+
+ return useAxios({
+ url: '/v1/zone/home',
+ encryptType: 'android',
+ method: 'GET',
+ params: dataMap,
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'yuekucategory.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/kmr_audio_mv.js b/third_party/MoeKoeMusic/api/module/kmr_audio_mv.js
new file mode 100644
index 0000000..20dc37c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/kmr_audio_mv.js
@@ -0,0 +1,20 @@
+const { appid, clientver } = require('../util');
+// 根据 album_audio_id/MixSongID 获取歌曲 相对应的 mv
+module.exports = (params, useAxios) => {
+ const resource = (params?.album_audio_id || '').split(',').map((s) => ({ album_audio_id: s }));
+
+ const paramsMap = {
+ data: resource,
+ fields: params.fields || '',
+ };
+
+
+ return useAxios({
+ url: '/kmr/v1/audio/mv',
+ method: 'POST',
+ data: paramsMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: {'x-router': 'openapi.kugou.com', 'KG-TID': 38},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/krm_audio.js b/third_party/MoeKoeMusic/api/module/krm_audio.js
new file mode 100644
index 0000000..f036265
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/krm_audio.js
@@ -0,0 +1,18 @@
+// 根据 album_audio_id/MixSongID 获取歌曲 相对应的 歌手/专辑/歌曲信息
+module.exports = (params, useAxios) => {
+ const resource = (params?.album_audio_id || '').split(',').map((s) => ({ entity_id: Number(s) }));
+
+ const paramsMap = {
+ data: resource,
+ fields: params.fields || 'base',
+ };
+
+ return useAxios({
+ url: '/kmr/v2/audio',
+ method: 'POST',
+ data: paramsMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: {'x-router': 'openapi.kugou.com', 'KG-TID': 238},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/lastest_songs_listen.js b/third_party/MoeKoeMusic/api/module/lastest_songs_listen.js
new file mode 100644
index 0000000..548ef94
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/lastest_songs_listen.js
@@ -0,0 +1,21 @@
+// 获取继续播放信息
+module.exports = (params, useAxios) => {
+ const token = params?.token || params.cookie?.token || '';
+ const userid = Number(params?.userid || params?.cookie?.userid || 0);
+ const paramsMap = {
+ area_code: '1',
+ sources: ['pc', 'mobile', 'tv', 'car'],
+ userid,
+ ret_info: 1,
+ token,
+ pagesize: Number(params.pagesize || 30)
+ }
+
+ return useAxios({
+ url: '/playque/devque/v1/get_latest_songs',
+ encryptType: 'android',
+ method: 'POST',
+ data: paramsMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/login.js b/third_party/MoeKoeMusic/api/module/login.js
new file mode 100644
index 0000000..bf39f9b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/login.js
@@ -0,0 +1,51 @@
+const { cryptoAesDecrypt, cryptoAesEncrypt, cryptoRSAEncrypt } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const dateNow = Date.now();
+ const encrypt = cryptoAesEncrypt({ pwd: params?.password || '', code: params?.code || '', clienttime_ms: dateNow });
+ const dataMap = {
+ plat: 1,
+ support_multi: 1,
+ clienttime_ms: dateNow,
+ t1: '562a6f12a6e803453647d16a08f5f0c2ff7eee692cba2ab74cc4c8ab47fc467561a7c6b586ce7dc46a63613b246737c03a1dc8f8d162d8ce1d2c71893d19f1d4b797685a4c6d3d81341cbde65e488c4829a9b4d42ef2df470eb102979fa5adcdd9b4eecfea8b909ff7599abeb49867640f10c3c70fc444effca9d15db44a9a6c907731e2bb0f22cd9b3536380169995693e5f0e2424e3378097d3813186e3fe96bbe7023808a0981b4e2b6135a76faac',
+ t2: '31c4daf4cf480169ccea1cb7d4a209295865a9d2b788510301694db229b87807469ea0d41b4d4b9173c2151da7294aeebfc9738df154bbdf11a4e117bb5dff6a3af8ce5ce333e681c1f29a44038f27567d58992eb81283e080778ac77db1400fdf49b7cf7e26be2e5af4da7830cc3be4',
+ t3: 'MCwwLDAsMCwwLDAsMCwwLDA=',
+ username: params?.username,
+ params: encrypt.str,
+ pk: cryptoRSAEncrypt({ 'clienttime_ms': dateNow, key: encrypt.key }).toUpperCase(),
+ };
+
+ return new Promise((resolve, reject) => {
+ useAxios({
+ url: '/v9/login_by_pwd',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'login.user.kugou.com' },
+ })
+ .then((res) => {
+ const { body } = res;
+ if (body?.status && body?.status === 1) {
+ if (body?.data?.secu_params) {
+ const getToken = cryptoAesDecrypt(body.data.secu_params, encrypt.key);
+ if (typeof getToken === 'object') {
+ res.body.data = { ...body.data, ...getToken };
+ Object.keys(getToken).forEach((key) => res.cookie.push(`${key}=${getToken[key]}`));
+ } else {
+ res.body.data['token'] = getToken;
+ res.cookie.push(`token=${getToken}`);
+ }
+ res.cookie.push(`userid=${res.body.data?.userid || 0}`);
+ res.cookie.push(`vip_type=${res.body.data?.vip_type || 0}`);
+ res.cookie.push(`vip_token=${res.body.data?.vip_token || ''}`);
+
+ resolve(res);
+ return;
+ }
+ }
+ resolve(res);
+ })
+ .catch((e) => reject(e));
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/login_cellphone.js b/third_party/MoeKoeMusic/api/module/login_cellphone.js
new file mode 100644
index 0000000..25c1f6d
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/login_cellphone.js
@@ -0,0 +1,76 @@
+// 手机登录
+const { cryptoAesDecrypt, cryptoAesEncrypt, cryptoRSAEncrypt, signParamsKey, isLite, randomString } = require('../util');
+
+let liteT2Key = 'fd14b35e3f81af3817a20ae7adae7020';
+let liteT2Iv = '17a20ae7adae7020';
+let liteT1Key = '5e4ef500e9597fe004bd09a46d8add98';
+let liteT1Iv = '04bd09a46d8add98';
+
+module.exports = (params, useAxios) => {
+ const dateTime = Date.now();
+ const encrypt = cryptoAesEncrypt({ mobile: params?.mobile || '', code: params?.code || '' });
+ const mobile = params?.mobile && `${params.mobile.toString().substring(0, 2)}*****${params.mobile.toString().substring(10, 11)}`;
+ const dfid = params?.cookie?.dfid ?? randomString(24);
+ const t2 = cryptoAesEncrypt(
+ `${params.cookie?.KUGOU_API_GUID}|0f607264fc6318a92b9e13c65db7cd3c|${params.cookie?.KUGOU_API_MAC}|${params.cookie?.KUGOU_API_DEV}|${dateTime}`,
+ { key: liteT2Key, iv: liteT2Iv }
+ );
+ const t1 = cryptoAesEncrypt(`|${dateTime}`, { key: liteT1Key, iv: liteT1Iv });
+ let dataMap = {
+ plat: 1,
+ support_multi: 1,
+ t1: isLite ? t1 : 0,
+ t2: isLite ? t2 : 0,
+ clienttime_ms: dateTime,
+ mobile,
+ key: signParamsKey(dateTime),
+ pk: cryptoRSAEncrypt({ 'clienttime_ms': dateTime, key: encrypt.key }).toUpperCase(),
+ params: encrypt.str,
+ };
+
+ if (params?.userid) dataMap['userid'] = params.userid;
+
+ if (isLite) {
+ dataMap['dfid'] = dfid;
+ dataMap['dev'] = params.cookie?.KUGOU_API_DEV;
+ dataMap['gitversion'] = '5f0b7c4';
+ } else {
+ dataMap['t3'] = 'MCwwLDAsMCwwLDAsMCwwLDA=';
+ }
+
+ return new Promise((resolve, reject) => {
+ useAxios({
+ baseURL: 'https://loginserviceretry.kugou.com',
+ url: `/v7/login_by_verifycode`,
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ headers: {
+ 'support-calm': '1',
+ 'User-Agent': 'Android16-1070-11440-130-0-LOGIN-wifi',
+ },
+ cookie: params?.cookie || {},
+ })
+ .then((res) => {
+ const { body } = res;
+ if (body?.status && body?.status === 1) {
+ if (body?.data?.secu_params) {
+ const getToken = cryptoAesDecrypt(body.data.secu_params, encrypt.key);
+ if (typeof getToken === 'object') {
+ res.body.data = { ...body.data, ...getToken };
+ Object.keys(getToken).forEach((key) => res.cookie.push(`${key}=${getToken[key]}`));
+ } else {
+ res.body.data['token'] = getToken;
+ }
+ }
+ res.cookie.push(`t1=${res.body.data['t1']}`);
+ res.cookie.push(`token=${res.body.data['token']}`);
+ res.cookie.push(`userid=${res.body.data?.userid || 0}`);
+ res.cookie.push(`vip_type=${res.body.data?.vip_type || 0}`);
+ res.cookie.push(`vip_token=${res.body.data?.vip_token || ''}`);
+ }
+ resolve(res);
+ })
+ .catch((e) => reject(e));
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/login_device.js b/third_party/MoeKoeMusic/api/module/login_device.js
new file mode 100644
index 0000000..95984f6
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/login_device.js
@@ -0,0 +1,22 @@
+const { cryptoAesEncrypt, cryptoRSAEncrypt } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const clienttime_ms = parseInt(new Date().getTime());
+ const encrypt = cryptoAesEncrypt({ token: params.token || params.cookie?.token });
+ const dataMap = {
+ plat: 1,
+ userid: params.userid || params.cookie?.userid || 0,
+ clienttime_ms,
+ pk: cryptoRSAEncrypt({ clienttime_ms, key: encrypt.key }).toUpperCase(),
+ params: encrypt.str,
+ };
+
+ return useAxios({
+ baseURL: 'https://userinfoservice.kugou.com',
+ url: '/v2/get_dev',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/login_device_kick.js b/third_party/MoeKoeMusic/api/module/login_device_kick.js
new file mode 100644
index 0000000..f195c6d
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/login_device_kick.js
@@ -0,0 +1,34 @@
+//登出选定设备
+const { cryptoAesEncrypt, cryptoRSAEncrypt, appid, clientver, srcappid,signParamsKey } = require('../util');
+module.exports = (params, useAxios) => {
+ const clienttime_ms = parseInt(new Date().getTime());
+ const encrypt = cryptoAesEncrypt({ token: params.token || params.cookie?.token });
+ const mid = calculateMid(params.mid)
+ const dateTime = Date.now();
+ const dataMap = {
+ appid,
+ clientver,
+ clienttime: clienttime_ms,
+ mid: mid,
+ uuid,
+ dfid,
+ plat: 1,
+ userid,
+ token: encrypt,
+ t_mid :guid,
+ t : dateTime,
+ t_appid: 3116,
+ t_clientver: 10597,
+ srcappid,
+ signature: signParamsKey(dateTime),
+ };
+
+return useAxios({
+ url: '/loginservice/v1/dev_logout',
+ encryptType: 'android',
+ method: 'GET',
+ data: dataMap,
+ cookie: params?.cookie,
+ headers: { 'Host':'gateway.kugou.com'}
+ });
+};
\ No newline at end of file
diff --git a/third_party/MoeKoeMusic/api/module/login_openplat.js b/third_party/MoeKoeMusic/api/module/login_openplat.js
new file mode 100644
index 0000000..ecd892f
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/login_openplat.js
@@ -0,0 +1,84 @@
+// 开放平台登录
+const axios = require('axios');
+const { wx_appid, wx_secret, cryptoAesDecrypt, cryptoAesEncrypt, cryptoRSAEncrypt, wx_lite_appid, wx_lite_secret, isLite } = require('../util');
+const appid = isLite ? wx_lite_appid : wx_appid;
+const secret = isLite ? wx_lite_secret : wx_secret;
+
+let liteT2Key = 'fd14b35e3f81af3817a20ae7adae7020';
+let liteT2Iv = '17a20ae7adae7020';
+let liteT1Key = '5e4ef500e9597fe004bd09a46d8add98';
+let liteT1Iv = '04bd09a46d8add98';
+
+const assetsToken = (code) => {
+ return axios({
+ url: 'https://api.weixin.qq.com/sns/oauth2/access_token',
+ method: 'POST',
+ params: { secret, appid, code, grant_type: 'authorization_code' },
+ });
+};
+
+module.exports = (params, useAxios) => {
+ const answer = { status: 500, body: {}, cookie: [] };
+ return new Promise(async (resolve, reject) => {
+ try {
+ const assetsTokenResp = await assetsToken(params?.code || '');
+
+ if (assetsTokenResp.data?.access_token && assetsTokenResp.data?.openid) {
+ const dateNow = Date.now();
+ const encrypt = cryptoAesEncrypt({ access_token: assetsTokenResp.data.access_token });
+ const pk = cryptoRSAEncrypt({ 'clienttime_ms': dateNow, key: encrypt.key }).toUpperCase();
+ const t2 = cryptoAesEncrypt(
+ `${params.cookie?.KUGOU_API_GUID}|0f607264fc6318a92b9e13c65db7cd3c|${params.cookie?.KUGOU_API_MAC}|${params.cookie?.KUGOU_API_DEV}|${dateNow}`,
+ { key: liteT2Key, iv: liteT2Iv }
+ );
+ const t1 = cryptoAesEncrypt(`|${dateNow}`, { key: liteT1Key, iv: liteT1Iv });
+
+ const dataMap = {
+ dev: params.cookie?.KUGOU_API_DEV,
+ force_login: 1,
+ partnerid: 36,
+ clienttime_ms: dateNow,
+ t1: isLite ? t1 : 0,
+ t2: isLite ? t2 : 0,
+ t3: 'MCwwLDAsMCwwLDAsMCwwLDA=',
+ openid: assetsTokenResp.data.openid,
+ params: encrypt.str,
+ pk,
+ };
+
+ const response = await useAxios({
+ url: `/v6/login_by_openplat`,
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie,
+ encryptType: 'android',
+ headers: { 'x-router': 'login.user.kugou.com' },
+ });
+
+ if (response.body?.status === 1) {
+ const getToken = cryptoAesDecrypt(response.body.data?.secu_params, encrypt.key);
+ if (typeof getToken === 'object') {
+ response.body.data = { ...response.body.data, ...getToken };
+ Object.keys(getToken).forEach((key) => response.cookie.push(`${key}=${getToken[key]}`));
+ } else {
+ response.body.data['token'] = getToken;
+ response.cookie.push(`token=${getToken}`);
+ }
+ response.cookie.push(`t1=${response.body.data?.t1 ?? ''}`);
+ response.cookie.push(`userid=${response.body.data?.userid || 0}`);
+ response.cookie.push(`vip_type=${response.body.data?.vip_type || 0}`);
+ response.cookie.push(`vip_token=${response.body.data?.vip_token || ''}`);
+ }
+ resolve(response);
+ } else {
+ answer.status = 502;
+ answer.body = { status: 0, msg: assetsTokenResp.data };
+ reject(answer);
+ }
+ } catch (error) {
+ answer.status = 502;
+ answer.body = { status: 0, msg: error };
+ reject(answer);
+ }
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/login_qr_check.js b/third_party/MoeKoeMusic/api/module/login_qr_check.js
new file mode 100644
index 0000000..591b3f5
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/login_qr_check.js
@@ -0,0 +1,22 @@
+const { srcappid, appid } = require('../util');
+
+// 酷狗二维码状态检测
+// 0 为二维码过期,1 为等待扫码,2 为待确认,4 为授权登录成功(4 状态码下会返回 token)
+module.exports = (params, useAxios) => {
+ return new Promise((resolve, reject) => {
+ useAxios({
+ baseURL: 'https://login-user.kugou.com',
+ url: '/v2/get_userinfo_qrcode',
+ method: 'GET',
+ params: { plat: 4, appid, srcappid, qrcode: params?.key },
+ encryptType: 'web',
+ cookie: params?.cookie || {},
+ }).then(resp => {
+ if (resp.body?.data?.status == 4) {
+ resp.cookie.push(`token=${resp.body?.data?.token}`);
+ resp.cookie.push(`userid=${resp.body?.data?.userid}`);
+ }
+ resolve(resp);
+ }).catch(e => reject(e));
+ });
+};
\ No newline at end of file
diff --git a/third_party/MoeKoeMusic/api/module/login_qr_create.js b/third_party/MoeKoeMusic/api/module/login_qr_create.js
new file mode 100644
index 0000000..af78fea
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/login_qr_create.js
@@ -0,0 +1,19 @@
+const qrcode = require('qrcode');
+// 酷狗二维码生成
+
+module.exports = (params, useAxios) => {
+ return new Promise(async (resolve) => {
+ const url = `https://h5.kugou.com/apps/loginQRCode/html/index.html?qrcode=${params.key}`
+ return resolve({
+ code: 200,
+ status: 200,
+ body: {
+ code: 200,
+ data: {
+ url: url,
+ base64: params?.qrimg ? await qrcode.toDataURL(url) : '',
+ },
+ },
+ })
+ })
+}
\ No newline at end of file
diff --git a/third_party/MoeKoeMusic/api/module/login_qr_key.js b/third_party/MoeKoeMusic/api/module/login_qr_key.js
new file mode 100644
index 0000000..c2e46e7
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/login_qr_key.js
@@ -0,0 +1,19 @@
+const { srcappid, appid } = require('../util');
+
+// 二维码 key 生成接口
+module.exports = (params, useAxios) => {
+ return useAxios({
+ baseURL: 'https://login-user.kugou.com',
+ url: '/v2/qrcode',
+ method: 'GET',
+ params: {
+ appid: params?.type === 'web' ? 1014 : 1001,
+ type: 1,
+ plat: 4,
+ qrcode_txt: `https://h5.kugou.com/apps/loginQRCode/html/index.html?appid=${appid}&`,
+ srcappid,
+ },
+ encryptType: 'web',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/login_token.js b/third_party/MoeKoeMusic/api/module/login_token.js
new file mode 100644
index 0000000..de593aa
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/login_token.js
@@ -0,0 +1,79 @@
+// 刷新登录
+const { cryptoAesDecrypt, cryptoAesEncrypt, cryptoRSAEncrypt, isLite } = require('../util');
+
+const key = '90b8382a1bb4ccdcf063102053fd75b8';
+const iv = 'f063102053fd75b8';
+
+const liteKey = 'c24f74ca2820225badc01946dba4fdf7';
+const liteIv = 'adc01946dba4fdf7';
+
+let liteT2Key = 'fd14b35e3f81af3817a20ae7adae7020';
+let liteT2Iv = '17a20ae7adae7020';
+let liteT1Key = '5e4ef500e9597fe004bd09a46d8add98';
+let liteT1Iv = '04bd09a46d8add98';
+
+module.exports = (params, useAxios) => {
+ const dateNow = Date.now();
+ const token = params?.token || params?.cookie?.token || '';
+ const userid = params?.userid || params?.cookie?.userid || '0';
+ const encrypt = cryptoAesEncrypt({ clienttime: Math.floor(dateNow / 1000), token }, { key: isLite ? liteKey : key, iv: isLite ? liteIv : iv });
+ const encryptParams = cryptoAesEncrypt({});
+ const pk = cryptoRSAEncrypt({ clienttime_ms: dateNow, key: encryptParams.key });
+
+ const t2 = cryptoAesEncrypt(
+ `${params.cookie?.KUGOU_API_GUID}|0f607264fc6318a92b9e13c65db7cd3c|${params.cookie?.KUGOU_API_MAC}|${params.cookie?.KUGOU_API_DEV}|${dateNow}`,
+ { key: liteT2Key, iv: liteT2Iv }
+ );
+ const t1 = cryptoAesEncrypt(params.cookie?.t1 ? `${params.cookie?.t1}|${dateNow}` : `|${dateNow}`, { key: liteT1Key, iv: liteT1Iv });
+
+ const dataMap = {
+ dfid: params?.cookie?.dfid || '-',
+ p3: encrypt,
+ plat: 1,
+ t1: isLite ? t1 : 0,
+ t2: isLite ? t2 : 0,
+ t3: 'MCwwLDAsMCwwLDAsMCwwLDA=',
+ pk,
+ params: encryptParams.str,
+ userid,
+ clienttime_ms: dateNow,
+ };
+
+ if (isLite) {
+ dataMap['dev'] = params.cookie?.KUGOU_API_DEV;
+ }
+
+
+ return new Promise((resolve, reject) => {
+ useAxios({
+ baseURL: 'http://login.user.kugou.com',
+ url: `/v5/login_by_token`,
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie,
+ encryptType: 'android',
+ })
+ .then((res) => {
+ const { body } = res;
+ if (body?.status && body?.status === 1) {
+ if (body?.data?.secu_params) {
+ const getToken = cryptoAesDecrypt(body.data.secu_params, encryptParams.key);
+ if (typeof getToken === 'object') {
+ res.body.data = { ...body.data, ...getToken };
+ Object.keys(getToken).forEach((key) => res.cookie.push(`${key}=${getToken[key]}`));
+ } else {
+ res.body.data['token'] = getToken;
+ }
+ }
+
+ res.cookie.push(`t1=${res.body.data['t1']}`);
+ res.cookie.push(`token=${res.body.data['token']}`);
+ res.cookie.push(`userid=${res.body.data?.userid || 0}`);
+ res.cookie.push(`vip_type=${res.body.data?.vip_type || 0}`);
+ res.cookie.push(`vip_token=${res.body.data?.vip_token || ''}`);
+ }
+ resolve(res);
+ })
+ .catch((e) => reject(e));
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/login_wx_check.js b/third_party/MoeKoeMusic/api/module/login_wx_check.js
new file mode 100644
index 0000000..166770c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/login_wx_check.js
@@ -0,0 +1,20 @@
+
+const axios = require('axios');
+
+module.exports = (params, useAxios) => {
+ const answer = { status: 500, body: {}, cookie: [] };
+ return new Promise(async (resolve, reject) => {
+ try {
+ const resp = await axios({ url: `https://long.open.weixin.qq.com/connect/l/qrconnect?f=json&uuid=${params?.uuid || ''}` });
+
+ answer.status = 200;
+ answer.body = resp.data;
+
+ resolve(answer);
+ } catch (err) {
+ answer.status = 502;
+ answer.body = { status: 0, msg: err };
+ reject(answer);
+ }
+ });
+}
diff --git a/third_party/MoeKoeMusic/api/module/login_wx_create.js b/third_party/MoeKoeMusic/api/module/login_wx_create.js
new file mode 100644
index 0000000..f108d88
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/login_wx_create.js
@@ -0,0 +1,61 @@
+const axios = require('axios');
+const { wx_appid, wx_secret, cryptoMd5, cryptoSha1, randomString, wx_lite_appid, wx_lite_secret, isLite } = require('../util');
+
+const appid = isLite ? wx_lite_appid : wx_appid;
+const secret = isLite ? wx_lite_secret : wx_secret;
+
+const accessToken = () => {
+ return axios({ url: 'https://api.weixin.qq.com/cgi-bin/token', params: { appid, secret, grant_type: 'client_credential' } });
+};
+
+/**
+ *
+ * @param {string} accessToken
+ * @returns
+ */
+const ticket = (accessToken) => axios({ url: 'https://api.weixin.qq.com/cgi-bin/ticket/getticket', params: { access_token: accessToken, type: 2 } });
+
+module.exports = (params, useAxios) => {
+ const answer = { status: 500, body: {}, cookie: [] };
+ return new Promise(async (resolve, reject) => {
+ try {
+ const accessTokenResp = await accessToken();
+ if (accessTokenResp.data?.access_token) {
+ const ticketResp = await ticket(accessTokenResp.data.access_token);
+
+ if (ticketResp.data?.errcode === 0) {
+ const ticket = ticketResp.data.ticket;
+ const timestamp = Date.now();
+ const noncestr = cryptoMd5(randomString());
+ const signaturePrams = `appid=${appid}&noncestr=${noncestr}&sdk_ticket=${ticket}×tamp=${timestamp}`;
+ const signature = cryptoSha1(signaturePrams);
+ const params = { appid: appid, noncestr, timestamp, scope: 'snsapi_userinfo', signature };
+ const connect = await axios({ url: 'https://open.weixin.qq.com/connect/sdk/qrconnect', params });
+
+ if (connect.data?.errcode === 0) {
+ answer.status = 200;
+ connect.data.qrcode['qrcodeurl'] = `https://open.weixin.qq.com/connect/confirm?uuid=${connect.data.uuid}`;
+ answer.body = connect.data;
+ resolve(answer);
+ } else {
+ answer.status = 502;
+ answer.body = { status: 0, msg: connect.data };
+ reject(answer);
+ }
+ } else {
+ answer.status = 502;
+ answer.body = { status: 0, msg: ticketResp.data };
+ reject(answer);
+ }
+ } else {
+ answer.status = 502;
+ answer.body = { status: 0, msg: accessTokenResp.data };
+ reject(answer);
+ }
+ } catch (e) {
+ answer.status = 502;
+ answer.body = { status: 0, msg: e };
+ reject(answer);
+ }
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/longaudio_album_audios.js b/third_party/MoeKoeMusic/api/module/longaudio_album_audios.js
new file mode 100644
index 0000000..3073b0b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/longaudio_album_audios.js
@@ -0,0 +1,16 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: `/longaudio/v2/album_audios`,
+ method: 'post',
+ encryptType: 'android',
+ data: {
+ album_id: params.album_id,
+ area_code: 1,
+ tagid: 0,
+ page: params.page || 1,
+ pagesize: params.pagesize || 30,
+ },
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'openapi.kugou.com', 'KG-TID': '78' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/longaudio_album_detail.js b/third_party/MoeKoeMusic/api/module/longaudio_album_detail.js
new file mode 100644
index 0000000..4e10c69
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/longaudio_album_detail.js
@@ -0,0 +1,15 @@
+module.exports = (params, useAxios) => {
+ const data = (params.album_id || '').split(',').map((s) => ({ album_id: s }));
+ return useAxios({
+ url: `/openapi/v2/broadcast`,
+ method: 'post',
+ encryptType: 'android',
+ data: {
+ data,
+ show_album_tag: 1,
+ fields: "album_name,album_id,category,authors,sizable_cover,intro,author_name,trans_param,album_tag,mix_intro,full_intro,is_publish"
+ },
+ cookie: params?.cookie || {},
+ headers: {'KG-TID': '78'}
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/longaudio_daily_recommend.js b/third_party/MoeKoeMusic/api/module/longaudio_daily_recommend.js
new file mode 100644
index 0000000..1e7497e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/longaudio_daily_recommend.js
@@ -0,0 +1,9 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: `/longaudio/v1/home_new/daily_recommend`,
+ method: 'post',
+ encryptType: 'android',
+ params: { module_id: 1, size: params.pagesize || 30, page: params.page || 1 },
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/longaudio_rank_recommend.js b/third_party/MoeKoeMusic/api/module/longaudio_rank_recommend.js
new file mode 100644
index 0000000..bb738ff
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/longaudio_rank_recommend.js
@@ -0,0 +1,9 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: `/longaudio/v1/home_new/rank_card_recommend`,
+ method: 'get',
+ encryptType: 'android',
+ params: {platform: 'ios'},
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/longaudio_vip_recommend.js b/third_party/MoeKoeMusic/api/module/longaudio_vip_recommend.js
new file mode 100644
index 0000000..de5fe91
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/longaudio_vip_recommend.js
@@ -0,0 +1,10 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: `/longaudio/v1/home_new/vip_select_recommend`,
+ method: 'post',
+ encryptType: 'android',
+ data: {album_playlist: []},
+ params: {position: '2', clientver: 12329},
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/longaudio_week_recommend.js b/third_party/MoeKoeMusic/api/module/longaudio_week_recommend.js
new file mode 100644
index 0000000..2a610f8
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/longaudio_week_recommend.js
@@ -0,0 +1,10 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: `/longaudio/v1/home_new/week_new_albums_recommend`,
+ method: 'post',
+ encryptType: 'android',
+ data: {album_playlist: []},
+ params: {clientver: 12329},
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/lyric.js b/third_party/MoeKoeMusic/api/module/lyric.js
new file mode 100644
index 0000000..b7d247f
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/lyric.js
@@ -0,0 +1,35 @@
+// 歌词获取
+const { decodeLyrics } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ ver: 1,
+ client: params?.client || 'android',
+ id: params?.id,
+ accesskey: params?.accesskey,
+ fmt: params.fmt || 'krc',
+ charset: 'utf8',
+ };
+
+ return new Promise((resolve, reject) => {
+ useAxios({
+ baseURL: 'https://lyrics.kugou.com',
+ url: '/download',
+ method: 'GET',
+ params: dataMap,
+ cookie: params?.cookie || {},
+ encryptType: 'android',
+ })
+ .then((res) => {
+ if (params?.decode) {
+ if (res.body?.content) {
+ res.body['decodeContent'] = params?.fmt == 'lrc' || Number(res.body?.contenttype) !== 0 ? Buffer.from(res.body?.content, 'base64').toString() : decodeLyrics(res.body.content);
+ resolve(res);
+ return;
+ }
+ }
+ resolve(res);
+ })
+ .catch((e) => reject(e));
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/pc_diantai.js b/third_party/MoeKoeMusic/api/module/pc_diantai.js
new file mode 100644
index 0000000..d2cd1c4
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/pc_diantai.js
@@ -0,0 +1,17 @@
+// 电台 banner
+module.exports = (params, useAxios) => {
+ const userid = params?.cookie?.userid || params?.userid || 0;
+ const dataMap = {
+ isvip: 0,
+ userid,
+ vipType: 0,
+ };
+ return useAxios({
+ baseURL: 'https://adservice.kugou.com',
+ url: '/v3/pc_diantai',
+ data: dataMap,
+ method: 'post',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/personal_fm.js b/third_party/MoeKoeMusic/api/module/personal_fm.js
new file mode 100644
index 0000000..d6dcb59
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/personal_fm.js
@@ -0,0 +1,46 @@
+const { signParamsKey, appid, clientver } = require('../util');
+module.exports = (params, useAxios) => {
+ const userid = params?.cookie?.userid || params?.userid || 0;
+ const token = params?.cookie?.token || params?.token || 0;
+ const vip_type = params?.cookie?.vip_type || params?.vipType || 0;
+ const dateTime = Date.now();
+
+ const dataMap = {
+ appid,
+ clienttime: dateTime,
+ mid: params?.cookie?.KUGOU_API_MID,
+ action: params?.action || 'play',
+ recommend_source_locked: 0,
+ song_pool_id: Number(params?.song_pool_id || 0),
+ callerid: 0,
+ m_type: 1,
+ platform: params?.platform || 'ios',
+ area_code: 1,
+ remain_songcnt: Number(params?.remain_songcnt || 0),
+ clientver,
+ is_overplay: params?.is_overplay ? 1 : 0,
+ mode: params?.mode || 'normal',
+ fakem: 'ca981cfc583a4c37f28d2d49000013c16a0a',
+ key: signParamsKey(dateTime),
+ };
+
+ if (userid) {
+ dataMap['userid'] = userid;
+ dataMap['kguid'] = userid;
+ }
+ if (token) dataMap['token'] = token;
+ if (vip_type) dataMap['vip_type'] = vip_type;
+
+ if (params?.hash) dataMap['hash'] = params.hash;
+ if (params?.songid) dataMap['songid'] = params.songid;
+ if (params?.playtime) dataMap['playtime'] = params.playtime;
+
+ return useAxios({
+ url: '/v2/personal_recommend',
+ data: dataMap,
+ method: 'post',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'persnfm.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/playhistory_upload.js b/third_party/MoeKoeMusic/api/module/playhistory_upload.js
new file mode 100644
index 0000000..ac9086e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/playhistory_upload.js
@@ -0,0 +1,17 @@
+// 提交听歌历史
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params?.cookie?.token || '';
+
+ const songs = [{ mxid: Number(params.mxid), op: 1, ot: Number(params.time || Math.floor(Date.now() / 1000)), pc: Number(params.pc || 1) }];
+ const dataMap = { songs, token, userid };
+
+ return useAxios({
+ url: '/playhistory/v1/upload_songs',
+ data: dataMap,
+ encryptType: 'android',
+ method: 'POST',
+ params: { plat: 3 },
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/playlist_add.js b/third_party/MoeKoeMusic/api/module/playlist_add.js
new file mode 100644
index 0000000..59acccb
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/playlist_add.js
@@ -0,0 +1,33 @@
+// 收藏歌单
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params.cookie?.token || '';
+ const clienttime = Math.floor(Date.now() / 1000);
+
+ const dataMap = {
+ userid,
+ token,
+ total_ver: 0,
+ name: params.name,
+ type: params.type || 0,
+ source: params.source === 0 ? 0 : params.source || 1,
+ is_pri: 0,
+ list_create_userid: params.list_create_userid,
+ list_create_listid: params.list_create_listid,
+ list_create_gid: params.list_create_gid || '',
+ from_shupinmv: 0,
+ };
+
+ if (params.type === 0) {
+ dataMap['is_pri'] = params.is_pri || 0;
+ }
+
+ return useAxios({
+ url: '/cloudlist.service/v5/add_list',
+ data: dataMap,
+ params: params.type === 0 ? { last_time: clienttime, last_area: 'gztx', userid, token } : {},
+ method: 'post',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/playlist_del.js b/third_party/MoeKoeMusic/api/module/playlist_del.js
new file mode 100644
index 0000000..5c24623
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/playlist_del.js
@@ -0,0 +1,52 @@
+const { playlistAesEncrypt, playlistAesDecrypt, rsaEncrypt2, signParamsKey, clientver, appid } = require('../util');
+
+// 取消收藏歌单
+module.exports = (params, useAxios) => {
+ const answer = { status: 500, body: {}, cookie: [] };
+ return new Promise(async (resolve) => {
+ try {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params.cookie?.token || '';
+ const clienttime = Math.floor(Date.now() / 1000);
+
+ const dataMap = {
+ listid: Number(params.listid),
+ total_ver: 0,
+ type: 1,
+ };
+
+ const aesEncrypt = playlistAesEncrypt(dataMap);
+
+ const p = rsaEncrypt2({ aes: aesEncrypt.key, uid: userid, token }).toUpperCase();
+
+ const paramsMap = {
+ clienttime,
+ key: signParamsKey(clienttime.toString()),
+ last_area: 'gztx',
+ clientver,
+ appid,
+ last_time: clienttime,
+ p,
+ };
+
+ const respone = await useAxios({
+ url: '/v2/delete_list',
+ params: paramsMap,
+ data: aesEncrypt.str,
+ method: 'post',
+ encryptType: 'android',
+ headers: { 'x-router': 'cloudlist.service.kugou.com' },
+ responseType: 'arraybuffer',
+ cookie: params?.cookie || {},
+ });
+
+ respone.body = playlistAesDecrypt({ str: respone.body.toString('base64'), key: aesEncrypt.key });
+
+ resolve(respone);
+ } catch (error) {
+ console.log(error);
+ answer.body = error;
+ resolve(answer);
+ }
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/playlist_detail.js b/third_party/MoeKoeMusic/api/module/playlist_detail.js
new file mode 100644
index 0000000..6b27f6b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/playlist_detail.js
@@ -0,0 +1,22 @@
+// 获取歌单详情
+
+module.exports = (params, useAxios) => {
+ const data = (params?.ids || '').split(',').map(s => ({'global_collection_id': s }));
+
+ const dataMap = {
+ data,
+ userid: params?.userid || params?.cookie?.userid || 0,
+ token: params?.token || params?.cookie?.token || ''
+ };
+
+ return useAxios({
+ url: '/v3/get_list_info',
+ method: 'POST',
+ encryptType: 'android',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ headers: {'x-router': 'pubsongs.kugou.com'}
+ })
+
+
+}
\ No newline at end of file
diff --git a/third_party/MoeKoeMusic/api/module/playlist_effect.js b/third_party/MoeKoeMusic/api/module/playlist_effect.js
new file mode 100644
index 0000000..b79a45d
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/playlist_effect.js
@@ -0,0 +1,16 @@
+// 获取音效歌单
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ page: params?.page || 1,
+ pagesize: params?.pagesize || 30,
+ };
+
+ return useAxios({
+ url: '/pubsongs/v1/get_sound_effect_list',
+ method: 'POST',
+ encryptType: 'android',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/playlist_similar.js b/third_party/MoeKoeMusic/api/module/playlist_similar.js
new file mode 100644
index 0000000..8a5326b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/playlist_similar.js
@@ -0,0 +1,26 @@
+const { appid, clientver, signParamsKey } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const data = (params?.ids || '').split(',').map((s) => ({ 'global_collection_id': s }));
+ const clienttime = Date.now();
+
+ const dataMap = {
+ appid,
+ clientver,
+ clienttime,
+ key: signParamsKey(clienttime),
+ userid: params?.userid || params?.cookie?.userid || 0,
+ ugc: 1,
+ show_list: 1,
+ need_songs: 1,
+ data,
+ };
+
+ return useAxios({
+ url: '/pubsongs/v1/kmr_get_similar_lists',
+ method: 'POST',
+ encryptType: 'android',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/playlist_tags.js b/third_party/MoeKoeMusic/api/module/playlist_tags.js
new file mode 100644
index 0000000..192460a
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/playlist_tags.js
@@ -0,0 +1,17 @@
+// 获取歌单分类
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ tag_type: 'collection',
+ tag_id: 0,
+ source: 3,
+ };
+
+ return useAxios({
+ url: '/pubsongs/v1/get_tags_by_type',
+ method: 'POST',
+ encryptType: 'android',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/playlist_track_all.js b/third_party/MoeKoeMusic/api/module/playlist_track_all.js
new file mode 100644
index 0000000..f8f6fb9
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/playlist_track_all.js
@@ -0,0 +1,26 @@
+// 获取歌单所有歌曲
+module.exports = (params, useAxios) => {
+ const pagesize = params?.pagesize || 30;
+ const paramsMap = {
+ area_code: 1,
+ begin_idx: (Number(params.page || 1) - 1) * pagesize,
+ plat: 1,
+ type: 1,
+ // module: 'NONE',
+ mode: 1,
+ personal_switch: 1,
+ extend_fields: 'abtags,hot_cmt,popularization',
+ // page: params?.page || 1,
+ pagesize ,
+ global_collection_id: params?.id,
+ };
+
+ return useAxios({
+ url: '/pubsongs/v2/get_other_list_file_nofilt',
+ method: 'GET',
+ encryptType: 'android',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ // headers: { 'x-router': 'pubsongscdn.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/playlist_track_all_new.js b/third_party/MoeKoeMusic/api/module/playlist_track_all_new.js
new file mode 100644
index 0000000..5b57456
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/playlist_track_all_new.js
@@ -0,0 +1,26 @@
+// 获取歌单所有歌曲
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || '0';
+ const token = params?.token || params?.cookie?.token || '0';
+ const dataMap = {
+ listid: params.listid,
+ userid,
+ area_code: 1,
+ show_relate_goods: 0,
+ pagesize: params.pagesize || 30,
+ allplatform: 1,
+ show_cover: 1,
+ type: 0,
+ token,
+ page: params.page || 1,
+ };
+
+ return useAxios({
+ url: '/v4/get_list_all_file',
+ method: 'post',
+ encryptType: 'android',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'cloudlist.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/playlist_tracks_add.js b/third_party/MoeKoeMusic/api/module/playlist_tracks_add.js
new file mode 100644
index 0000000..0fdfa1b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/playlist_tracks_add.js
@@ -0,0 +1,42 @@
+// 对歌单添加歌曲
+// listid, 歌单listid
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params.cookie?.token || '';
+ const clienttime = Math.floor(Date.now() / 1000);
+
+ const resource = (params.data || '').split(',').map((s) => {
+ const data = s.split('|');
+ return {
+ number: 1,
+ name: data[0] || '',
+ hash: data[1] || '',
+ size: 0,
+ sort: 0,
+ timelen: 0,
+ bitrate: 0,
+ album_id: Number(data[2] || 0),
+ mixsongid: Number(data[3] || 0),
+ };
+ });
+
+ const dataMap = {
+ userid,
+ token,
+ listid: params.listid,
+ list_ver: 0,
+ type: 0,
+ slow_upload: 1,
+ scene: 'false;null',
+ data: resource,
+ };
+
+ return useAxios({
+ url: '/cloudlist.service/v6/add_song',
+ data: dataMap,
+ params: { last_time: clienttime, last_area: 'gztx', userid, token },
+ method: 'post',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/playlist_tracks_del.js b/third_party/MoeKoeMusic/api/module/playlist_tracks_del.js
new file mode 100644
index 0000000..f264e8a
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/playlist_tracks_del.js
@@ -0,0 +1,26 @@
+// 对歌单删除歌曲
+
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params.cookie?.token || '';
+
+ const resource = (params.fileids || '').split(',').map((s) => ({ fileid: Number(s) }));
+
+ const dataMap = {
+ listid: params.listid,
+ userid,
+ data: resource,
+ type: 0,
+ token,
+ list_ver: 0,
+ };
+
+ return useAxios({
+ url: '/v4/delete_songs',
+ data: dataMap,
+ method: 'post',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'cloudlist.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/privilege_lite.js b/third_party/MoeKoeMusic/api/module/privilege_lite.js
new file mode 100644
index 0000000..78f288b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/privilege_lite.js
@@ -0,0 +1,28 @@
+// 获取歌曲信息
+const { appid, clientver } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const resource = (params?.hash || '').split(',').map((s) => ({ type: 'audio', page_id: 0, hash: s, album_id: 0 }));
+ (params?.album_id || '').split(',').forEach((s, l) => (resource[l]['album_id'] = s));
+
+ const dataMap = {
+ appid,
+ area_code: 1,
+ behavior: 'play',
+ clientver,
+ need_hash_offset: 1,
+ relate: 1,
+ support_verify: 1,
+ resource,
+ qualities: ['128', '320', 'flac', 'high', 'viper_atmos', 'viper_tape', 'viper_clear', 'super', 'multitrack'],
+ };
+
+ return useAxios({
+ url: '/v2/get_res_privilege/lite',
+ data: dataMap,
+ method: 'post',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'media.store.kugou.com', 'Content-Type': 'application/json' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/rank_audio.js b/third_party/MoeKoeMusic/api/module/rank_audio.js
new file mode 100644
index 0000000..4efb403
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/rank_audio.js
@@ -0,0 +1,22 @@
+// 获取排行榜音乐列表
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ show_portrait_mv: 1,
+ show_type_total: 1,
+ filter_original_remarks: 1,
+ area_code: 1,
+ pagesize: params.pagesize || 30,
+ rank_cid: params.rank_cid || 0,
+ type: 1,
+ page: params.page || 1,
+ rank_id: params.rankid,
+ };
+ return useAxios({
+ url: '/openapi/kmr/v2/rank/audio',
+ method: 'post',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'kg-tid': '369' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/rank_info.js b/third_party/MoeKoeMusic/api/module/rank_info.js
new file mode 100644
index 0000000..a5ddbfd
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/rank_info.js
@@ -0,0 +1,17 @@
+// 获取排行榜详情
+module.exports = (params, useAxios) => {
+ const parmasMap = {
+ rank_cid: params.rank_cid || 0,
+ rankid: params.rankid,
+ with_album_img: params.album_img || 1,
+ zone: params.zone || '',
+ };
+
+ return useAxios({
+ url: '/ocean/v6/rank/info',
+ method: 'get',
+ encryptType: 'android',
+ params: parmasMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/rank_list.js b/third_party/MoeKoeMusic/api/module/rank_list.js
new file mode 100644
index 0000000..0eda2d8
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/rank_list.js
@@ -0,0 +1,17 @@
+// 获取排行榜列表
+
+module.exports = (params, useAxios) => {
+ const parmasMap = {
+ plat: 2,
+ withsong: params.withsong || 1,
+ parentid: 0,
+ };
+
+ return useAxios({
+ url: '/ocean/v6/rank/list',
+ method: 'get',
+ encryptType: 'android',
+ params: parmasMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/rank_top.js b/third_party/MoeKoeMusic/api/module/rank_top.js
new file mode 100644
index 0000000..b1ad889
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/rank_top.js
@@ -0,0 +1,10 @@
+// 获取排行榜推荐列表
+
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/mobileservice/api/v5/rank/rec_rank_list',
+ method: 'get',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/rank_vol.js b/third_party/MoeKoeMusic/api/module/rank_vol.js
new file mode 100644
index 0000000..c315160
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/rank_vol.js
@@ -0,0 +1,18 @@
+// 获取排行榜往期列表
+module.exports = (params, useAxios) => {
+ const parmasMap = {
+ rank_cid: params.rank_cid || 0,
+ rankid: params.rankid,
+ ranktype: 1,
+ type: 0,
+ plat: 2,
+ };
+
+ return useAxios({
+ url: '/ocean/v6/rank/vol',
+ method: 'get',
+ encryptType: 'android',
+ params: parmasMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/recommend_songs.js b/third_party/MoeKoeMusic/api/module/recommend_songs.js
new file mode 100644
index 0000000..b25d26e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/recommend_songs.js
@@ -0,0 +1,17 @@
+// 每日推荐歌曲
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ platform: params?.platform || 'android',
+ userid: params?.userid || params?.cookie?.userid || '0',
+ };
+
+ return useAxios({
+ url: '/everyday_song_recommend',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'everydayrec.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/register_dev.js b/third_party/MoeKoeMusic/api/module/register_dev.js
new file mode 100644
index 0000000..9b1f3f4
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/register_dev.js
@@ -0,0 +1,131 @@
+const { playlistAesEncrypt, playlistAesDecrypt, rsaEncrypt2, signParamsKey, clientver, appid } = require('../util');
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params.cookie?.token || '';
+ //可用内存,单位是字节
+ const availableRamSize = params?.availableRamSize || 4983533568;
+ //内部存储可用空间,单位是字节(约 48 MB)
+ const availableRomSize = params?.availableRomSize || 48114719;
+ //外部存储可用空间,单位是字节(约 48 MB)
+ const availableSDSize = params?.availableSDSize || 48114717;
+ //基带版本
+ const basebandVer = params?.basebandVer || '';
+ //电池电量百分比
+ const batteryLevel = params?.batteryLevel || 100;
+ //电池状态
+ const batteryStatus = params?.batteryStatus || 3;
+ //品牌
+ const brand = params?.brand || 'Redmi';
+ //设备序号
+ const buildSerial = params?.buildSerial || 'unknown';
+ //设备代号
+ const device = params?.device || 'marble';
+ //IMEI 号
+ const imei = params?.imei || params.cookie?.KUGOU_API_GUID;
+ //sim 卡号序号
+ const imsi = params?.imsi || '';
+ //厂商
+ const manufacturer = params?.manufacturer || 'Xiaomi';
+ //设备uuid
+ const uuid = params?.uuid || params.cookie?.KUGOU_API_GUID;
+ //是否有加速度传感器
+ const accelerometer = params?.accelerometer || false;
+ //加速度传感器值
+ const accelerometerValue = params?.accelerometerValue || '';
+ //是否有重力传感器
+ const gravity = params?.gravity || false;
+ //重力传感器的值
+ const gravityValue = params?.gravityValue || '';
+ //是否有陀螺仪
+ const gyroscope = params?.gyroscope || false;
+ //陀螺仪的值
+ const gyroscopeValue = params?.gyroscopeValue || '';
+ //是否有光线传感器
+ const light = params?.light || false;
+ //光线传感器的值
+ const lightValue = params?.lightValue || '';
+ //是否有磁力传感器
+ const magnetic = params?.magnetic || false;
+ //磁力传感器的值
+ const magneticValue = params?.magneticValue || '';
+ //是否有方向传感器
+ const orientation = params?.orientation || false;
+ //方向传感器的值
+ const orientationValue = params?.orientationValue || '';
+ //是否有压力传感器
+ const pressure = params?.pressure|| false;
+ //压力传感器的值
+ const pressureValue = params?.pressureValue || '';
+ //是否有步数传感器
+ const step_counter = params?.step_counter || false;
+ //步数传感器的值
+ const step_counterValue = params?.step_counterValue || '';
+ //是否有温度传感器
+ const temperature = params?.temperature || false;
+ //温度传感器的值
+ const temperatureValue = params?.temperatureValue || '';
+
+
+
+ const dataMap = {
+ 'availableRamSize': availableRamSize,
+ 'availableRomSize': availableRomSize,
+ 'availableSDSize': availableSDSize,
+ 'basebandVer': basebandVer,
+ 'batteryLevel': batteryLevel,
+ 'batteryStatus': batteryStatus,
+ 'brand': brand,
+ 'buildSerial': buildSerial,
+ 'device': device,
+ 'imei': imei,
+ 'imsi': imsi,
+ 'manufacturer': manufacturer,
+ 'uuid': uuid,
+ 'accelerometer': accelerometer,
+ 'accelerometerValue': accelerometerValue,
+ 'gravity': gravity,
+ 'gravityValue': gravityValue,
+ 'gyroscope': gyroscope,
+ 'gyroscopeValue': gyroscopeValue,
+ 'light': light,
+ 'lightValue': lightValue,
+ 'magnetic': magnetic,
+ 'magneticValue': magneticValue,
+ 'orientation': orientation,
+ 'orientationValue': orientationValue,
+ 'pressure': pressure,
+ 'pressureValue': pressureValue,
+ 'step_counter': step_counter,
+ 'step_counterValue': step_counterValue,
+ 'temperature': temperature,
+ 'temperatureValue': temperatureValue,
+ };
+
+ const aesEncrypt = playlistAesEncrypt(dataMap);
+
+ const p = rsaEncrypt2({ aes: aesEncrypt.key, uid: userid, token });
+
+ return new Promise((resolve, reject) => {
+ useAxios({
+ baseURL: 'https://userservice.kugou.com',
+ url: '/risk/v2/r_register_dev',
+ method: 'POST',
+ data: aesEncrypt.str,
+ params: { part: 1, platid: 1, p },
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ responseType: 'arraybuffer',
+ })
+ .then((res) => {
+ res.body = playlistAesDecrypt({ str: res.body.toString('base64'), key: aesEncrypt.key });
+
+ const { body } = res;
+ if (body?.status === 1 && body?.data) {
+ res.cookie.push(`dfid=${res.body.data['dfid']}`);
+ }
+
+ resolve(res);
+ })
+ .catch((e) => reject(e));
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/scene_audio_list.js b/third_party/MoeKoeMusic/api/module/scene_audio_list.js
new file mode 100644
index 0000000..9fb1882
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/scene_audio_list.js
@@ -0,0 +1,22 @@
+const { appid, clientver } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params.cookie?.token || '';
+
+ const dataMap = {
+ appid,
+ clientver,
+ token,
+ userid,
+ };
+
+ return useAxios({
+ url: '/scene/v1/scene/audio_list',
+ method: 'POST',
+ encryptType: 'android',
+ params: { scene_id: params.id, module_id: params.module_id, tag: params.tag, page: params.page || 1, page_size: params.pagesize || 30 },
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/scene_collection_list.js b/third_party/MoeKoeMusic/api/module/scene_collection_list.js
new file mode 100644
index 0000000..79b8b45
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/scene_collection_list.js
@@ -0,0 +1,25 @@
+const { appid, clientver } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params.cookie?.token || '';
+
+ const dataMap = {
+ appid,
+ clientver,
+ token,
+ userid,
+ tag_id: params.tag_id,
+ page: params.page || 1,
+ page_size: params.pagesize || 30,
+ exposed_data: [],
+ };
+
+ return useAxios({
+ url: '/scene/v1/distribution/collection_list',
+ method: 'POST',
+ encryptType: 'android',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/scene_lists.js b/third_party/MoeKoeMusic/api/module/scene_lists.js
new file mode 100644
index 0000000..3541a2a
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/scene_lists.js
@@ -0,0 +1,8 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/scene/v1/scene/list',
+ method: 'GET',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/scene_lists_v2.js b/third_party/MoeKoeMusic/api/module/scene_lists_v2.js
new file mode 100644
index 0000000..6eaba4a
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/scene_lists_v2.js
@@ -0,0 +1,20 @@
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || '0';
+
+ const sortType = { rec: 1, hot: 2, new: 3 };
+
+ return useAxios({
+ url: '/scene/v1/scene/list_v2',
+ params: {
+ scene_id: params.id,
+ page: params.page || 1,
+ pagesize: params.pagesize || 30,
+ sort_type: sortType[params.sort || 'rec'] || 1,
+ kugouid: userid,
+ },
+ data: { exposure: [] },
+ method: 'POST',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/scene_module.js b/third_party/MoeKoeMusic/api/module/scene_module.js
new file mode 100644
index 0000000..db4e213
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/scene_module.js
@@ -0,0 +1,9 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/scene/v1/scene/module',
+ params: { scene_id: params.id },
+ method: 'POST',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/scene_module_info.js b/third_party/MoeKoeMusic/api/module/scene_module_info.js
new file mode 100644
index 0000000..ad049d0
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/scene_module_info.js
@@ -0,0 +1,9 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/scene/v1/scene/module_info',
+ params: { scene_id: params.id, module_id: params.module_id },
+ method: 'GET',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/scene_music.js b/third_party/MoeKoeMusic/api/module/scene_music.js
new file mode 100644
index 0000000..84008f6
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/scene_music.js
@@ -0,0 +1,10 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/genesisapi/v1/scene_music/rec_music',
+ params: { scene_id: params.id, page: params.page || 1, pagesize: params.pagesize || 30 },
+ data: { exposure: [] },
+ method: 'POST',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/scene_video_list.js b/third_party/MoeKoeMusic/api/module/scene_video_list.js
new file mode 100644
index 0000000..59ef124
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/scene_video_list.js
@@ -0,0 +1,25 @@
+const { appid, clientver } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params.cookie?.token || '';
+
+ const dataMap = {
+ appid,
+ clientver,
+ token,
+ userid,
+ tag_id: params.tag_id,
+ page: params.page || 1,
+ page_size: params.pagesize || 30,
+ exposed_data: [],
+ };
+
+ return useAxios({
+ url: '/scene/v1/distribution/video_list',
+ method: 'POST',
+ encryptType: 'android',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/search.js b/third_party/MoeKoeMusic/api/module/search.js
new file mode 100644
index 0000000..e90fad5
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/search.js
@@ -0,0 +1,25 @@
+// 搜索
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ // token: '',
+ albumhide: 0,
+ iscorrection: 1,
+ keyword: params?.keywords || '',
+ nocollect: 0,
+ page: params?.page || 1,
+ pagesize: params?.pagesize || 30,
+ platform: 'AndroidFilter',
+ };
+
+
+ const type = ['special', 'lyric', 'song', 'album', 'author', 'mv'].includes(params.type) ? params.type : 'song';
+
+ return useAxios({
+ url: `/${type === 'song' ? 'v3' : 'v1'}/search/${type}`,
+ method: 'GET',
+ params: dataMap,
+ encryptType: 'android',
+ headers: { 'x-router': 'complexsearch.kugou.com' },
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/search_complex.js b/third_party/MoeKoeMusic/api/module/search_complex.js
new file mode 100644
index 0000000..5a02661
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/search_complex.js
@@ -0,0 +1,19 @@
+// 综合搜索
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ platform: 'AndroidFilter',
+ keyword: params.keywords,
+ page: params?.page || 1,
+ pagesize: params?.pagesize || 30,
+ cursor: 0,
+ };
+
+ return useAxios({
+ baseURL: 'https://complexsearch.kugou.com',
+ url: '/v6/search/complex',
+ method: 'GET',
+ params: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/search_default.js b/third_party/MoeKoeMusic/api/module/search_default.js
new file mode 100644
index 0000000..642b050
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/search_default.js
@@ -0,0 +1,26 @@
+const { clientver } = require('../util');
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const vip_type = params?.vip_type || params?.cookie?.vip_type || 65530;
+ const dataMap = {
+ 'plat': 0,
+ 'userid': Number(userid),
+ 'tags': '{}',
+ 'vip_type': vip_type,
+ 'm_type': 0,
+ 'own_ads': {},
+ 'ability': '3',
+ 'sources': [],
+ 'bitmap': 2,
+ 'mode': 'normal',
+ };
+
+ return useAxios({
+ url: '/searchnofocus/v1/search_no_focus_word',
+ method: 'POST',
+ data: dataMap,
+ params: { clientver: 12329 },
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/search_hot.js b/third_party/MoeKoeMusic/api/module/search_hot.js
new file mode 100644
index 0000000..2169651
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/search_hot.js
@@ -0,0 +1,16 @@
+// 热搜
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ navid: 1,
+ plat: 2,
+ };
+
+ return useAxios({
+ url: '/api/v3/search/hot_tab',
+ method: 'GET',
+ params: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: {'x-router': 'msearch.kugou.com'}
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/search_lyric.js b/third_party/MoeKoeMusic/api/module/search_lyric.js
new file mode 100644
index 0000000..06e0d45
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/search_lyric.js
@@ -0,0 +1,26 @@
+// 歌词搜索
+const { appid, clientver } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ album_audio_id: params?.album_audio_id || 0,
+ appid,
+ clientver,
+ duration: params.duration || 0,
+ hash: params?.hash || '',
+ keyword: params?.keywords || '',
+ lrctxt: 1,
+ man: params.man ?? 'no',
+ };
+
+ return useAxios({
+ baseURL: 'https://lyrics.kugou.com',
+ url: '/v1/search',
+ method: 'GET',
+ params: dataMap,
+ cookie: params?.cookie || {},
+ encryptType: 'android',
+ clearDefaultParams: true,
+ notSign: true,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/search_mixed.js b/third_party/MoeKoeMusic/api/module/search_mixed.js
new file mode 100644
index 0000000..359cc78
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/search_mixed.js
@@ -0,0 +1,38 @@
+const { cryptoMd5 } = require('../util');
+// 综合搜索
+module.exports = (params, useAxios) => {
+ const time = Date.now();
+
+ const dataMap = {
+ ab_tag: 0,
+ ability: 511,
+ albumhide: 0,
+ apiver: 22,
+ area_code: 1,
+ clientver: 20125,
+ cursor: 0,
+ is_gpay: 0,
+ iscorrection: 1,
+ keyword: params.keyword,
+ nocollect: 0,
+ osversion: 16.5,
+ platform: 'IOSFilter',
+ recver: 2,
+ req_ai: 1,
+ requestid: `${cryptoMd5(`bdaa53d04e7475feb9024164a47032f9${time}`)}_0`,
+ search_ability: 3,
+ sec_aggre: 1,
+ sec_aggre_bitmap: 0,
+ style_type: 3,
+ tag: 'em',
+ };
+
+ return useAxios({
+ url: '/v3/search/mixed',
+ method: 'GET',
+ params: dataMap,
+ encryptType: 'android',
+ headers: { 'x-router': 'complexsearch.kugou.com', 'kg-clienttimems': time.toString() },
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/search_suggest.js b/third_party/MoeKoeMusic/api/module/search_suggest.js
new file mode 100644
index 0000000..9f269c5
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/search_suggest.js
@@ -0,0 +1,17 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/v2/getSearchTip',
+ method: 'GET',
+ params: {
+ keyword: params.keywords,
+ AlbumTipCount: params.albumTipCount || 10,
+ CorrectTipCount: params.correctTipCount || 10,
+ MVTipCount: params.mvTipCount || 10,
+ MusicTipCount: params.musicTipCount || 10,
+ radiotip: 1
+ },
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'searchtip.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/server_now.js b/third_party/MoeKoeMusic/api/module/server_now.js
new file mode 100644
index 0000000..195a7b6
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/server_now.js
@@ -0,0 +1,15 @@
+// 获取服务器时间
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params?.cookie?.token || '';
+
+ return useAxios({
+ url: '/v1/server_now',
+ data: { token, userid },
+ encryptType: 'android',
+ method: 'POST',
+ params: { plat: 3 },
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'usercenter.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/sheet_collection.js b/third_party/MoeKoeMusic/api/module/sheet_collection.js
new file mode 100644
index 0000000..3185129
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/sheet_collection.js
@@ -0,0 +1,15 @@
+const { srcappid} = require('../util');
+// 乐谱详情
+module.exports = (params, useAxios) => {
+ const paramsMap = {
+ srcappid,
+ position: params.position ?? 2
+ }
+ return useAxios({
+ url: '/miniyueku/v1/opern_square/get_home_module_config',
+ encryptType: 'web',
+ method: 'GET',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/sheet_detail.js b/third_party/MoeKoeMusic/api/module/sheet_detail.js
new file mode 100644
index 0000000..f64fd90
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/sheet_detail.js
@@ -0,0 +1,13 @@
+// 乐谱详情
+module.exports = (params, useAxios) => {
+ const paramsMap = {
+ opern_id: params.id,
+ }
+ return useAxios({
+ url: '/opern/v1/detail/info',
+ encryptType: 'android',
+ method: 'GET',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/sheet_explore.js b/third_party/MoeKoeMusic/api/module/sheet_explore.js
new file mode 100644
index 0000000..054e11f
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/sheet_explore.js
@@ -0,0 +1,11 @@
+// 乐谱 推荐 3、钢琴(0:基础,1:进阶),1、吉他(1: 进阶, 2:基础,0:中级),2、尤克里里(0:基础,1:进阶),4:简谱(0:基础)
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/opern/v1/home/get_rec_opern',
+ method: 'POST',
+ encryptType: 'android',
+ data: { exposure_mixids: '' },
+ params: { pagesize: params?.pagesize || 30, page: params?.page || 1, opern_level: params.level || 0, instruments: params?.instruments || 1, tagid: params?.tagid || 0 },
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/sheet_rank.js b/third_party/MoeKoeMusic/api/module/sheet_rank.js
new file mode 100644
index 0000000..3670597
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/sheet_rank.js
@@ -0,0 +1,12 @@
+
+// 获取排行榜 // 乐谱列表 3、钢琴(0:基础,1:进阶),1、吉他(1: 进阶, 2:基础,0:中级),2、尤克里里(0:基础,1:进阶),4:简谱(0:基础)
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/opern/v1/home/get_rank_opern',
+ encryptType: 'android',
+ method: 'POST',
+ params: { pagesize: params?.pagesize || 30, page: params?.page || 1, opern_level: params?.level || 0, instruments: params?.instruments || 1, tagid: params?.tagid || 0 },
+ cookie: params?.cookie || {},
+ });
+};
+
diff --git a/third_party/MoeKoeMusic/api/module/sheet_song.js b/third_party/MoeKoeMusic/api/module/sheet_song.js
new file mode 100644
index 0000000..4bc1ba7
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/sheet_song.js
@@ -0,0 +1,15 @@
+// 乐谱列表 3、钢琴(0:基础,1:进阶),1、吉他(1: 进阶, 2:基础,0:中级),2、尤克里里(0:基础,1:进阶),4:简谱(0:基础)
+module.exports = (params, useAxios) => {
+ const paramsMap = {
+ mixsongid: params.album_audio_id,
+ instruments: params.instruments ?? 1,
+ opern_level: params?.level ?? 0
+ }
+ return useAxios({
+ url: '/opern/v1/detail/song_info',
+ encryptType: 'android',
+ method: 'GET',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/sheet_tags.js b/third_party/MoeKoeMusic/api/module/sheet_tags.js
new file mode 100644
index 0000000..095e12a
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/sheet_tags.js
@@ -0,0 +1,11 @@
+
+// 获取乐谱 tag
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/opern/v1/home/get_tags',
+ encryptType: 'android',
+ method: 'GET',
+ cookie: params?.cookie || {},
+ });
+};
+
diff --git a/third_party/MoeKoeMusic/api/module/sidedt.js b/third_party/MoeKoeMusic/api/module/sidedt.js
new file mode 100644
index 0000000..cf1a8ec
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/sidedt.js
@@ -0,0 +1,27 @@
+/**
+ * sidEdt.js
+ * ----------
+ * 完整移植自你提供的浏览器 script(未删减任何函数),
+ * 只把浏览器专属的 API 替换为 Node 等价实现。
+ *
+ * 使用方法:
+ * const { generateSidEdt } = require('./sidEdt');
+ * const { sid, edt } = await generateSidEdt({ userid, dfid, mid });
+ *
+ * // sid、edt 均为 Base64 编码,可直接放到登录请求的 URL/JSON 中
+ */
+
+'use strict';
+const { generateSimulate } = require('../util/generate_simulate');
+const { generateWebGLHash } = require('../util/util');
+const verifyUserInfo = require('./verify_user_info');
+
+module.exports = async (params, useAxios) => {
+ const webglHash = params?.cookie.KUGOU_API_WEBGL || generateWebGLHash(); // WebGL 指纹哈希
+ const userid = params?.userid || params?.cookie.userid || '0';
+ const dfid = params?.dfid || params?.cookie.dfid || '0';
+ const mid = params?.mid || params?.cookie.KUGOU_API_MID || '0';
+ const value = generateSimulate(mid, userid, dfid, webglHash);
+ const userParams = { ...params, edt: value.edt, sid: value.sid };
+ return verifyUserInfo(userParams, useAxios);
+};
diff --git a/third_party/MoeKoeMusic/api/module/singer_list.js b/third_party/MoeKoeMusic/api/module/singer_list.js
new file mode 100644
index 0000000..b99d360
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/singer_list.js
@@ -0,0 +1,16 @@
+// 获取歌手列表
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/ocean/v6/singer/list',
+ encryptType: 'android',
+ method: 'GET',
+ params: {
+ hotsize: params?.hotsize ?? 200,
+ musician: 0,
+ sextype: params?.sextype ?? 0,
+ showtype: 2,
+ type: params?.type ?? 0,
+ },
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/song_climax.js b/third_party/MoeKoeMusic/api/module/song_climax.js
new file mode 100644
index 0000000..482bfd5
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/song_climax.js
@@ -0,0 +1,14 @@
+// 获取音频高潮部分
+
+module.exports = (params, useAxios) => {
+ const data = (params?.hash || '').split(',').map((s) => ({ hash: s }));
+
+ return useAxios({
+ baseURL: 'https://expendablekmrcdn.kugou.com',
+ url: '/v1/audio_climax/audio',
+ method: 'GET',
+ params: { data: JSON.stringify(data) },
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/song_ranking.js b/third_party/MoeKoeMusic/api/module/song_ranking.js
new file mode 100644
index 0000000..6194228
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/song_ranking.js
@@ -0,0 +1,12 @@
+// 歌曲成绩单
+
+module.exports = (params, useAxios) => {
+
+ return useAxios({
+ url: '/grow/v1/song_ranking/play_page/ranking_info',
+ method: 'GET',
+ params: { album_audio_id: params.album_audio_id },
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/song_ranking_filter.js b/third_party/MoeKoeMusic/api/module/song_ranking_filter.js
new file mode 100644
index 0000000..6d09b41
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/song_ranking_filter.js
@@ -0,0 +1,12 @@
+// 歌曲成绩单
+
+module.exports = (params, useAxios) => {
+
+ return useAxios({
+ url: '/grow/v1/song_ranking/unlock/v2/ranking_filter',
+ method: 'GET',
+ params: { album_audio_id: params.album_audio_id, page: params.page || 1, pagesize: params.pagesize || 30 },
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/song_url.js b/third_party/MoeKoeMusic/api/module/song_url.js
new file mode 100644
index 0000000..464136c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/song_url.js
@@ -0,0 +1,52 @@
+const { randomString } = require('../util/util');
+
+// 获取音乐urls
+// quality 支持 魔法音乐
+// piano 钢琴
+// acappella 人声 伴奏 分离
+// subwoofer 乐器
+// ancient 尤克里里
+// dj dj
+module.exports = (params, useAxios) => {
+ const quality = ['piano', 'acappella', 'subwoofer', 'ancient', 'dj', 'surnay'].includes(params.quality)
+ ? `magic_${params?.quality}`
+ : params.quality;
+
+ const isLite = process.env.platform === 'lite';
+ const page_id = isLite ? 967177915 : 151369488;
+ const ppage_id = isLite
+ ? (params.ppage_id || '356753938,823673182,967485191')
+ : '463467626,350369493,788954147';
+
+ const dataMap = {
+ album_id: Number(params.album_id ?? 0),
+ area_code: 1,
+ hash: (params?.hash || '').toLowerCase(),
+ ssa_flag: 'is_fromtrack',
+ version: 11430,
+ page_id,
+ quality: quality || 128,
+ album_audio_id: Number(params.album_audio_id ?? 0),
+
+ behavior: 'play',
+ pid: isLite ? 411 : 2,
+ cmd: 26,
+ pidversion: 3001,
+ IsFreePart: params?.free_part ? 1 : 0, //是否返回试听部分(仅部分歌曲)
+ ppage_id,
+ cdnBackup: 1,
+ module: '',
+ clientver: 11430,
+ };
+
+ return useAxios({
+ url: '/v5/url',
+ method: 'GET',
+ params: dataMap,
+ encryptType: 'android',
+ headers: { 'x-router': 'trackercdn.kugou.com'},
+ encryptKey: true,
+ notSign: true,
+ cookie: Object.assign({}, {dfid: randomString(24)}, params?.cookie ),
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/song_url_new.js b/third_party/MoeKoeMusic/api/module/song_url_new.js
new file mode 100644
index 0000000..13f54b5
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/song_url_new.js
@@ -0,0 +1,54 @@
+const { randomString, appid, cryptoMd5 } = require('../util');
+module.exports = (params, useAxios) => {
+ // const quality = ['piano', 'acappella', 'subwoofer', 'ancient', 'dj', 'surnay'].includes(params.quality)
+ // ? `magic_${params?.quality}`
+ // : params.quality;
+
+ const vip_token = params?.vip_token || params?.cookie?.vip_token || '';
+ const token = params?.token || params?.cookie?.token || '';
+ const clienttime_ms = Date.now();
+ const userid = Number(params?.userid || params?.cookie?.userid || '0');
+ const dfid = params?.dfid || params?.cookie?.dfid || randomString(24); // 自定义
+ const vip_type = params?.cookie?.vip_type || params?.vipType || 0;
+
+ const dataMap = {
+ area_code: '1',
+ behavior: 'play',
+ qualities: ['128', '320', 'flac', 'high', 'multitrack', 'viper_atmos', 'viper_tape', 'viper_clear', 'super'],
+ 'resource': {
+ 'album_audio_id': params.album_audio_id,
+ 'collect_list_id': '3',
+ 'collect_time': clienttime_ms,
+ 'hash': params.hash,
+ 'id': 0,
+ 'page_id': 1,
+ 'type': 'audio',
+ },
+ token,
+ 'tracker_param': {
+ all_m: 1,
+ auth: '',
+ is_free_part: params?.free_part ? 1 : 0,
+ key: cryptoMd5(`${params.hash}185672dd44712f60bb1736df5a377e82${appid}${params?.cookie?.KUGOU_API_MID}${userid}`),
+ module_id: 0,
+ need_climax: 1,
+ need_xcdn: 1,
+ open_time: '',
+ pid: '411',
+ pidversion: '3001',
+ priv_vip_type: '6',
+ viptoken: vip_token,
+ },
+ userid: `${userid}`,
+ 'vip': vip_type,
+ };
+
+ return useAxios({
+ baseURL: 'http://tracker.kugou.com',
+ url: '/v6/priv_url',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: Object.assign({}, { dfid }, params?.cookie),
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/theme_music.js b/third_party/MoeKoeMusic/api/module/theme_music.js
new file mode 100644
index 0000000..dae8f2e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/theme_music.js
@@ -0,0 +1,19 @@
+// 获取主题音乐
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ platform: 'android',
+ clienttime: Math.floor(Date.now() / 1000),
+ show_theme_category_ids: params?.ids,
+ userid: params?.userid || params?.cookie?.userid || 0,
+ module_id: 508,
+ };
+
+ return useAxios({
+ url: '/everydayrec.service/v1/mul_theme_category_recommend',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/theme_music_detail.js b/third_party/MoeKoeMusic/api/module/theme_music_detail.js
new file mode 100644
index 0000000..5dc85b0
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/theme_music_detail.js
@@ -0,0 +1,19 @@
+// 获取主题音乐详情
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ platform: 'android',
+ clienttime: Math.floor(Date.now() / 1000),
+ theme_category_id: params?.id,
+ show_theme_category_id: 0,
+ userid: params?.userid || params?.cookie?.userid || 0,
+ module_id: 508,
+ };
+
+ return useAxios({
+ url: '/everydayrec.service/v1/theme_category_recommend',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/theme_playlist.js b/third_party/MoeKoeMusic/api/module/theme_playlist.js
new file mode 100644
index 0000000..de8f0cd
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/theme_playlist.js
@@ -0,0 +1,21 @@
+// 主题歌单
+const { clientver } = require('../util');
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ platform: 'android',
+ clientver,
+ clienttime: Date.now(),
+ area_code: 1,
+ module_id: 1,
+ userid: params?.userid || params?.cookie?.userid || 0,
+ };
+
+ return useAxios({
+ url: '/v2/getthemelist',
+ method: 'POST',
+ encryptType: 'android',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'everydayrec.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/theme_playlist_track.js b/third_party/MoeKoeMusic/api/module/theme_playlist_track.js
new file mode 100644
index 0000000..e76d13d
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/theme_playlist_track.js
@@ -0,0 +1,22 @@
+// 获取主题歌单说有歌曲
+const { clientver } = require('../util');
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ platform: 'android',
+ clientver,
+ clienttime: Date.now(),
+ area_code: 1,
+ module_id: 1,
+ userid: params?.userid || params?.cookie?.userid || 0,
+ theme_id: params?.theme_id,
+ };
+
+ return useAxios({
+ url: '/v2/gettheme_songidlist',
+ method: 'POST',
+ encryptType: 'android',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'everydayrec.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/top_album.js b/third_party/MoeKoeMusic/api/module/top_album.js
new file mode 100644
index 0000000..9729a92
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/top_album.js
@@ -0,0 +1,20 @@
+// 推荐专辑
+const { apiver } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ apiver,
+ token: params?.token || params?.cookie?.token || '',
+ page: params?.page || 1,
+ pagesize: params?.pagesize || 30,
+ withpriv: 1,
+ };
+
+ return useAxios({
+ url: '/musicadservice/v1/mobile_newalbum_sp',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/top_card.js b/third_party/MoeKoeMusic/api/module/top_card.js
new file mode 100644
index 0000000..2a50075
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/top_card.js
@@ -0,0 +1,39 @@
+// 热门好歌精选
+// song_module_1 card_id_1: 精选好歌随心听 || 私人专属好歌
+// song_module_2 card_id_2: 经典怀旧金曲
+// song_module_3 card_id_3: 热门好歌精选
+// song_module_4 card_id_4: 小众宝藏佳作
+// song_module_6 card_id_6: vip专属推荐
+
+const { appid, clientver, cryptoMd5, signParamsKey } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const dfid = params?.dfid || params?.cookie?.dfid || '-';
+ const fakem = '60f7ebf1f812edbac3c63a7310001701760f';
+ const mid = params?.cookie?.KUGOU_API_MID;
+ const dateTime = Date.now();
+
+ const dataMap = {
+ appid,
+ clientver,
+ platform: 'android',
+ clienttime: dateTime,
+ userid: params?.userid || params?.cookie?.userid || 0,
+ key: signParamsKey(dateTime),
+ fakem,
+ area_code: 1,
+ mid,
+ uuid: '-',
+ client_playlist: [],
+ u_info: 'a0c35cd40af564444b5584c2754dedec',
+ };
+
+ return useAxios({
+ url: '/singlecardrec.service/v1/single_card_recommend',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ params: { 'card_id': params?.card_id || 1, fakem, area_code: 1, platform: 'ios'},
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/top_card_youth.js b/third_party/MoeKoeMusic/api/module/top_card_youth.js
new file mode 100644
index 0000000..ae2a65b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/top_card_youth.js
@@ -0,0 +1,26 @@
+// 热门好歌精选
+// 3006: VIP专属推荐
+// 3001: 私人专属好歌
+// 3004: 小众宝藏佳作
+// 3014: 喜欢这首歌的TA也喜欢
+// 3102: 喜欢「欧美」的TA也喜欢
+// 3101: 概念er新推
+// 3005: 潮流尝鲜
+
+module.exports = (params, useAxios) => {
+
+ const dataMap = {
+ tagid: params.tagid ?? '',
+ u_info: '',
+ source_mixsong: ''
+ };
+
+ return useAxios({
+ url: 'youth/v1/song/single_card_recommend',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ params: { 'card_id': params?.card_id || 3005, area_code: 1, platform: 'ios', module_id: 1, ver: 'v2', pagesize: params.pagesize ?? 30 , module_id: 1, clientver: 11490 },
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/top_ip.js b/third_party/MoeKoeMusic/api/module/top_ip.js
new file mode 100644
index 0000000..d62fb81
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/top_ip.js
@@ -0,0 +1,40 @@
+/**
+ *
+ * @param {Record} params
+ * @param {useAxios} useAxios
+ * @returns
+ */
+module.exports = (params, useAxios) => {
+ const dataMap = { tags: {} };
+ return new Promise((resolve, reject) => {
+ useAxios({
+ baseURL: 'http://musicadservice.kugou.com',
+ url: '/v1/daily_recommend',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ params: {
+ clientver: 12349,
+ area_code: 1,
+ },
+ cookie: params?.cookie || {},
+ })
+ .then((resp) => {
+ if (resp.body.status == 1) {
+ const list = Array.isArray(resp.body.data.list) ? [...resp.body.data.list] : [];
+ list.forEach((s, index) => {
+ const inner_url = s?.extra?.inner_url;
+ if (inner_url) {
+ const findIndex = inner_url.lastIndexOf('ip_id');
+ if (findIndex !== -1) {
+ list[index]['extra']['ip_id'] = Number(inner_url.substring(findIndex + 6));
+ }
+ }
+ });
+ resp.body.data.list = list;
+ }
+ resolve(resp);
+ })
+ .catch((e) => reject(e));
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/top_playlist.js b/third_party/MoeKoeMusic/api/module/top_playlist.js
new file mode 100644
index 0000000..110387c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/top_playlist.js
@@ -0,0 +1,44 @@
+// 歌单
+// categoryid 0:推荐,11292:HI-RES
+
+const { appid, clientver, signParamsKey } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const dateTime = (Date.now() / 1000).toFixed(0);
+ const specialRecommend = {
+ withtag: params?.withtag || 1,
+ withsong: params?.withsong || 1,
+ sort: params?.sort || 1,
+ ugc: 1,
+ is_selected: 0,
+ withrecommend: 1,
+ area_code: 1,
+ categoryid: params?.category_id || 0,
+ };
+
+ const dataMap = {
+ appid,
+ mid: params?.cookie?.KUGOU_API_MID,
+ clientver,
+ platform: 'android',
+ clienttime: dateTime,
+ userid: params?.userid || params?.cookie?.userid || 0,
+ module_id: params?.module_id || 1,
+ page: params?.page || 1,
+ pagesize: params?.pagesize || 30,
+ key: signParamsKey(dateTime.toString()),
+ special_recommend: specialRecommend,
+ req_multi: 1,
+ retrun_min: 5,
+ return_special_falg: 1,
+ };
+
+ return useAxios({
+ url: '/v2/special_recommend',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'specialrec.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/top_song.js b/third_party/MoeKoeMusic/api/module/top_song.js
new file mode 100644
index 0000000..fda015e
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/top_song.js
@@ -0,0 +1,20 @@
+/**
+ * 新歌速递
+ */
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ rank_id: params?.type || 21608,
+ userid: params?.userid || params?.cookie?.userid || 0,
+ page: params?.page || 1,
+ pagesize: params?.pagesize || 30,
+ tags: [],
+ };
+
+ return useAxios({
+ url: '/musicadservice/container/v1/newsong_publish',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/top_tag_card_youth.js b/third_party/MoeKoeMusic/api/module/top_tag_card_youth.js
new file mode 100644
index 0000000..cb01c5f
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/top_tag_card_youth.js
@@ -0,0 +1,19 @@
+// 曲风盲盒 · 随机心动
+
+module.exports = (params, useAxios) => {
+
+ const dataMap = {
+ tagid: '',
+ u_info: '',
+ source_mixsong: ''
+ };
+
+ return useAxios({
+ url: '/youth/v1/song/tag_card_recommend',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ params: { ver: 'v2', area_code: 1, platform: 'ios', module_id: 1, clientver: 11490},
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_cloud.js b/third_party/MoeKoeMusic/api/module/user_cloud.js
new file mode 100644
index 0000000..0a02d39
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_cloud.js
@@ -0,0 +1,52 @@
+const { playlistAesEncrypt, playlistAesDecrypt, rsaEncrypt2, signParamsKey, clientver, appid } = require('../util');
+module.exports = (params, useAxios) => {
+ const answer = { status: 500, body: {}, cookie: [] };
+ return new Promise(async (resolve) => {
+ try {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params.cookie?.token || '';
+ const mid = params?.cookie?.KUGOU_API_MID; // 可以自定义
+ const clienttime = Math.floor(Date.now() / 1000);
+
+ const dataMap = {
+ page: params.page ?? 1,
+ pagesize: params.pagesize ?? 30,
+ getkmr: 1,
+ };
+
+ const aesEncrypt = playlistAesEncrypt(dataMap);
+
+ const p = rsaEncrypt2({ aes: aesEncrypt.key, uid: userid, token }).toUpperCase();
+
+ const paramsMap = {
+ clienttime,
+ mid,
+ key: signParamsKey(clienttime.toString(), appid),
+ clientver,
+ appid,
+ p,
+ };
+
+ const respone = await useAxios({
+ baseURL: 'https://mcloudservice.kugou.com',
+ url: '/v1/get_list',
+ params: paramsMap,
+ data: Buffer.from(aesEncrypt.str, 'base64'),
+ method: 'post',
+ encryptType: 'android',
+ responseType: 'arraybuffer',
+ cookie: params?.cookie || {},
+ clearDefaultParams: true,
+ notSignature: true,
+ });
+
+ respone.body = playlistAesDecrypt({ str: respone.body.toString('base64'), key: aesEncrypt.key });
+
+ resolve(respone);
+ } catch (error) {
+ console.log(error);
+ answer.body = error;
+ resolve(answer);
+ }
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_cloud_del.js b/third_party/MoeKoeMusic/api/module/user_cloud_del.js
new file mode 100644
index 0000000..0694388
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_cloud_del.js
@@ -0,0 +1,96 @@
+// 删除用户云盘音乐
+const { playlistAesEncrypt, playlistAesDecrypt, rsaEncrypt2, signParamsKey, clientver, appid } = require('../util');
+
+const splitList = (value) =>
+ []
+ .concat(value || [])
+ .flatMap((item) => {
+ if (Array.isArray(item)) return item;
+ const text = String(item).trim();
+ if (!text) return [];
+ if (text.startsWith('[') && text.endsWith(']')) {
+ try {
+ const parsed = JSON.parse(text);
+ if (Array.isArray(parsed)) return parsed;
+ } catch (e) {}
+ }
+ return text.split(',');
+ })
+ .map((item) => String(item).trim())
+ .filter(Boolean);
+
+module.exports = (params, useAxios) => {
+ const answer = { status: 500, body: {}, cookie: [] };
+ return new Promise(async (resolve) => {
+ try {
+ const userid = String(params?.userid || params?.cookie?.userid || 0);
+ const token = params?.token || params?.cookie?.token || '';
+ const mid = params?.cookie?.KUGOU_API_MID;
+ const requestAppid = params?.appid || appid;
+ const requestClientver = params?.clientver || clientver;
+ const clienttime = Math.floor(Date.now() / 1000);
+
+ const fileids = splitList(params?.fileids || params?.fileid || params?.kv_ids || params?.kv_id);
+ const albumAudioIds = splitList(params?.album_audio_ids || params?.album_audio_id);
+
+ if (!fileids.length) {
+ throw new Error('请传入 fileid 或 kv_id');
+ }
+
+ const dataMap = {
+ data: fileids.map((id, index) => ({
+ kv_id: Number(id) || id,
+ album_audio_id: Number(albumAudioIds[index] || albumAudioIds[0] || params?.mixid || params?.mix_id || 0),
+ })),
+ };
+
+ const aesEncrypt = playlistAesEncrypt(dataMap);
+ const p = rsaEncrypt2({ aes: aesEncrypt.key, uid: userid, token }).toUpperCase();
+
+ const respone = await useAxios({
+ baseURL: 'https://mcloudservice.kugou.com',
+ url: '/v1/del_files',
+ params: {
+ clienttime,
+ mid,
+ key: signParamsKey(clienttime.toString(), requestAppid, requestClientver),
+ clientver: requestClientver,
+ appid: requestAppid,
+ p,
+ },
+ data: Buffer.from(aesEncrypt.str, 'base64'),
+ method: 'post',
+ encryptType: 'android',
+ responseType: 'arraybuffer',
+ cookie: params?.cookie || {},
+ clearDefaultParams: true,
+ notSignature: true,
+ });
+
+ try {
+ respone.body = playlistAesDecrypt({ str: respone.body.toString('base64'), key: aesEncrypt.key });
+ } catch (e) {
+ try {
+ respone.body = JSON.parse(respone.body.toString());
+ } catch (e2) {
+ respone.body = respone.body.toString();
+ }
+ }
+
+ resolve(respone);
+ } catch (error) {
+ console.log(error);
+ let upstream = error?.body?.msg?.response?.data;
+ if (upstream && Buffer.isBuffer(upstream)) upstream = upstream.toString();
+ answer.body = {
+ status: 0,
+ msg: upstream
+ ? typeof upstream === 'string'
+ ? upstream
+ : JSON.stringify(upstream)
+ : error?.body?.msg?.message || error?.message || String(error),
+ };
+ resolve(answer);
+ }
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_cloud_match.js b/third_party/MoeKoeMusic/api/module/user_cloud_match.js
new file mode 100644
index 0000000..ce7e232
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_cloud_match.js
@@ -0,0 +1,96 @@
+// 云盘上传前曲库匹配
+const crypto = require('crypto');
+const { signParamsKey, appid, clientver } = require('../util');
+
+const fileMd5 = (buffer) => crypto.createHash('md5').update(buffer).digest('hex');
+
+const splitList = (value) =>
+ []
+ .concat(value || [])
+ .flatMap((item) => String(item).split(','))
+ .map((item) => item.trim())
+ .filter(Boolean);
+
+const firstCandidate = (item) => (Array.isArray(item) ? item[0] : item);
+
+const normalizeMatch = (candidate) => {
+ if (!candidate || typeof candidate !== 'object') return null;
+ const audioInfo = candidate.audio_info || {};
+ const albumAudioId = Number(candidate.album_audio_id) || 0;
+ const audioId = Number(audioInfo.audio_id || candidate.audio_id) || 0;
+ const hashStd = audioInfo.hash || candidate.hash || '';
+ if (!albumAudioId && !audioId && !hashStd) return null;
+ return {
+ album_audio_id: albumAudioId,
+ audio_id: audioId,
+ hash_std: hashStd,
+ hash: hashStd,
+ author_name: candidate.author_name || '',
+ audio_name: candidate.ori_audio_name || candidate.audio_name || candidate.songname || '',
+ suffix_audio_name: candidate.suffix_audio_name || '',
+ album_info: candidate.album_info || null,
+ raw: candidate,
+ };
+};
+
+module.exports = (params, useAxios) => {
+ const answer = { status: 500, body: {}, cookie: [] };
+ return new Promise(async (resolve) => {
+ try {
+ const requestAppid = params?.appid || appid;
+ const requestClientver = params?.clientver || clientver;
+ const clienttime = Math.floor(Date.now() / 1000);
+ const hashes = splitList(params?.hash || params?.filename);
+ if (!hashes.length && params?.data?.length) hashes.push(fileMd5(params.data).toLowerCase());
+ if (!hashes.length) throw new Error('请传入 hash,或通过请求体传入文件二进制数据');
+
+ const albumAudioIds = splitList(params?.album_audio_ids || params?.album_audio_id || params?.mixid || params?.mix_id);
+ const data = hashes.map((hash, index) => {
+ const item = { hash: String(hash).toLowerCase() };
+ const albumAudioId = albumAudioIds[index] || albumAudioIds[0];
+ if (Number(albumAudioId) > 0) item.album_audio_id = String(albumAudioId);
+ return item;
+ });
+
+ const dataMap = {
+ appid: requestAppid,
+ clienttime,
+ clientver: requestClientver,
+ data,
+ dfid: params?.cookie?.dfid || params?.dfid || '-',
+ key: signParamsKey(clienttime.toString(), requestAppid, requestClientver),
+ mid: params?.cookie?.KUGOU_API_MID || params?.mid || '',
+ show_privilege: 0,
+ show_author_alias: 0,
+ show_rel_album_audio_info: 0,
+ show_remarks: 0,
+ };
+
+ const respone = await useAxios({
+ baseURL: 'http://kmr.service.kugou.com',
+ url: '/v2/album_audio/audio',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ clearDefaultParams: true,
+ notSignature: true,
+ headers: { 'x-router': 'kmr.service.kugou.com', 'Content-Type': 'application/json' },
+ });
+
+ if (respone?.body?.status === 1 && Array.isArray(respone.body.data)) {
+ respone.body.match_list = respone.body.data.map((item) => normalizeMatch(firstCandidate(item))).filter(Boolean);
+ respone.body.match = respone.body.match_list[0] || null;
+ }
+
+ resolve(respone);
+ } catch (error) {
+ console.log(error);
+ answer.body = {
+ status: 0,
+ msg: error?.body?.msg?.message || error?.message || String(error),
+ };
+ resolve(answer);
+ }
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_cloud_upload.js b/third_party/MoeKoeMusic/api/module/user_cloud_upload.js
new file mode 100644
index 0000000..64b6192
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_cloud_upload.js
@@ -0,0 +1,326 @@
+// 上传音乐文件到用户云盘
+// 流程:获取授权 → 初始化分片上传 → 上传分片 → 完成上传 → 添加文件到云盘
+// add_files 请求体结构与客户端云盘请求保持一致
+const axios = require('axios');
+const crypto = require('crypto');
+const {
+ playlistAesEncrypt,
+ playlistAesDecrypt,
+ rsaEncrypt2,
+ signParamsKey,
+ signatureAndroidParams,
+ cryptoMd5,
+ clientver,
+ appid,
+} = require('../util');
+const { resolveProxy } = require('../util/runtime');
+const userCloudMatch = require('./user_cloud_match');
+
+// 计算文件内容的 MD5(crypto-js 的 cryptoMd5 对 Buffer 会执行 JSON 序列化,不能用于文件)
+const fileMd5 = (buffer) => crypto.createHash('md5').update(buffer).digest('hex');
+const bssVerifyCode = (bucket, requestAppid) => cryptoMd5(`${requestAppid}${bucket}8ae10344e9738dcb`);
+const boolParam = (value, defaultValue = true) => {
+ if (value === undefined || value === null || value === '') return defaultValue;
+ return !['0', 'false', 'no'].includes(String(value).toLowerCase());
+};
+const firstCandidate = (item) => (Array.isArray(item) ? item[0] : item);
+const normalizeMatch = (candidate) => {
+ if (!candidate || typeof candidate !== 'object') return null;
+ const audioInfo = candidate.audio_info || {};
+ const albumAudioId = Number(candidate.album_audio_id) || 0;
+ const audioId = Number(audioInfo.audio_id || candidate.audio_id) || 0;
+ const hashStd = audioInfo.hash || candidate.hash || '';
+ if (!albumAudioId && !audioId && !hashStd) return null;
+ return {
+ album_audio_id: albumAudioId,
+ audio_id: audioId,
+ hash_std: hashStd,
+ author_name: candidate.author_name || '',
+ audio_name: candidate.ori_audio_name || candidate.audio_name || candidate.songname || '',
+ suffix_audio_name: candidate.suffix_audio_name || '',
+ raw: candidate,
+ };
+};
+const extractMatch = (body) => {
+ if (body?.match) return body.match;
+ if (Array.isArray(body?.match_list) && body.match_list.length) return body.match_list[0];
+ if (Array.isArray(body?.data) && body.data.length) return normalizeMatch(firstCandidate(body.data[0]));
+ return null;
+};
+const withProtocol = (host) => (/^https?:\/\//i.test(host || '') ? host : `http://${host}`);
+
+module.exports = (params, useAxios) => {
+ const answer = { status: 500, body: {}, cookie: [] };
+ return new Promise(async (resolve) => {
+ try {
+ // userid 必须为字符串(p 参数 RSA 加密时服务端校验 uid 类型)
+ const userid = String(params?.userid || params?.cookie?.userid || 0);
+ const token = params?.token || params?.cookie?.token || '';
+ const mid = params?.cookie?.KUGOU_API_MID || params?.mid || '';
+ const dfid = params?.cookie?.dfid || params?.dfid || '-';
+ const uuid = params?.cookie?.KUGOU_API_GUID || params?.uuid || '-';
+ const requestAppid = params?.appid || appid;
+ const requestClientver = params?.clientver || clientver;
+ const clienttime = Math.floor(Date.now() / 1000);
+
+ // 文件数据(由 HTTP 层接收的二进制 body 传入)
+ const fileData = params?.data || Buffer.alloc(0);
+ if (!fileData.length) throw new Error('请通过请求体传入文件二进制数据');
+
+ // filename 即文件 MD5(小写),可传入覆盖,默认自动计算
+ const filename = String(params?.filename || fileMd5(fileData)).toLowerCase();
+ // 扩展名(自动去掉点号)
+ const extendname = String(params?.extendname || 'mp3').replace(/^\./, '');
+ const bucket = 'musicclound';
+ const version = requestClientver;
+ let matchInfo = null;
+ const needMatch =
+ boolParam(params?.auto_match, true) &&
+ !(params?.hash_std && params?.audio_id && (params?.album_audio_id || params?.mixid || params?.mix_id));
+
+ if (needMatch) {
+ const matchRes = await userCloudMatch(
+ {
+ ...params,
+ data: undefined,
+ hash: filename,
+ appid: requestAppid,
+ clientver: requestClientver,
+ },
+ useAxios
+ );
+ if (matchRes?.body?.status === 1) matchInfo = extractMatch(matchRes.body);
+ }
+
+ const hashStd = String(params?.hash_std || matchInfo?.hash_std || filename).toLowerCase();
+ const audioId = Number(params?.audio_id || matchInfo?.audio_id) || 0;
+ const albumAudioId = Number(params?.album_audio_id || params?.mixid || params?.mix_id || matchInfo?.album_audio_id) || 0;
+ const author_name = params?.author_name || matchInfo?.author_name || '';
+ const trackName = params?.track_name || params?.songname || matchInfo?.audio_name || filename;
+ const name = params?.name || `${author_name ? `${author_name} - ` : ''}${trackName}.${extendname}`;
+
+ const signBssParams = (paramsMap) => {
+ paramsMap.signature = signatureAndroidParams(paramsMap, '');
+ return paramsMap;
+ };
+
+ // 原生 axios 请求(步骤 1-4 不需要签名,尽量贴近抓包)
+ const http = (options) => {
+ const proxyConfig = resolveProxy();
+ const headers = {
+ 'User-Agent': `Android15-1070-${requestClientver}-201-0-wifi`,
+ 'KG-RC': '1',
+ 'KG-Rec': '1',
+ // 每次请求生成随机的 KG-THash(模拟客户端行为,固定 7 位 hex)
+ 'KG-THash': Math.floor(Math.random() * 0xfffffff)
+ .toString(16)
+ .padStart(7, '0'),
+ ...(options.headers || {}),
+ };
+ return axios({ ...options, headers, ...(proxyConfig ? { proxy: proxyConfig } : {}) });
+ };
+
+ // ========== 步骤1 获取上传授权 ==========
+ const authParams = signBssParams({
+ bucket,
+ filename,
+ method: 'POST',
+ loginType: token && userid !== '0' ? 1 : 0,
+ buVerifyCode: bssVerifyCode(bucket, requestAppid),
+ extranet: 1,
+ userid,
+ token,
+ version,
+ dfid,
+ mid,
+ uuid,
+ appid: requestAppid,
+ clientver: requestClientver,
+ clienttime: Math.floor(Date.now() / 1000),
+ });
+ const authRes = await http({
+ method: 'get',
+ url: 'https://gateway.kugou.com/bsstrackercdngz/v1/upload/auth',
+ params: authParams,
+ });
+ const authorization = authRes?.data?.data?.authorization;
+ if (!authorization) throw new Error(JSON.stringify(authRes?.data) || '获取授权失败');
+
+ // ========== 步骤2 初始化分片上传 ==========
+ const initParams = signBssParams({
+ bucket,
+ filename,
+ ssl: 1,
+ extendname,
+ version,
+ userid,
+ token,
+ authorization,
+ dfid,
+ mid,
+ uuid,
+ appid: requestAppid,
+ clientver: requestClientver,
+ clienttime: Math.floor(Date.now() / 1000),
+ });
+ const initRes = await http({
+ method: 'post',
+ url: 'http://bssulbig.kugou.com/v2/multipart/initiate/music',
+ params: initParams,
+ headers: { Authorization: authorization },
+ });
+ const initData = initRes?.data?.data || {};
+ const { external_host, upload_id } = initData;
+ let bssFileHash = initData['x-bss-filename'] || filename;
+
+ // 秒传分支:upload_id 为空且返回 x-bss-hash 说明文件已在服务器,跳过步骤3/4
+ if (upload_id) {
+ if (!external_host) throw new Error(JSON.stringify(initRes?.data) || '初始化上传失败');
+ // ========== 步骤3 上传分片(默认 1MB 一片) ==========
+ const partSize = 1024 * 1024;
+ const partCount = Math.max(1, Math.ceil(fileData.length / partSize));
+ for (let i = 0; i < partCount; i++) {
+ const part = fileData.slice(i * partSize, (i + 1) * partSize);
+ const uploadParams = signBssParams({
+ bucket,
+ authorization,
+ filename,
+ partnumber: i + 1,
+ upload_id,
+ body_empty: 1,
+ version,
+ userid,
+ token,
+ dfid,
+ mid,
+ uuid,
+ appid: requestAppid,
+ clientver: requestClientver,
+ clienttime: Math.floor(Date.now() / 1000),
+ });
+ const uploadRes = await http({
+ method: 'post',
+ url: `${withProtocol(external_host)}/v3/multipart/upload`,
+ params: uploadParams,
+ headers: { Authorization: authorization, 'Content-Type': 'application/octet-stream' },
+ data: part,
+ });
+ if (uploadRes?.data?.status !== 1) throw new Error(JSON.stringify(uploadRes?.data) || '分片上传失败');
+ }
+
+ // ========== 步骤4 完成上传 ==========
+ const completeParams = signBssParams({
+ bucket,
+ authorization,
+ filename,
+ partnumber: partCount,
+ upload_id,
+ md5: filename,
+ version,
+ userid,
+ token,
+ if_id3: 1,
+ dfid,
+ mid,
+ uuid,
+ appid: requestAppid,
+ clientver: requestClientver,
+ clienttime: Math.floor(Date.now() / 1000),
+ });
+ const completeRes = await http({
+ method: 'post',
+ url: `${withProtocol(external_host)}/v3/multipart/complete`,
+ params: completeParams,
+ headers: { Authorization: authorization },
+ });
+ if (completeRes?.data?.status !== 1) throw new Error(JSON.stringify(completeRes?.data) || '完成上传失败');
+ bssFileHash = completeRes?.data?.data?.['x-bss-filename'] || bssFileHash;
+ }
+
+ // ========== 步骤5 添加文件到云盘(AES 加密 body + RSA 加密密钥) ==========
+ // 请求体结构:data 数组(所有字段必需)+ list_ver
+ // 数字字段必须为 number 类型(query 传入时为字符串,需转换,否则服务端返回 500)
+ const aesEncrypt = playlistAesEncrypt({
+ data: [
+ {
+ name,
+ ext: extendname,
+ author_name,
+ hash: bssFileHash,
+ hash_std: hashStd,
+ audio_id: audioId,
+ bitrate: Number(params?.bitrate) || 4,
+ album_audio_id: albumAudioId,
+ size: fileData.length,
+ timelen: Number(params?.timelen) || 0,
+ },
+ ],
+ list_ver: Number(params?.list_ver) || 0,
+ });
+ const p = rsaEncrypt2({ aes: aesEncrypt.key, uid: userid, token }).toUpperCase();
+
+ const respone = await useAxios({
+ baseURL: 'https://mcloudservice.kugou.com',
+ url: '/v1/add_files',
+ params: {
+ clienttime,
+ mid,
+ key: signParamsKey(clienttime.toString(), requestAppid, requestClientver),
+ clientver: requestClientver,
+ appid: requestAppid,
+ p,
+ },
+ data: Buffer.from(aesEncrypt.str, 'base64'),
+ method: 'post',
+ encryptType: 'android',
+ responseType: 'arraybuffer',
+ cookie: params?.cookie || {},
+ clearDefaultParams: true,
+ notSignature: true,
+ });
+
+ // 尝试解密响应,失败则按明文 JSON 处理(如服务端返回错误信息)
+ try {
+ respone.body = playlistAesDecrypt({ str: respone.body.toString('base64'), key: aesEncrypt.key });
+ } catch (e) {
+ try {
+ respone.body = JSON.parse(respone.body.toString());
+ } catch (e2) {
+ respone.body = respone.body.toString();
+ }
+ }
+
+ // 附加整个上传流程的关键信息,便于排查
+ respone.body.uploadInfo = {
+ authorization,
+ external_host,
+ upload_id,
+ hash: bssFileHash,
+ local_hash: filename,
+ hash_std: hashStd,
+ audio_id: audioId,
+ album_audio_id: albumAudioId,
+ matched: Boolean(matchInfo),
+ filesize: fileData.length,
+ };
+
+ resolve(respone);
+ } catch (error) {
+ console.log(error);
+ // useAxios 失败时 reject 的是 { status, body } 结构,body.msg 为 AxiosError,
+ // 从中提取上游响应内容(arraybuffer 需转字符串),便于定位酷狗侧的具体错误
+ let upstream = error?.body?.msg?.response?.data;
+ if (upstream && Buffer.isBuffer(upstream)) upstream = upstream.toString();
+ answer.body = {
+ status: 0,
+ msg: upstream
+ ? typeof upstream === 'string'
+ ? upstream
+ : JSON.stringify(upstream)
+ : error?.message || String(error),
+ stack: error?.stack,
+ };
+ resolve(answer);
+ }
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_cloud_url.js b/third_party/MoeKoeMusic/api/module/user_cloud_url.js
new file mode 100644
index 0000000..7cb7eaa
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_cloud_url.js
@@ -0,0 +1,27 @@
+// 获取云盘音乐url
+const { signCloudKey } = require('../util');
+module.exports = (params, useAxios) => {
+ const hash = String(params.hash).toLocaleLowerCase();
+ const paramsMap = {
+ hash: hash,
+ ssa_flag: 'is_fromtrack',
+ version: '20102',
+ ssl: 0,
+ album_audio_id: params.album_audio_id ?? 0,
+ pid: 20026,
+ audio_id: params.audio_id ?? 0,
+ kv_id: 2,
+ key: signCloudKey(hash, 20026),
+ bucket: 'musicclound',
+ name: params.name ?? '',
+ with_res_tag: 0,
+ };
+
+ return useAxios({
+ url: '/bsstrackercdngz/v2/query_musicclound_url',
+ encryptType: 'android',
+ method: 'get',
+ params: paramsMap,
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_detail.js b/third_party/MoeKoeMusic/api/module/user_detail.js
new file mode 100644
index 0000000..8ccd233
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_detail.js
@@ -0,0 +1,24 @@
+const { cryptoRSAEncrypt } = require('../util');
+module.exports = (params, useAxios) => {
+ const token = params?.token || params?.cookie?.token || '';
+ const userid = Number(params?.userid || params?.cookie?.userid || '0');
+ const clienttime_ms = Math.floor(Date.now() / 1000);
+ const pk = cryptoRSAEncrypt({ token, clienttime: clienttime_ms }).toUpperCase();
+
+ const dataMap = {
+ visit_time: clienttime_ms,
+ usertype: 1,
+ p: pk,
+ userid,
+ };
+
+ return useAxios({
+ url: '/v3/get_my_info',
+ method: 'POST',
+ data: dataMap,
+ params: { plat: 1 },
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'usercenter.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_follow.js b/third_party/MoeKoeMusic/api/module/user_follow.js
new file mode 100644
index 0000000..c47e400
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_follow.js
@@ -0,0 +1,28 @@
+const { cryptoRSAEncrypt } = require('../util');
+/**
+ * 获取用户关注的歌手
+ */
+module.exports = (params, useAxios) => {
+ const token = params?.token || params?.cookie?.token || '';
+ const userid = params?.userid || params?.cookie?.userid || '0';
+ const dateTime = Math.floor(Date.now() / 1000);
+ const dataMap = {
+ merge: 2,
+ need_iden_type: 1,
+ ext_params: 'k_pic,jumptype,singerid,score',
+ userid,
+ type: 0,
+ id_type: 0,
+ p: cryptoRSAEncrypt({ clienttime: dateTime, token }).toUpperCase(),
+ };
+
+ return useAxios({
+ url: '/v4/follow_list',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ params: { plat: 1 },
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'relationuser.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_follow_message.js b/third_party/MoeKoeMusic/api/module/user_follow_message.js
new file mode 100644
index 0000000..9f385db
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_follow_message.js
@@ -0,0 +1,15 @@
+/**
+ * 获取用户关注的歌手的消息
+ */
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || '0';
+
+
+ return useAxios({
+ url: '/msg.mobile/v3/msgtag/history',
+ encryptType: 'android',
+ method: 'GET',
+ params: { filter: 1, maxid: 0, pagesize: params.pagesize ?? 30, tag: `chat:${userid}_${params.id}`},
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_history.js b/third_party/MoeKoeMusic/api/module/user_history.js
new file mode 100644
index 0000000..31eec66
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_history.js
@@ -0,0 +1,18 @@
+// 获取用户听歌排行
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || 0;
+ const token = params?.token || params?.cookie?.token || '';
+
+ const dataMap = { token, userid, source_classify: 'app', to_subdivide_sr: 1 };
+
+ if (params.bp) dataMap['bp'] = params.bp;
+
+ return useAxios({
+ url: '/playhistory/v1/get_songs',
+ data: dataMap,
+ encryptType: 'android',
+ method: 'POST',
+ cookie: params?.cookie || {},
+ });
+
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_listen.js b/third_party/MoeKoeMusic/api/module/user_listen.js
new file mode 100644
index 0000000..2238fcb
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_listen.js
@@ -0,0 +1,27 @@
+const { cryptoRSAEncrypt } = require('../util');
+module.exports = (params, useAxios) => {
+ const token = params?.token || params?.cookie?.token || '';
+ const userid = params?.userid || params?.cookie?.userid || '0';
+
+ const clienttime = Math.floor(Date.now() / 1000);
+
+ const p = cryptoRSAEncrypt({ clienttime, token }).toUpperCase();
+ const dataMap = {
+ t_userid: userid,
+ userid,
+ list_type: params.type || 0,
+ area_code: 1,
+ cover: 2,
+ p,
+ };
+
+ return useAxios({
+ baseURL: 'https://listenservice.kugou.com',
+ url: '/v2/get_list',
+ data: dataMap,
+ params: { clienttime, plat: 0 },
+ method: 'POST',
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_playlist.js b/third_party/MoeKoeMusic/api/module/user_playlist.js
new file mode 100644
index 0000000..f4fed8a
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_playlist.js
@@ -0,0 +1,25 @@
+// 获取用户歌单
+
+module.exports = (params, useAxios) => {
+ const userid = params?.cookie?.userid || params?.userid || 0;
+ const token = params?.cookie?.token || params?.token || '';
+
+ const dataMap = {
+ userid,
+ token,
+ total_ver: 979,
+ type: 2,
+ page: params?.page || 1,
+ pagesize: params?.pagesize || 30,
+ };
+
+ return useAxios({
+ url: '/v7/get_all_list',
+ encryptType: 'android',
+ method: 'post',
+ data: dataMap,
+ params: { plat: 1, userid: Number(userid), token },
+ cookie: params?.cookie,
+ headers: { 'x-router': 'cloudlist.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_purchased_albums.js b/third_party/MoeKoeMusic/api/module/user_purchased_albums.js
new file mode 100644
index 0000000..34d54bb
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_purchased_albums.js
@@ -0,0 +1,22 @@
+// 已购专辑
+const { appid, clientver } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ appid,
+ userid: Number(params?.cookie?.userid) || 0,
+ token: params?.cookie?.token || '',
+ page: Number(params?.page) || 1,
+ pagesize: Number(params?.pagesize) || 15,
+ clientver: String(clientver),
+ deleted: 0,
+ };
+
+ return useAxios({
+ url: '/openapi/v1/copyright/get_album_goods',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_purchased_songs.js b/third_party/MoeKoeMusic/api/module/user_purchased_songs.js
new file mode 100644
index 0000000..bbc1a5b
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_purchased_songs.js
@@ -0,0 +1,24 @@
+// 已购单曲
+const { appid, clientver } = require('../util');
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ appid,
+ userid: Number(params?.cookie?.userid) || 0,
+ token: params?.cookie?.token || '',
+ page: Number(params?.page) || 1,
+ pagesize: Number(params?.pagesize) || 50,
+ clientver: String(clientver),
+ deleted: 0,
+ need_audio_info: 1,
+ area_code: '1',
+ };
+
+ return useAxios({
+ url: '/openapi/copyright/v1/audio/get_goods',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_video_collect.js b/third_party/MoeKoeMusic/api/module/user_video_collect.js
new file mode 100644
index 0000000..6acbe66
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_video_collect.js
@@ -0,0 +1,23 @@
+const { cryptoRSAEncrypt } = require('../util');
+/**
+ * 获取用户关注的歌手
+ */
+module.exports = (params, useAxios) => {
+ const token = params?.token || params?.cookie?.token || '';
+ const userid = params?.userid || params?.cookie?.userid || '0';
+ const dataMap = {
+ userid,
+ token,
+ page: params?.page ?? 1,
+ pagesize: params?.pagesize ?? 30,
+ };
+
+ return useAxios({
+ url: '/collectservice/v2/collect_list_mixvideo',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ params: { plat: 1 },
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_video_love.js b/third_party/MoeKoeMusic/api/module/user_video_love.js
new file mode 100644
index 0000000..afec6ba
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_video_love.js
@@ -0,0 +1,22 @@
+const { cryptoRSAEncrypt } = require('../util');
+/**
+ * 获取用户关注的歌手
+ */
+module.exports = (params, useAxios) => {
+ const userid = params?.userid || params?.cookie?.userid || '0';
+ const paramsMap = {
+ kugouid: userid,
+ pagesize: params?.pagesize ?? 30,
+ load_video_info: 1,
+ p: 1,
+ plat: 1
+ };
+
+ return useAxios({
+ url: '/m.comment.service/v1/get_user_like_video',
+ encryptType: 'android',
+ method: 'get',
+ params: paramsMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/user_vip_detail.js b/third_party/MoeKoeMusic/api/module/user_vip_detail.js
new file mode 100644
index 0000000..661db25
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/user_vip_detail.js
@@ -0,0 +1,10 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ baseURL: 'https://kugouvip.kugou.com',
+ url: '/v1/get_union_vip',
+ method: 'GET',
+ params: {busi_type: 'concept'},
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/verify_user_info.js b/third_party/MoeKoeMusic/api/module/verify_user_info.js
new file mode 100644
index 0000000..e463695
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/verify_user_info.js
@@ -0,0 +1,49 @@
+const { cryptoAesEncrypt, cryptoRSAEncrypt } = require('../util');
+// 验证验证码数据
+module.exports = (params, useAxios) => {
+ const clienttime = Date.now();
+ const v_type = Number(params?.v_type || 23);
+ const userid = Number(params?.userid || params?.cookie?.userid || '0');
+ //
+ let dataMap = {
+ eventid: params?.eventid,
+ userid,
+ platid: params?.platid || 2,
+ v_type,
+ wasm: 1,
+ i: '',
+ sid: decodeURIComponent(params?.sid || ''),
+ edt: decodeURIComponent(params?.edt || ''),
+ };
+
+ if (v_type === 23) {
+ const encrypt = cryptoAesEncrypt({});
+ dataMap = {
+ ...dataMap,
+ verifycode: decodeURIComponent(params?.verifycode || ''),
+ pk: cryptoRSAEncrypt({ key: encrypt.key }),
+ params: encrypt.str,
+ };
+ }
+
+ if (v_type === 32) {
+ const encrypt = cryptoAesEncrypt({ code: params?.verifycode || '' });
+ dataMap = {
+ ...dataMap,
+ code: decodeURIComponent(params?.verifycode || ''),
+ pk: cryptoRSAEncrypt({ key: encrypt.key }),
+ params: encrypt.str,
+ };
+ }
+
+
+ return useAxios({
+ baseURL: 'https://verifyservice.kugou.com',
+ url: '/v4/verify_user_info',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ params: { clientver: 11510 },
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/video_detail.js b/third_party/MoeKoeMusic/api/module/video_detail.js
new file mode 100644
index 0000000..ba6b0c6
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/video_detail.js
@@ -0,0 +1,33 @@
+const { appid, clientver, signParamsKey, cryptoMd5 } = require('../util');
+// 获取视频详情
+module.exports = (params, useAxios) => {
+ const dfid = params?.cookie?.dfid || '-'; // 自定义
+ const mid = params?.cookie?.KUGOU_API_MID; // 可以自定义
+ const uuid = cryptoMd5(`${dfid}${mid}`); // 可以自定义
+ const token = params?.token || params?.cookie?.token || '';
+ const clienttime = Math.floor(new Date().getTime() / 1000);
+
+ const resource = (params.id || '').split(',').map((s) => ({ video_id: s }));
+
+ const dataMap = {
+ appid,
+ clientver,
+ clienttime,
+ mid,
+ uuid,
+ dfid,
+ token: token || '',
+ key: signParamsKey(clienttime.toString()),
+ show_resolution: 1,
+ data: resource,
+ };
+ return useAxios({
+ url: '/v1/video',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ clearDefaultParams: true,
+ headers: { 'x-router': 'kmr.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/video_privilege.js b/third_party/MoeKoeMusic/api/module/video_privilege.js
new file mode 100644
index 0000000..639a53d
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/video_privilege.js
@@ -0,0 +1,31 @@
+const { cryptoMd5, signParamsKey, clientver, appid } = require('../util');
+// 获取视频特权
+
+module.exports = (params, useAxios) => {
+ const dfid = params?.cookie?.dfid || '-';
+ const mid = params?.cookie?.KUGOU_API_MID;
+
+ const resource = (params?.hash || '').split(',').map((s) => ({ hash: s, id: 0, name: '' }));
+
+ const dataMap = {
+ appid,
+ area_code: 1,
+ behavior: 'play',
+ clientver,
+ dfid,
+ mid,
+ resource,
+ token: params?.cookie?.token || '',
+ userid: params?.cookie?.userid || 0,
+ vip: params?.cookie?.vip_type || 0,
+ };
+
+ return useAxios({
+ url: '/v1/get_video_privilege',
+ method: 'POST',
+ data: dataMap,
+ encryptType: 'android',
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'media.store.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/video_url.js b/third_party/MoeKoeMusic/api/module/video_url.js
new file mode 100644
index 0000000..7aaeaf3
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/video_url.js
@@ -0,0 +1,21 @@
+// 获取视频urls
+module.exports = (params, useAxios) => {
+ const paramsMap = {
+ backupdomain: 1,
+ cmd: 123,
+ ext: 'mp4',
+ ismp3: 0,
+ hash: params.hash,
+ pid: 1,
+ type: 1,
+ };
+
+ return useAxios({
+ url: '/v2/interface/index',
+ method: 'GET',
+ params: paramsMap,
+ encryptType: 'android',
+ encryptKey: true,
+ headers: { 'x-router': 'trackermv.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_channel_all.js b/third_party/MoeKoeMusic/api/module/youth_channel_all.js
new file mode 100644
index 0000000..aba1e92
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_channel_all.js
@@ -0,0 +1,9 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/youth/v2/channel/channel_all_list',
+ encryptType: 'android',
+ method: 'get',
+ params: { page: params.page || 1, pagesize: params.pagesize || 30, type: 1},
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_channel_amway.js b/third_party/MoeKoeMusic/api/module/youth_channel_amway.js
new file mode 100644
index 0000000..bb56835
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_channel_amway.js
@@ -0,0 +1,9 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/youth/api/amway/v2/index',
+ encryptType: 'android',
+ method: 'get',
+ params: { global_collection_id: params.global_collection_id },
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_channel_detail.js b/third_party/MoeKoeMusic/api/module/youth_channel_detail.js
new file mode 100644
index 0000000..9439916
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_channel_detail.js
@@ -0,0 +1,10 @@
+module.exports = (params, useAxios) => {
+ const data = (params.global_collection_id || '').split(',').map((s) => ({ global_collection_id: s }));
+ return useAxios({
+ url: '/youth/api/channel/v1/channel_list_by_id',
+ encryptType: 'android',
+ method: 'post',
+ data: { data },
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_channel_similar.js b/third_party/MoeKoeMusic/api/module/youth_channel_similar.js
new file mode 100644
index 0000000..963ac9f
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_channel_similar.js
@@ -0,0 +1,15 @@
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/youth/v1/channel/get_friendly_channel',
+ encryptType: 'android',
+ method: 'post',
+ data: {
+ area_code: 1,
+ playlist_ver: 2,
+ vip_type: params?.vip_type || params?.cookie?.vip_type || 0,
+ platform: 'ios'
+ },
+ params: { channel_id: params.channel_id },
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_channel_song.js b/third_party/MoeKoeMusic/api/module/youth_channel_song.js
new file mode 100644
index 0000000..bfdb157
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_channel_song.js
@@ -0,0 +1,16 @@
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ global_collection_id: params.global_collection_id,
+ pagesize: params.pagesize || 30,
+ page: params.page || 1,
+ is_filter: 0,
+ }
+ return useAxios({
+ url: '/youth/api/channel/v1/channel_get_song_audit_passed',
+ encryptType: 'android',
+ method: 'get',
+ params: dataMap,
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_channel_song_detail.js b/third_party/MoeKoeMusic/api/module/youth_channel_song_detail.js
new file mode 100644
index 0000000..d1d71e0
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_channel_song_detail.js
@@ -0,0 +1,14 @@
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ global_collection_id: params.global_collection_id,
+ fileid: params.fileid
+ }
+ return useAxios({
+ url: '/youth/v2/post/get_song_detail',
+ encryptType: 'android',
+ method: 'get',
+ params: dataMap,
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_channel_sub.js b/third_party/MoeKoeMusic/api/module/youth_channel_sub.js
new file mode 100644
index 0000000..e06658c
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_channel_sub.js
@@ -0,0 +1,10 @@
+module.exports = (params, useAxios) => {
+ const t = Number(params.t) === 0 ? 0 : 1;
+ return useAxios({
+ url: `/youth/v1/channel${t === 0 ? '_un' : ''}_subscribe`,
+ encryptType: 'android',
+ method: t === 0 ? 'delete': 'post',
+ params: { global_collection_id: params.global_collection_id, source: 1},
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_day_vip.js b/third_party/MoeKoeMusic/api/module/youth_day_vip.js
new file mode 100644
index 0000000..e84a37d
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_day_vip.js
@@ -0,0 +1,13 @@
+// 领取vip(领取一天) 需要登录
+const { srcappid } = require("../util");
+
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/youth/v1/recharge/receive_vip_listen_song',
+ encryptType: 'android',
+ method: 'post',
+ params: { source_id: 90139, receive_day: params.receive_day },
+ headers: {'content-type': 'application/x-www-form-urlencoded' },
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_day_vip_upgrade.js b/third_party/MoeKoeMusic/api/module/youth_day_vip_upgrade.js
new file mode 100644
index 0000000..a06e457
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_day_vip_upgrade.js
@@ -0,0 +1,20 @@
+const { srcappid } = require("../util");
+
+//升级vip
+module.exports = (params, useAxios) => {
+ const paramsMap = {
+ kugouid: Number(params?.userid || params?.cookie?.userid || 0),
+ ad_type: 1,
+ }
+
+
+
+
+ return useAxios({
+ url: '/youth/v1/listen_song/upgrade_vip_reward',
+ encryptType: 'android',
+ method: 'post',
+ params: paramsMap,
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_dynamic.js b/third_party/MoeKoeMusic/api/module/youth_dynamic.js
new file mode 100644
index 0000000..25480bc
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_dynamic.js
@@ -0,0 +1,9 @@
+
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/youth/v3/user/get_dynamic',
+ encryptType: 'android',
+ method: 'get',
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_dynamic_recent.js b/third_party/MoeKoeMusic/api/module/youth_dynamic_recent.js
new file mode 100644
index 0000000..9691609
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_dynamic_recent.js
@@ -0,0 +1,9 @@
+
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/youth/v3/user/recent_dynamic',
+ encryptType: 'android',
+ method: 'get',
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_listen_song.js b/third_party/MoeKoeMusic/api/module/youth_listen_song.js
new file mode 100644
index 0000000..62f7123
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_listen_song.js
@@ -0,0 +1,19 @@
+// 听歌领取vip 需要登录
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ mixsongid: params?.mixsongid || 666075191
+ }
+
+ return useAxios({
+ url: '/youth/v2/report/listen_song',
+ data: dataMap,
+ method: 'POST',
+ encryptTyPe: 'android',
+ params: { clientver: 10566 },
+ cookie: params?.cookie,
+ headers: {
+ "user-agent": "Android13-1070-10566-201-0-ReportPlaySongToServerProtocol-wifi",
+ "content-type": "application/json; charset=utf-8",
+ },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_month_vip_record.js b/third_party/MoeKoeMusic/api/module/youth_month_vip_record.js
new file mode 100644
index 0000000..369841d
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_month_vip_record.js
@@ -0,0 +1,11 @@
+
+module.exports = (params, useAxios) => {
+
+ return useAxios({
+ url: '/youth/v1/activity/get_month_vip_record',
+ encryptType: 'android',
+ params: { latest_limit: 100 },
+ method: 'get',
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_union_vip.js b/third_party/MoeKoeMusic/api/module/youth_union_vip.js
new file mode 100644
index 0000000..eb1a93a
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_union_vip.js
@@ -0,0 +1,17 @@
+// 领取vip 需要登录
+module.exports = (params, useAxios) => {
+ const paramsMap = {
+ busi_type: 'concept',
+ opt_product_types: 'dvip,qvip',
+ product_type: 'svip',
+ }
+
+ return useAxios({
+ baseURL: 'https://kugouvip.kugou.com',
+ url: '/v1/get_union_vip',
+ encryptType: 'android',
+ method: 'get',
+ params: paramsMap,
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_user_song.js b/third_party/MoeKoeMusic/api/module/youth_user_song.js
new file mode 100644
index 0000000..0aaa647
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_user_song.js
@@ -0,0 +1,18 @@
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ filter_video: 0,
+ type: params?.type || 0,
+ userid: params.userid,
+ pagesize: params.pagesize || 30,
+ page: params.page || 1,
+ is_filter: 0,
+ }
+ return useAxios({
+ url: '/youth/v1/get_user_song_public',
+ encryptType: 'android',
+ method: 'get',
+ params: dataMap,
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/youth_vip.js b/third_party/MoeKoeMusic/api/module/youth_vip.js
new file mode 100644
index 0000000..b5a66d1
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/youth_vip.js
@@ -0,0 +1,17 @@
+// 领取vip 需要登录
+module.exports = (params, useAxios) => {
+ const time = Date.now();
+ const dataMap = {
+ ad_id: 12307537187,
+ play_end: time,
+ play_start: time - 30000,
+ };
+
+ return useAxios({
+ url: '/youth/v1/ad/play_report',
+ encryptType: 'android',
+ method: 'post',
+ data: dataMap,
+ cookie: params?.cookie,
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/yueku.js b/third_party/MoeKoeMusic/api/module/yueku.js
new file mode 100644
index 0000000..1e97fb9
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/yueku.js
@@ -0,0 +1,12 @@
+// 获取安卓乐库相关内容
+
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/v1/yueku/recommend_v2',
+ encryptType: 'android',
+ method: 'GET',
+ params: { operator: 7, plat: 0, type: 11, area_code: 1, req_multi: 1 },
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'service.mobile.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/yueku_banner.js b/third_party/MoeKoeMusic/api/module/yueku_banner.js
new file mode 100644
index 0000000..f429842
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/yueku_banner.js
@@ -0,0 +1,25 @@
+// 获取乐库下的 banner
+
+module.exports = (params, useAxios) => {
+ const dataMap = {
+ plat: 0,
+ channel: 201,
+ operator: 7,
+ networktype: 2,
+ userid: params?.userid || params?.cookie?.userid || 0,
+ vip_type: 0,
+ m_type: 0,
+ tags: [],
+ apiver: 5,
+ ability: 2,
+ mode: 'normal',
+ };
+
+ return useAxios({
+ url: '/ads.gateway/v3/listen_banner',
+ encryptType: 'android',
+ method: 'POST',
+ data: dataMap,
+ cookie: params?.cookie || {},
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/module/yueku_fm.js b/third_party/MoeKoeMusic/api/module/yueku_fm.js
new file mode 100644
index 0000000..420a102
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/module/yueku_fm.js
@@ -0,0 +1,12 @@
+// 获取乐库下的 fm
+
+module.exports = (params, useAxios) => {
+ return useAxios({
+ url: '/v1/time_fm_info',
+ encryptType: 'android',
+ method: 'GET',
+ params: { operator: 7, plat: 0, type: 11, area_code: 1, req_multi: 1 },
+ cookie: params?.cookie || {},
+ headers: { 'x-router': 'fm.service.kugou.com' },
+ });
+};
diff --git a/third_party/MoeKoeMusic/api/nodemon.json b/third_party/MoeKoeMusic/api/nodemon.json
new file mode 100644
index 0000000..74f5df8
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/nodemon.json
@@ -0,0 +1,25 @@
+{
+ "restartable": "rs",
+ "ignore": [
+ ".git",
+ "node_modules/",
+ "dist/",
+ "coverage/"
+ ],
+ "watch": [
+ "module/**",
+ "plugins/**",
+ "util/**",
+ "types/**",
+ "./server.js",
+ "./index.js",
+ "./main.js"
+ ],
+ "execMap": {
+ "js": "node "
+ },
+ "env": {
+ "NODE_ENV": "development"
+ },
+ "ext": "js,json,ts"
+}
\ No newline at end of file
diff --git a/third_party/MoeKoeMusic/api/package.json b/third_party/MoeKoeMusic/api/package.json
new file mode 100644
index 0000000..cab7953
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "kugoumusicapi",
+ "version": "1.6.0",
+ "description": "",
+ "main": "main.js",
+ "bin": "./app.js",
+ "types": "./interface.d.ts",
+ "pkg": {
+ "scripts": "module/*.js",
+ "assets": [
+ "node_modules/axios",
+ "node_modules/express",
+ "node_modules/pako",
+ "node_modules/qrcode",
+ "node_modules/safe-decode-uri-component",
+ "module",
+ "public"
+ ]
+ },
+ "scripts": {
+ "dev": "nodemon --config nodemon.json index.js",
+ "test:ymhut": "node --test test/ymhut-sidecar.test.js",
+ "start": "node app.js",
+ "pkgwin": "pkg . -t node14-win-x64 -C GZip -o bin/app_win --no-bytecode",
+ "pkglinux": "pkg . -t node14-linux-x64 -C GZip -o bin/app_linux --no-bytecode",
+ "pkglinux-arm64": "pkg . -t node14-linux-arm64 -C GZip -o bin/app_linux --no-bytecode",
+ "pkgmacos": "pkg . -t node14-macos-x64 -C GZip -o bin/app_macos --no-bytecode",
+ "pkgjs": "tsdown"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "keywords": [
+ "酷狗音乐",
+ "酷狗",
+ "音乐",
+ "酷狗音乐nodejs"
+ ],
+ "author": "Lines",
+ "license": "MIT",
+ "packageManager": "pnpm@10.14.0",
+ "devDependencies": {
+ "@types/express": "^4.17.17",
+ "@types/node": "^18.11.9",
+ "@types/pako": "^2.0.0",
+ "@types/qrcode": "^1.5.0",
+ "nodemon": "^3.1.10",
+ "pkg": "^5.8.1",
+ "prettier": "^2.7.1",
+ "ts-node": "^10.9.1",
+ "tsdown": "^0.22.3",
+ "typescript": "^5.9.3"
+ },
+ "dependencies": {
+ "axios": "^1.1.3",
+ "big-integer": "^1.6.52",
+ "crypto-js": "^4.2.0",
+ "dotenv": "^16.4.5",
+ "express": "^4.18.2",
+ "node-forge": "^1.3.3",
+ "pako": "^2.1.0",
+ "qrcode": "^1.5.3",
+ "safe-decode-uri-component": "^1.2.1",
+ "url": "^0.11.4"
+ }
+}
diff --git a/third_party/MoeKoeMusic/api/pnpm-lock.yaml b/third_party/MoeKoeMusic/api/pnpm-lock.yaml
new file mode 100644
index 0000000..c486180
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/pnpm-lock.yaml
@@ -0,0 +1,2747 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ axios:
+ specifier: ^1.1.3
+ version: 1.10.0
+ big-integer:
+ specifier: ^1.6.52
+ version: 1.6.52
+ crypto-js:
+ specifier: ^4.2.0
+ version: 4.2.0
+ dotenv:
+ specifier: ^16.4.5
+ version: 16.6.1
+ express:
+ specifier: ^4.18.2
+ version: 4.21.2
+ node-forge:
+ specifier: ^1.3.3
+ version: 1.3.3
+ pako:
+ specifier: ^2.1.0
+ version: 2.1.0
+ qrcode:
+ specifier: ^1.5.3
+ version: 1.5.4
+ safe-decode-uri-component:
+ specifier: ^1.2.1
+ version: 1.2.1
+ url:
+ specifier: ^0.11.4
+ version: 0.11.4
+ devDependencies:
+ '@types/express':
+ specifier: ^4.17.17
+ version: 4.17.23
+ '@types/node':
+ specifier: ^18.11.9
+ version: 18.19.115
+ '@types/pako':
+ specifier: ^2.0.0
+ version: 2.0.3
+ '@types/qrcode':
+ specifier: ^1.5.0
+ version: 1.5.5
+ nodemon:
+ specifier: ^3.1.10
+ version: 3.1.10
+ pkg:
+ specifier: ^5.8.1
+ version: 5.8.1(supports-color@5.5.0)
+ prettier:
+ specifier: ^2.7.1
+ version: 2.8.8
+ ts-node:
+ specifier: ^10.9.1
+ version: 10.9.2(@types/node@18.19.115)(typescript@5.9.3)
+ tsdown:
+ specifier: ^0.22.3
+ version: 0.22.3(typescript@5.9.3)
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+
+packages:
+
+ '@babel/generator@7.18.2':
+ resolution: {integrity: sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@8.0.0':
+ resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+
+ '@babel/helper-string-parser@7.27.1':
+ resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@8.0.0':
+ resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+
+ '@babel/helper-validator-identifier@7.27.1':
+ resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@8.0.0':
+ resolution: {integrity: sha512-kXxQVZHNOctSJJsqzmcbPSCEkM6oHNnDIkua7g9RCO9xRHj2eCiKvRx2KPdfWR9QxcGWnK/oArrtunmie3rL9g==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+
+ '@babel/parser@7.18.4':
+ resolution: {integrity: sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/parser@8.0.0':
+ resolution: {integrity: sha512-aLxAE+imI9bCcyaPrUDjBv3uSkWieifjLe0kuFOZF0zli0L6GCsTmsePnTr55adbIAgYz2zhN1vnFimCBUYcRQ==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+ hasBin: true
+
+ '@babel/types@7.19.0':
+ resolution: {integrity: sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@8.0.0':
+ resolution: {integrity: sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+
+ '@cspotcode/source-map-support@0.8.1':
+ resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
+ engines: {node: '>=12'}
+
+ '@emnapi/core@1.11.0':
+ resolution: {integrity: sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==}
+
+ '@emnapi/runtime@1.11.0':
+ resolution: {integrity: sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==}
+
+ '@emnapi/wasi-threads@1.2.2':
+ resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
+
+ '@jridgewell/gen-mapping@0.3.12':
+ resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/sourcemap-codec@1.5.4':
+ resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
+
+ '@jridgewell/trace-mapping@0.3.29':
+ resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
+
+ '@jridgewell/trace-mapping@0.3.9':
+ resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
+
+ '@napi-rs/wasm-runtime@1.1.5':
+ resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1
+ '@emnapi/runtime': ^1.7.1
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@oxc-project/types@0.135.0':
+ resolution: {integrity: sha512-wR+xRdFkUBMvcAjBJ2q2kcZM6d+DKu2NgoOyxZgYwZdLhmiv6+rnO8PZ/P68kMiZtIKm+pW7zyEJ4kSOs0vo+Q==}
+
+ '@quansync/fs@1.0.0':
+ resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
+
+ '@rolldown/binding-android-arm64@1.1.1':
+ resolution: {integrity: sha512-BLf9Wak/gfwVb7NQTQW4wBgL3oAfPy7ArEkhwV543OVw/uY6B47z5xYsqPSZ9PDOorvURPinws6ThaFuNgGLgA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.1.1':
+ resolution: {integrity: sha512-rRZRPy/Ynb+Mxu0O6tfPldHeDgAn0sRij+IOUy6sFdUlv3hArGW/DloE3GfAxtqpOJuRNgF74Nr5gM4xBeU2jQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.1.1':
+ resolution: {integrity: sha512-/MtefPxhKPyWWFM8L45OWiEqRf+eSU2Qv9ZAyTaoZOoGcoPKxbbhjTJO2/U2IThv0uDZ4NWHc3/oTsR6IEOtww==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.1.1':
+ resolution: {integrity: sha512-202K+cpIi1kx/Zn7AtxBi4LTXSY67Aszb2K9rNsuW7FeBeh0nqoNmYLOSZidV0p88VPBzMmTZcHAdPNo3kRYzQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.1':
+ resolution: {integrity: sha512-wl9NfeXNUwrXtUc063tddmZFUI6qiNs1CNOwni0OL4vC7MqVSYugra3ZgtDmtVy8e0DluJTENmzIv2BwqLzT4Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.1':
+ resolution: {integrity: sha512-at2EO4o7D/PJLC4Xik16bU4CcjQE2tSv1LfqMA0TRYQYQihRm3gZeDB8xaX28A9SFedibcAk5DeMCKt4REKG0A==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.1.1':
+ resolution: {integrity: sha512-5PUjZx366h9tkJTPJF5eibxOlK3sGoeRiBJLLjjEB5/kLDuhr6qB3LkhqLz1smXNgsX+pBhnbcJBrPE30HznAA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.1':
+ resolution: {integrity: sha512-1WK84XPeio3tjP1sM/TMXiC0G1i1iq1qGZ71KfNQjEFLU1kwD+Cv5T8nGySg/JUFwLbaScu6ve9DmeXlmqpkFA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.1':
+ resolution: {integrity: sha512-1nS1X5z1uMJ369RU25hTpKCFvUwXZp12dIzlzk4S+UxCTcSVGsAE6tzkOSufv/7jnmAtK0ZlrsJxh2fGmsnVSw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.1.1':
+ resolution: {integrity: sha512-NwX/wspnq4vYyMFsqbYvzums3ki/Tk8FZbMzMAovPDp3OfLeYKby/D+9osokadXuYEV3OvpeHlwnr/bG8QMixA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.1.1':
+ resolution: {integrity: sha512-+n46LhDrJFQM+229y4oXtVpj1G50U/+XuHMlpnisFTEXhrg9f/YIjp/HymX+PVJjBEr7XHRs3CFLelV464pqwA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.1.1':
+ resolution: {integrity: sha512-qGwEu47zOWYo7LdRHhCWTNhzwGtxXpdY6CERs8QEOqC0PXGGics/e3vHnyEUKt8xK6YkbZXFUCeklrpB6js8ag==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-wasm32-wasi@1.1.1':
+ resolution: {integrity: sha512-qczfgEH8u0wHGGOXtA7UMAybNKuQjjEXairyQaw4WzjiMztfbgatG1h4OKays/smhtwbWltpKCRGtVhU6h40Sg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [wasm32]
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.1':
+ resolution: {integrity: sha512-4psXSh63mSbwJF+mB8/9yfUUEzBiHYcUjxa32EO9ZwKy0Ypwjcg4F10D8SvVXgd+isy2UUUjF9HJJnDu1T/4Gg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.1.1':
+ resolution: {integrity: sha512-MUvC/HLXVjzkQkWiExdVTEEWf0py+GfWm8WKSZsekG3ih6a21iy0BHPF07X3JIf3ifoklZXTIaHTLPBgH1C3dw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@tsconfig/node10@1.0.11':
+ resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
+
+ '@tsconfig/node12@1.0.11':
+ resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
+
+ '@tsconfig/node14@1.0.3':
+ resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
+
+ '@tsconfig/node16@1.0.4':
+ resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
+
+ '@tybys/wasm-util@0.10.2':
+ resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
+
+ '@types/body-parser@1.19.6':
+ resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
+
+ '@types/connect@3.4.38':
+ resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/express-serve-static-core@4.19.6':
+ resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==}
+
+ '@types/express@4.17.23':
+ resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==}
+
+ '@types/http-errors@2.0.5':
+ resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
+
+ '@types/jsesc@2.5.1':
+ resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==}
+
+ '@types/mime@1.3.5':
+ resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
+
+ '@types/node@18.19.115':
+ resolution: {integrity: sha512-kNrFiTgG4a9JAn1LMQeLOv3MvXIPokzXziohMrMsvpYgLpdEt/mMiVYc4sGKtDfyxM5gIDF4VgrPRyCw4fHOYg==}
+
+ '@types/pako@2.0.3':
+ resolution: {integrity: sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==}
+
+ '@types/qrcode@1.5.5':
+ resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
+
+ '@types/qs@6.14.0':
+ resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
+
+ '@types/range-parser@1.2.7':
+ resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
+
+ '@types/send@0.17.5':
+ resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==}
+
+ '@types/serve-static@1.15.8':
+ resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==}
+
+ accepts@1.3.8:
+ resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
+ engines: {node: '>= 0.6'}
+
+ acorn-walk@8.3.4:
+ resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
+ engines: {node: '>=0.4.0'}
+
+ acorn@8.15.0:
+ resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ agent-base@6.0.2:
+ resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
+ engines: {node: '>= 6.0.0'}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ ansis@4.3.1:
+ resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==}
+ engines: {node: '>=14'}
+
+ anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+
+ arg@4.1.3:
+ resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
+
+ array-flatten@1.1.1:
+ resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
+
+ array-union@2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+
+ ast-kit@3.0.0:
+ resolution: {integrity: sha512-8OG92q3R35qjC/4i6BLBMg8IB+fClWu/1PEwg2Z9Rn+BuNaiEgJzpzn+pxWOdHJWDCAwu2JP0wCDTozAM4QirQ==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ at-least-node@1.0.0:
+ resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
+ engines: {node: '>= 4.0.0'}
+
+ axios@1.10.0:
+ resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==}
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
+ big-integer@1.6.52:
+ resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
+ engines: {node: '>=0.6'}
+
+ binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+
+ birpc@4.0.0:
+ resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==}
+
+ bl@4.1.0:
+ resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
+
+ body-parser@1.20.3:
+ resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
+ engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+
+ brace-expansion@1.1.12:
+ resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ buffer@5.7.1:
+ resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
+
+ bytes@3.1.2:
+ resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
+ engines: {node: '>= 0.8'}
+
+ cac@7.0.0:
+ resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
+ engines: {node: '>=20.19.0'}
+
+ call-bind-apply-helpers@1.0.2:
+ resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+ engines: {node: '>= 0.4'}
+
+ call-bound@1.0.4:
+ resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+ engines: {node: '>= 0.4'}
+
+ camelcase@5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ chokidar@3.6.0:
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+ engines: {node: '>= 8.10.0'}
+
+ chownr@1.1.4:
+ resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
+
+ cliui@6.0.0:
+ resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
+
+ cliui@7.0.4:
+ resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ content-disposition@0.5.4:
+ resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
+ engines: {node: '>= 0.6'}
+
+ content-type@1.0.5:
+ resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
+ engines: {node: '>= 0.6'}
+
+ cookie-signature@1.0.6:
+ resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
+
+ cookie@0.7.1:
+ resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
+ engines: {node: '>= 0.6'}
+
+ core-util-is@1.0.3:
+ resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+
+ create-require@1.1.1:
+ resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
+
+ crypto-js@4.2.0:
+ resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
+
+ debug@2.6.9:
+ resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ debug@4.4.1:
+ resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decamelize@1.2.0:
+ resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
+ engines: {node: '>=0.10.0'}
+
+ decompress-response@6.0.0:
+ resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
+ engines: {node: '>=10'}
+
+ deep-extend@0.6.0:
+ resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
+ engines: {node: '>=4.0.0'}
+
+ defu@6.1.7:
+ resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
+ depd@2.0.0:
+ resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
+ engines: {node: '>= 0.8'}
+
+ destroy@1.2.0:
+ resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
+ engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+
+ detect-libc@2.0.4:
+ resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
+ engines: {node: '>=8'}
+
+ diff@4.0.2:
+ resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
+ engines: {node: '>=0.3.1'}
+
+ dijkstrajs@1.0.3:
+ resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
+
+ dir-glob@3.0.1:
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+ engines: {node: '>=8'}
+
+ dotenv@16.6.1:
+ resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
+ engines: {node: '>=12'}
+
+ dts-resolver@3.0.0:
+ resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==}
+ engines: {node: ^22.18.0 || >=24.0.0}
+ peerDependencies:
+ oxc-resolver: '>=11.0.0'
+ peerDependenciesMeta:
+ oxc-resolver:
+ optional: true
+
+ dunder-proto@1.0.1:
+ resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+ engines: {node: '>= 0.4'}
+
+ ee-first@1.1.1:
+ resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ empathic@2.0.1:
+ resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==}
+ engines: {node: '>=14'}
+
+ encodeurl@1.0.2:
+ resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
+ engines: {node: '>= 0.8'}
+
+ encodeurl@2.0.0:
+ resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
+ engines: {node: '>= 0.8'}
+
+ end-of-stream@1.4.5:
+ resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
+
+ es-define-property@1.0.1:
+ resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-object-atoms@1.1.1:
+ resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+ engines: {node: '>= 0.4'}
+
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ etag@1.8.1:
+ resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
+ engines: {node: '>= 0.6'}
+
+ expand-template@2.0.3:
+ resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
+ engines: {node: '>=6'}
+
+ express@4.21.2:
+ resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
+ engines: {node: '>= 0.10.0'}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fastq@1.19.1:
+ resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ finalhandler@1.3.1:
+ resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
+ engines: {node: '>= 0.8'}
+
+ find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+
+ follow-redirects@1.15.9:
+ resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
+ form-data@4.0.3:
+ resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==}
+ engines: {node: '>= 6'}
+
+ forwarded@0.2.0:
+ resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
+ engines: {node: '>= 0.6'}
+
+ fresh@0.5.2:
+ resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
+ engines: {node: '>= 0.6'}
+
+ from2@2.3.0:
+ resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==}
+
+ fs-constants@1.0.0:
+ resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
+
+ fs-extra@9.1.0:
+ resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
+ engines: {node: '>=10'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-intrinsic@1.3.0:
+ resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+ engines: {node: '>= 0.4'}
+
+ get-proto@1.0.1:
+ resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+ engines: {node: '>= 0.4'}
+
+ get-tsconfig@5.0.0-beta.5:
+ resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==}
+ engines: {node: '>=20.20.0'}
+
+ github-from-package@0.0.0:
+ resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ globby@11.1.0:
+ resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
+ engines: {node: '>=10'}
+
+ gopd@1.2.0:
+ resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+ engines: {node: '>= 0.4'}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ has-flag@3.0.0:
+ resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
+ engines: {node: '>=4'}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-symbols@1.1.0:
+ resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+ engines: {node: '>= 0.4'}
+
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
+ has@1.0.4:
+ resolution: {integrity: sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==}
+ engines: {node: '>= 0.4.0'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
+ hookable@6.1.1:
+ resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==}
+
+ http-errors@2.0.0:
+ resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
+ engines: {node: '>= 0.8'}
+
+ https-proxy-agent@5.0.1:
+ resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
+ engines: {node: '>= 6'}
+
+ iconv-lite@0.4.24:
+ resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
+ engines: {node: '>=0.10.0'}
+
+ ieee754@1.2.1:
+ resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+
+ ignore-by-default@1.0.1:
+ resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ import-without-cache@0.4.0:
+ resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==}
+ engines: {node: ^22.18.0 || >=24.0.0}
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+
+ into-stream@6.0.0:
+ resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==}
+ engines: {node: '>=10'}
+
+ ipaddr.js@1.9.1:
+ resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
+ engines: {node: '>= 0.10'}
+
+ is-binary-path@2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+
+ is-core-module@2.16.1:
+ resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
+ engines: {node: '>= 0.4'}
+
+ is-core-module@2.9.0:
+ resolution: {integrity: sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ isarray@1.0.0:
+ resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+
+ jsesc@2.5.2:
+ resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ jsesc@3.1.0:
+ resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ jsonfile@6.1.0:
+ resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
+
+ locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+
+ make-error@1.3.6:
+ resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
+
+ math-intrinsics@1.1.0:
+ resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+ engines: {node: '>= 0.4'}
+
+ media-typer@0.3.0:
+ resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
+ engines: {node: '>= 0.6'}
+
+ merge-descriptors@1.0.3:
+ resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ methods@1.1.2:
+ resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
+ engines: {node: '>= 0.6'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ mime@1.6.0:
+ resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ mimic-response@3.1.0:
+ resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
+ engines: {node: '>=10'}
+
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+ mkdirp-classic@0.5.3:
+ resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
+
+ ms@2.0.0:
+ resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ multistream@4.1.0:
+ resolution: {integrity: sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==}
+
+ napi-build-utils@1.0.2:
+ resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
+
+ negotiator@0.6.3:
+ resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
+ engines: {node: '>= 0.6'}
+
+ node-abi@3.75.0:
+ resolution: {integrity: sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==}
+ engines: {node: '>=10'}
+
+ node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+
+ node-forge@1.3.3:
+ resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
+ engines: {node: '>= 6.13.0'}
+
+ nodemon@3.1.10:
+ resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
+ object-inspect@1.13.4:
+ resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+ engines: {node: '>= 0.4'}
+
+ obug@2.1.3:
+ resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==}
+ engines: {node: '>=12.20.0'}
+
+ on-finished@2.4.1:
+ resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
+ engines: {node: '>= 0.8'}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ p-is-promise@3.0.0:
+ resolution: {integrity: sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==}
+ engines: {node: '>=8'}
+
+ p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+
+ p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+
+ p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+
+ pako@2.1.0:
+ resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
+
+ parseurl@1.3.3:
+ resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
+ engines: {node: '>= 0.8'}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
+ path-to-regexp@0.1.12:
+ resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
+
+ path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.4:
+ resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
+ engines: {node: '>=12'}
+
+ pkg-fetch@3.4.2:
+ resolution: {integrity: sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==}
+ hasBin: true
+
+ pkg@5.8.1:
+ resolution: {integrity: sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==}
+ hasBin: true
+ peerDependencies:
+ node-notifier: '>=9.0.1'
+ peerDependenciesMeta:
+ node-notifier:
+ optional: true
+
+ pngjs@5.0.0:
+ resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
+ engines: {node: '>=10.13.0'}
+
+ prebuild-install@7.1.1:
+ resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
+ engines: {node: '>=10'}
+ deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
+ hasBin: true
+
+ prettier@2.8.8:
+ resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+
+ process-nextick-args@2.0.1:
+ resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+
+ progress@2.0.3:
+ resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
+ engines: {node: '>=0.4.0'}
+
+ proxy-addr@2.0.7:
+ resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ engines: {node: '>= 0.10'}
+
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+ pstree.remy@1.1.8:
+ resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
+
+ pump@3.0.3:
+ resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
+
+ punycode@1.4.1:
+ resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
+
+ qrcode@1.5.4:
+ resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+
+ qs@6.13.0:
+ resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
+ engines: {node: '>=0.6'}
+
+ quansync@1.0.0:
+ resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ range-parser@1.2.1:
+ resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
+ engines: {node: '>= 0.6'}
+
+ raw-body@2.5.2:
+ resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
+ engines: {node: '>= 0.8'}
+
+ rc@1.2.8:
+ resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
+ hasBin: true
+
+ readable-stream@2.3.8:
+ resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+
+ readable-stream@3.6.2:
+ resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+ engines: {node: '>= 6'}
+
+ readdirp@3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ require-main-filename@2.0.0:
+ resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
+
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+ resolve@1.22.10:
+ resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
+ reusify@1.1.0:
+ resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rolldown-plugin-dts@0.26.0:
+ resolution: {integrity: sha512-e+kEPtUiDES0htk5iqkSeF4EzAV7R+vugGB44iPDuw1Kw9E+WyL1VG7PaV0IIjGHLiacztMBcMTyrr8ON9CT1Q==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+ peerDependencies:
+ '@ts-macro/tsc': ^0.3.6
+ '@typescript/native-preview': '>=7.0.0-dev.20260325.1'
+ rolldown: ^1.0.0
+ typescript: ^5.0.0 || ^6.0.0
+ vue-tsc: ~3.2.0 || ~3.3.0
+ peerDependenciesMeta:
+ '@ts-macro/tsc':
+ optional: true
+ '@typescript/native-preview':
+ optional: true
+ typescript:
+ optional: true
+ vue-tsc:
+ optional: true
+
+ rolldown@1.1.1:
+ resolution: {integrity: sha512-IN750c0p+s3jqJIsFLRZrQazmbAB1kkQDTtQjSt/gbS2ywLhlv4R5Shazer0FZKmuo/BsO3/w2UoYnUjuOZqHg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ safe-buffer@5.1.2:
+ resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
+ safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+ safe-decode-uri-component@1.2.1:
+ resolution: {integrity: sha512-j0PKk0v8qPOD0goqRAvoI7GKy+HwLHXEpXaQlA1IqJ65jjiILRK1InlfNdIKUUUYuLtERADaJumgYKNfyQBO+w==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ semver@7.7.2:
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ semver@7.8.4:
+ resolution: {integrity: sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ send@0.19.0:
+ resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
+ engines: {node: '>= 0.8.0'}
+
+ serve-static@1.16.2:
+ resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
+ engines: {node: '>= 0.8.0'}
+
+ set-blocking@2.0.0:
+ resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+
+ setprototypeof@1.2.0:
+ resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+
+ side-channel-list@1.0.0:
+ resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-map@1.0.1:
+ resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+ engines: {node: '>= 0.4'}
+
+ side-channel-weakmap@1.0.2:
+ resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+ engines: {node: '>= 0.4'}
+
+ side-channel@1.1.0:
+ resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+ engines: {node: '>= 0.4'}
+
+ simple-concat@1.0.1:
+ resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
+
+ simple-get@4.0.1:
+ resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
+
+ simple-update-notifier@2.0.0:
+ resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
+ engines: {node: '>=10'}
+
+ slash@3.0.0:
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+ engines: {node: '>=8'}
+
+ statuses@2.0.1:
+ resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
+ engines: {node: '>= 0.8'}
+
+ stream-meter@1.0.4:
+ resolution: {integrity: sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string_decoder@1.1.1:
+ resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+
+ string_decoder@1.3.0:
+ resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-json-comments@2.0.1:
+ resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
+ engines: {node: '>=0.10.0'}
+
+ supports-color@5.5.0:
+ resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
+ engines: {node: '>=4'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
+ tar-fs@2.1.3:
+ resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==}
+
+ tar-stream@2.2.0:
+ resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
+ engines: {node: '>=6'}
+
+ tinyexec@1.2.4:
+ resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ to-fast-properties@2.0.0:
+ resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
+ engines: {node: '>=4'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ toidentifier@1.0.1:
+ resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
+ engines: {node: '>=0.6'}
+
+ touch@3.1.1:
+ resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
+ hasBin: true
+
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+ tree-kill@1.2.2:
+ resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
+ hasBin: true
+
+ ts-node@10.9.2:
+ resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
+ hasBin: true
+ peerDependencies:
+ '@swc/core': '>=1.2.50'
+ '@swc/wasm': '>=1.2.50'
+ '@types/node': '*'
+ typescript: '>=2.7'
+ peerDependenciesMeta:
+ '@swc/core':
+ optional: true
+ '@swc/wasm':
+ optional: true
+
+ tsdown@0.22.3:
+ resolution: {integrity: sha512-louqbfA8Qf//B9jTTL0FPtXTNpjCWv1VPkbcmQMph2pTpzs+LnB1tbe4tDDRVpo2BjF5SgUXaTZe45SxB8pWHg==}
+ engines: {node: ^22.18.0 || >=24.11.0}
+ hasBin: true
+ peerDependencies:
+ '@arethetypeswrong/core': ^0.18.1
+ '@tsdown/css': 0.22.3
+ '@tsdown/exe': 0.22.3
+ '@vitejs/devtools': '*'
+ publint: ^0.3.8
+ tsx: '*'
+ typescript: ^5.0.0 || ^6.0.0
+ unplugin-unused: ^0.5.0
+ unrun: '*'
+ peerDependenciesMeta:
+ '@arethetypeswrong/core':
+ optional: true
+ '@tsdown/css':
+ optional: true
+ '@tsdown/exe':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ publint:
+ optional: true
+ tsx:
+ optional: true
+ typescript:
+ optional: true
+ unplugin-unused:
+ optional: true
+ unrun:
+ optional: true
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ tunnel-agent@0.6.0:
+ resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
+
+ type-is@1.6.18:
+ resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
+ engines: {node: '>= 0.6'}
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ unconfig-core@7.5.0:
+ resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==}
+
+ undefsafe@2.0.5:
+ resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
+
+ undici-types@5.26.5:
+ resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
+
+ universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+
+ unpipe@1.0.0:
+ resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
+ engines: {node: '>= 0.8'}
+
+ url@0.11.4:
+ resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
+ engines: {node: '>= 0.4'}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ utils-merge@1.0.1:
+ resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
+ engines: {node: '>= 0.4.0'}
+
+ v8-compile-cache-lib@3.0.1:
+ resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
+
+ vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+
+ webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+ whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
+ which-module@2.0.1:
+ resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
+
+ wrap-ansi@6.2.0:
+ resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ engines: {node: '>=8'}
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ y18n@4.0.3:
+ resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
+ yargs-parser@18.1.3:
+ resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
+ engines: {node: '>=6'}
+
+ yargs-parser@20.2.9:
+ resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
+ engines: {node: '>=10'}
+
+ yargs@15.4.1:
+ resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
+ engines: {node: '>=8'}
+
+ yargs@16.2.0:
+ resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
+ engines: {node: '>=10'}
+
+ yn@3.1.1:
+ resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
+ engines: {node: '>=6'}
+
+snapshots:
+
+ '@babel/generator@7.18.2':
+ dependencies:
+ '@babel/types': 7.19.0
+ '@jridgewell/gen-mapping': 0.3.12
+ jsesc: 2.5.2
+
+ '@babel/generator@8.0.0':
+ dependencies:
+ '@babel/parser': 8.0.0
+ '@babel/types': 8.0.0
+ '@jridgewell/gen-mapping': 0.3.12
+ '@jridgewell/trace-mapping': 0.3.29
+ '@types/jsesc': 2.5.1
+ jsesc: 3.1.0
+
+ '@babel/helper-string-parser@7.27.1': {}
+
+ '@babel/helper-string-parser@8.0.0': {}
+
+ '@babel/helper-validator-identifier@7.27.1': {}
+
+ '@babel/helper-validator-identifier@8.0.0': {}
+
+ '@babel/parser@7.18.4':
+ dependencies:
+ '@babel/types': 7.19.0
+
+ '@babel/parser@8.0.0':
+ dependencies:
+ '@babel/types': 8.0.0
+
+ '@babel/types@7.19.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.27.1
+ '@babel/helper-validator-identifier': 7.27.1
+ to-fast-properties: 2.0.0
+
+ '@babel/types@8.0.0':
+ dependencies:
+ '@babel/helper-string-parser': 8.0.0
+ '@babel/helper-validator-identifier': 8.0.0
+
+ '@cspotcode/source-map-support@0.8.1':
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.9
+
+ '@emnapi/core@1.11.0':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.2
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.11.0':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@jridgewell/gen-mapping@0.3.12':
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.4
+ '@jridgewell/trace-mapping': 0.3.29
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/sourcemap-codec@1.5.4': {}
+
+ '@jridgewell/trace-mapping@0.3.29':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.4
+
+ '@jridgewell/trace-mapping@0.3.9':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.4
+
+ '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)':
+ dependencies:
+ '@emnapi/core': 1.11.0
+ '@emnapi/runtime': 1.11.0
+ '@tybys/wasm-util': 0.10.2
+ optional: true
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.19.1
+
+ '@oxc-project/types@0.135.0': {}
+
+ '@quansync/fs@1.0.0':
+ dependencies:
+ quansync: 1.0.0
+
+ '@rolldown/binding-android-arm64@1.1.1':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.1.1':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.1.1':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.1.1':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.1.1':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.1.1':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.1.1':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.1.1':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.1.1':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.1.1':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.1.1':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.1.1':
+ optional: true
+
+ '@rolldown/binding-wasm32-wasi@1.1.1':
+ dependencies:
+ '@emnapi/core': 1.11.0
+ '@emnapi/runtime': 1.11.0
+ '@napi-rs/wasm-runtime': 1.1.5(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.1.1':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.1.1':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@tsconfig/node10@1.0.11': {}
+
+ '@tsconfig/node12@1.0.11': {}
+
+ '@tsconfig/node14@1.0.3': {}
+
+ '@tsconfig/node16@1.0.4': {}
+
+ '@tybys/wasm-util@0.10.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/body-parser@1.19.6':
+ dependencies:
+ '@types/connect': 3.4.38
+ '@types/node': 18.19.115
+
+ '@types/connect@3.4.38':
+ dependencies:
+ '@types/node': 18.19.115
+
+ '@types/estree@1.0.9': {}
+
+ '@types/express-serve-static-core@4.19.6':
+ dependencies:
+ '@types/node': 18.19.115
+ '@types/qs': 6.14.0
+ '@types/range-parser': 1.2.7
+ '@types/send': 0.17.5
+
+ '@types/express@4.17.23':
+ dependencies:
+ '@types/body-parser': 1.19.6
+ '@types/express-serve-static-core': 4.19.6
+ '@types/qs': 6.14.0
+ '@types/serve-static': 1.15.8
+
+ '@types/http-errors@2.0.5': {}
+
+ '@types/jsesc@2.5.1': {}
+
+ '@types/mime@1.3.5': {}
+
+ '@types/node@18.19.115':
+ dependencies:
+ undici-types: 5.26.5
+
+ '@types/pako@2.0.3': {}
+
+ '@types/qrcode@1.5.5':
+ dependencies:
+ '@types/node': 18.19.115
+
+ '@types/qs@6.14.0': {}
+
+ '@types/range-parser@1.2.7': {}
+
+ '@types/send@0.17.5':
+ dependencies:
+ '@types/mime': 1.3.5
+ '@types/node': 18.19.115
+
+ '@types/serve-static@1.15.8':
+ dependencies:
+ '@types/http-errors': 2.0.5
+ '@types/node': 18.19.115
+ '@types/send': 0.17.5
+
+ accepts@1.3.8:
+ dependencies:
+ mime-types: 2.1.35
+ negotiator: 0.6.3
+
+ acorn-walk@8.3.4:
+ dependencies:
+ acorn: 8.15.0
+
+ acorn@8.15.0: {}
+
+ agent-base@6.0.2(supports-color@5.5.0):
+ dependencies:
+ debug: 4.4.1(supports-color@5.5.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ ansi-regex@5.0.1: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ ansis@4.3.1: {}
+
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+
+ arg@4.1.3: {}
+
+ array-flatten@1.1.1: {}
+
+ array-union@2.1.0: {}
+
+ ast-kit@3.0.0:
+ dependencies:
+ '@babel/parser': 8.0.0
+ estree-walker: 3.0.3
+ pathe: 2.0.3
+
+ asynckit@0.4.0: {}
+
+ at-least-node@1.0.0: {}
+
+ axios@1.10.0:
+ dependencies:
+ follow-redirects: 1.15.9
+ form-data: 4.0.3
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
+ balanced-match@1.0.2: {}
+
+ base64-js@1.5.1: {}
+
+ big-integer@1.6.52: {}
+
+ binary-extensions@2.3.0: {}
+
+ birpc@4.0.0: {}
+
+ bl@4.1.0:
+ dependencies:
+ buffer: 5.7.1
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+
+ body-parser@1.20.3:
+ dependencies:
+ bytes: 3.1.2
+ content-type: 1.0.5
+ debug: 2.6.9
+ depd: 2.0.0
+ destroy: 1.2.0
+ http-errors: 2.0.0
+ iconv-lite: 0.4.24
+ on-finished: 2.4.1
+ qs: 6.13.0
+ raw-body: 2.5.2
+ type-is: 1.6.18
+ unpipe: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ brace-expansion@1.1.12:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ buffer@5.7.1:
+ dependencies:
+ base64-js: 1.5.1
+ ieee754: 1.2.1
+
+ bytes@3.1.2: {}
+
+ cac@7.0.0: {}
+
+ call-bind-apply-helpers@1.0.2:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+
+ call-bound@1.0.4:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ get-intrinsic: 1.3.0
+
+ camelcase@5.3.1: {}
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ chokidar@3.6.0:
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.3
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ chownr@1.1.4: {}
+
+ cliui@6.0.0:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 6.2.0
+
+ cliui@7.0.4:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ concat-map@0.0.1: {}
+
+ content-disposition@0.5.4:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ content-type@1.0.5: {}
+
+ cookie-signature@1.0.6: {}
+
+ cookie@0.7.1: {}
+
+ core-util-is@1.0.3: {}
+
+ create-require@1.1.1: {}
+
+ crypto-js@4.2.0: {}
+
+ debug@2.6.9:
+ dependencies:
+ ms: 2.0.0
+
+ debug@4.4.1(supports-color@5.5.0):
+ dependencies:
+ ms: 2.1.3
+ optionalDependencies:
+ supports-color: 5.5.0
+
+ decamelize@1.2.0: {}
+
+ decompress-response@6.0.0:
+ dependencies:
+ mimic-response: 3.1.0
+
+ deep-extend@0.6.0: {}
+
+ defu@6.1.7: {}
+
+ delayed-stream@1.0.0: {}
+
+ depd@2.0.0: {}
+
+ destroy@1.2.0: {}
+
+ detect-libc@2.0.4: {}
+
+ diff@4.0.2: {}
+
+ dijkstrajs@1.0.3: {}
+
+ dir-glob@3.0.1:
+ dependencies:
+ path-type: 4.0.0
+
+ dotenv@16.6.1: {}
+
+ dts-resolver@3.0.0: {}
+
+ dunder-proto@1.0.1:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-errors: 1.3.0
+ gopd: 1.2.0
+
+ ee-first@1.1.1: {}
+
+ emoji-regex@8.0.0: {}
+
+ empathic@2.0.1: {}
+
+ encodeurl@1.0.2: {}
+
+ encodeurl@2.0.0: {}
+
+ end-of-stream@1.4.5:
+ dependencies:
+ once: 1.4.0
+
+ es-define-property@1.0.1: {}
+
+ es-errors@1.3.0: {}
+
+ es-object-atoms@1.1.1:
+ dependencies:
+ es-errors: 1.3.0
+
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
+ escalade@3.2.0: {}
+
+ escape-html@1.0.3: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.9
+
+ etag@1.8.1: {}
+
+ expand-template@2.0.3: {}
+
+ express@4.21.2:
+ dependencies:
+ accepts: 1.3.8
+ array-flatten: 1.1.1
+ body-parser: 1.20.3
+ content-disposition: 0.5.4
+ content-type: 1.0.5
+ cookie: 0.7.1
+ cookie-signature: 1.0.6
+ debug: 2.6.9
+ depd: 2.0.0
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ etag: 1.8.1
+ finalhandler: 1.3.1
+ fresh: 0.5.2
+ http-errors: 2.0.0
+ merge-descriptors: 1.0.3
+ methods: 1.1.2
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ path-to-regexp: 0.1.12
+ proxy-addr: 2.0.7
+ qs: 6.13.0
+ range-parser: 1.2.1
+ safe-buffer: 5.2.1
+ send: 0.19.0
+ serve-static: 1.16.2
+ setprototypeof: 1.2.0
+ statuses: 2.0.1
+ type-is: 1.6.18
+ utils-merge: 1.0.1
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fastq@1.19.1:
+ dependencies:
+ reusify: 1.1.0
+
+ fdir@6.5.0(picomatch@4.0.4):
+ optionalDependencies:
+ picomatch: 4.0.4
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ finalhandler@1.3.1:
+ dependencies:
+ debug: 2.6.9
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ on-finished: 2.4.1
+ parseurl: 1.3.3
+ statuses: 2.0.1
+ unpipe: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ find-up@4.1.0:
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+
+ follow-redirects@1.15.9: {}
+
+ form-data@4.0.3:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.2
+ mime-types: 2.1.35
+
+ forwarded@0.2.0: {}
+
+ fresh@0.5.2: {}
+
+ from2@2.3.0:
+ dependencies:
+ inherits: 2.0.4
+ readable-stream: 2.3.8
+
+ fs-constants@1.0.0: {}
+
+ fs-extra@9.1.0:
+ dependencies:
+ at-least-node: 1.0.0
+ graceful-fs: 4.2.11
+ jsonfile: 6.1.0
+ universalify: 2.0.1
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ get-caller-file@2.0.5: {}
+
+ get-intrinsic@1.3.0:
+ dependencies:
+ call-bind-apply-helpers: 1.0.2
+ es-define-property: 1.0.1
+ es-errors: 1.3.0
+ es-object-atoms: 1.1.1
+ function-bind: 1.1.2
+ get-proto: 1.0.1
+ gopd: 1.2.0
+ has-symbols: 1.1.0
+ hasown: 2.0.2
+ math-intrinsics: 1.1.0
+
+ get-proto@1.0.1:
+ dependencies:
+ dunder-proto: 1.0.1
+ es-object-atoms: 1.1.1
+
+ get-tsconfig@5.0.0-beta.5:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
+ github-from-package@0.0.0: {}
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ globby@11.1.0:
+ dependencies:
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.3.3
+ ignore: 5.3.2
+ merge2: 1.4.1
+ slash: 3.0.0
+
+ gopd@1.2.0: {}
+
+ graceful-fs@4.2.11: {}
+
+ has-flag@3.0.0: {}
+
+ has-flag@4.0.0: {}
+
+ has-symbols@1.1.0: {}
+
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
+ has@1.0.4: {}
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
+ hookable@6.1.1: {}
+
+ http-errors@2.0.0:
+ dependencies:
+ depd: 2.0.0
+ inherits: 2.0.4
+ setprototypeof: 1.2.0
+ statuses: 2.0.1
+ toidentifier: 1.0.1
+
+ https-proxy-agent@5.0.1(supports-color@5.5.0):
+ dependencies:
+ agent-base: 6.0.2(supports-color@5.5.0)
+ debug: 4.4.1(supports-color@5.5.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ iconv-lite@0.4.24:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ ieee754@1.2.1: {}
+
+ ignore-by-default@1.0.1: {}
+
+ ignore@5.3.2: {}
+
+ import-without-cache@0.4.0: {}
+
+ inherits@2.0.4: {}
+
+ ini@1.3.8: {}
+
+ into-stream@6.0.0:
+ dependencies:
+ from2: 2.3.0
+ p-is-promise: 3.0.0
+
+ ipaddr.js@1.9.1: {}
+
+ is-binary-path@2.1.0:
+ dependencies:
+ binary-extensions: 2.3.0
+
+ is-core-module@2.16.1:
+ dependencies:
+ hasown: 2.0.2
+
+ is-core-module@2.9.0:
+ dependencies:
+ has: 1.0.4
+
+ is-extglob@2.1.1: {}
+
+ is-fullwidth-code-point@3.0.0: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-number@7.0.0: {}
+
+ isarray@1.0.0: {}
+
+ jsesc@2.5.2: {}
+
+ jsesc@3.1.0: {}
+
+ jsonfile@6.1.0:
+ dependencies:
+ universalify: 2.0.1
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
+ locate-path@5.0.0:
+ dependencies:
+ p-locate: 4.1.0
+
+ make-error@1.3.6: {}
+
+ math-intrinsics@1.1.0: {}
+
+ media-typer@0.3.0: {}
+
+ merge-descriptors@1.0.3: {}
+
+ merge2@1.4.1: {}
+
+ methods@1.1.2: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ mime-db@1.52.0: {}
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ mime@1.6.0: {}
+
+ mimic-response@3.1.0: {}
+
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.12
+
+ minimist@1.2.8: {}
+
+ mkdirp-classic@0.5.3: {}
+
+ ms@2.0.0: {}
+
+ ms@2.1.3: {}
+
+ multistream@4.1.0:
+ dependencies:
+ once: 1.4.0
+ readable-stream: 3.6.2
+
+ napi-build-utils@1.0.2: {}
+
+ negotiator@0.6.3: {}
+
+ node-abi@3.75.0:
+ dependencies:
+ semver: 7.7.2
+
+ node-fetch@2.7.0:
+ dependencies:
+ whatwg-url: 5.0.0
+
+ node-forge@1.3.3: {}
+
+ nodemon@3.1.10:
+ dependencies:
+ chokidar: 3.6.0
+ debug: 4.4.1(supports-color@5.5.0)
+ ignore-by-default: 1.0.1
+ minimatch: 3.1.2
+ pstree.remy: 1.1.8
+ semver: 7.7.2
+ simple-update-notifier: 2.0.0
+ supports-color: 5.5.0
+ touch: 3.1.1
+ undefsafe: 2.0.5
+
+ normalize-path@3.0.0: {}
+
+ object-inspect@1.13.4: {}
+
+ obug@2.1.3: {}
+
+ on-finished@2.4.1:
+ dependencies:
+ ee-first: 1.1.1
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ p-is-promise@3.0.0: {}
+
+ p-limit@2.3.0:
+ dependencies:
+ p-try: 2.2.0
+
+ p-locate@4.1.0:
+ dependencies:
+ p-limit: 2.3.0
+
+ p-try@2.2.0: {}
+
+ pako@2.1.0: {}
+
+ parseurl@1.3.3: {}
+
+ path-exists@4.0.0: {}
+
+ path-parse@1.0.7: {}
+
+ path-to-regexp@0.1.12: {}
+
+ path-type@4.0.0: {}
+
+ pathe@2.0.3: {}
+
+ picomatch@2.3.1: {}
+
+ picomatch@4.0.4: {}
+
+ pkg-fetch@3.4.2(supports-color@5.5.0):
+ dependencies:
+ chalk: 4.1.2
+ fs-extra: 9.1.0
+ https-proxy-agent: 5.0.1(supports-color@5.5.0)
+ node-fetch: 2.7.0
+ progress: 2.0.3
+ semver: 7.7.2
+ tar-fs: 2.1.3
+ yargs: 16.2.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
+ pkg@5.8.1(supports-color@5.5.0):
+ dependencies:
+ '@babel/generator': 7.18.2
+ '@babel/parser': 7.18.4
+ '@babel/types': 7.19.0
+ chalk: 4.1.2
+ fs-extra: 9.1.0
+ globby: 11.1.0
+ into-stream: 6.0.0
+ is-core-module: 2.9.0
+ minimist: 1.2.8
+ multistream: 4.1.0
+ pkg-fetch: 3.4.2(supports-color@5.5.0)
+ prebuild-install: 7.1.1
+ resolve: 1.22.10
+ stream-meter: 1.0.4
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
+ pngjs@5.0.0: {}
+
+ prebuild-install@7.1.1:
+ dependencies:
+ detect-libc: 2.0.4
+ expand-template: 2.0.3
+ github-from-package: 0.0.0
+ minimist: 1.2.8
+ mkdirp-classic: 0.5.3
+ napi-build-utils: 1.0.2
+ node-abi: 3.75.0
+ pump: 3.0.3
+ rc: 1.2.8
+ simple-get: 4.0.1
+ tar-fs: 2.1.3
+ tunnel-agent: 0.6.0
+
+ prettier@2.8.8: {}
+
+ process-nextick-args@2.0.1: {}
+
+ progress@2.0.3: {}
+
+ proxy-addr@2.0.7:
+ dependencies:
+ forwarded: 0.2.0
+ ipaddr.js: 1.9.1
+
+ proxy-from-env@1.1.0: {}
+
+ pstree.remy@1.1.8: {}
+
+ pump@3.0.3:
+ dependencies:
+ end-of-stream: 1.4.5
+ once: 1.4.0
+
+ punycode@1.4.1: {}
+
+ qrcode@1.5.4:
+ dependencies:
+ dijkstrajs: 1.0.3
+ pngjs: 5.0.0
+ yargs: 15.4.1
+
+ qs@6.13.0:
+ dependencies:
+ side-channel: 1.1.0
+
+ quansync@1.0.0: {}
+
+ queue-microtask@1.2.3: {}
+
+ range-parser@1.2.1: {}
+
+ raw-body@2.5.2:
+ dependencies:
+ bytes: 3.1.2
+ http-errors: 2.0.0
+ iconv-lite: 0.4.24
+ unpipe: 1.0.0
+
+ rc@1.2.8:
+ dependencies:
+ deep-extend: 0.6.0
+ ini: 1.3.8
+ minimist: 1.2.8
+ strip-json-comments: 2.0.1
+
+ readable-stream@2.3.8:
+ dependencies:
+ core-util-is: 1.0.3
+ inherits: 2.0.4
+ isarray: 1.0.0
+ process-nextick-args: 2.0.1
+ safe-buffer: 5.1.2
+ string_decoder: 1.1.1
+ util-deprecate: 1.0.2
+
+ readable-stream@3.6.2:
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+
+ readdirp@3.6.0:
+ dependencies:
+ picomatch: 2.3.1
+
+ require-directory@2.1.1: {}
+
+ require-main-filename@2.0.0: {}
+
+ resolve-pkg-maps@1.0.0: {}
+
+ resolve@1.22.10:
+ dependencies:
+ is-core-module: 2.16.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
+ reusify@1.1.0: {}
+
+ rolldown-plugin-dts@0.26.0(rolldown@1.1.1)(typescript@5.9.3):
+ dependencies:
+ '@babel/generator': 8.0.0
+ '@babel/helper-validator-identifier': 8.0.0
+ '@babel/parser': 8.0.0
+ ast-kit: 3.0.0
+ birpc: 4.0.0
+ dts-resolver: 3.0.0
+ get-tsconfig: 5.0.0-beta.5
+ obug: 2.1.3
+ rolldown: 1.1.1
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - oxc-resolver
+
+ rolldown@1.1.1:
+ dependencies:
+ '@oxc-project/types': 0.135.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm64': 1.1.1
+ '@rolldown/binding-darwin-arm64': 1.1.1
+ '@rolldown/binding-darwin-x64': 1.1.1
+ '@rolldown/binding-freebsd-x64': 1.1.1
+ '@rolldown/binding-linux-arm-gnueabihf': 1.1.1
+ '@rolldown/binding-linux-arm64-gnu': 1.1.1
+ '@rolldown/binding-linux-arm64-musl': 1.1.1
+ '@rolldown/binding-linux-ppc64-gnu': 1.1.1
+ '@rolldown/binding-linux-s390x-gnu': 1.1.1
+ '@rolldown/binding-linux-x64-gnu': 1.1.1
+ '@rolldown/binding-linux-x64-musl': 1.1.1
+ '@rolldown/binding-openharmony-arm64': 1.1.1
+ '@rolldown/binding-wasm32-wasi': 1.1.1
+ '@rolldown/binding-win32-arm64-msvc': 1.1.1
+ '@rolldown/binding-win32-x64-msvc': 1.1.1
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ safe-buffer@5.1.2: {}
+
+ safe-buffer@5.2.1: {}
+
+ safe-decode-uri-component@1.2.1: {}
+
+ safer-buffer@2.1.2: {}
+
+ semver@7.7.2: {}
+
+ semver@7.8.4: {}
+
+ send@0.19.0:
+ dependencies:
+ debug: 2.6.9
+ depd: 2.0.0
+ destroy: 1.2.0
+ encodeurl: 1.0.2
+ escape-html: 1.0.3
+ etag: 1.8.1
+ fresh: 0.5.2
+ http-errors: 2.0.0
+ mime: 1.6.0
+ ms: 2.1.3
+ on-finished: 2.4.1
+ range-parser: 1.2.1
+ statuses: 2.0.1
+ transitivePeerDependencies:
+ - supports-color
+
+ serve-static@1.16.2:
+ dependencies:
+ encodeurl: 2.0.0
+ escape-html: 1.0.3
+ parseurl: 1.3.3
+ send: 0.19.0
+ transitivePeerDependencies:
+ - supports-color
+
+ set-blocking@2.0.0: {}
+
+ setprototypeof@1.2.0: {}
+
+ side-channel-list@1.0.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-map@1.0.1:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+
+ side-channel-weakmap@1.0.2:
+ dependencies:
+ call-bound: 1.0.4
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-map: 1.0.1
+
+ side-channel@1.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ object-inspect: 1.13.4
+ side-channel-list: 1.0.0
+ side-channel-map: 1.0.1
+ side-channel-weakmap: 1.0.2
+
+ simple-concat@1.0.1: {}
+
+ simple-get@4.0.1:
+ dependencies:
+ decompress-response: 6.0.0
+ once: 1.4.0
+ simple-concat: 1.0.1
+
+ simple-update-notifier@2.0.0:
+ dependencies:
+ semver: 7.7.2
+
+ slash@3.0.0: {}
+
+ statuses@2.0.1: {}
+
+ stream-meter@1.0.4:
+ dependencies:
+ readable-stream: 2.3.8
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string_decoder@1.1.1:
+ dependencies:
+ safe-buffer: 5.1.2
+
+ string_decoder@1.3.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-json-comments@2.0.1: {}
+
+ supports-color@5.5.0:
+ dependencies:
+ has-flag: 3.0.0
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-preserve-symlinks-flag@1.0.0: {}
+
+ tar-fs@2.1.3:
+ dependencies:
+ chownr: 1.1.4
+ mkdirp-classic: 0.5.3
+ pump: 3.0.3
+ tar-stream: 2.2.0
+
+ tar-stream@2.2.0:
+ dependencies:
+ bl: 4.1.0
+ end-of-stream: 1.4.5
+ fs-constants: 1.0.0
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+
+ tinyexec@1.2.4: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.4)
+ picomatch: 4.0.4
+
+ to-fast-properties@2.0.0: {}
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ toidentifier@1.0.1: {}
+
+ touch@3.1.1: {}
+
+ tr46@0.0.3: {}
+
+ tree-kill@1.2.2: {}
+
+ ts-node@10.9.2(@types/node@18.19.115)(typescript@5.9.3):
+ dependencies:
+ '@cspotcode/source-map-support': 0.8.1
+ '@tsconfig/node10': 1.0.11
+ '@tsconfig/node12': 1.0.11
+ '@tsconfig/node14': 1.0.3
+ '@tsconfig/node16': 1.0.4
+ '@types/node': 18.19.115
+ acorn: 8.15.0
+ acorn-walk: 8.3.4
+ arg: 4.1.3
+ create-require: 1.1.1
+ diff: 4.0.2
+ make-error: 1.3.6
+ typescript: 5.9.3
+ v8-compile-cache-lib: 3.0.1
+ yn: 3.1.1
+
+ tsdown@0.22.3(typescript@5.9.3):
+ dependencies:
+ ansis: 4.3.1
+ cac: 7.0.0
+ defu: 6.1.7
+ empathic: 2.0.1
+ hookable: 6.1.1
+ import-without-cache: 0.4.0
+ obug: 2.1.3
+ picomatch: 4.0.4
+ rolldown: 1.1.1
+ rolldown-plugin-dts: 0.26.0(rolldown@1.1.1)(typescript@5.9.3)
+ semver: 7.8.4
+ tinyexec: 1.2.4
+ tinyglobby: 0.2.17
+ tree-kill: 1.2.2
+ unconfig-core: 7.5.0
+ optionalDependencies:
+ typescript: 5.9.3
+ transitivePeerDependencies:
+ - '@ts-macro/tsc'
+ - '@typescript/native-preview'
+ - oxc-resolver
+ - vue-tsc
+
+ tslib@2.8.1:
+ optional: true
+
+ tunnel-agent@0.6.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ type-is@1.6.18:
+ dependencies:
+ media-typer: 0.3.0
+ mime-types: 2.1.35
+
+ typescript@5.9.3: {}
+
+ unconfig-core@7.5.0:
+ dependencies:
+ '@quansync/fs': 1.0.0
+ quansync: 1.0.0
+
+ undefsafe@2.0.5: {}
+
+ undici-types@5.26.5: {}
+
+ universalify@2.0.1: {}
+
+ unpipe@1.0.0: {}
+
+ url@0.11.4:
+ dependencies:
+ punycode: 1.4.1
+ qs: 6.13.0
+
+ util-deprecate@1.0.2: {}
+
+ utils-merge@1.0.1: {}
+
+ v8-compile-cache-lib@3.0.1: {}
+
+ vary@1.1.2: {}
+
+ webidl-conversions@3.0.1: {}
+
+ whatwg-url@5.0.0:
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+
+ which-module@2.0.1: {}
+
+ wrap-ansi@6.2.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrappy@1.0.2: {}
+
+ y18n@4.0.3: {}
+
+ y18n@5.0.8: {}
+
+ yargs-parser@18.1.3:
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+
+ yargs-parser@20.2.9: {}
+
+ yargs@15.4.1:
+ dependencies:
+ cliui: 6.0.0
+ decamelize: 1.2.0
+ find-up: 4.1.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 4.2.3
+ which-module: 2.0.1
+ y18n: 4.0.3
+ yargs-parser: 18.1.3
+
+ yargs@16.2.0:
+ dependencies:
+ cliui: 7.0.4
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 20.2.9
+
+ yn@3.1.1: {}
diff --git a/third_party/MoeKoeMusic/api/public/.gitignore b/third_party/MoeKoeMusic/api/public/.gitignore
new file mode 100644
index 0000000..fac30cb
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/public/.gitignore
@@ -0,0 +1 @@
+audio_match_demo
\ No newline at end of file
diff --git a/third_party/MoeKoeMusic/api/public/audio_match.html b/third_party/MoeKoeMusic/api/public/audio_match.html
new file mode 100644
index 0000000..d2af19d
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/public/audio_match.html
@@ -0,0 +1,429 @@
+
+
+
+
+
+ 听歌识曲
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
← 返回首页
+
+
+
+
+
diff --git a/third_party/MoeKoeMusic/api/public/fingerprint.js b/third_party/MoeKoeMusic/api/public/fingerprint.js
new file mode 100644
index 0000000..c1aaae6
--- /dev/null
+++ b/third_party/MoeKoeMusic/api/public/fingerprint.js
@@ -0,0 +1,365 @@
+/**
+ * 浏览器端行为指纹生成工具
+ *
+ * 从 login_captcha_simulate.html 提取的独立模块,提供:
+ * - generateWebGLHash(): 生成 WebGL 指纹哈希值
+ * - generateEDTData(opts): 生成用户行为指纹数据(用于 sid/edt 加密的 data 字段)
+ *
+ * 适用于 login_captcha.html 和 login_captcha_simulate.html 共用
+ */
+
+(function (root) {
+ 'use strict';
+
+ /**
+ * 生成 [min, max] 范围内的随机整数(包含两端)
+ * @param {number} min - 最小值
+ * @param {number} max - 最大值
+ * @returns {number} 随机整数
+ */
+ function ri(min, max) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+ }
+
+ // ============================================================
+ // 事件记录格式化函数
+ // ============================================================
+
+ /**
+ * 格式化 type-3 事件(鼠标/触摸移动)
+ * @param {number} t - 时间差(毫秒)
+ * @param {number} i - 子索引(0 或 1)
+ * @param {number} x - 鼠标 X 坐标
+ * @param {number} y - 鼠标 Y 坐标
+ * @returns {string} 格式: "3,时间差,子索引,X,Y"
+ */
+ function f3(t, i, x, y) {
+ return '3,' + t + ',' + i + ',' + x + ',' + y;
+ }
+
+ /**
+ * 格式化 type-5 事件(滚动/计时)
+ * @param {number} t - 时间差(毫秒)
+ * @param {number} i - 事件索引
+ * @returns {string} 格式: "5,时间差,事件索引"
+ */
+ function f5(t, i) {
+ return '5,' + t + ',' + i;
+ }
+
+ /**
+ * 格式化 type-6 事件(窗口事件)
+ * @param {number} t - 时间差(毫秒)
+ * @param {number} i - 事件索引
+ * @param {number} x - 窗口宽度
+ * @param {number} y - 窗口高度
+ * @returns {string} 格式: "6,时间差,事件索引,宽,高"
+ */
+ function f6(t, i, x, y) {
+ return '6,' + t + ',' + i + ',' + x + ',' + y;
+ }
+
+ /**
+ * 格式化 type-3 哨兵记录(鼠标事件结束标记)
+ * @param {number} sentinel - 哨兵值
+ * @param {number} i - 子索引
+ * @param {number} x - X 坐标
+ * @param {number} y - Y 坐标
+ * @returns {string} 格式: "3,SENTINEL,子索引,X,Y"
+ */
+ function fs3(sentinel, i, x, y) {
+ return '3,' + sentinel + ',' + i + ',' + x + ',' + y;
+ }
+
+ /**
+ * 格式化 type-5 哨兵记录(滚动事件结束标记)
+ * @param {number} sentinel - 哨兵值
+ * @param {number} i - 事件索引
+ * @returns {string} 格式: "5,SENTINEL,事件索引"
+ */
+ function fs5(sentinel, i) {
+ return '5,' + sentinel + ',' + i;
+ }
+
+ /**
+ * 格式化 type-6 哨兵记录(窗口事件结束标记)
+ * @param {number} sentinel - 哨兵值
+ * @param {number} i - 事件索引
+ * @param {number} x - 窗口宽度
+ * @param {number} y - 窗口高度
+ * @returns {string} 格式: "6,SENTINEL,事件索引,宽,高"
+ */
+ function fs6(sentinel, i, x, y) {
+ return '6,' + sentinel + ',' + i + ',' + x + ',' + y;
+ }
+
+ // ============================================================
+ // 贝塞尔曲线鼠标路径生成
+ // ============================================================
+
+ /**
+ * 用三阶贝塞尔曲线生成模拟真人的鼠标移动路径
+ *
+ * 真人鼠标轨迹特点:
+ * - 不是直线,有弧度和加速减速
+ * - 有微小抖动(手抖)
+ * - 起步慢、中间快、结束减速
+ *
+ * @param {number} sx - 起点 X
+ * @param {number} sy - 起点 Y
+ * @param {number} ex - 终点 X
+ * @param {number} ey - 终点 Y
+ * @param {number} n - 采样点数
+ * @returns {Array<{x:number, y:number}>} 路径点数组
+ */
+ function bezierPath(sx, sy, ex, ey, n) {
+ const c1x = sx + (ex - sx) * 0.3 + ri(-80, 80);
+ const c1y = sy + (ey - sy) * 0.2 + ri(-60, 60);
+ const c2x = sx + (ex - sx) * 0.7 + ri(-60, 60);
+ const c2y = sy + (ey - sy) * 0.8 + ri(-40, 40);
+
+ const pts = [];
+ for (let i = 0; i <= n; i++) {
+ const t = i / n;
+ const u = 1 - t;
+
+ const x = u * u * u * sx + 3 * u * u * t * c1x + 3 * u * t * t * c2x + t * t * t * ex;
+ const y = u * u * u * sy + 3 * u * u * t * c1y + 3 * u * t * t * c2y + t * t * t * ey;
+
+ const jitter = Math.max(0.5, 3 - t * 2.5);
+ pts.push({
+ x: x + (Math.random() - 0.5) * jitter,
+ y: y + (Math.random() - 0.5) * jitter,
+ });
+ }
+ return pts;
+ }
+
+ // ============================================================
+ // 二进制/hex/Base64 转换工具
+ // ============================================================
+
+ /**
+ * hex 字符串转 ArrayBuffer
+ * @param {string} hex - hex 字符串(如 "6b75676f")
+ * @returns {ArrayBuffer} 对应的二进制缓冲区
+ */
+ function hex2buf(hex) {
+ const arr = new Uint8Array(hex.length / 2);
+ for (let i = 0; i < arr.length; i++) arr[i] = parseInt(hex.substr(i * 2, 2), 16);
+ return arr.buffer;
+ }
+
+ /**
+ * ArrayBuffer 转 hex 字符串
+ * @param {ArrayBuffer} buf - 二进制缓冲区
+ * @returns {string} hex 字符串(每个字节两位,小写)
+ */
+ function buf2hex(buf) {
+ return Array.from(new Uint8Array(buf))
+ .map((b) => b.toString(16).padStart(2, '0'))
+ .join('');
+ }
+
+ /**
+ * hex 字符串转 Base64 字符串
+ * @param {string} hex - hex 字符串
+ * @returns {string} Base64 编码的字符串
+ */
+ function hexToBase64(hex) {
+ const bytes = new Uint8Array(hex.length / 2);
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
+ let binary = '';
+ for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
+ return btoa(binary);
+ }
+
+ // ============================================================
+ // 加密常量
+ // ============================================================
+
+ /**
+ * RSA 公钥(SPKI DER 格式的 hex 字符串)
+ * 从 WASM 二进制中提取,用于 RSA-OAEP SHA-256 加密 AES 密钥
+ * 算法: RSA-2048,公钥指数 65537 (0x10001)
+ */
+ const RSA_SPKI_HEX =
+ '30820122300d06092a864886f70d01010105000382010f003082010a0282010100a16dbe625a3c00b78f4904cfd31045945984387bc10fdb52facec30657ca12edd1cf3bd94da5f526d61b5f8f80554aa3e80473f0833e08a072a8616f6c737f5bae17c4d23eabbcf7e9a8c22f75532765b91bd302262b5cea819b8ab7b83507e1684ab49c2fa1c41590bc26c815f940d88b6b2d46d253bcf56c703f6be8e5426e0e5af63e20a9d3af23894cfb93d7234e5636c9f3004b2b2d83810afda4fa963e6110b46a51e4833d57c29aa3a3da49d29839619b5f78b6f91cc82a1bd9531c6d2707556ea3e50cf956f61e3fc4805ce7a2e0bebe1a225f2716dc1b8f85095544c5b86aecd2d63d1ffb57bd9db675408ab86c56fe05bb645fa05f3eaf1ed61aad0203010001';
+
+ /**
+ * AES 初始化向量(固定值)
+ * ASCII 解码为 "kugousecurity123"
+ * WASM 中硬编码,每次加密都使用相同的 IV
+ */
+ const AES_IV_HEX = '6b75676f757365637572697479313233';
+
+ // ============================================================
+ // 加密流程
+ // ============================================================
+
+ /**
+ * 完整的 sid 加密流程(纯 JS 实现,不依赖 WASM)
+ *
+ * 加密方案:
+ * 1. 生成随机 AES-128 密钥
+ * 2. 用 AES-128-CBC 加密行为指纹明文 → 得到 EDT
+ * 3. 用 RSA-OAEP SHA-256 加密 AES 密钥 → 得到 SID
+ * 4. 服务端用 RSA 私钥解密 SID 得到 AES 密钥,再用 AES 密钥解密 EDT 得到行为数据
+ *
+ * @param {string} plaintext - 待加密的明文(行为指纹数据)
+ * @returns {Promise