├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── dataSources.xml ├── encodings.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── README_CLI.md ├── README_ENG.md ├── markdownResources ├── Alipay WeChatPay.jpg ├── Sponsorship.png └── cover.png ├── pom.xml └── src └── main └── java ├── Main.java ├── converter └── Universal.java ├── database └── Database.java └── utils ├── FileOperation.java ├── FindStringArray.java ├── Logger.java ├── MapSort.java ├── MarkdownLog.java ├── PropertiesRelated.java ├── Sleep.java ├── Statistic.java ├── StringSimilarityCompare.java ├── TablePrinter.java └── TerminalCharSetDetect.java /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea/modules.xml 8 | .idea/jarRepositories.xml 9 | .idea/compiler.xml 10 | .idea/libraries/ 11 | *.iws 12 | *.iml 13 | *.ipr 14 | 15 | ### Eclipse ### 16 | .apt_generated 17 | .classpath 18 | .factorypath 19 | .project 20 | .settings 21 | .springBeans 22 | .sts4-cache 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | build/ 31 | !**/src/main/**/build/ 32 | !**/src/test/**/build/ 33 | 34 | ### VS Code ### 35 | .vscode/ 36 | 37 | ### Mac OS ### 38 | .DS_Store -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sqlite.xerial 6 | true 7 | org.sqlite.JDBC 8 | jdbc:sqlite:$PROJECT_DIR$/SQLite/cloudmusic.db 9 | $ProjectFileDir$ 10 | 11 | 12 | sqlite.xerial 13 | true 14 | org.sqlite.JDBC 15 | jdbc:sqlite:$PROJECT_DIR$/SQLite/QQMusic 16 | $ProjectFileDir$ 17 | 18 | 19 | sqlite.xerial 20 | true 21 | org.sqlite.JDBC 22 | jdbc:sqlite:$PROJECT_DIR$/SQLite/kwplayer.db 23 | $ProjectFileDir$ 24 | 25 | 26 | sqlite.xerial 27 | true 28 | org.sqlite.JDBC 29 | jdbc:sqlite:$PROJECT_DIR$/SQLite/kugou_music_phone_v7.db 30 | $ProjectFileDir$ 31 | 32 | 33 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 HWinZnieJ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

中文 / English

2 |

3 | cover 4 |

5 |

欢迎使用 椒盐歌单助手

6 | 7 | --- 8 | 9 | ### 项目介绍 10 | 11 | 现在,已经有大量用户将主力使用的音乐软件从**在线音乐平台**转为了**本地音乐播放器**,虽然歌曲可以很顺利且快速地进行迁移操作,但是无法将自己创建的歌单、或者其他用户的优秀歌单一并进行快速迁移操作,这个项目就是为了解决这个问题而诞生的。 12 | 13 | - 原计划基于CLI实现,但由于Windows终端的编码问题,导致无法正常显示中文/韩文/日文等字符,所以现在改为使用前后端分离来实现。 14 | - 重构版 前端仓库:[SaltPlayerConverterFrontEnd](https://github.com/Winnie0408/SaltPlayerConverterFrontEnd) 15 | - 重构版 后端仓库:[SaltPlayerConverterBackEnd](https://github.com/Winnie0408/SaltPlayerConverterBackEnd) 16 | - **Android版 仓库:[歌单无界](https://github.com/Winnie0408/LocalMusicHelper) 于2024年1月23日正式上线啦!🎉🎉🎉** 17 | - 若您出于各种原因不太想使用前后端重构版,并能够使用Linux或者Mac OS系统,那么您可以使用CLI版,匹配的核心算法是相同的,详情请查看[CLI版README](README_CLI.md)。 18 | 19 | ### 视频教程 20 | 21 | ~~哔哩哔哩(已被B站下线):[椒盐音乐 歌单助手 使用指北](https://www.bilibili.com/video/BV1Tw411s7aL/)~~ 22 | 23 | YouTube:[椒盐音乐 歌单助手 使用指北](https://youtu.be/w2UMsPFbOro) 24 | 25 | 酷友`@TianMiao8152`友情提供的(国内可访问):[视频教程直链](https://www.tianmiao.fun/video/JY.mp4) 或 [视频教程在线播放](https://www.tianmiao.fun/posts/22023/) 26 | 27 | QQ群友`这是一个名字`友情提供的(国内可访问):[视频教程直链](https://files.imgoss.top/videoplayback.mp4) 28 | 29 | ### 需要使用的硬件与软件 30 | 31 | #### 硬件 32 | 33 | - 电脑或其他中大屏(普通平板大小及以上)横屏设备 *1 34 | - Android设备: 35 | - 若使用的主力设备**已获取**Root权限,则只需要一台即可,**无视**后文中主力机与备用机的区分。 36 | - 若使用的主力设备**未获取**Root权限,则需要两台设备: 37 | - 一台获取了Root权限的Android设备,真机或虚拟机皆可,后文中称其为**备用设备**。 38 | - 一台主力Android设备,后文中称其为**主力设备**。 39 | 40 | #### 软件 41 | 42 | 本项目需要配合以下软件一起使用: 43 | 44 | - 现代的浏览器(拥有一个即可) 45 | - [Google Chrome](https://www.google.cn/chrome/) 46 | - [Microsoft Edge](https://www.microsoft.com/zh-cn/edge) 47 | - [Firefox](https://www.mozilla.org/zh-CN/firefox/new/) 48 | - [Safari](https://www.apple.com.cn/safari/) 49 | - [Opera](https://www.opera.com/zh-cn) 50 | - 系统自带的浏览器 51 | - 受支持的[在线音乐平台](README.md#音乐平台的选择)的Android客户端(均为普通版本,选择自己使用的一个或多个平台) 52 | - [网易云音乐](https://music.163.com/)**(推荐)** 53 | - [QQ音乐](https://y.qq.com/)**(推荐)** 54 | - [酷狗音乐](https://www.kugou.com/) 55 | - [酷我音乐](https://www.kuwo.cn/) 56 | - 音乐标签 57 | - [Windows](https://www.cnblogs.com/vinlxc/p/11347744.html) 58 | - [Android](https://www.coolapk.com/apk/com.xjcheng.musictageditor)**(推荐)** 59 | - 获取音乐的标签信息(下载Release页面中的app-release.apk即可) 60 | - [Android (项目介绍)](https://github.com/Winnie0408/MusicID3TagGetter) 61 | - [Android (下载页面)](https://github.com/Winnie0408/MusicID3TagGetter/releases) 62 | - 文件管理器(选择一个即可) 63 | - [MT管理器](https://www.coolapk.com/apk/bin.mt.plus)**(推荐)** 64 | - [ES文件浏览器](https://www.coolapk.com/apk/com.estrongs.android.pop) 65 | - [MiXplorer](https://mixplorer.com) 66 | - Android虚拟机(当前使用的主力设备**未获取**Root权限时需要,选择一个即可) 67 | - [VMOS Pro](https://www.coolapk.com/apk/com.vmos.pro):在手机上使用的虚拟机 ~~(可能需要使用VIP版,详情查看该应用的酷安评论区)~~ 68 | - [MuMu模拟器](https://mumu.163.com):在电脑上使用的虚拟机 69 | - 椒盐音乐(或糖醋音乐) 70 | 71 | ## 使用方法 72 | 73 | > **Note** 74 | > 75 | > **强烈推荐**您将本README文件完整阅读后,再进行相关操作! 76 | 77 | ### 0. 准备工作 78 | 79 | 安装上述软件 80 | 81 | - **在线音乐平台客户端**与**文件管理器**,安装到有Root权限的设备(或虚拟机)上。 82 | - **音乐标签**、**获取音乐标签**、**椒盐音乐**、文件管理器(可选),安装到主力设备上。 83 | - 现代的浏览器,安装到电脑或其他中大屏设备上。 84 | 85 | ### 1. 获取在线音乐平台的歌单数据 86 | 87 | **(在备用设备上操作)** 88 | 89 | 1. 打开需要使用的在线音乐平台客户端APP。 90 | 2. 登录账号。 91 | 3. **依次**点击进入自己的所有歌单(或者需要导出的歌单),并滑动到歌单页的**最底部**,加载当前歌单的所有歌曲。 92 | 4. 重复上述步骤,直到要所有导出的歌单**都加载过一次**。 93 | 5. 主动关闭在线音乐平台客户端(在软件菜单中选择**关闭**\[推荐\],或直接在后台界面中将其划掉)。 94 | 6. 打开文件管理器,**授予Root权限**,进入在线音乐平台客户端的**数据目录**,找到**databases**文件夹,找到指定的数据库文件。
95 | *若觉得各个软件的数据目录比较难找,可以使用MT管理器快速定位:`点击左上角菜单-点击安装包提取-选择需要的音乐APP-点击数据目录1`,即可快速跳转到数据目录。* 96 | - 网易云音乐 97 | - 数据目录:`/data/user/0/com.netease.cloudmusic/databases` 98 | - 数据库文件:`cloudmusic.db` 99 | - QQ音乐 100 | - 数据目录: `/data/user/0/com.tencent.qqmusic/databases` 101 | - 数据库文件:`QQMusic` 102 | - 酷狗音乐 103 | - 数据目录:`/data/user/0/com.kugou.android/databases` 104 | - 数据库文件:`kugou_music_phone_v7.db` 105 | - 酷我音乐 106 | - 数据目录:`/data/user/0/cn.kuwo.player/databases` 107 | - 数据库文件:`kwplayer.db` 108 | 7. 将数据库文件发送到电脑或其他中大屏设备上。 109 | 110 | ### 2. 刷新本地音乐的标签信息 111 | 112 | > **Warning** 113 | > 114 | > 本步骤会**覆盖**您本地音乐的标签信息,**请谨慎操作**! 115 | > 116 | > 若您之前已经自行匹配(或修改)过歌曲的标签信息,可跳过本步骤。 117 | > 118 | > 若后续匹配结果不理想,再重新进行此步骤即可。 119 | 120 | **(在主力设备上操作)** 121 | 122 | 1. 将音乐文件保存在手机里(相信您已经完成这个步骤了)。 123 | 2. 打开**音乐标签**APP。 124 | 1. 点击右上角**刷新**按钮,令其扫描手机中的音乐文件。 125 | 2. 点击左上角**菜单**按钮,点击弹出菜单底部的**设置**。 126 | 3. 点击**组合标签源**,**仅启用**与歌单来源平台**对应的**数据源,点击确定(比如,歌单来源平台为**网易云音乐**,则只启用**网易云**标签源,其他标签源都应**禁用**,若歌单来源平台为**酷狗音乐**,则启用**QQ与酷我**标签源,且QQ的优先级**高于**酷我)。 127 | 4. 返回到音乐标签主界面,点击右下角的**编辑**按钮,点击**自动匹配标签**。 128 | 5. 在弹出的对话框中,**仅勾选**标题、艺术家、专辑,**并同时启用**其右侧的覆盖选项,按需调整“网络搜索线程数”,点击确定。 129 | 6. 等待音乐标签批量匹配完成。 130 | 131 | ### 3. 获取本地音乐的标签信息 132 | 133 | **(在主力设备上操作)** 134 | 135 | 1. 打开**获取音乐标签**APP。 136 | 1. 点击下方的**选择目录**按钮,根据提示授予所需权限。 137 | 2. 选择音乐存放的目录(具体选择方式请查看[这里](https://github.com/Winnie0408/MusicID3TagGetter/blob/master/README.md#使用方法)),点击屏幕底部的**使用此文件夹**,在弹出的对话框中点击**允许**。 138 | 3. 等待软件扫描并导出手机中音乐的标签信息。 139 | 4. 前往软件的**导出目录**(手机存储目录中的Download目录)查看导出的标签信息文件**本地音乐导出.txt**。 140 | 5. 将导出的标签信息文件发送到电脑或其他中大屏设备上。 141 | 142 | ### 4. 进行歌单转换操作 143 | 144 | **(在电脑或其他中大屏设备上操作)** 145 | 146 | 1. 打开[椒盐歌单助手](https://saltconv.hwinzniej.top:45999/)页面。(或[使用其他运行项目的方法](README.md#项目的使用与运行)) 147 | 2. 按照您的意愿,开启或关闭**允许发送统计数据**[(会发送哪些数据?)](README.md#发送的统计数据),点击**开始**按钮。 148 | 3. 选择**歌单来源**,点击下一步。 149 | 4. 上传**本地音乐导出.txt**文件,点击下一步。 150 | 5. 上传**数据库**文件,点击下一步。 151 | 6. 选择您本次要转换的歌单,点击下一步。 152 | 7. 根据您的需要,在页面左侧调整匹配的参数与设置,然后点击**预览歌单**按钮。 153 | 8. 在页面右侧查看匹配结果,若您对匹配结果不满意,可以重新调整参数与设置并再次点击**预览歌单**按钮,若您对匹配结果满意,点击**导出歌单**。 154 | 9. 若自动匹配的结果无法很好地满足您的需求,您可点击**跳转到第一个匹配失败的项**按钮,表格就会自动滚动并展开第一个匹配失败歌曲的详情,根据您的需要点击: 155 | - **相同**按钮:详情中展示的匹配结果正确(只是相似度没达到您设置的阈值要求)。(点击按钮立即生效) 156 | - **编辑**按钮:详情中展示的匹配结果错误,且您的本地歌曲中**有**歌单中对应的歌曲,您可以在弹窗中手动修改匹配结果。(在弹出的弹框中点击**确认**按钮后,该操作才生效) 157 | - **放弃**按钮:详情中展示的匹配结果错误,且您的本地歌曲中**无**歌单中对应的歌曲,您可以放弃匹配该歌曲。(在弹出的弹框中点击**确认**按钮后,该操作才生效) 158 | - 弹框中可以开启**放弃当前歌单所有自动匹配失败的歌曲**,若您启用该项,则该歌单中所有**自动匹配失败**(表格最右侧列为红色的**否**)的歌曲项将从表格(转换结果)中删除。 159 | 10. 重复第9步,直至当前歌单所有(自动匹配失败的)歌曲您都检查过一遍,点击**保存当前歌单**按钮,在弹窗中选择要保存的类型,并预览转换结果,完成后点击保存,保存完成后会自动开始下一个歌单的匹配操作。 160 | 11. 若您不想转换当前歌单了,可以点击**放弃当前歌单**按钮,会自动开始下一个歌单的匹配操作。 161 | 12. 若您第6步中选择的所有歌单全部匹配完成,会弹出**匹配完成**弹框,点击**确认**按钮,会跳转到下载转换结果页面。 162 | 13. 在下载转换结果页面,点击**下载转换结果**按钮,即可下载包含转换结果的压缩文件。 163 | 14. 您看选择**手动删除**您本次转换操作中使用的所有文件:数据库文件、本地音乐导出文件、转换结果压缩文件(若您没有手动删除,这些文件也将在3天后**自动删除**)。 164 | 15. 将压缩文件(解压后)发送到主力设备上。 165 | 166 | ### 5. 将歌单导入椒盐音乐 167 | 168 | **(在主力设备上操作)** 169 | 170 | 1. 打开**椒盐音乐**APP。 171 | 2. 右滑或点击右上角菜单按钮,进入**菜单**,点击**歌单**。 172 | 3. 点击**导入歌单 (.txt)**。 173 | 4. (将压缩文件解压,)**逐个选择**压缩文件中的转换结果文件,将其导入到椒盐音乐中。 174 | 5. 在椒盐音乐中查看导入的歌单。 175 | 176 | --- 177 | 178 | ## 其他事项 179 | 180 | ### 项目的使用与运行 181 | 182 | #### 1. 直接使用我提供的服务 183 | 184 | [椒盐歌单转换助手](https://saltconv.hwinzniej.top:45999/) 185 | 186 | #### 2. 自行从源码编译运行 187 | 188 | 过程比较繁琐,不推荐普通用户使用,故不在本项目的README中介绍,对项目感兴趣的大佬可见: 189 | 190 | 前端:[SaltPlayerConverterFrontEnd](https://github.com/Winnie0408/SaltPlayerConverterFrontEnd) 191 | 192 | 后端:[SaltPlayerConverterBackEnd](https://github.com/Winnie0408/SaltPlayerConverterBackEnd) 193 | 194 | #### 3. 自行使用Docker运行 195 | 196 | 1. 运行命令,从Docker Hub拉取镜像,使用镜像创建并运行容器 197 | 198 | **(推荐)** 199 | 200 | ```bash 201 | docker run -d -it --shm-size=2G -p 45999:45999 -p 46000:46000 -e TZ=Asia/Shanghai --name salt-converter hwinzniej/salt-converter:latest 202 | ``` 203 | 204 | 或 205 | 206 | ```bash 207 | docker run -d -it --shm-size=2G --net=host -e TZ=Asia/Shanghai --name salt-converter hwinzniej/salt-converter:latest 208 | ``` 209 | 210 | 2. 打开浏览器,访问 211 | 212 | ```text 213 | http://127.0.0.1:45999/ 214 | ``` 215 | 216 | --- 217 | 218 | ##### P.S. 219 | 220 | 1. 若`45999`或/与`46000`端口被占用,可手动修改容器映射到宿主机的端口。如:修改为`55999`与`56000`: 221 | 222 | ```bash 223 | docker run -d -it --shm-size=2G -p 55999:45999 -p 56000:46000 -e TZ=Asia/Shanghai --name salt-converter hwinzniej/salt-converter:latest 224 | ``` 225 | 226 | 2. 若您的机器内存小于2G,可能需要修改共享内存的大小。如:改为`1G`: 227 | 228 | ```bash 229 | docker run -d -it --shm-size=1G -p 45999:45999 -p 46000:46000 -e TZ=Asia/Shanghai --name salt-converter hwinzniej/salt-converter:latest 230 | ``` 231 | 232 | 3. 若需要停止容器,可使用 233 | 234 | ```bash 235 | docker stop salt-converter 236 | ``` 237 | 238 | 4. 若需要启动容器,可使用 239 | 240 | ```bash 241 | docker start salt-converter 242 | ``` 243 | 244 | ### 发送的统计数据 245 | 246 | **不包含**您的任何隐私数据,仅包含以下信息: 247 | 248 | - 当前会话的Session ID(随机生成) 249 | - 开始转换的时间 250 | - 结束转换的时间 251 | - 匹配模式(总体/分离) 252 | - 歌单来源 253 | - 当前歌单包含的歌曲数量 254 | - 匹配成功的歌曲数 255 | - 自动匹配成功歌曲数量 256 | - 使用的相似度阈值 257 | - 括号去除是否启用 258 | - 歌手匹配是否启用 259 | - 专辑匹配是否启用 260 | - 最终保存了多少首歌曲 261 | 262 | ### 匹配模式 263 | 264 | 例如: 265 | 266 | 歌单中歌曲的信息如下: 267 | 268 | 歌名:小幸运 269 | 270 | 歌手:田馥甄 271 | 272 | 专辑:我的少女时代 电影原声带 273 | 274 | --- 275 | 276 | 本地歌曲的信息如下: 277 | 278 | 歌名:小幸运 279 | 280 | 歌手:田馥甄 281 | 282 | 专辑:我的少女时代 电影原声大碟 283 | 284 | #### 总体匹配 285 | 286 | 将歌曲的[歌名] [歌手] [专辑]拼接成一个字符串,进行匹配,找到相似度最大的歌曲。表格中将显示整体匹配的相似度。 287 | 288 | 本匹配方法将使用: 289 | 290 | `小幸运田馥甄我的少女时代 电影原声带` 291 | 292 | 与 293 | 294 | `小幸运田馥甄我的少女时代 电影原声大碟` 295 | 296 | 进行匹配,相似度结果为:89.47%。 297 | 298 | #### 分离匹配 299 | 300 | 将歌曲的[歌名] [歌手] [专辑]分别进行匹配, 找到相似度最大的歌曲。 表格中将显示每个匹配项的相似度。 301 | 302 | 本匹配方法将分别使用: 303 | 304 | - `小幸运`与`小幸运` 305 | - `田馥甄`与`田馥甄` 306 | - `我的少女时代 电影原声带`与`我的少女时代 电影原声大碟` 307 | 308 | 进行匹配,相似度结果分别为: 309 | 310 | - 100% 311 | - 100% 312 | - 84.62% 313 | 314 | ### 括号去除 315 | 316 | 大部分音乐平台对外语歌曲信息的命名方式一般为: `外文 (中文翻译)`或`外文 (歌曲来源、歌曲版本等)`。如`City Of Stars (From "La La Land" Soundtrack)`、`CALL ME BABY (叫我) (Chinese Ver.)`、`桜色舞うころ (樱花纷飞时)`。 317 | 318 | 启用此功能可以将字符串中的括号部分删去,只保留外文名,即:`外文`。如:`City Of Stars`、`CALL ME BABY`、`桜色舞うころ`,以此提高自动匹配成功率。 319 | 320 | 但需要注意,部分歌曲会在歌名后用括号注明歌曲版本:`歌名 (歌曲版本)`。如`曾经我也想过一了百了 (Live)`、`TruE (Ed Ver.)`,在这种情况下,若启用了本功能,会将其变成:`曾经我也想过一了百了`、`TruE`,继而**可能会出现匹配错误**。 321 | 322 | 请您根据您的实际情况,决定是否使用本功能。 323 | 324 | ### 音乐平台的选择 325 | 326 | #### **网易云音乐** 与/或 **QQ音乐** 327 | 328 | 这两个平台的歌曲信息正确率较高,且较为完整、权威,可以有效提高自动匹配的成功率。 329 | 330 | #### 酷狗音乐 331 | 332 | 该平台歌曲信息不太符合规范,合唱歌曲的艺术家名使用`、`分隔,且括号、斜杠的使用比较混乱,且**非【我喜欢】歌单**中歌曲的专辑信息不会保存到数据库中,导致匹配精确度下降,不太建议使用。 333 | 334 | #### 酷我音乐 335 | 336 | 该平台歌曲信息不太符合规范,合唱歌曲的艺术家名使用`&`分隔,且括号、斜杠的使用比较混乱,且有很多用户自行上传的歌曲,这些歌曲的标签信息大部分都不完整且不合规范,可能导致匹配精确度下降,不太建议使用。 337 | 338 | ### 相似度阈值 339 | 340 | 程序认为两个字符串**相同**的相似度大小,详情: 341 | 342 | 若当前阈值为0.8: 343 | 344 | - **相同**
345 | 字符串1:想いの眠るゆりかご (回忆长眠的摇篮)
346 | 字符串2:想いの眠るゆりかご (回忆长眠的摇篮)
347 | 相似度:1.0 348 | 349 | - **相同**
350 | 字符串1:伤感 II
351 | 字符串2:伤感 I
352 | 相似度:0.8 353 | 354 | - **不相同**
355 | 字符串1:I'M OK
356 | 字符串2:I AM OK
357 | 相似度:0.7142857142857143 358 | 359 | - **不相同**
360 | 字符串1:BANG BANG BANG (뱅뱅뱅)
361 | 字符串2:BANG BANG BANG
362 | 相似度:0.7 363 | 364 | - **不相同**
365 | 字符串1:이 사랑 (这份爱) (Inst.)
366 | 字符串2:이 사랑 (这份爱)
367 | 相似度:0.5555555555555556 368 | 369 | - **不相同**
370 | 字符串1:aaabbbccc
371 | 字符串2:abcabcabc
372 | 相似度:0.33333333333333337 373 | 374 | ## 赞助与支持 375 | 376 | 🥰🥰🥰 377 | 378 | 如果这个项目对您有所帮助,您可以给我一颗免费的⭐,或者请我喝杯咖啡!
379 | 非常感谢您的支持!
380 | ⬇️⬇️⬇️
381 | 382 | Sponsorship.jpg 383 | 384 | 385 | -------------------------------------------------------------------------------- /README_CLI.md: -------------------------------------------------------------------------------- 1 |

