├── .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 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
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 |
2 |
3 |
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 |
383 |
384 |
385 |
--------------------------------------------------------------------------------
/README_CLI.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
398 |
399 |
400 |
--------------------------------------------------------------------------------
/README_ENG.md:
--------------------------------------------------------------------------------
1 | 中文 / English
2 |
3 |
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 |
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 |
--------------------------------------------------------------------------------