借鉴了github上一个开源的unity项目,将核心逻辑修改后转为java代码。
如果帮到您了,开心之余可以考虑扫描右侧的打赏码 ————————>
package cn.firegod.tools.music.metafix;
import com.google.gson.Gson;
import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.tag.FieldKey;
import org.jaudiotagger.tag.Tag;
import org.jaudiotagger.tag.TagField;
import org.jaudiotagger.tag.id3.ID3v1Tag;
import java.io.File;
import java.io.Serializable;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
public class NeteaseMusicCommentRepair {
private static final String DETAIL_API = "http://music.163.com/api/song/detail?ids=";
private static final int DETAIL_NUM = 20;
private final Gson gson = new Gson();
private final HttpClient httpClient = HttpClient.newHttpClient();
private boolean needReOpen = false;
// 日志相关
private final Queue<String> logQueue = new LinkedList<>();
private int ignoreNum = 0;
private int failNum = 0;
private int allNum = 0;
private int dealNum = 0;
public void repairMusicTag(String apiUrl, String filePath, boolean isDiy, int diySongId) {
try {
File dir = new File(filePath);
List<String> pathList = new ArrayList<>();
if (dir.isFile()) {
pathList.add(filePath);
} else {
if (dir.exists() && dir.isDirectory()) {
File[] files = dir.listFiles((d, name) -> name.toLowerCase().endsWith(".mp3"));
if (files != null) {
for (File file : files) {
pathList.add(file.getAbsolutePath());
}
}
}
}
allNum = pathList.size();
long startTime = System.currentTimeMillis();
Map<String, BeanInfo.SongInfo> mapKeyInfo = new HashMap<>();
for (int i = 0; i < pathList.size(); i++) {
String path = pathList.get(i);
processMusicFile(apiUrl, path, isDiy, diySongId, mapKeyInfo);
// 每20条或最后一条时获取详情
if (i == pathList.size() - 1 || mapKeyInfo.size() >= DETAIL_NUM) {
processBatchSongDetails(mapKeyInfo);
mapKeyInfo.clear();
}
}
long endTime = System.currentTimeMillis();
String log = String.format("处理完毕: %d/%d,忽略: %d,失败: %d,耗时: %.2f秒",
dealNum, allNum, ignoreNum, failNum, (endTime - startTime) / 1000.0);
log(log);
if (needReOpen && dealNum > 0) {
log("请重启网易云生效");
}
} catch (Exception e) {
e.printStackTrace();
log("处理过程中发生错误: " + e.getMessage());
}
}
private void processMusicFile(String apiUrl, String path, boolean isDiy, int diySongId,
Map<String, BeanInfo.SongInfo> mapKeyInfo) {
try {
File file = new File(path);
AudioFile audioFile = AudioFileIO.read(file);
Tag tag = audioFile.getTag();
String fileName = file.getName().substring(0, file.getName().lastIndexOf('.'));
// 检查是否需要跳过
if (tag != null && tag.hasField(FieldKey.COMMENT)) {
String comment = tag.getFirst(FieldKey.COMMENT);
if (comment.contains("163")) {
ignoreNum++;
return;
}
}
if (tag != null && tag.hasField(FieldKey.TITLE) && tag.hasField(FieldKey.ARTIST)) {
//判断标题是不是由中文或英文字母组成
String title = tag.getFirst(FieldKey.TITLE);
String artist = tag.getFirst(FieldKey.ARTIST);
if (fileName.contains("-")) {
String[] parts = fileName.split("-");
String part1 = parts[0].trim();
String part2 = parts[1].trim();
if ((part2.contains(title) || title.contains(part2) )&& (artist.contains(part1) || part1.contains(artist))) {
ignoreNum++;
return;
}
} else if (fileName.contains(title) || title.contains(fileName)) {
ignoreNum++;
return;
}
}
dealNum++;
String status = String.format("正在处理: %d/%d,忽略: %d,失败: %d,%s", dealNum, allNum, ignoreNum, failNum, path);
log(status);
if (isDiy) {
// 自定义修改
BeanInfo.SongInfo songInfo = new BeanInfo.SongInfo();
songInfo.id = diySongId;
mapKeyInfo.put(path, songInfo);
} else {
// 搜索歌曲信息
String songName = tag.getFirst(FieldKey.TITLE);
String artist = tag.getFirst(FieldKey.ARTIST);
if (songName == null || songName.trim().isEmpty()) {
songName = fileName;
}
if (fileName.contains("-")) {
String[] parts = fileName.split("-");
String part1 = parts[0].trim();
String part2 = parts[1].trim();
BeanInfo.SongInfo simpleSong = new BeanInfo.SongInfo();
if (part2.contains("(")) {
part2 = part2.substring(0, part2.indexOf("(")).trim();
}
songName = part2;
simpleSong.name = part2;
BeanInfo.ArtistInfo artistInfo = new BeanInfo.ArtistInfo();
artist = part1;
String[] split = part1.replace("&", "/").split("/");
artistInfo.name = split[0].trim();
simpleSong.artists = new BeanInfo.ArtistInfo[]{artistInfo};
if (tag.hasField(FieldKey.COMMENT)) {
if (tag.getFirst(FieldKey.COMMENT).contains("https://music.163.com/#/song?id=")
&& (tag.getFirst(FieldKey.TITLE).equals(part2) || tag.getFirst(FieldKey.TITLE).equals(part1))) {
simpleSong.id = Long.valueOf(tag.getFirst(FieldKey.COMMENT).replace("https://music.163.com/#/song?id=", ""));
}
if (tag.getFirst(FieldKey.COMMENT).contains("163:")
&& (tag.getFirst(FieldKey.TITLE).equals(part2) || tag.getFirst(FieldKey.TITLE).equals(part1))) {
simpleSong.id = Long.valueOf(tag.getFirst(FieldKey.COMMENT).replace("163:", ""));
}
}
if (simpleSong.id != 0) {
mapKeyInfo.put(path, simpleSong);
return;
}
} else if (!fileName.contains(songName)) {
songName = fileName;
artist = null;
}
String keywords = songName;
if (artist != null && !artist.trim().isEmpty()) {
keywords += " " + artist;
}
BeanInfo.SongInfo songInfo = searchSongInfo(apiUrl, keywords, songName, artist);
if (songInfo != null) {
mapKeyInfo.put(path, songInfo);
} else {
failNum++;
}
}
} catch (Exception e) {
e.printStackTrace();
failNum++;
log("处理文件失败: " + path + ",错误: " + e.getMessage());
}
}
private void log(String str) {
System.out.println(str);
}
private BeanInfo.SongInfo searchSongInfo(String apiUrl, String keyword, String songName, String artistStr) {
// 搜索歌曲信息,最多重试3次
BeanInfo.SongInfo songInfo = null;
boolean found = false;
int maxTries = 0;
for (int i = 0; i <= maxTries && !found; i++) {
try {
int pageSize = 30;
BeanInfo.SearchInfo searchResult = search(apiUrl, keyword, pageSize, i * pageSize);
if (searchResult != null && searchResult.songs != null && !searchResult.songs.isEmpty()) {
songInfo = getMatchSongInfo(songName, artistStr, searchResult.songs);
if (songInfo == null) {
continue;
}
if (songInfo.isMatch) {
found = true;
} else if (i == 0) {
// 保存第一次搜索结果的第一首歌
songInfo = searchResult.songs.get(0);
}
}
} catch (Exception e) {
log("搜索失败,重试中: " + e.getMessage());
}
}
return songInfo;
}
private BeanInfo.SearchInfo search(String apiUrl, String keyword, int limit, int offset) {
try {
String encodedKeyword = java.net.URLEncoder.encode(keyword, StandardCharsets.UTF_8.toString());
String url = String.format("%s?type=1&limit=%d&offset=%d&s=%s", apiUrl, limit, offset, encodedKeyword);
String header = HeaderUtils.getHeader(dealNum);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("User-Agent", header)
.timeout(java.time.Duration.ofSeconds(5))
.GET()
.build();
log(url);
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
String body = response.body();
BeanInfo.ApiInfo<BeanInfo.SearchInfo> apiResponse = gson.fromJson(body,
new com.google.gson.reflect.TypeToken<BeanInfo.ApiInfo<BeanInfo.SearchInfo>>() {
}.getType());
if (apiResponse.code == 200 && apiResponse.result != null) {
return apiResponse.result;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private void processBatchSongDetails(Map<String, BeanInfo.SongInfo> mapKeyInfo) {
if (mapKeyInfo.isEmpty()) return;
// 写入MP3文件
for (Map.Entry<String, BeanInfo.SongInfo> entry : mapKeyInfo.entrySet()) {
String path = entry.getKey();
BeanInfo.SongInfo songInfo = entry.getValue();
writeSongCommentToFile(path, songInfo);
}
// try {
// List<Long> idList = new ArrayList<>();
// for (BeanInfo.SongInfo songInfo : mapKeyInfo.values()) {
// if (songInfo.id != 0) {
// idList.add(songInfo.id);
// }
// }
//
// if (idList.isEmpty()) {
// return;
// }
//
//
//
// List<BeanInfo.SongInfo> songDetails = getSongDetails(idList);
// if (songDetails != null && !songDetails.isEmpty()) {
// // 更新map中的歌曲信息
// for (Map.Entry<String, BeanInfo.SongInfo> entry : mapKeyInfo.entrySet()) {
// String path = entry.getKey();
// BeanInfo.SongInfo originalInfo = entry.getValue();
// boolean isMatch = originalInfo.isMatch;
//
// for (BeanInfo.SongInfo detail : songDetails) {
// if (detail.id == originalInfo.id) {
// detail.isMatch = isMatch;
// mapKeyInfo.put(path, detail);
// break;
// }
// }
// }
// } else {
// failNum += idList.size();
// }
// } catch (Exception e) {
// e.printStackTrace();
// failNum += mapKeyInfo.size();
// }
}
private List<BeanInfo.SongInfo> getSongDetails(List<Long> ids) {
try {
StringBuilder idStr = new StringBuilder();
idStr.append("[");
for (int i = 0; i < ids.size(); i++) {
idStr.append(ids.get(i));
if (i < ids.size() - 1) {
idStr.append(",");
}
}
idStr.append("]");
String encodedIds = java.net.URLEncoder.encode(idStr.toString(), StandardCharsets.UTF_8.toString());
String url = DETAIL_API + encodedIds;
String header = HeaderUtils.getHeader(dealNum);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("User-Agent", header)
.timeout(java.time.Duration.ofSeconds(5))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
String body = response.body();
BeanInfo.SearchInfo searchInfo = gson.fromJson(body, BeanInfo.SearchInfo.class);
if (searchInfo.code == 200 && searchInfo.songs != null && !searchInfo.songs.isEmpty()) {
return searchInfo.songs;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private void writeSongCommentToFile(String path, BeanInfo.SongInfo songInfo) {
try {
File file = new File(path);
AudioFile audioFile = AudioFileIO.read(file);
convertToID3v24(audioFile);
Tag tag = audioFile.getTag();
// // 写入评论和描述
tag.setField(FieldKey.COMMENT, "https://music.163.com/#/song?id=" + songInfo.id);
tag.setField(FieldKey.TITLE, songInfo.name);
if (songInfo.artists.length > 0) {
tag.setField(FieldKey.ARTIST, Arrays.stream(songInfo.artists).map(a -> a.name).collect(Collectors.joining(" ")));
}
log("写入文件成功: " + path + " ,标题:" + songInfo.name + " 艺术家:" + tag.getFirst(FieldKey.ARTIST));
audioFile.commit();
} catch (Exception e) {
e.printStackTrace();
failNum++;
log("写入文件失败: " + path + ",错误: " + e.getMessage());
} finally {
}
}
private BeanInfo.SongInfo getMatchSongInfo(String songNameOrArtist1, String songNameOrArtist2, List<BeanInfo.SongInfo> list) {
if (list == null || list.isEmpty()) {
return null;
}
BeanInfo.SongInfo bestMatch = list.get(0);
for (BeanInfo.SongInfo songInfo : list) {
List<String> artistNames = new ArrayList<>();
if (songInfo.artists != null) {
for (BeanInfo.ArtistInfo artist : songInfo.artists) {
artistNames.add(artist.name);
}
}
if (songInfo.name.equals(songNameOrArtist1)) {
bestMatch = filterSongInfo(songNameOrArtist2, bestMatch, songInfo, artistNames);
} else if (songInfo.name.equals(songNameOrArtist2)) {
bestMatch = filterSongInfo(songNameOrArtist1, bestMatch, songInfo, artistNames);
}
}
return bestMatch;
}
private BeanInfo.SongInfo filterSongInfo(String songNameOrArtist, BeanInfo.SongInfo bestMatch, BeanInfo.SongInfo songInfo, List<String> artistNames) {
songInfo.isMatch = songNameOrArtist == null || songNameOrArtist.isEmpty();
if (songNameOrArtist != null) {
for (String artist : songNameOrArtist.replace("&", "/").split("/")) {
if (artistNames.contains(artist.trim().toLowerCase())) {
bestMatch = songInfo;
songInfo.isMatch = true;
break;
}
}
}
return bestMatch;
}
// 添加一个新方法用于转换标签格式
public void convertAllFilesToID3v24(String path) {
File dir = new File(path);
if (!dir.exists()) {
log("目录不存在: " + path);
return;
}
if (dir.isDirectory()) {
File[] files = dir.listFiles((d, name) -> name.toLowerCase().endsWith(".mp3"));
if (files == null || files.length == 0) {
log("目录中没有MP3文件: " + path);
return;
}
log("找到 " + files.length + " 个MP3文件,开始转换标签格式...");
int convertedCount = 0;
for (File file : files) {
convertedCount = convert(file, convertedCount);
}
log("转换完成,共转换 " + convertedCount + " 个文件");
} else {
convert(dir, 0);
}
}
private int convert(File file, int convertedCount) {
try {
AudioFile audioFile = AudioFileIO.read(file);
Tag tag = audioFile.getTag();
if (tag == null || !(tag instanceof org.jaudiotagger.tag.id3.ID3v24Tag)) {
convertToID3v24(audioFile);
audioFile.commit();
convertedCount++;
log("已转换: " + file.getName());
} else {
log("已是ID3v2.4格式,跳过: " + file.getName());
}
} catch (Exception e) {
log("转换失败: " + file.getName() + ",错误: " + e.getMessage());
}
return convertedCount;
}
// 单个文件的标签转换方法
private void convertToID3v24(AudioFile audioFile) throws Exception {
Tag oldTag = audioFile.getTag();
if (oldTag == null || !(oldTag instanceof org.jaudiotagger.tag.id3.ID3v24Tag)) {
return;
}
org.jaudiotagger.tag.id3.ID3v24Tag newTag = new org.jaudiotagger.tag.id3.ID3v24Tag();
if (oldTag instanceof org.jaudiotagger.tag.id3.ID3v1Tag) {
newTag.setField(FieldKey.TITLE, ((ID3v1Tag) oldTag).getFirstTitle());
newTag.setField(FieldKey.ARTIST, ((ID3v1Tag) oldTag).getFirstArtist());
newTag.setField(FieldKey.ALBUM, ((ID3v1Tag) oldTag).getFirstAlbum());
newTag.setField(FieldKey.GENRE, ((ID3v1Tag) oldTag).getFirstGenre());
newTag.setField(FieldKey.COMMENT, ((ID3v1Tag) oldTag).getFirstComment());
} else {
// 如果有旧标签,复制所有字段
for (Iterator<TagField> it = oldTag.getFields(); it.hasNext(); ) {
TagField fieldKey = it.next();
switch (fieldKey.getId()) {
case "COMM":
// 跳过评论字段
continue;
case "TIT2":
newTag.setField(FieldKey.TITLE, oldTag.getFirst(fieldKey.getId()));
break;
case "TPE1":
newTag.setField(FieldKey.ARTIST, oldTag.getFirst(fieldKey.getId()));
break;
case "TALB":
newTag.setField(FieldKey.ALBUM, oldTag.getFirst(fieldKey.getId()));
break;
case "TRCK":
newTag.setField(FieldKey.TRACK, oldTag.getFirst(fieldKey.getId()));
break;
default:
break;
}
}
}
// 设置新标签并保存
audioFile.setTag(newTag);
}
// 主方法示例
public static void main(String[] args) {
NeteaseMusicCommentRepair repairTool = new NeteaseMusicCommentRepair();
repairTool.repairMusicTag(
"http://music.163.com/api/search/get/web",
"/home/yang0328/Music/酷我音乐/",
false, 0);
}
public static class BeanInfo {
// 艺术家信息类
public static class ArtistInfo implements Serializable {
public long id;
public String name;
public String picUrl;
public String img1v1Url;
public long albumSize;
public long picId;
public long img1v1;
public String[] alias;
}
// 专辑信息类
public static class AlbumInfo implements Serializable {
public long id;
public String name;
public long publishTime;
public long picId;
public String picUrl;
public long size;
public long copyrightId;
public long status;
public long mark;
public ArtistInfo artist;
}
// 歌曲信息类
public static class SongInfo implements Serializable {
public long id;
public String name;
public long duration;
public long bitrate; // 从文件读取
public long copyrightId;
public long status;
public ArtistInfo[] artists;
public AlbumInfo album;
public long rtype;
public long ftype;
public long mvid;
public long fee;
public long rUrl;
public long mark;
public boolean isMatch; // 是否完全匹配
public KeyInfo toKeyInfo() {
List<String[]> art = new ArrayList<>();
if (artists != null) {
for (ArtistInfo artist : artists) {
art.add(new String[]{artist.name, String.valueOf(artist.id)});
}
}
KeyInfo keyInfo = new KeyInfo();
keyInfo.musicId = id;
keyInfo.musicName = name;
keyInfo.artist = art;
keyInfo.albumId = (album != null) ? album.id : 0;
keyInfo.album = (album != null) ? album.name : "";
keyInfo.albumPicDocId = (album != null) ? String.valueOf(album.picId) : "0";
keyInfo.albumPic = (album != null) ? album.picUrl : "";
keyInfo.bitrate = bitrate;
keyInfo.duration = this.duration;
keyInfo.mvId = this.mvid;
keyInfo.alias = new String[]{};
keyInfo.transNames = new String[]{};
keyInfo.format = "mp3";
return keyInfo;
}
}
// 搜索结果信息类
public static class SearchInfo implements Serializable {
public List<SongInfo> songs;
public boolean hasMore;
public long songCount;
public long code;
}
// API返回信息封装类
public static class ApiInfo<T> implements Serializable {
public T result;
public long code;
}
// 网易云音乐Key信息类
public static class KeyInfo implements Serializable {
public String format;
public long musicId;
public String musicName;
public List<String[]> artist;
public String album;
public long albumId;
public String albumPicDocId;
public String albumPic;
public long mvId;
public long bitrate;
public long duration;
public String[] alias;
public String[] transNames;
}
}
public static class HeaderUtils {
private static final String[] REQUEST_HEADER = {
// Safari 5.1 – MAC
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
// Safari 5.1 – Windows
"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50",
// Firefox 4.0.1 – MAC
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
// Firefox 4.0.1 – Windows
"Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
// 更多User-Agent..
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:34.0) Gecko/20100101 Firefox/34.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10; rv:33.0) Gecko/20100101 Firefox/33.0",
"Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11",
"Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11",
"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.57.2 (KHTML, like Gecko) Version/4.0.5 Safari/534.57.2",
};
public static String getHeader(int index) {
if (index >= 0) {
return REQUEST_HEADER[index % REQUEST_HEADER.length];
}
throw new IllegalArgumentException("UserAgent索引错误: " + index);
}
}
}
了解 工作生活心情记忆 的更多信息
订阅后即可通过电子邮件收到最新文章。