中文 / English

2 |

3 | cover 4 |

5 |

椒盐歌单助手

6 |

命令行(CLI)版

7 | 8 | --- 9 | 10 | ### ⚠️请注意⚠️ 11 | 12 | - 由于Windows终端的字符编码有问题,故CLI版**无法**在Windows上使用,只能在Linux、Mac OS等系统上使用。 13 | - 如果你的歌单中的歌曲信息**全为[ASCII字符](https://baike.baidu.com/item/ASCII/309296)**,那也可以在Windows上使用CLI版。 14 | - 本项目相比前后端重构版,缺失了: 15 | - 友好的用户界面 16 | - 选择匹配模式(默认为**分离**) 17 | - 自定义是否启用**歌手匹配**与**专辑匹配** 18 | 19 | ### 需要使用的硬件与软件 20 | 21 | #### 硬件 22 | 23 | - 电脑 *1 (系统不限,安装了Java运行环境\[JRE\]即可) 24 | - Android设备: 25 | - 若使用的主力设备**已获取**Root权限,则只需要一台即可,**无视**后文中主力机与备用机的区分。 26 | - 若使用的主力设备**未获取**Root权限,则需要两台设备: 27 | - 一台获取了Root权限的Android设备,真机或虚拟机皆可,后文中称其为**备用设备**。 28 | - 一台主力Android设备,后文中称其为**主力设备**。 29 | 30 | #### 软件 31 | 32 | 本项目需要配合以下软件一起使用: 33 | 34 | - 受支持的[在线音乐平台](README_CLI.md#音乐平台的选择)的Android客户端(均为普通版本,选择自己使用的一个或多个平台) 35 | - [网易云音乐](https://music.163.com/)**(推荐)** 36 | - [QQ音乐](https://y.qq.com/)**(推荐)** 37 | - [酷狗音乐](https://www.kugou.com/) 38 | - [酷我音乐](https://www.kuwo.cn/) 39 | - 音乐标签 40 | - [Windows](https://www.cnblogs.com/vinlxc/p/11347744.html) 41 | - [Android](https://www.coolapk.com/apk/com.xjcheng.musictageditor)**(推荐)** 42 | - 获取ID3标签(下载Release页面中的app-release.apk即可) 43 | - [Android (项目介绍)](https://github.com/Winnie0408/MusicID3TagGetter) 44 | - [Android (下载页面)](https://github.com/Winnie0408/MusicID3TagGetter/releases) 45 | - 文件管理器(选择一个即可) 46 | - [MT管理器](https://www.coolapk.com/apk/bin.mt.plus)**(推荐)** 47 | - [ES文件浏览器](https://www.coolapk.com/apk/com.estrongs.android.pop) 48 | - [MiXplorer](https://mixplorer.com) 49 | - Android虚拟机(当前使用的主力设备**未获取**Root权限时需要,选择一个即可) 50 | - [VMOS Pro](https://www.coolapk.com/apk/com.vmos.pro):在手机上使用的虚拟机 ~~(可能需要使用VIP版,详情查看该应用的酷安评论区)~~ 51 | - [MuMu模拟器](https://mumu.163.com):在电脑上使用的虚拟机 52 | - 椒盐音乐(或糖醋音乐) 53 | 54 | ## 使用方法 55 | 56 | > **Note** 57 | > 58 | > **强烈推荐**您将本README文件完整阅读后,再进行相关操作! 59 | 60 | ### 0. 准备工作 61 | 62 | 安装上述软件 63 | 64 | - **在线音乐平台客户端**与**文件管理器**,安装到有Root权限的设备(或虚拟机)上。 65 | - **音乐标签**、**获取ID3标签**、**椒盐音乐**、文件管理器(可选),安装到主力设备上。 66 | 67 | ### 1. 获取在线音乐平台的歌单数据 68 | 69 | **(在备用设备上操作)** 70 | 71 | 1. 打开需要使用的在线音乐平台客户端APP。 72 | 2. 登录账号。 73 | 3. **依次**点击进入自己的所有歌单(或者需要导出的歌单),并滑动到歌单页的**最底部**,加载当前歌单的所有歌曲。 74 | 4. 重复上述步骤,直到要所有导出的歌单**都加载过一次**。 75 | 5. 主动关闭在线音乐平台客户端(在软件菜单中选择**关闭**\[推荐\],或直接在后台界面中将其划掉)。 76 | 6. 打开文件管理器,**授予Root权限**,进入在线音乐平台客户端的**数据目录**,找到**databases**文件夹,找到指定的数据库文件。
77 | *若觉得各个软件的数据目录比较难找,可以使用MT管理器快速定位:`点击左上角菜单-点击安装包提取-选择需要的音乐APP-点击数据目录1`,即可快速跳转到数据目录。* 78 | - 网易云音乐 79 | - 数据目录:`/data/user/0/com.netease.cloudmusic/databases` 80 | - 数据库文件:`cloudmusic.db` 81 | - QQ音乐 82 | - 数据目录: `/data/user/0/com.tencent.qqmusic/databases` 83 | - 数据库文件:`QQMusic` 84 | - 酷狗音乐 85 | - 数据目录:`/data/user/0/com.kugou.android/databases` 86 | - 数据库文件:`kugou_music_phone_v7.db` 87 | - 酷我音乐 88 | - 数据目录:`/data/user/0/cn.kuwo.player/databases` 89 | - 数据库文件:`kwplayer.db` 90 | 7. 将数据库文件发送到电脑上。 91 | 92 | ### 2. 获取本地音乐的标签(ID3 Tag)信息 93 | 94 | > **Warning** 95 | > 96 | > 本步骤会**覆盖**您本地音乐的ID3标签信息,**请谨慎操作**! 97 | > 98 | > 若您之前已经自行匹配(或修改)过歌曲的ID3信息,可跳过本步骤。 99 | > 100 | > 若后续匹配结果不理想,再重新进行此步骤即可。 101 | 102 | **(在主力设备上操作)** 103 | 104 | 1. 将音乐文件保存在手机里(相信您已经完成这个步骤了)。 105 | 2. 打开**音乐标签**APP。 106 | 1. 点击右上角**刷新**按钮,令其扫描手机中的音乐文件。 107 | 2. 点击左上角**菜单**按钮,点击弹出菜单底部的**设置**。 108 | 3. 点击**组合标签源**,**仅启用**与歌单来源平台**对应的**数据源,点击确定(比如,歌单来源平台为**网易云音乐**,则只启用**网易云**标签源,其他标签源都应**禁用**,若歌单来源平台为**酷狗音乐**,则启用**QQ与酷我**标签源,且QQ的优先级**高于**酷我)。 109 | 4. 返回到音乐标签主界面,点击右下角的**编辑**按钮,点击**自动匹配标签**。 110 | 5. 在弹出的对话框中,**仅勾选**标题、艺术家、专辑,**并同时启用**其右侧的覆盖选项,按需调整“网络搜索线程数”,点击确定。 111 | 6. 等待音乐标签批量匹配完成。 112 | 3. 打开**获取ID3标签**APP。 113 | 1. 点击下方的**选择目录**按钮,根据提示授予所需权限。 114 | 2. 选择音乐存放的目录(具体选择方式请查看[这里](https://github.com/Winnie0408/MusicID3TagGetter/blob/master/README_CLI.md#使用方法)),点击屏幕底部的**使用此文件夹**,在弹出的对话框中点击**允许**。 115 | 3. 等待软件扫描并导出手机中音乐的ID3标签信息。 116 | 4. 前往软件的**导出目录**(手机存储目录中的Download)查看导出的ID3标签信息文件**本地音乐导出.txt**。 117 | 5. 将导出的ID3标签信息文件发送到电脑上。 118 | 119 | ### 3. 进行歌单转换操作 120 | 121 | **(在电脑上操作)** 122 | 123 | 1. 运行本项目([怎么运行?](README_CLI.md#项目的使用与运行)以下方式三选一即可)。 124 | - 使用Java IDE(如IntelliJ IDEA、Eclipse等)从源码运行(需要电脑已配置JDK \[Java开发工具包\])。 125 | - 使用Maven从源码编译、构建,并运行JAR包。 126 | - 下载并运行JAR包(需要电脑已安装JRE \[Java运行环境\])。 127 | 2. 新建**SQLite**目录,并将步骤1.6中获取到的数据库文件复制进去 **(可选,注意不要更改数据库的文件名)**。 128 | - 若从源码运行,则在项目根目录下新建**SQLite**目录。 129 | - 若从JAR包或EXE文件运行,则在JAR包或EXE文件同级目录下新建**SQLite**目录。 130 | 3. 根据本项目的提示,进行文字输入或选择操作。 131 | 4. 等待您选择的所有歌单转换完成。 132 | 5. 使用本地或在线的Markdown编辑器,查看转换过程中在项目根目录生成的日志文件`ConvertLog.md`,查看转换过程是否出现错误或意外。**(可选)** 133 | 6. 获取转换结果文件(文件名为歌单的名称),将其发送到主力Android设备上。 134 | - 若从源码运行,则在项目根目录下的**Result**目录中。 135 | - 若从JAR包或EXE文件运行,则在JAR包或EXE文件同级目录下的**Result**目录中。 136 | 137 | ### 4. 将歌单导入椒盐音乐 138 | 139 | **(在主力设备上操作)** 140 | 141 | 1. 打开**椒盐音乐**APP。 142 | 2. 右滑或点击右上角菜单按钮,进入**菜单**,点击**歌单**。 143 | 3. 点击**导入歌单 (.txt)**。 144 | 4. **逐个选择**转换结果文件,将其导入到椒盐音乐中。 145 | 5. 在椒盐音乐中查看导入的歌单。 146 | 147 | ## 其他事项 148 | 149 | ### 项目的使用与运行 150 | 151 | #### 1. 使用Java IDE(如IntelliJ IDEA、Eclipse等)从源码运行 152 | 153 | 1. 克隆或下载本项目的源码。 154 | 155 | ```bash 156 | git clone git@github.com:Winnie0408/SaltPlayerConverter.git 157 | ``` 158 | 159 | 2. 使用Java IDE打开本并信任项目。 160 | 3. 打开项目根目录下的`pom.xml`文件,使用IDE自带的Maven工具下载项目所需的依赖。(推荐[配置Maven源为阿里云或其他国内镜像](README_CLI.md#配置maven镜像源),以加快下载速度)。 161 | 4. 运行项目中的`src/main/java/Main.java`文件。 162 | 163 | #### 2. 使用Maven从源码编译、并运行JAR包 164 | 165 | 1. 克隆或下载本项目的源码。 166 | 167 | ```bash 168 | git clone git@github.com:Winnie0408/SaltPlayerConverter.git 169 | ``` 170 | 171 | 2. 进入项目目录,使用Maven编译项目。(推荐[配置Maven源为阿里云或其他国内镜像](README_CLI.md#配置maven镜像源),以加快下载速度)。 172 | 173 | ```bash 174 | mvn clean package 175 | ``` 176 | 177 | 3. 等待编译完成,控制台输出`BUILD SUCCESS`,进入项目中的`target`目录,运行以`.jar`结尾的文件。 178 | 179 | ```bash 180 | java -jar [FileName].jar 181 | ``` 182 | 183 | #### 3. 下载并运行JAR包 184 | 185 | 1. 在项目的[Release页面](https://github.com/Winnie0408/SaltPlayerConverter/releases),找到最新版本,下载以`.jar`结尾的文件。 186 | 2. 运行刚刚下载的JAR包。 187 | 188 | ```bash 189 | java -jar [FileName].jar 190 | ``` 191 | 192 | ### 配置Maven镜像源 193 | 194 | #### Windows 195 | 196 | - 使用自行安装的Maven 197 | 1. 在Maven的安装目录中,找到并打开`conf/settings.xml`文件(没有的话就自行创建一个)。 198 | 2. 在该文件的``节点中添加子节点。 199 | - 使用IDE自带的Maven 200 | 1. 进入`C:\Users\[Username]\.m2`目录,找到并打开`settings.xml`文件(没有的话就自行创建一个)。 201 | 2. 在该文件的``节点中添加子节点。 202 | 203 | #### Linux 204 | 205 | 1. 进入`/etc/maven/conf`目录,找到并打开`settings.xml`文件。 206 | 2. 在该文件的``节点中添加子节点。 207 | 208 | #### 可用子节点(添加一个或多个皆可) 209 | 210 | - 阿里云 211 | 212 | ```xml 213 | 214 | 215 | aliyunmaven 216 | * 217 | 阿里云公共仓库 218 | https://maven.aliyun.com/repository/public 219 | 220 | ``` 221 | 222 | - 网易 223 | 224 | ```xml 225 | 226 | 227 | netease 228 | http://maven.netease.com/repository/public/ 229 | central 230 | 231 | ``` 232 | 233 | - 中国科学技术大学USTC 234 | 235 | ```xml 236 | 237 | 238 | ustc 239 | http://mirrors.ustc.edu.cn/maven/maven2/ 240 | central 241 | 242 | ``` 243 | 244 | - [其他镜像源](https://blog.csdn.net/qq_38217990/article/details/129257106) 245 | 246 | #### 完整配置文件示例 247 | 248 | ```xml 249 | 250 | 253 | 254 | 255 | 256 | alimaven 257 | central 258 | aliyun maven 259 | http://maven.aliyun.com/nexus/content/repositories/central/ 260 | 261 | 262 | 263 | 264 | 265 | repo1 266 | central 267 | Human Readable Name for this Mirror. 268 | https://repo1.maven.org/maven2/ 269 | 270 | 271 | 272 | 273 | 274 | repo2 275 | central 276 | Human Readable Name for this Mirror. 277 | https://repo2.maven.org/maven2/ 278 | 279 | 280 | 281 | repo2 282 | central 283 | Human Readable Name for this Mirror. 284 | https://search.maven.org/ 285 | 286 | 287 | 288 | 289 | ``` 290 | 291 | ### 配置文件 292 | 293 | 本项目在运行时会读取并写入**项目根目录**下的`config.properties`文件,当该文件不存在时会自动创建。 294 | 295 | #### 配置文件的格式 296 | 297 | `配置名=配置值` 298 | 299 | (如:`version=0.99`、`enableStatistic=true`) 300 | 301 | #### 可使用的配置项 302 | 303 | - `uuid`: 为这台电脑生成的唯一ID。将程序第一次运行,且`enableStatistic`值不为`false`时生成。 **强烈不建议**您手动修改本项的值。 304 | - `enableStatistic`: 是否启用**统计数据发送**功能。 305 | - `true`: **启用**统计数据发送功能,程序会在**每转换完一个歌单后**,向统计分析服务器发送本次转换的相关统计信息,该信息**不包含您的任何敏感信息**,只会发送部分程序运行效率相关的数据[(包含哪些数据?)](README_CLI.md#发送的统计数据)。 306 | - `false`: **禁用**统计数据发送功能,程序**不会**向统计分析服务器发送任何数据,**不会连接互联网**;若**第一次运行程序时**就将其值设置为`false`,则不会生成`uuid`。 307 | - 配置文件中不包含该项 或 值为其他文本: 同`true`。 308 | - `.*DatabasePath`: 用于保存您上一次输入的数据库文件的绝对路径。若您将数据库文件存放在项目的SQLite目录下,并在程序运行时直接`回车`,则该项不会生成。 309 | - `musicOutputPath`: 用于保存您上一次输入的`本地音乐导出.txt`文件的绝对路径。 310 | - `enableParenthesesRemoval`: 是否启用**括号内容去除**功能。启用此功能可以大幅提升外语歌曲的识别正确率[(为什么?)](README_CLI.md#括号内容去除)。 311 | - `true`: **启用**括号内容去除功能。 312 | - `false`: **禁用**括号内容去除功能。 313 | - 配置文件中不包含该项 或 值为其他文本: 忽略本项的值,并在运行时询问用户是否启用括号内容去除功能。 314 | 315 | ### 发送的统计数据 316 | 317 | - 当前电脑的唯一ID 318 | - 开始转换的时间 319 | - 结束转换的时间 320 | - 歌单来源 321 | - 当前歌单包含的歌曲数量 322 | - 匹配成功的歌曲数 323 | - 自动匹配成功歌曲数量 324 | - 使用的相似度阈值 325 | - 括号去除是否启用 326 | - 专辑匹配是否启用 327 | - 最终保存了多少首歌曲 328 | 329 | ### 括号内容去除 330 | 331 | 大部分音乐平台对外语歌曲信息的命名方式一般为: `外文 (中文翻译)`或`外文 (歌曲来源、歌曲版本等)`。如`City Of Stars (From "La La Land" Soundtrack)`、`CALL ME BABY (叫我) (Chinese Ver.)`、`桜色舞うころ (樱花纷飞时)`。 332 | 333 | 启用此功能可以将字符串中的括号部分删去,只保留外文名,即:`外文`。如:`City Of Stars`、`CALL ME BABY`、`桜色舞うころ`,以此提高自动匹配成功率。 334 | 335 | 但需要注意,部分歌曲会在歌名后用括号注明歌曲版本:`歌名 (歌曲版本)`。如`曾经我也想过一了百了 (Live)`、`TruE (Ed Ver.)`,在这种情况下,若启用了本功能,会将其变成:`曾经我也想过一了百了`、`TruE`,继而**可能会出现匹配错误**。 336 | 337 | 请您根据您的实际情况,决定是否使用本功能。 338 | 339 | ### 音乐平台的选择 340 | 341 | #### **网易云音乐** 与/或 **QQ音乐** 342 | 343 | 这两个平台的歌曲信息正确率较高,且较为完整、权威,可以有效提高自动匹配的成功率。 344 | 345 | #### 酷狗音乐 346 | 347 | 该平台歌曲信息不太符合规范,合唱歌曲的艺术家名使用`、`分隔,且括号、斜杠的使用比较混乱,且**非【我喜欢】歌单**中歌曲的专辑信息不会保存到数据库中,导致匹配精确度下降,不太建议使用。 348 | 349 | #### 酷我音乐 350 | 351 | 该平台歌曲信息不太符合规范,合唱歌曲的艺术家名使用`&`分隔,且括号、斜杠的使用比较混乱,且有很多用户自行上传的歌曲,这些歌曲的ID3信息大部分都不完整且不合规范,可能导致匹配精确度下降,不太建议使用。 352 | 353 | ### 相似度阈值 354 | 355 | 程序认为两个字符串**相同**的相似度大小,详情: 356 | 357 | 若当前阈值为0.8: 358 | 359 | - **相同**
360 | 字符串1:想いの眠るゆりかご (回忆长眠的摇篮)
361 | 字符串2:想いの眠るゆりかご (回忆长眠的摇篮)
362 | 相似度:1.0 363 | 364 | - **相同**
365 | 字符串1:伤感 II
366 | 字符串2:伤感 I
367 | 相似度:0.8 368 | 369 | - **不相同**
370 | 字符串1:I'M OK
371 | 字符串2:I AM OK
372 | 相似度:0.7142857142857143 373 | 374 | - **不相同**
375 | 字符串1:BANG BANG BANG (뱅뱅뱅)
376 | 字符串2:BANG BANG BANG
377 | 相似度:0.7 378 | 379 | - **不相同**
380 | 字符串1:이 사랑 (这份爱) (Inst.)
381 | 字符串2:이 사랑 (这份爱)
382 | 相似度:0.5555555555555556 383 | 384 | - **不相同**
385 | 字符串1:aaabbbccc
386 | 字符串2:abcabcabc
387 | 相似度:0.33333333333333337 388 | 389 | ## 赞助与支持 390 | 391 | 🥰🥰🥰 392 | 393 | 如果这个项目对您有所帮助,您可以给我一颗免费的⭐,或者请我喝杯咖啡!
394 | 非常感谢您的支持!
395 | ⬇️⬇️⬇️
396 | 397 | Sponsorship.jpg 398 | 399 | 400 | -------------------------------------------------------------------------------- /README_ENG.md: -------------------------------------------------------------------------------- 1 |

