Java 批量修复mp3歌曲元数据

0
(0)

借鉴了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);
        }
    }
}

这篇文章有用吗?

平均评分 0 / 5. 投票数: 0

到目前为止还没有投票!成为第一位评论此文章。

很抱歉,这篇文章对您没有用!

让我们改善这篇文章!

告诉我们我们如何改善这篇文章?


了解 工作生活心情记忆 的更多信息

订阅后即可通过电子邮件收到最新文章。