中文 / English

2 |

3 | cover 4 |

Welcome to Salt Player Song List Conversion Assistant!

5 |

6 | 7 | --- 8 | 9 | # Under Construction…… 10 | 11 | # 🛠️🛠️🛠️ 12 | 13 | ## 🛠️🛠️ 14 | 15 | ### 🛠️ 16 | 17 | ## Sponsorship & Support 18 | 19 | You can give me a free ⭐ or buy me a cup of coffee if my project is helpful to you!
20 | Thank you very much for your support!
21 | 22 | Sponsorship.jpg 23 | 24 | 25 | -------------------------------------------------------------------------------- /markdownResources/Alipay WeChatPay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Winnie0408/SaltPlayerConverter/9b8e3862d842b4ff16cfc5ddb817e0cf368f475e/markdownResources/Alipay WeChatPay.jpg -------------------------------------------------------------------------------- /markdownResources/Sponsorship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Winnie0408/SaltPlayerConverter/9b8e3862d842b4ff16cfc5ddb817e0cf368f475e/markdownResources/Sponsorship.png -------------------------------------------------------------------------------- /markdownResources/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Winnie0408/SaltPlayerConverter/9b8e3862d842b4ff16cfc5ddb817e0cf368f475e/markdownResources/cover.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.tools 8 | ConvertToSaltPlayer_ReNew 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 21 13 | 21 14 | UTF-8 15 | 16 | 17 | 18 | 19 | org.xerial 20 | sqlite-jdbc 21 | 3.42.0.0 22 | 23 | 24 | 25 | com.alibaba 26 | fastjson 27 | 2.0.25 28 | 29 | 30 | 31 | junit 32 | junit 33 | 4.13.2 34 | test 35 | 36 | 37 | 38 | org.apache.httpcomponents 39 | httpclient 40 | 4.5.14 41 | 42 | 43 | 44 | 45 | 46 | 47 | maven-assembly-plugin 48 | 49 | 50 | jar-with-dependencies 51 | 52 | 53 | 54 | true 55 | Main 56 | 57 | 58 | 59 | 60 | 61 | make-my-jar-with-dependencies 62 | package 63 | 64 | single 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/java/Main.java: -------------------------------------------------------------------------------- 1 | import converter.Universal; 2 | import utils.MarkdownLog; 3 | import utils.PropertiesRelated; 4 | import utils.Statistic; 5 | import utils.TerminalCharSetDetect; 6 | 7 | import java.util.Scanner; 8 | 9 | /** 10 | * @description: 欢迎页面,功能入口 11 | * @author: HWinZnieJ 12 | * @create: 2023-09-04 16:46 13 | **/ 14 | 15 | public class Main { 16 | public static void main(String[] args) { 17 | TerminalCharSetDetect.get(); 18 | 19 | Statistic.saveUuid(); 20 | MarkdownLog.checkLogFile(); 21 | System.out.println("欢迎使用椒盐音乐歌单转换小助手!"); 22 | while (true) { 23 | System.out.print(""" 24 | \t1. 网易云音乐 25 | \t2. QQ音乐 26 | \t3. 酷狗音乐 27 | \t4. 酷我音乐 28 | \t其他字符. 退出程序 29 | 请选择歌单来源(输入数字):"""); 30 | Scanner scanner = new Scanner(System.in, PropertiesRelated.read().getProperty("terminalCharSet")); 31 | switch (scanner.next()) { 32 | case "1" -> new Universal().init("CloudMusic"); 33 | case "2" -> new Universal().init("QQMusic"); 34 | case "3" -> new Universal().init("KugouMusic"); 35 | case "4" -> new Universal().init("KuwoMusic"); 36 | default -> { 37 | System.out.println("感谢您的使用,再见!"); 38 | System.exit(0); 39 | } 40 | } 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/converter/Universal.java: -------------------------------------------------------------------------------- 1 | package converter; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import database.Database; 5 | import utils.*; 6 | 7 | import java.io.File; 8 | import java.io.FileWriter; 9 | import java.io.IOException; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.sql.Connection; 13 | import java.sql.ResultSet; 14 | import java.sql.SQLException; 15 | import java.sql.Statement; 16 | import java.text.SimpleDateFormat; 17 | import java.util.*; 18 | import java.util.stream.Collectors; 19 | 20 | /** 21 | * @description: 歌单来源:统一调用 22 | * @author: HWinZnieJ 23 | * @create: 2023-09-06 15:28 24 | **/ 25 | 26 | public class Universal { 27 | private static String SOURCE_ENG; //歌单来源英文名 28 | private static String SOURCE_CHN; //歌单来源中文名 29 | private static String DATABASE_NAME; //数据库名 30 | private static String SONG_LIST_TABLE_NAME; //歌单表名 31 | private static String SONG_LIST_ID; //歌单表的歌单ID列名 32 | private static String SONG_LIST_NAME; //歌单表的歌单名列名 33 | private static String SONG_LIST_SONG_INFO_TABLE_NAME; //歌单歌曲信息表名 34 | private static String SONG_LIST_SONG_INFO_PLAYLIST_ID; //歌单歌曲信息表中的歌单ID字段名 35 | private static String SONG_LIST_SONG_INFO_SONG_ID; //歌单歌曲信息表中的歌曲ID字段名 36 | private static String SONG_INFO_TABLE_NAME; //歌曲信息表名 37 | private static String SORT_FIELD; //歌单中歌曲的排序方式 38 | private static String SONG_INFO_SONG_ID; //歌曲信息表的歌曲ID字段名 39 | private static String SONG_INFO_SONG_NAME; //歌曲信息表的歌曲名字段名 40 | private static String SONG_INFO_SONG_ARTIST; //歌曲信息表的歌手名字段名 41 | private static String SONG_INFO_SONG_ALBUM; //歌曲信息表的专辑名字段名 42 | 43 | Scanner scanner = new Scanner(System.in, PropertiesRelated.read().getProperty("terminalCharSet")); //从标准输入获取数据 44 | Database database = new Database(); //数据库操作 45 | Connection conn; //数据库连接 46 | String[][] localMusic; //存放本地音乐信息的二维数组 [歌曲名][歌手名][专辑名][文件路径] 47 | ArrayList playListId = new ArrayList<>(); //存放歌单ID 48 | ArrayList playListName = new ArrayList<>(); //存放歌单名 49 | Map songNum; //存放某ID所对应歌单中的歌曲数量 [歌单ID][歌曲数量] 50 | Queue selectedPlayListId = new LinkedList<>(); //存放用户选择的歌单序号 51 | Properties prop; //存放配置文件 52 | SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //日志时间格式 53 | 54 | /** 55 | * 初始化 56 | */ 57 | public void init(String sourceApp) { 58 | switch (sourceApp) { 59 | case "QQMusic" -> { 60 | SOURCE_ENG = "QQMusic"; 61 | SOURCE_CHN = "QQ音乐"; 62 | DATABASE_NAME = "QQMusic"; 63 | SONG_LIST_TABLE_NAME = "User_Folder_table"; 64 | SONG_LIST_ID = "folderid"; 65 | SONG_LIST_NAME = "foldername"; 66 | SONG_LIST_SONG_INFO_TABLE_NAME = "User_Folder_Song_table"; 67 | SONG_LIST_SONG_INFO_PLAYLIST_ID = "folderid"; 68 | SONG_LIST_SONG_INFO_SONG_ID = "id"; 69 | SONG_INFO_TABLE_NAME = "Song_table"; 70 | SORT_FIELD = "position"; 71 | SONG_INFO_SONG_ID = "id"; 72 | SONG_INFO_SONG_NAME = "name"; 73 | SONG_INFO_SONG_ARTIST = "singername"; 74 | SONG_INFO_SONG_ALBUM = "albumname"; 75 | } 76 | case "CloudMusic" -> { 77 | SOURCE_ENG = "CloudMusic"; 78 | SOURCE_CHN = "网易云音乐"; 79 | DATABASE_NAME = "cloudmusic.db"; 80 | SONG_LIST_TABLE_NAME = "playlist"; 81 | SONG_LIST_ID = "_id"; 82 | SONG_LIST_NAME = "name"; 83 | SONG_LIST_SONG_INFO_TABLE_NAME = "playlist_track"; 84 | SONG_LIST_SONG_INFO_PLAYLIST_ID = "playlist_id"; 85 | SONG_LIST_SONG_INFO_SONG_ID = "track_id"; 86 | SONG_INFO_TABLE_NAME = "track"; 87 | SORT_FIELD = "track_order"; 88 | SONG_INFO_SONG_ID = "id"; 89 | SONG_INFO_SONG_NAME = "name"; 90 | SONG_INFO_SONG_ARTIST = "artists"; 91 | SONG_INFO_SONG_ALBUM = "album_name"; 92 | } 93 | case "KugouMusic" -> { 94 | SOURCE_ENG = "KuGouMusic"; 95 | SOURCE_CHN = "酷狗音乐"; 96 | DATABASE_NAME = "kugou_music_phone_v7.db"; 97 | SONG_LIST_TABLE_NAME = "kugou_playlists"; 98 | SONG_LIST_ID = "_id"; 99 | SONG_LIST_NAME = "name"; 100 | SONG_LIST_SONG_INFO_TABLE_NAME = "playlistsong"; 101 | SONG_LIST_SONG_INFO_PLAYLIST_ID = "plistid"; 102 | SONG_LIST_SONG_INFO_SONG_ID = "songid"; 103 | SONG_INFO_TABLE_NAME = "kugou_songs"; 104 | SORT_FIELD = "cloudfileorderweight"; 105 | SONG_INFO_SONG_ID = "_id"; 106 | SONG_INFO_SONG_NAME = "trackName"; 107 | SONG_INFO_SONG_ARTIST = "artistName"; 108 | SONG_INFO_SONG_ALBUM = "albumName"; 109 | } 110 | case "KuwoMusic" -> { 111 | SOURCE_ENG = "KuWoMusic"; 112 | SOURCE_CHN = "酷我音乐"; 113 | DATABASE_NAME = "kwplayer.db"; 114 | SONG_LIST_TABLE_NAME = "v3_list"; 115 | SONG_LIST_ID = "id"; 116 | SONG_LIST_NAME = "showname"; 117 | SONG_LIST_SONG_INFO_TABLE_NAME = "v3_music"; 118 | SONG_LIST_SONG_INFO_PLAYLIST_ID = "listid"; 119 | SONG_LIST_SONG_INFO_SONG_ID = "rid"; 120 | SONG_INFO_TABLE_NAME = "v3_music"; 121 | SORT_FIELD = "id"; 122 | SONG_INFO_SONG_ID = "rid"; 123 | SONG_INFO_SONG_NAME = "name"; 124 | SONG_INFO_SONG_ARTIST = "artist"; 125 | SONG_INFO_SONG_ALBUM = "album"; 126 | } 127 | } 128 | Logger.info("您选择了源格式为【" + SOURCE_CHN + "】的歌单"); 129 | 130 | Statistic.usage("Access"); 131 | 132 | Sleep.start(500); 133 | 134 | //读取配置文件 135 | prop = PropertiesRelated.read(); 136 | 137 | FileOperation.createDir(new File("./Result/" + SOURCE_ENG)); 138 | FileOperation.checkDir(new File("./Result/" + SOURCE_ENG)); 139 | 140 | readTxtFile(); 141 | readDatabase(); 142 | if (prepareSummary() == 1) return; 143 | start(); 144 | afterwards(); 145 | } 146 | 147 | /** 148 | * 读取数据库,并处理数据,获取歌单ID与歌单名 149 | */ 150 | private void readDatabase() { 151 | while (true) { 152 | if (prop.getProperty(SOURCE_ENG + "DatabasePath") != null) { 153 | System.out.println("\t直接回车使用上次输入的路径【" + prop.getProperty(SOURCE_ENG + "DatabasePath") + "】"); 154 | System.out.print("请输入" + SOURCE_CHN + "数据库文件的绝对路径:"); 155 | } else 156 | System.out.print("请输入" + SOURCE_CHN + "数据库文件的绝对路径," + 157 | "\n或将数据库文件复制到项目的SQLite目录后,按下回车:"); 158 | String input = scanner.nextLine(); 159 | 160 | if (input.isEmpty() && prop.getProperty(SOURCE_ENG + "DatabasePath") == null) { 161 | conn = database.getConnection("SQLite/" + DATABASE_NAME); 162 | if (conn == null) { 163 | Logger.error("项目SQLite目录内的" + DATABASE_NAME + "不存在,请检查!"); 164 | continue; 165 | } 166 | Logger.info("使用项目SQLite目录内的" + DATABASE_NAME + "文件"); 167 | } else if (input.isEmpty() && prop.getProperty(SOURCE_ENG + "DatabasePath") != null) { 168 | input = prop.getProperty(SOURCE_ENG + "DatabasePath"); 169 | File path = new File(input); 170 | if (!path.exists()) { 171 | Logger.error("上次使用路径" + input + "的数据库文件不存在,请检查并重新输入!"); 172 | continue; 173 | } 174 | conn = database.getConnection(input); 175 | if (conn == null) { 176 | Logger.error("上次使用路径" + input + "的数据库文件无法读取,请检查并重新输入!"); 177 | continue; 178 | } 179 | Logger.info("使用上次输入的路径" + input + "的数据库文件"); 180 | } else { 181 | input = FileOperation.deleteQuotes(input); 182 | File path = new File(input); 183 | if (!path.exists()) { 184 | Logger.error("指定路径" + input + "的数据库文件不存在,请检查并重新输入!"); 185 | continue; 186 | } 187 | conn = database.getConnection(input); 188 | if (conn == null) { 189 | Logger.error("指定路径" + input + "的数据库文件无法读取,请检查并重新输入!"); 190 | continue; 191 | } 192 | Logger.info("使用指定路径" + input + "的数据库文件"); 193 | PropertiesRelated.save(SOURCE_ENG + "DatabasePath", input); 194 | if (!input.isEmpty()) 195 | Logger.success("已将您本次输入的路径保存至配置文件"); 196 | } 197 | break; 198 | } 199 | Sleep.start(800); 200 | 201 | try { 202 | Statement stmt = conn.createStatement(); 203 | ResultSet rs; 204 | if (SOURCE_ENG.equals("KuWoMusic")) 205 | rs = stmt.executeQuery("SELECT " + SONG_LIST_ID + ", " + SONG_LIST_NAME + " FROM " + SONG_LIST_TABLE_NAME + " WHERE uid NOT NULL"); // 读取歌单列表 206 | else 207 | rs = stmt.executeQuery("SELECT " + SONG_LIST_ID + ", " + SONG_LIST_NAME + " FROM " + SONG_LIST_TABLE_NAME); // 读取歌单列表 208 | 209 | while (rs.next()) { 210 | playListId.add(rs.getString(SONG_LIST_ID)); // 保存歌单ID 211 | playListName.add(rs.getString(SONG_LIST_NAME)); // 保存歌单名 212 | } 213 | 214 | songNum = new HashMap<>(); 215 | 216 | ArrayList listToBeDelete = new ArrayList<>(); 217 | 218 | for (int i = 0; i < playListId.size(); i++) { 219 | //检查歌单是否包含歌曲 220 | if (stmt.executeQuery("SELECT COUNT(*) FROM " + SONG_LIST_SONG_INFO_TABLE_NAME + " WHERE " + SONG_LIST_SONG_INFO_PLAYLIST_ID + "=" + playListId.get(i)).getInt(1) == 0) { 221 | Logger.warning("歌单【" + playListName.get(i) + "】不包含任何歌曲,请您在" + SOURCE_CHN + "APP中重新打开该歌单后再试"); 222 | listToBeDelete.add(i); 223 | } else { 224 | rs.close(); 225 | rs = stmt.executeQuery("SELECT COUNT(*) FROM " + SONG_LIST_SONG_INFO_TABLE_NAME + " WHERE " + SONG_LIST_SONG_INFO_PLAYLIST_ID + "=" + playListId.get(i)); 226 | songNum.put(playListId.get(i), String.valueOf(rs.getInt(1))); 227 | } 228 | } 229 | 230 | //删除不包含歌曲的歌单 231 | for (int i = listToBeDelete.size() - 1; i >= 0; i--) { 232 | playListName.remove(playListName.get(listToBeDelete.get(i))); 233 | playListId.remove(playListId.get(listToBeDelete.get(i))); 234 | } 235 | 236 | } catch (SQLException e) { 237 | Logger.error("很抱歉!程序运行出现错误,请重试\n错误详情:" + e); 238 | } 239 | } 240 | 241 | /** 242 | * 读取歌单txt文件 243 | */ 244 | private void readTxtFile() { 245 | boolean readFromConfig = false; //是否从配置文件中读取路径 246 | while (true) { 247 | if (prop.getProperty("musicOutputPath") != null) { 248 | System.out.println("\t直接回车使用上次输入的路径【" + prop.getProperty("musicOutputPath") + "】"); 249 | } 250 | System.out.print("请输入手机导出的“本地音乐导出.txt”文件绝对路径:"); 251 | String input = scanner.nextLine(); 252 | if (input.isEmpty() && prop.getProperty("musicOutputPath") != null) { 253 | input = prop.getProperty("musicOutputPath"); 254 | readFromConfig = true; 255 | } 256 | input = FileOperation.deleteQuotes(input); 257 | try { 258 | String[] localMusicFile = Files.readString(Paths.get(input)).split("\n"); 259 | localMusic = new String[localMusicFile.length][4]; 260 | int a = 0; 261 | for (String i : localMusicFile) { 262 | localMusic[a][0] = i.split("#\\*#")[0]; 263 | localMusic[a][1] = i.split("#\\*#")[1]; 264 | localMusic[a][2] = i.split("#\\*#")[2]; 265 | localMusic[a][3] = i.split("#\\*#")[3]; 266 | a++; 267 | } 268 | PropertiesRelated.save("musicOutputPath", input); 269 | Logger.success("文件解析成功"); 270 | if (!readFromConfig) 271 | Logger.success("已将您本次输入的路径保存至配置文件"); 272 | break; 273 | } catch (Exception e) { 274 | Logger.error("无法读取指定路径【" + input + "】的文件,请检查!错误信息:" + e); 275 | continue; 276 | } 277 | } 278 | } 279 | 280 | /** 281 | * 输出准备完成的相关信息 282 | */ 283 | private int prepareSummary() { 284 | Logger.info("共读取到" + playListId.size() + "个有效歌单"); 285 | for (int i = 0; i < playListId.size(); i++) { 286 | System.out.println("\t歌单" + (i + 1) + ". 【" + playListName.get(i) + "】,包含" + songNum.get(playListId.get(i)) + "首歌曲"); 287 | } 288 | System.out.println("请结合" + SOURCE_CHN + "APP中显示的歌单数据,检查以上歌单信息是否正确"); 289 | Statistic.usage("Uploaded"); 290 | 291 | while (true) { 292 | System.out.print(""" 293 | \t输入“Y/y”导出全部歌单; 294 | \t输入歌单名称前的序号导出所选歌单(可多选,输入示例:1 2 6 7 8 10); 295 | \t输入其他任意字符返回主菜单 296 | 请选择:"""); 297 | String input = scanner.nextLine(); 298 | input = input.toLowerCase(); 299 | 300 | try { 301 | if (input.contains(" ")) { 302 | // 使用Stream将字符串分割、转换为整数、排序、再转换回字符串 303 | selectedPlayListId = Arrays.stream(input.split(" ")) 304 | .map(Integer::parseInt) 305 | .sorted() 306 | .map(String::valueOf) 307 | .collect(Collectors.toCollection(LinkedList::new)); 308 | if (Integer.parseInt(((LinkedList) selectedPlayListId).get(selectedPlayListId.size() - 1).toString()) > playListId.size()) { 309 | Logger.error("输入的歌单序号超出范围,请重新输入!"); 310 | Sleep.start(500); 311 | continue; 312 | } 313 | selectedPlayListId.add("-1"); 314 | } else if (input.matches("[0-9]+")) { 315 | if (Integer.parseInt(input) > playListId.size()) { 316 | Logger.error("输入的歌单序号超出范围,请重新输入!"); 317 | Sleep.start(500); 318 | continue; 319 | } 320 | selectedPlayListId.add(input); 321 | selectedPlayListId.add("-1"); 322 | } else if (!input.equals("y")) { 323 | Logger.info("返回主菜单"); 324 | Sleep.start(500); 325 | return 1; 326 | } 327 | } catch (Exception e) { 328 | Logger.error("输入有误,请重新输入!"); 329 | Sleep.start(500); 330 | continue; 331 | } 332 | return 0; 333 | } 334 | } 335 | 336 | /** 337 | * 开始匹配 338 | */ 339 | private void start() { 340 | double similaritySame; //认定为两个字符串相同的相似度阈值 341 | while (true) { 342 | System.out.print("请输入您认为两首歌的信息相同的相似度阈值(0.0~1.0,默认为0.85):"); 343 | String input = scanner.nextLine(); 344 | if (input.isEmpty()) { 345 | similaritySame = 0.85; 346 | } else { 347 | if (Double.parseDouble(input) < 0.0 || Double.parseDouble(input) > 1.0) { 348 | Logger.warning("输入的值不在0.0~1.0之间,请重新输入!"); 349 | continue; 350 | } 351 | similaritySame = Double.parseDouble(input); 352 | } 353 | break; 354 | } 355 | 356 | boolean parenthesesRemoval = false; //是否启用括号内容去除 357 | 358 | Logger.info("开始匹配"); 359 | Sleep.start(300); 360 | try { 361 | if (prop.getProperty("enableParenthesesRemoval") != null) { 362 | if (prop.getProperty("enableParenthesesRemoval").equals("true")) { 363 | Logger.info("您已在配置文件中【启用】括号去除"); 364 | Sleep.start(500); 365 | parenthesesRemoval = true; 366 | } else if (prop.getProperty("enableParenthesesRemoval").equals("false")) { 367 | Logger.info("您已在配置文件中【禁用】括号去除"); 368 | Sleep.start(500); 369 | parenthesesRemoval = false; 370 | } 371 | } else { 372 | while (true) { 373 | System.out.print("是否对此歌单启用括号去除?启用此功能(应该)可以大幅提升外语歌曲的识别正确率 (y/N):"); 374 | String input = scanner.nextLine(); 375 | if (input.equalsIgnoreCase("y")) { 376 | parenthesesRemoval = true; 377 | // PropertiesRelated.save("parenthesesRemoval", "true"); 378 | Logger.info("已启用括号去除"); 379 | // Logger.success("已将您的选择保存至配置文件"); 380 | } else if (input.equalsIgnoreCase("n") || input.isEmpty()) { 381 | parenthesesRemoval = false; 382 | // PropertiesRelated.save("parenthesesRemoval", "false"); 383 | Logger.info("已禁用括号去除"); 384 | // Logger.success("已将您的选择保存至配置文件"); 385 | } else { 386 | Logger.warning("输入有误,请重新输入!"); 387 | continue; 388 | } 389 | Sleep.start(500); 390 | break; 391 | } 392 | } 393 | 394 | boolean stop = false; 395 | 396 | for (int i = 0; i < playListId.size(); i++) { //遍历读取到的所有歌单 397 | if (!selectedPlayListId.isEmpty()) { //获取用户选择的歌单 398 | //如果用户选择的歌单序号不等于当前歌单序号,则跳过当前歌单 399 | if (Integer.parseInt(selectedPlayListId.peek()) - 1 != i) 400 | continue; 401 | else 402 | selectedPlayListId.poll(); //如果用户选择的歌单序号等于当前歌单序号,则弹出当前歌单序号 403 | } 404 | 405 | Statistic.usage("Save"); 406 | 407 | Statement stmt = conn.createStatement(); 408 | Statement stmt1 = conn.createStatement(); 409 | ResultSet rs; 410 | ResultSet rs1; 411 | 412 | String songName; //歌曲名 413 | String songArtist; //歌手名 414 | String songAlbum; //专辑名 415 | int num = 0; //当前第几首歌 416 | int successNum = 0; //成功匹配的歌曲数量 417 | int autoSuccessCount = 0; //自动匹配成功的歌曲数量 418 | long startTime = System.currentTimeMillis(); //开始时间 419 | 420 | Logger.info("======正在匹配歌单【" + playListName.get(i) + "】======"); 421 | MarkdownLog.date(dateFormat.format(new Date())); 422 | MarkdownLog.playListTitle(playListName.get(i)); 423 | Sleep.start(300); 424 | 425 | boolean disableAlbumNameMatch = false; //是否禁用专辑名称匹配 426 | 427 | if (SOURCE_ENG.equals("KugouMusic") && !playListName.get(i).equals("我喜欢")) { 428 | System.out.println(""" 429 | \t检测到您正在匹配【酷狗音乐】的非【我喜欢】歌单, 430 | \t由于酷狗音乐自身原因,非【我喜欢】歌单中歌曲的专辑信息未保存到数据库中, 431 | \t建议对此类歌单禁用【专辑名称匹配】功能,以提升自动匹配成功率 432 | \t(禁用后,专辑名称的相似度将始终显示为100%)"""); 433 | System.out.print("是否对歌单【" + playListName.get(i) + "】禁用专辑名称匹配? (Y/n)"); 434 | while (true) { 435 | String input = scanner.nextLine(); 436 | if (input.isEmpty() || input.equalsIgnoreCase("y")) { 437 | Logger.info("已禁用专辑名称匹配"); 438 | disableAlbumNameMatch = true; 439 | break; 440 | } else if (input.equalsIgnoreCase("n")) { 441 | Logger.info("专辑名称匹配保持启用"); 442 | disableAlbumNameMatch = false; 443 | break; 444 | } else { 445 | Logger.warning("输入有误,请重新输入!"); 446 | } 447 | } 448 | } 449 | 450 | Sleep.start(300); 451 | 452 | boolean allYes = false; 453 | boolean allNo = false; 454 | 455 | //遍历歌单中的所有歌曲 456 | rs = stmt.executeQuery("SELECT " + SONG_LIST_SONG_INFO_SONG_ID + " FROM " + SONG_LIST_SONG_INFO_TABLE_NAME + " WHERE " + SONG_LIST_SONG_INFO_PLAYLIST_ID + "='" + playListId.get(i) + "'ORDER BY " + SORT_FIELD); 457 | while (rs.next()) { 458 | String trackId = rs.getString(SONG_LIST_SONG_INFO_SONG_ID); //歌曲ID 459 | rs1 = stmt1.executeQuery("SELECT " + SONG_INFO_SONG_NAME + ", " + SONG_INFO_SONG_ARTIST + ", " + SONG_INFO_SONG_ALBUM + " FROM " + SONG_INFO_TABLE_NAME + " WHERE " + SONG_INFO_SONG_ID + "=" + trackId); //使用歌曲ID查询歌曲信息 460 | 461 | songName = rs1.getString(SONG_INFO_SONG_NAME); 462 | if (songName == null) songName = ""; 463 | 464 | songArtist = rs1.getString(SONG_INFO_SONG_ARTIST); 465 | if (songArtist == null) songArtist = ""; 466 | //网易云音乐歌手名为JSON格式,需要特殊处理 467 | if (SOURCE_ENG.equals("CloudMusic")) 468 | songArtist = JSON.parseObject(songArtist.substring(1, songArtist.length() - 1)).getString("name"); 469 | songArtist = songArtist.replaceAll(" ?& ?", "/").replaceAll("、", "/"); 470 | 471 | songAlbum = rs1.getString(SONG_INFO_SONG_ALBUM); 472 | if (songAlbum == null) songAlbum = ""; 473 | 474 | Map nameSimilarityArray = new HashMap<>(); //歌曲名相似度键值对 475 | Map artistSimilarityArray = new HashMap<>(); //歌手名相似度键值对 476 | Map albumSimilarityArray = new HashMap<>(); //专辑名相似度键值对 477 | 478 | boolean matched = false; //是否匹配成功 479 | 480 | File file = new File("./Result/" + SOURCE_ENG + "/" + playListName.get(i) + ".txt"); 481 | //若文件不存在,则创建歌单文件 482 | if (!file.exists()) 483 | file.createNewFile(); 484 | 485 | FileWriter fileWriter = new FileWriter(file.getAbsoluteFile(), true); 486 | 487 | //获取歌曲名相似度列表 488 | if (parenthesesRemoval) 489 | for (int k = 0; k < localMusic.length; k++) { 490 | nameSimilarityArray.put(String.valueOf(k), StringSimilarityCompare.similarityRatio(songName.replaceAll("(?i) ?\\((?!inst|[^()]* ver)[^)]*\\) ?", "").toLowerCase(), localMusic[k][0].replaceAll("(?i) ?\\((?!inst|[^()]* ver)[^)]*\\) ?", "").toLowerCase())); 491 | } 492 | else 493 | for (int k = 0; k < localMusic.length; k++) { 494 | nameSimilarityArray.put(String.valueOf(k), StringSimilarityCompare.similarityRatio(songName.toLowerCase(), localMusic[k][0].toLowerCase())); 495 | } 496 | 497 | Map.Entry maxValue = MapSort.getMaxValue(nameSimilarityArray); //获取键值对表中相似度的最大值所在的键值对 498 | double songNameMaxSimilarity = maxValue.getValue(); //获取相似度的最大值 499 | String songNameMaxKey = maxValue.getKey(); //获取相似度的最大值对应的歌曲在localMusic数组中的位置 500 | 501 | //获取歌手名相似度列表 502 | if (parenthesesRemoval) 503 | for (int k = 0; k < localMusic.length; k++) { 504 | artistSimilarityArray.put(String.valueOf(k), StringSimilarityCompare.similarityRatio(songArtist.replaceAll("(?i) ?\\((?!inst|[^()]* ver)[^)]*\\) ?", "").toLowerCase(), localMusic[k][1].replaceAll("(?i) ?\\((?!inst|[^()]* ver)[^)]*\\) ?", "").toLowerCase())); 505 | } 506 | else 507 | for (int k = 0; k < localMusic.length; k++) { 508 | artistSimilarityArray.put(String.valueOf(k), StringSimilarityCompare.similarityRatio(songArtist.toLowerCase(), localMusic[k][1].toLowerCase())); 509 | } 510 | maxValue = MapSort.getMaxValue(artistSimilarityArray); //获取键值对表中相似度的最大值所在的键值对 511 | double songArtistMaxSimilarity = maxValue.getValue(); //获取相似度的最大值 512 | String songArtistMaxKey = maxValue.getKey(); //获取相似度的最大值对应的歌手名 513 | 514 | //获取专辑名相似度列表 515 | double songAlbumMaxSimilarity; 516 | if (!disableAlbumNameMatch) { 517 | if (parenthesesRemoval) 518 | for (int k = 0; k < localMusic.length; k++) { 519 | albumSimilarityArray.put(String.valueOf(k), StringSimilarityCompare.similarityRatio(songAlbum.replaceAll("(?i) ?\\((?!inst|[^()]* ver)[^)]*\\) ?", "").toLowerCase(), localMusic[k][2].replaceAll("(?i) ?\\((?!inst|[^()]* ver)[^)]*\\) ?", "").toLowerCase())); 520 | } 521 | else 522 | for (int k = 0; k < localMusic.length; k++) { 523 | albumSimilarityArray.put(String.valueOf(k), StringSimilarityCompare.similarityRatio(songAlbum.toLowerCase(), localMusic[k][2].toLowerCase())); 524 | } 525 | maxValue = MapSort.getMaxValue(albumSimilarityArray); //获取键值对表中相似度的最大值所在的键值对 526 | songAlbumMaxSimilarity = maxValue.getValue(); //获取相似度的最大值 527 | String songAlbumMaxKey = maxValue.getKey(); //获取相似度的最大值对应的专辑名 528 | } else { 529 | songAlbumMaxSimilarity = 1.0; 530 | } 531 | 532 | System.out.println(); 533 | if (songNameMaxSimilarity >= similaritySame && songArtistMaxSimilarity >= similaritySame && songAlbumMaxSimilarity >= similaritySame) { 534 | //歌曲名、歌手名、专辑名均匹配成功 535 | autoSuccessCount++; 536 | Logger.success("第" + (++num) + "首,共" + songNum.get(playListId.get(i)) + "首,歌曲《" + songName + "》匹配成功!歌手:" + songArtist + ",专辑:" + songAlbum); 537 | String[] header = {"类型", SOURCE_CHN, "本地音乐", "相似度"}; 538 | String[][] data = {{"歌名", songName, localMusic[Integer.parseInt(songNameMaxKey)][0], String.format("%.1f%%", songNameMaxSimilarity * 100)}, {"歌手", songArtist, localMusic[Integer.parseInt(songNameMaxKey)][1], String.format("%.1f%%", songArtistMaxSimilarity * 100)}, {"专辑", songAlbum, localMusic[Integer.parseInt(songNameMaxKey)][2], String.format("%.1f%%", songAlbumMaxSimilarity * 100)}}; 539 | TablePrinter.printTable(header, data, "匹配详情"); 540 | matched = true; 541 | successNum++; 542 | fileWriter.write(localMusic[Integer.parseInt(songNameMaxKey)][3] + "\n"); 543 | fileWriter.close(); 544 | MarkdownLog.succeedConvertResult(header, data, num, songNum.get(playListId.get(i))); 545 | } else { 546 | //歌曲名、歌手名、专辑名中的一或多项匹配失败 547 | Logger.warning("第" + (++num) + "首,共" + songNum.get(playListId.get(i)) + "首,歌曲《" + songName + "》匹配失败!歌手:" + songArtist + ",专辑:" + songAlbum); 548 | 549 | String[] header = {"类型", SOURCE_CHN, "本地音乐", "相似度"}; 550 | String[][] data = {{"歌名", songName, localMusic[Integer.parseInt(songNameMaxKey)][0], String.format("%.1f%%", songNameMaxSimilarity * 100)}, {"歌手", songArtist, localMusic[Integer.parseInt(songNameMaxKey)][1], String.format("%.1f%%", songArtistMaxSimilarity * 100)}, {"专辑", songAlbum, localMusic[Integer.parseInt(songNameMaxKey)][2], String.format("%.1f%%", songAlbumMaxSimilarity * 100)}}; 551 | TablePrinter.printTable(header, data, "匹配详情"); 552 | String input; 553 | if (!allYes && !allNo) { 554 | System.out.print(""" 555 | \tY/y/直接回车:按照表格中的信息添加到歌单 556 | \tN/n:不添加到歌单 557 | 若在以上选项【前】加上[A/a],本次选择的操作将应用于当前歌单的所有后续歌曲 558 | \t输入歌曲相关信息,自行手动完成匹配 559 | \t#*abort*#:提前结束本歌单的匹配操作,进入下一歌单 560 | \t#*ABORT*#:提前结束本音乐平台的匹配操作,返回主菜单 561 | 请选择您的操作:"""); 562 | input = scanner.nextLine(); 563 | if (input.equals("#*abort*#")) { 564 | Logger.warning("已提前结束本歌单的匹配操作,进入下一歌单\n"); 565 | Sleep.start(500); 566 | break; 567 | } 568 | if (input.equals("#*ABORT*#")) { 569 | stop = true; 570 | break; 571 | } 572 | input = input.toLowerCase(); 573 | } else if (allYes) { 574 | input = "y"; 575 | Logger.success("使用默认操作:添加到歌单"); 576 | } else { 577 | input = "n"; 578 | Logger.warning("使用默认操作:不添加到歌单"); 579 | MarkdownLog.failedConvertResult(songName, songArtist, songAlbum, num, songNum.get(playListId.get(i))); 580 | } 581 | 582 | if (input.endsWith("n") && input.length() < 3) { 583 | if (!allNo) { 584 | Logger.warning("已跳过"); 585 | MarkdownLog.failedConvertResult(songName, songArtist, songAlbum, num, songNum.get(playListId.get(i))); 586 | Sleep.start(300); 587 | } 588 | if (input.startsWith("a")) { 589 | allNo = true; 590 | Logger.info("当前歌单的所有后续歌曲,遇到转换冲突时将默认跳过,不添加到歌单"); 591 | MarkdownLog.info("======默认跳过已启用======"); 592 | Sleep.start(1000); 593 | } 594 | continue; 595 | } else if (input.isEmpty() || (input.endsWith("y") && input.length() < 3)) { 596 | if (!allYes) { 597 | Logger.success("已添加到歌单"); 598 | Sleep.start(300); 599 | } 600 | if (input.startsWith("a")) { 601 | allYes = true; 602 | Logger.info("当前歌单的所有后续歌曲,遇到转换冲突时将默认添加到歌单"); 603 | MarkdownLog.info("======默认添加已启用======"); 604 | Sleep.start(1000); 605 | } 606 | 607 | matched = true; 608 | successNum++; 609 | fileWriter.write(localMusic[Integer.parseInt(songNameMaxKey)][3] + "\n"); 610 | fileWriter.close(); 611 | MarkdownLog.succeedConvertResult(header, data, num, songNum.get(playListId.get(i))); 612 | } else { 613 | while (true) { 614 | String[][] manualSearchResult = FindStringArray.findStringArray(localMusic, input); 615 | for (int j = 0; j < manualSearchResult.length; j++) { 616 | System.out.println("\t" + (j + 1) + ". " + manualSearchResult[j][0] + " - " + manualSearchResult[j][1] + " - " + manualSearchResult[j][2]); 617 | } 618 | if (manualSearchResult.length == 0) { 619 | Logger.error("未找到匹配的歌曲,请重新输入!"); 620 | Sleep.start(300); 621 | System.out.print("请输入歌曲相关信息;或输入N/n跳过当前歌曲(不添加到歌单):"); 622 | input = scanner.nextLine().toLowerCase(); 623 | continue; 624 | } 625 | System.out.print("请选择正确的歌曲序号;或输入N/n跳过当前歌曲(不添加到歌单);或重新输入歌曲信息:"); 626 | String choice = scanner.nextLine().toLowerCase(); 627 | if (choice.matches("[0-9]+")) { 628 | if (Integer.parseInt(choice) > manualSearchResult.length || Integer.parseInt(choice) < 1) { 629 | Logger.error("输入错误,请重新输入!"); 630 | continue; 631 | } 632 | Logger.success("已添加到歌单"); 633 | matched = true; 634 | successNum++; 635 | fileWriter.write(manualSearchResult[Integer.parseInt(choice) - 1][3] + "\n"); 636 | fileWriter.close(); 637 | data = new String[][]{{"歌名", songName, manualSearchResult[Integer.parseInt(choice) - 1][0], "手动匹配"}, {"歌手", songArtist, manualSearchResult[Integer.parseInt(choice) - 1][1], "手动匹配"}, {"专辑", songAlbum, manualSearchResult[Integer.parseInt(choice) - 1][2], "手动匹配"}}; 638 | MarkdownLog.succeedConvertResult(header, data, num, songNum.get(playListId.get(i))); 639 | Sleep.start(300); 640 | break; 641 | } else if (choice.equals("n")) { 642 | Logger.warning("已跳过"); 643 | MarkdownLog.failedConvertResult(songName, songArtist, songAlbum, num, songNum.get(playListId.get(i))); 644 | Sleep.start(300); 645 | break; 646 | } else { 647 | input = choice; 648 | continue; 649 | } 650 | } 651 | } 652 | } 653 | } 654 | if (stop) 655 | break; 656 | long endTime = System.currentTimeMillis(); //结束时间 657 | System.out.println("\n======共" + songNum.get(playListId.get(i)) + "首,成功" + successNum + "首======\n" + 658 | "======歌单" + (i + 1) + "【" + playListName.get(i) + "】匹配完成,剩余" + (selectedPlayListId.size() - 1) + "个歌单======"); 659 | 660 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 661 | 662 | Map result = new HashMap<>(); 663 | result.put("sourceEng", SOURCE_ENG); 664 | result.put("sourceChn", SOURCE_CHN); 665 | result.put("totalCount", songNum.get(playListId.get(i))); 666 | result.put("successCount", successNum); 667 | result.put("enableParenthesesRemoval", parenthesesRemoval); 668 | result.put("similarity", similaritySame * 100); 669 | result.put("autoSuccessCount", autoSuccessCount); 670 | result.put("startTime", sdf.format(startTime)); 671 | result.put("endTime", sdf.format(endTime)); 672 | result.put("enableAlbumNameMatch", !disableAlbumNameMatch); 673 | Statistic.report(result); 674 | 675 | if ((selectedPlayListId.size() - 1) > 0) { 676 | Logger.info("3秒后继续匹配下一歌单"); 677 | System.out.print("3 "); 678 | Sleep.start(1000); 679 | System.out.print("2 "); 680 | Sleep.start(1000); 681 | System.out.println("1"); 682 | Sleep.start(1000); 683 | } else 684 | System.out.println(); 685 | } 686 | if (stop) 687 | Logger.warning("已提前结束本音乐平台的匹配操作,返回主菜单\n"); 688 | else 689 | Logger.success(SOURCE_CHN + "所有歌单匹配完成,返回主菜单\n"); 690 | } catch (SQLException | IOException | NullPointerException e) { 691 | Logger.error("很抱歉!程序运行出现错误,请重试\n错误详情:" + e); 692 | } 693 | } 694 | 695 | 696 | /** 697 | * 释放资源 698 | */ 699 | private void afterwards() { 700 | database.closeConnection(conn); 701 | } 702 | } 703 | -------------------------------------------------------------------------------- /src/main/java/database/Database.java: -------------------------------------------------------------------------------- 1 | package database; 2 | 3 | import utils.Logger; 4 | 5 | import java.sql.Connection; 6 | import java.sql.DriverManager; 7 | import java.sql.SQLException; 8 | 9 | /** 10 | * @description: 数据库相关操作 11 | * @author: HWinZnieJ 12 | * @create: 2023-09-04 16:36 13 | **/ 14 | 15 | public class Database { 16 | 17 | /** 18 | * 获取数据库的连接 19 | * 20 | * @param dbPath 数据库文件的名称或绝对路径 21 | * @return 数据库连接接口 22 | */ 23 | public Connection getConnection(String dbPath) { 24 | Connection conn = null; 25 | try { 26 | String url = "jdbc:sqlite:" + dbPath; 27 | conn = DriverManager.getConnection(url); 28 | Logger.success("成功连接SQLite数据库"); 29 | } catch (SQLException e) { 30 | Logger.error("数据库连接失败!\n错误详情:" + e.getMessage()); 31 | } 32 | return conn; 33 | } 34 | 35 | /** 36 | * 关闭到数据库的连接 37 | * 38 | * @param conn 数据库连接接口 39 | */ 40 | public void closeConnection(Connection conn) { 41 | try { 42 | conn.close(); 43 | Logger.success("成功断开SQLite数据库"); 44 | } catch (SQLException e) { 45 | Logger.error("数据库断开失败!\n错误详情:" + e.getMessage()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/utils/FileOperation.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.io.File; 4 | import java.util.Scanner; 5 | 6 | /** 7 | * @description: 文件相关操作 8 | * @author: HWinZnieJ 9 | * @create: 2023-09-04 16:49 10 | **/ 11 | 12 | public class FileOperation { 13 | static Scanner scanner = new Scanner(System.in, PropertiesRelated.read().getProperty("terminalCharSet")); 14 | 15 | /** 16 | * 删除指定文件夹下的所有文件 17 | * 18 | * @param file 19 | */ 20 | public static void deleteSubItem(File file) { 21 | if (file.isFile() || file.list().length == 0) { 22 | file.delete(); 23 | } else { 24 | for (File f : file.listFiles()) { 25 | deleteSubItem(f); // 递归删除每一个文件 26 | } 27 | // file.delete(); // 删除文件夹 28 | } 29 | } 30 | 31 | /** 32 | * 创建所需目录 33 | * 34 | * @param file 35 | */ 36 | public static void createDir(File file) { 37 | if (!file.exists()) { 38 | Logger.info("输出目录不存在,已为您自动创建"); 39 | file.mkdirs(); 40 | } 41 | } 42 | 43 | /** 44 | * 检测指定目录是否为空: 45 | * 若不为空,询问用户是否对目录进行清空操作; 46 | * 47 | * @param file 48 | */ 49 | public static void checkDir(File file) { 50 | if (file.isDirectory()) { 51 | if (file.list().length > 0) { 52 | Logger.warning("输出目录" + (file.getParent() + '/' + file.getName()).replaceAll("\\\\", "/") + "不为空!推荐清空目录后再继续!"); 53 | while (true) { 54 | System.out.print("清空吗?(Y/n) "); 55 | String choice = scanner.nextLine(); 56 | if (choice.isEmpty() || choice.equalsIgnoreCase("y")) { 57 | Logger.info("输出目录已清空"); 58 | deleteSubItem(file); 59 | break; 60 | } else if (choice.equalsIgnoreCase("n")) { 61 | Logger.info("清空操作已取消"); 62 | break; 63 | } else { 64 | Logger.warning("输入有误,请重新输入!"); 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * 删除字符串中的双引号 73 | * 74 | * @param str 需要删除双引号的字符串 75 | * @return 删除结果 76 | */ 77 | public static String deleteQuotes(String str) { 78 | return str.replaceAll("\"", ""); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/utils/FindStringArray.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | /** 4 | * @description: 从字符串二维数组中找到包含关键词的所有元素 5 | * @author: HWinZnieJ 6 | * @create: 2023-09-13 10:52 7 | **/ 8 | 9 | public class FindStringArray { 10 | public static String[][] findStringArray(String[][] stringArray, String keyWord) { 11 | int count = 0; 12 | for (String[] strings : stringArray) { 13 | for (String string : strings) { 14 | if (string.toLowerCase().contains(keyWord.toLowerCase())) { 15 | count++; 16 | break; // 如果找到关键词,跳出内部循环 17 | } 18 | } 19 | } 20 | String[][] result = new String[count][]; 21 | int index = 0; 22 | for (String[] strings : stringArray) { 23 | for (String string : strings) { 24 | if (string.toLowerCase().contains(keyWord.toLowerCase())) { 25 | result[index] = strings.clone(); // 复制整行 26 | index++; 27 | break; // 如果找到关键词,跳出内部循环 28 | } 29 | } 30 | } 31 | return result; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/utils/Logger.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | /** 4 | * @description: 日志输出 5 | * @author: HWinZnieJ 6 | * @create: 2023-09-04 16:38 7 | **/ 8 | 9 | public class Logger { 10 | private static final String BOLD = "\033[1m"; 11 | private static final String RED = "\u001b[31m"; 12 | private static final String GREEN = "\u001b[32m"; 13 | private static final String BLUE = "\u001b[34m"; 14 | private static final String YELLOW = "\u001b[33m"; 15 | private static final String END = "\u001b[0m"; 16 | 17 | /** 18 | * 输出INFO级别的日志信息 19 | * 20 | * @param msg 要输出的字符串 21 | */ 22 | public static void info(String msg) { 23 | System.out.println(BLUE + BOLD + "[INFO] " + END + BLUE + msg + END); 24 | } 25 | 26 | /** 27 | * 输出WARNING级别的日志信息 28 | * 29 | * @param msg 要输出的字符串 30 | */ 31 | public static void warning(String msg) { 32 | System.out.println(YELLOW + BOLD + "[WARNING] " + END + YELLOW + msg + END); 33 | } 34 | 35 | /** 36 | * 输出ERROR级别的日志信息 37 | * 38 | * @param msg 要输出的字符串 39 | */ 40 | public static void error(String msg) { 41 | System.out.println(RED + BOLD + "[ERROR] " + END + RED + msg + END); 42 | } 43 | 44 | /** 45 | * 输出SUCCESS级别的日志信息 46 | * 47 | * @param msg 要输出的字符串 48 | */ 49 | public static void success(String msg) { 50 | System.out.println(GREEN + BOLD + "[SUCCESS] " + END + GREEN + msg + END); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/utils/MapSort.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Comparator; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | /** 9 | * @description: 对Map类型的数据进行排序 10 | * @author: HWinZnieJ 11 | * @create: 2023-09-04 19:39 12 | **/ 13 | 14 | public class MapSort { 15 | /** 16 | * 对Map类型的数据根据值(Value)进行排序 17 | * 18 | * @param map 要计算的Map集合 19 | * @param order A:升序 D:降序 20 | * @return 排序完成后的List 21 | */ 22 | public static List> sortByValue(Map map, char order) { 23 | List> entryList2 = new ArrayList<>(map.entrySet()); 24 | entryList2.sort(new Comparator<>() { 25 | @Override 26 | public int compare(Map.Entry me1, Map.Entry me2) { 27 | if (order == 'A') { 28 | return me1.getValue().compareTo(me2.getValue()); // 升序排序 29 | } else { 30 | return me2.getValue().compareTo(me1.getValue()); // 降序排序 31 | } 32 | } 33 | }); 34 | return entryList2; 35 | } 36 | 37 | /** 38 | * 对Map类型的数据根据键(Key)进行排序 39 | * 40 | * @param map 要计算的Map集合 41 | * @param order A:升序 D:降序 42 | * @return 排序完成后的List 43 | */ 44 | public static List> sortByKey(Map map, char order) { 45 | List> entryList1 = new ArrayList<>(map.entrySet()); 46 | entryList1.sort(new Comparator<>() { 47 | @Override 48 | public int compare(Map.Entry me1, Map.Entry me2) { 49 | if (order == 'A') { 50 | return me1.getValue().compareTo(me2.getValue()); // 升序排序 51 | } else { 52 | return me2.getValue().compareTo(me1.getValue()); // 降序排序 53 | } 54 | } 55 | }); 56 | return entryList1; 57 | } 58 | 59 | /** 60 | * 获取Map中Value最大的键值对 61 | * 62 | * @param map 要计算的Map集合 63 | * @return Map.Entry 集合中最大值(Value)的键值对 64 | */ 65 | public static Map.Entry getMaxValue(Map map) { 66 | Map.Entry maxEntry = null; 67 | for (Map.Entry entry : map.entrySet()) { 68 | if (maxEntry == null || entry.getValue().compareTo(maxEntry.getValue()) > 0) { 69 | maxEntry = entry; 70 | } 71 | } 72 | return maxEntry; 73 | } 74 | 75 | /** 76 | * 获取Map中Value最小的键值对 77 | * 78 | * @param map 要计算的Map集合 79 | * @return Map.Entry 集合中最小值(Value)的键值对 80 | */ 81 | public static Map.Entry getMinValue(Map map) { 82 | Map.Entry minEntry = null; 83 | for (Map.Entry entry : map.entrySet()) { 84 | if (minEntry == null || entry.getValue().compareTo(minEntry.getValue()) < 0) { 85 | minEntry = entry; 86 | } 87 | } 88 | return minEntry; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/utils/MarkdownLog.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.io.BufferedWriter; 4 | import java.io.File; 5 | import java.io.FileWriter; 6 | import java.io.IOException; 7 | import java.util.Scanner; 8 | 9 | /** 10 | * @description: 将日志信息以Markdown格式输出到文件 11 | * @author: HWinZnieJ 12 | * @create: 2023-09-19 14:36 13 | **/ 14 | 15 | public class MarkdownLog { 16 | private static final String BOLD = "**"; 17 | private static final String RED = ""; 18 | private static final String GREEN = ""; 19 | private static final String BLUE = ""; 20 | private static final String YELLOW = ""; 21 | private static final String END = ""; 22 | static Scanner scanner = new Scanner(System.in, PropertiesRelated.read().getProperty("terminalCharSet")); 23 | 24 | /** 25 | * 将内容写入文件 26 | * 27 | * @param content 要写入的内容 28 | */ 29 | private static void write(String content) { 30 | try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("./ConvertLog.md", true))) { 31 | bufferedWriter.write(content); 32 | } catch (IOException e) { 33 | Logger.error("很抱歉!在写入日志时出现错误!\n错误详情:" + e); 34 | } 35 | } 36 | 37 | /** 38 | * 换行 39 | */ 40 | private static void newLine() { 41 | try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("./ConvertLog.md", true))) { 42 | bufferedWriter.newLine(); 43 | } catch (IOException e) { 44 | Logger.error("很抱歉!在写入日志时出现错误!\n错误详情:" + e); 45 | } 46 | } 47 | 48 | /** 49 | * 将日期按预定格式写入文件 50 | * 51 | * @param date 当前日期 52 | */ 53 | public static void date(String date) { 54 | write("### " + date); 55 | newLine(); 56 | } 57 | 58 | /** 59 | * 将标题按预定格式写入文件 60 | * 61 | * @param title 要写入的标题 62 | */ 63 | public static void playListTitle(String title) { 64 | write("# " + BOLD + title + BOLD); 65 | newLine(); 66 | } 67 | 68 | /** 69 | * 将INFO级别的信息写入文件 70 | * 71 | * @param info 要写入的日志内容 72 | */ 73 | public static void info(String info) { 74 | write("## " + BLUE + info + END); 75 | newLine(); 76 | } 77 | 78 | /** 79 | * 转换成功的结果表格写入文件 80 | * 81 | * @param header 表头 82 | * @param data 表的数据 83 | * @param now 当前是第几首歌 84 | * @param total 总共有多少首歌 85 | */ 86 | public static void succeedConvertResult(String[] header, String[][] data, int now, String total) { 87 | write("## " + BOLD + GREEN + now + " / " + total + END + BOLD); 88 | newLine(); 89 | // Markdown表头 90 | write("|"); 91 | for (String s : header) { 92 | write(s + "|"); 93 | } 94 | newLine(); 95 | 96 | // Markdown表头分割线 97 | write("|"); 98 | for (String s : header) { 99 | write("-|"); 100 | } 101 | newLine(); 102 | 103 | // Markdown表格内容 104 | for (String[] datum : data) { 105 | write("|"); 106 | for (String s : datum) { 107 | write(s + "|"); 108 | } 109 | newLine(); 110 | } 111 | newLine(); 112 | } 113 | 114 | /** 115 | * 转换失败的结果详情写入文件 116 | * 117 | * @param songName 歌名 118 | * @param songArtist 艺术家 119 | * @param songAlbum 专辑名 120 | * @param now 当前是第几首歌 121 | * @param total 总共有多少首歌 122 | */ 123 | public static void failedConvertResult(String songName, String songArtist, String songAlbum, int now, String total) { 124 | write("## " + BOLD + RED + now + " / " + total + END + BOLD); 125 | newLine(); 126 | write("### " + BOLD + RED + "歌名:" + songName + END + BOLD); 127 | newLine(); 128 | write("### " + BOLD + RED + "艺术家:" + songArtist + END + BOLD); 129 | newLine(); 130 | write("### " + BOLD + RED + "专辑:" + songAlbum + END + BOLD); 131 | newLine(); 132 | } 133 | 134 | /** 135 | * 检查结果文件是否存在,如存在则询问用户是否删除 136 | */ 137 | public static void checkLogFile() { 138 | File logFile = new File("./ConvertLog.md"); 139 | if (logFile.exists()) { 140 | Logger.info("检测到上次运行本程序时创建的【./ConvertLog.md】转换结果文件,推荐删除后再继续!"); 141 | System.out.print("删除吗?(Y/n):"); 142 | while (true) { 143 | String choice = scanner.nextLine(); 144 | if (choice.isEmpty() || choice.equalsIgnoreCase("y")) { 145 | delLogFile(); 146 | Logger.info("转换结果文件已删除"); 147 | break; 148 | } else if (choice.equalsIgnoreCase("n")) { 149 | Logger.info("删除操作已取消,本次运行所产生的转换结果将追加在原文件尾部"); 150 | break; 151 | } else 152 | Logger.warning("输入有误,请重新输入!"); 153 | } 154 | System.out.println(); 155 | } 156 | } 157 | 158 | /** 159 | * 删除转换结果文件 160 | */ 161 | public static void delLogFile() { 162 | File logFile = new File("./ConvertLog.md"); 163 | logFile.delete(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/utils/PropertiesRelated.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.io.*; 4 | import java.nio.charset.StandardCharsets; 5 | import java.util.Properties; 6 | 7 | /** 8 | * @description: 读取和保存properties配置文件 9 | * @author: HWinZnieJ 10 | * @create: 2023-09-15 09:05 11 | **/ 12 | 13 | public class PropertiesRelated { 14 | /** 15 | * 保存properties配置到配置文件 16 | * 17 | * @param key 键名 18 | * @param value 键值 19 | */ 20 | public static void save(String key, String value) { 21 | Properties prop = PropertiesRelated.read(); 22 | prop.setProperty(key, value); 23 | try { 24 | FileOutputStream fos = new FileOutputStream("./config.properties"); 25 | OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); 26 | prop.store(osw, null); 27 | } catch (IOException e) { 28 | Logger.error("无法写入配置文件!\n错误详情:" + e); 29 | } 30 | } 31 | 32 | /** 33 | * 读取配置文件 34 | * 35 | * @return 包含配置信息的Properties类 36 | */ 37 | public static Properties read() { 38 | File file = new File("./config.properties"); 39 | if (file.exists()) { 40 | Properties prop = new Properties(); 41 | try { 42 | FileInputStream fis = new FileInputStream("./config.properties"); 43 | InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8); 44 | prop.load(isr); 45 | } catch (IOException e) { 46 | Logger.error("无法读取配置文件!\n错误详情:" + e); 47 | } 48 | return prop; 49 | } else { 50 | return new Properties(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/utils/Sleep.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | /** 4 | * @description: 主动让程序暂停 5 | * @author: HWinZnieJ 6 | * @create: 2023-09-04 17:03 7 | **/ 8 | 9 | public class Sleep { 10 | /** 11 | * 程序暂停 12 | * 13 | * @param miliseconds 要暂停的毫秒数 14 | */ 15 | public static void start(int miliseconds) { 16 | try { 17 | Thread.sleep(miliseconds); 18 | } catch (InterruptedException e) { 19 | Logger.error(e.getMessage()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/utils/Statistic.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import org.apache.http.client.methods.CloseableHttpResponse; 5 | import org.apache.http.client.methods.HttpPost; 6 | import org.apache.http.entity.StringEntity; 7 | import org.apache.http.impl.client.CloseableHttpClient; 8 | import org.apache.http.impl.client.HttpClientBuilder; 9 | import org.apache.http.util.EntityUtils; 10 | 11 | import java.io.IOException; 12 | import java.nio.charset.StandardCharsets; 13 | import java.text.SimpleDateFormat; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | import java.util.Properties; 17 | import java.util.UUID; 18 | 19 | /** 20 | * @description: 向统计服务器报告程序运行结果 21 | * @author: HWinZnieJ 22 | * @create: 2023-09-19 16:34 23 | **/ 24 | 25 | public class Statistic { 26 | 27 | private static final char[] CHAR_SET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".toCharArray(); 28 | 29 | /** 30 | * @description: 将一个长整数转换为指定进制的字符串 31 | * @param: [i, radix] 32 | * @return: 转换结果 33 | * @date: 2023-09-20 11:14 34 | */ 35 | private static String toCustomRadix(long i, int radix) { 36 | StringBuilder sb = new StringBuilder(); 37 | while (i != 0) { 38 | sb.append(CHAR_SET[(int) (i % radix)]); 39 | i /= radix; 40 | } 41 | return sb.reverse().toString(); 42 | } 43 | 44 | /** 45 | * @description: 获取当前机器的UUID 46 | * @date: 2023-09-20 11:15 47 | */ 48 | public static void saveUuid() { 49 | if (isEnable()) { 50 | Properties prop = PropertiesRelated.read(); 51 | if (prop.getProperty("uuid") == null || prop.getProperty("uuid").isEmpty()) { 52 | UUID uuid = UUID.randomUUID(); 53 | String str64 = toCustomRadix(uuid.getMostSignificantBits() & 0x7fffffffffffffffL, 64) + toCustomRadix(uuid.getLeastSignificantBits() & 0x7fffffffffffffffL, 64); 54 | PropertiesRelated.save("uuid", str64); 55 | } 56 | } else 57 | Logger.info("已禁用【发送统计数据】功能"); 58 | } 59 | 60 | /** 61 | * @description: 报告程序运行结果 62 | * @param: [type, result] 63 | * @date: 2023-09-20 11:15 64 | */ 65 | public static void report(Map result) { 66 | if (isEnable()) { 67 | Logger.info("正在向服务器发送本次转换的统计数据..."); 68 | Properties prop = PropertiesRelated.read(); 69 | String uuid = prop.getProperty("uuid"); 70 | result.put("uuid", uuid); 71 | result.put("mode", 1); 72 | result.put("enableArtistNameMatch", true); 73 | result.put("tool", "JavaSE"); 74 | send(new JSONObject(result)); 75 | } 76 | } 77 | 78 | /** 79 | * 向指定URL发送包含统计数据的POST请求 80 | * 81 | * @param data 待发送的统计数据 82 | */ 83 | private static void send(JSONObject data) { 84 | String url = "https://saltconv.hwinzniej.top:46000/statistic/save"; 85 | try { 86 | CloseableHttpClient httpClient = HttpClientBuilder.create().build(); 87 | HttpPost httpPost = new HttpPost(url); 88 | 89 | StringEntity stringEntity = new StringEntity(data.toString(), StandardCharsets.UTF_8); 90 | stringEntity.setContentType("application/json"); 91 | httpPost.setEntity(stringEntity); 92 | 93 | CloseableHttpResponse response = httpClient.execute(httpPost); 94 | int statusCode = response.getStatusLine().getStatusCode(); 95 | String responseBody = EntityUtils.toString(response.getEntity()); 96 | 97 | response.close(); 98 | httpClient.close(); 99 | if (statusCode == 200) 100 | Logger.success("统计数据发送成功!"); 101 | else 102 | Logger.error("统计数据发送失败!\n错误详情:" + responseBody); 103 | } catch (IOException e) { 104 | Logger.error("很抱歉!在发送统计数据时出现错误!\n错误详情:" + e); 105 | } 106 | } 107 | 108 | public static void usage(String type) { 109 | Properties prop = PropertiesRelated.read(); 110 | String uuid = prop.getProperty("uuid"); 111 | 112 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 113 | String time = sdf.format(System.currentTimeMillis()); 114 | 115 | Map result = new HashMap<>(); 116 | 117 | result.put("sessionId", uuid); 118 | result.put("time", time); 119 | result.put("type", type); 120 | result.put("tool", "JavaSE"); 121 | sendUsage(new JSONObject(result)); 122 | } 123 | 124 | private static void sendUsage(JSONObject data) { 125 | String url = "https://saltconv.hwinzniej.top:46000/statistic/usage"; 126 | try { 127 | CloseableHttpClient httpClient = HttpClientBuilder.create().build(); 128 | HttpPost httpPost = new HttpPost(url); 129 | 130 | StringEntity stringEntity = new StringEntity(data.toString(), StandardCharsets.UTF_8); 131 | stringEntity.setContentType("application/json"); 132 | httpPost.setEntity(stringEntity); 133 | 134 | CloseableHttpResponse response = httpClient.execute(httpPost); 135 | 136 | response.close(); 137 | httpClient.close(); 138 | } catch (IOException ignored) { 139 | } 140 | } 141 | 142 | /** 143 | * 判断是否在配置文件中启用了“发送统计数据”功能 144 | * 145 | * @return 启用-true 禁用-false 146 | */ 147 | private static boolean isEnable() { 148 | Properties prop = PropertiesRelated.read(); 149 | if (prop.get("enableStatistic") == null) 150 | return true; 151 | else return !prop.getProperty("enableStatistic").equals("false"); 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/utils/StringSimilarityCompare.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | /** 4 | * @description: 获取两个字符串的相似度 5 | * @author: HWinZnieJ 6 | * @create: 2023-09-04 16:42 7 | **/ 8 | 9 | public class StringSimilarityCompare { 10 | /** 11 | * 获取最长子串 (参数顺序与字符串长短无关) 12 | * 13 | * @param strA 14 | * @param strB 15 | * @return 16 | */ 17 | public static String longestCommonSubstringNoOrder(String strA, String strB) { 18 | if (strA.length() >= strB.length()) { 19 | return longestCommonSubstring(strA, strB); 20 | } else { 21 | return longestCommonSubstring(strB, strA); 22 | } 23 | } 24 | 25 | /** 26 | * 获取最长子串 (长串在前,短串在后) 27 | * 28 | * @param strLong 29 | * @param strShort 30 | * @return

summary

:较长的字符串放到前面有助于提交效率 31 | */ 32 | private static String longestCommonSubstring(String strLong, String strShort) { 33 | char[] chars_strA = strLong.toCharArray(); 34 | char[] chars_strB = strShort.toCharArray(); 35 | int m = chars_strA.length; 36 | int n = chars_strB.length; 37 | int[][] matrix = new int[m + 1][n + 1]; 38 | for (int i = 1; i <= m; i++) { 39 | for (int j = 1; j <= n; j++) { 40 | if (chars_strA[i - 1] == chars_strB[j - 1]) { 41 | matrix[i][j] = matrix[i - 1][j - 1] + 1; 42 | } else { 43 | matrix[i][j] = Math.max(matrix[i][j - 1], matrix[i - 1][j]); 44 | } 45 | } 46 | } 47 | char[] result = new char[matrix[m][n]]; 48 | int currentIndex = result.length - 1; 49 | while (matrix[m][n] != 0) { 50 | if (matrix[n] == matrix[n - 1]) { 51 | n--; 52 | } else if (matrix[m][n] == matrix[m - 1][n]) { 53 | m--; 54 | } else { 55 | result[currentIndex] = chars_strA[m - 1]; 56 | currentIndex--; 57 | n--; 58 | m--; 59 | } 60 | } 61 | return new String(result); 62 | } 63 | 64 | private static boolean charReg(char charValue) { 65 | return (charValue >= 0x4E00 && charValue <= 0X9FA5) || (charValue >= 'a' && charValue <= 'z') || (charValue >= 'A' && charValue <= 'Z') || (charValue >= '0' && charValue <= '9'); 66 | } 67 | 68 | private static String removeSign(String str) { 69 | StringBuilder sb = new StringBuilder(); 70 | for (char item : str.toCharArray()) { 71 | if (charReg(item)) { 72 | sb.append(item); 73 | } 74 | } 75 | return sb.toString(); 76 | } 77 | 78 | /** 79 | * 比较俩个字符串的相似度(方式一) 80 | * 步骤1:获取两个串中最长共同子串(有序非连续) 81 | * 步骤2:共同子串长度 除以 较长串的长度 82 | * 83 | * @param strA 84 | * @param strB 85 | * @return 两个字符串的相似度 86 | */ 87 | public static double SimilarDegree(String strA, String strB) { 88 | String newStrA = removeSign(strA); 89 | String newStrB = removeSign(strB); 90 | int temp = Math.max(newStrA.length(), newStrB.length()); 91 | int temp2 = longestCommonSubstringNoOrder(newStrA, newStrB).length(); 92 | return temp2 * 1.0 / temp; 93 | } 94 | 95 | /** 96 | * 第二种实现方式 (获取两串不匹配字符数) 97 | * 98 | * @param str 99 | * @param target 100 | * @return 101 | */ 102 | private static int compare(String str, String target) { 103 | int[][] d; // 矩阵 104 | int n = str.length(); 105 | int m = target.length(); 106 | int i; // 遍历str 107 | int j; // 遍历target 108 | char ch1; // str 109 | char ch2; // target 110 | int temp; // 记录相同字符,在某个矩阵位置值的增量,不是0就是1 111 | if (n == 0) { 112 | return m; 113 | } 114 | if (m == 0) { 115 | return n; 116 | } 117 | d = new int[n + 1][m + 1]; 118 | // 初始化第一列 119 | for (i = 0; i <= n; i++) { 120 | d[i][0] = i; 121 | } 122 | // 初始化第一行 123 | for (j = 0; j <= m; j++) { 124 | d[0][j] = j; 125 | } 126 | // 遍历str 127 | for (i = 1; i <= n; i++) { 128 | ch1 = str.charAt(i - 1); 129 | // 去匹配target 130 | for (j = 1; j <= m; j++) { 131 | ch2 = target.charAt(j - 1); 132 | if (ch1 == ch2) { 133 | temp = 0; 134 | } else { 135 | temp = 1; 136 | } 137 | // 左边+1,上边+1, 左上角+temp取最小 138 | d[i][j] = min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + temp); 139 | } 140 | } 141 | return d[n][m]; 142 | } 143 | 144 | private static int min(int one, int two, int three) { 145 | return (one = Math.min(one, two)) < three ? one : three; 146 | } 147 | 148 | /** 149 | * 比较俩个字符串的相似度(方式一) 150 | * 步骤1:获取两个串中不相同的字符数 151 | * 步骤2:不同字符数 除以 较长串的长度 152 | * 153 | * @param strA 154 | * @param strB 155 | * @return 156 | */ 157 | public static double similarityRatio(String strA, String strB) { 158 | return 1 - (double) compare(strA, strB) / Math.max(strA.length(), strB.length()); 159 | } 160 | 161 | } 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/main/java/utils/TablePrinter.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | /** 4 | * @description: 以表格形式输出所给数据 5 | * @author: HWinZnieJ 6 | * @create: 2023-09-04 16:44 7 | **/ 8 | 9 | import java.io.PrintStream; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | public class TablePrinter { 13 | private static final String HORIZONTAL_LINE = "-"; 14 | private static final String VERTICAL_LINE = "|"; 15 | private static final String CROSS_LINE = "+"; 16 | private static final String SPACE = " "; 17 | private static final String NEW_LINE = System.lineSeparator(); 18 | private static final PrintStream out = new PrintStream(System.out, true, StandardCharsets.UTF_8); 19 | 20 | /** 21 | * @param header 表头 22 | * @param data 数据 23 | * @param title 表的总标题 24 | */ 25 | public static void printTable(String[] header, String[][] data, String title) { 26 | // System.out.println("\n"); 27 | int numColumns = header.length; 28 | int numRows = data.length; 29 | 30 | // 计算每一列的最大宽度 31 | int[] columnWidths = new int[numColumns]; 32 | for (int i = 0; i < numColumns; i++) { 33 | int maxWidth = getDisplayWidth(header[i]); 34 | for (String[] datum : data) { 35 | int dataWidth = getDisplayWidth(datum[i]); 36 | if (dataWidth > maxWidth) { 37 | maxWidth = dataWidth; 38 | } 39 | } 40 | columnWidths[i] = maxWidth; 41 | } 42 | 43 | // 输出表格标题 44 | int titleWidth = getDisplayWidth(title); 45 | int padding = (numColumns * 3 - titleWidth) / 2; 46 | out.print(VERTICAL_LINE); 47 | for (int i = 0; i < padding; i++) { 48 | out.print(SPACE); 49 | } 50 | out.print(title); 51 | for (int i = padding + titleWidth; i < numColumns * 3; i++) { 52 | out.print(SPACE); 53 | } 54 | out.println(VERTICAL_LINE); 55 | 56 | // 输出表头 57 | out.print(VERTICAL_LINE); 58 | for (int i = 0; i < numColumns; i++) { 59 | out.print(SPACE); 60 | int paddingSize = columnWidths[i] - getDisplayWidth(header[i]); 61 | for (int j = 0; j < paddingSize / 2; j++) { 62 | out.print(SPACE); 63 | } 64 | out.print(header[i]); 65 | for (int j = paddingSize / 2 + (paddingSize % 2); j < paddingSize; j++) { 66 | out.print(SPACE); 67 | } 68 | out.print(SPACE + VERTICAL_LINE + SPACE); 69 | } 70 | out.println(); 71 | 72 | // 输出分隔线 73 | out.print(CROSS_LINE); 74 | for (int i = 0; i < numColumns; i++) { 75 | out.print(HORIZONTAL_LINE); 76 | for (int j = 0; j < columnWidths[i]; j++) { 77 | out.print(HORIZONTAL_LINE); 78 | } 79 | out.print(HORIZONTAL_LINE + CROSS_LINE); 80 | } 81 | out.println(); 82 | 83 | // 输出数据行 84 | for (String[] datum : data) { 85 | out.print(VERTICAL_LINE); 86 | for (int j = 0; j < numColumns; j++) { 87 | out.print(SPACE + datum[j]); 88 | int paddingSize = columnWidths[j] - getDisplayWidth(datum[j]); 89 | for (int k = 0; k < paddingSize; k++) { 90 | out.print(SPACE); 91 | } 92 | out.print(SPACE + VERTICAL_LINE + SPACE); 93 | } 94 | out.println(); 95 | } 96 | 97 | // 输出底部分隔线 98 | out.print(CROSS_LINE); 99 | for (int i = 0; i < numColumns; i++) { 100 | out.print(HORIZONTAL_LINE); 101 | for (int j = 0; j < columnWidths[i]; j++) { 102 | out.print(HORIZONTAL_LINE); 103 | } 104 | out.print(HORIZONTAL_LINE + CROSS_LINE); 105 | } 106 | out.println(); 107 | } 108 | 109 | /** 110 | * 获取字符串的宽度 111 | * 112 | * @param s 要处理的字符串 113 | * @return 字符串的宽度 114 | */ 115 | private static int getDisplayWidth(String s) { 116 | int width = 0; 117 | for (int i = 0; i < s.length(); i++) { 118 | if (s.charAt(i) >= '一' && s.charAt(i) <= '龥') { 119 | width += 2; 120 | } else { 121 | width++; 122 | } 123 | } 124 | return width; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/utils/TerminalCharSetDetect.java: -------------------------------------------------------------------------------- 1 | package utils; 2 | 3 | import java.util.Properties; 4 | import java.util.Scanner; 5 | 6 | /** 7 | * @description: 检测使用终端的字符编码 8 | * @author: HWinZnieJ 9 | * @create: 2023-09-27 19:13 10 | **/ 11 | 12 | public class TerminalCharSetDetect { 13 | public static void get() { 14 | String charSet = "UTF-8"; 15 | Properties props = PropertiesRelated.read(); 16 | if (props.getProperty("terminalCharSet") == null || props.getProperty("terminalCharSet").isEmpty()) { 17 | while (true) { 18 | Scanner scanner = new Scanner(System.in, charSet); 19 | Logger.info("检测到您未设置终端字符编码,将为您自动检测"); 20 | System.out.print("请随意输入几个中文字符:"); 21 | System.out.println(scanner.nextLine()); 22 | System.out.print("上面的中文字符与您之前输入的是否一致?(y/n):"); 23 | String s = scanner.nextLine(); 24 | if (s.equalsIgnoreCase("y")) { 25 | Logger.info("您当前终端的字符编码为:" + charSet); 26 | PropertiesRelated.save("terminalCharSet", charSet); 27 | break; 28 | } else if (s.equalsIgnoreCase("n")) { 29 | charSet = "GBK"; 30 | Logger.info("您当前终端的字符编码为:" + charSet); 31 | PropertiesRelated.save("terminalCharSet", charSet); 32 | break; 33 | } else { 34 | Logger.error("输入错误,请重新输入!"); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | --------------------------------------------------------------------------------