diff --git a/src/main/java/top/baogutang/music/client/KuGouMusicClient.java b/src/main/java/top/baogutang/music/client/KuGouMusicClient.java index d9e8cbd..d9fbecd 100644 --- a/src/main/java/top/baogutang/music/client/KuGouMusicClient.java +++ b/src/main/java/top/baogutang/music/client/KuGouMusicClient.java @@ -328,4 +328,5 @@ public class KuGouMusicClient implements ChannelClient queryMusicByPlatformIdList(List platformIdList) { return List.of(); } + } diff --git a/src/main/java/top/baogutang/music/client/NetEaseMusicClient.java b/src/main/java/top/baogutang/music/client/NetEaseMusicClient.java index 3300c55..2e0f1ae 100644 --- a/src/main/java/top/baogutang/music/client/NetEaseMusicClient.java +++ b/src/main/java/top/baogutang/music/client/NetEaseMusicClient.java @@ -16,6 +16,7 @@ import top.baogutang.music.processor.AbstractAudioProcessor; import top.baogutang.music.properties.NetEaseMusicProperties; import top.baogutang.music.service.IMusicDownloadRecordService; import top.baogutang.music.service.IMusicRecordService; +import top.baogutang.music.service.NcmDownloadService; import top.baogutang.music.utils.OkHttpUtil; import javax.annotation.Resource; @@ -28,11 +29,11 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; /** * * @description: - * * @author: N1KO * @date: 2024/12/10 : 17:57 */ @@ -51,6 +52,9 @@ public class NetEaseMusicClient implements ChannelClient() { - }); - if (Objects.nonNull(res)) { - res.setId(id); - String[] split = res.getUrl().split("\\."); - String fileType = split[split.length - 1]; - fileType = fileType.split("\\?")[0]; - res.setFileType(AudioFileTypeEnum.parse(fileType)); - } - return res; + try { + return ncmDownloadService.getDownloadRes(id, netEaseMusicProperties.getCookie()); + } catch (Exception e) { + log.error("net ease music download error:{}", e.getMessage(), e); + } + return null; + } @Override @@ -121,6 +121,14 @@ public class NetEaseMusicClient implements ChannelClient stream = Files.list(baseDir)) { + hasAnyFile = stream.anyMatch(Files::isRegularFile); + } + if (hasAnyFile) { + log.warn("path:{} has file", baseDir.toString()); + return; + } AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(res.getFileType()); try (InputStream musicIn = audioProcessor.processAudioTags(new URL(res.getUrl()).openStream(), res)) { if (Objects.nonNull(musicIn)) { diff --git a/src/main/java/top/baogutang/music/client/QQMusicClient.java b/src/main/java/top/baogutang/music/client/QQMusicClient.java index b8b44b3..7162949 100644 --- a/src/main/java/top/baogutang/music/client/QQMusicClient.java +++ b/src/main/java/top/baogutang/music/client/QQMusicClient.java @@ -36,7 +36,6 @@ import java.util.stream.Collectors; /** * * @description: - * * @author: N1KO * @date: 2024/12/12 : 15:00 */ @@ -268,12 +267,21 @@ public class QQMusicClient implements ChannelClient() { - }); - if (Objects.isNull(qqMusicDownloadRes) || Objects.isNull(qqMusicDownloadRes.getMusicUrlInfo())) { - downloadUrl = String.format(qqMusicProperties.getDownloadBaseUrl2(), id); + QQMusicDownloadRes qqMusicDownloadRes = null; + try { qqMusicDownloadRes = OkHttpUtil.get(downloadUrl, null, null, new TypeReference<>() { }); + } catch (Exception e) { + log.error("download url:{} error:{}", qqMusicProperties.getDownloadBaseUrl1(), e.getMessage()); + } + if (Objects.isNull(qqMusicDownloadRes) || Objects.isNull(qqMusicDownloadRes.getMusicUrlInfo())) { + downloadUrl = String.format(qqMusicProperties.getDownloadBaseUrl2(), id); + try { + qqMusicDownloadRes = OkHttpUtil.get(downloadUrl, null, null, new TypeReference<>() { + }); + } catch (Exception e) { + log.error("download url:{} error:{}", qqMusicProperties.getDownloadBaseUrl1(), e.getMessage()); + } if (Objects.isNull(qqMusicDownloadRes) || Objects.isNull(qqMusicDownloadRes.getMusicUrlInfo())) { return null; } @@ -307,7 +315,7 @@ public class QQMusicClient implements ChannelClient 1) { + fileName = split[1]; + } AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(res.getFileType()); try (InputStream musicIn = audioProcessor.processAudioTags(new URL(res.getUrl()).openStream(), res)) { if (Objects.nonNull(musicIn)) { - Files.copy(musicIn, baseDir.resolve(res.getName() + "." + res.getFileType().name().toLowerCase()), StandardCopyOption.REPLACE_EXISTING); + Files.copy(musicIn, baseDir.resolve(fileName + "." + res.getFileType().name().toLowerCase()), StandardCopyOption.REPLACE_EXISTING); } } } diff --git a/src/main/java/top/baogutang/music/controller/MusicTriggerController.java b/src/main/java/top/baogutang/music/controller/MusicTriggerController.java index a16da5f..a8641ec 100644 --- a/src/main/java/top/baogutang/music/controller/MusicTriggerController.java +++ b/src/main/java/top/baogutang/music/controller/MusicTriggerController.java @@ -6,13 +6,13 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import top.baogutang.music.domain.Results; import top.baogutang.music.schedule.QQDailyMusicHandler; +import top.baogutang.music.service.IMusicRecordService; import javax.annotation.Resource; /** * * @description: - * * @author: N1KO * @date: 2024/12/23 : 15:26 */ @@ -24,9 +24,19 @@ public class MusicTriggerController { @Resource private QQDailyMusicHandler qqDailyMusicHandler; + @Resource + private IMusicRecordService musicRecordService; + @GetMapping("/daily") public Results triggerDaily() { qqDailyMusicHandler.fetchDailyAndSave(); return Results.ok(); } + + @GetMapping("/recall") + public Results triggerRecall() { + musicRecordService.triggerRecall(); + return Results.ok(); + } + } diff --git a/src/main/java/top/baogutang/music/dao/mapper/MusicRecordMapper.java b/src/main/java/top/baogutang/music/dao/mapper/MusicRecordMapper.java index a2382c7..5306aef 100644 --- a/src/main/java/top/baogutang/music/dao/mapper/MusicRecordMapper.java +++ b/src/main/java/top/baogutang/music/dao/mapper/MusicRecordMapper.java @@ -1,6 +1,7 @@ package top.baogutang.music.dao.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Mapper; import top.baogutang.music.dao.entity.MusicRecordEntity; @@ -13,4 +14,7 @@ import top.baogutang.music.dao.entity.MusicRecordEntity; */ @Mapper public interface MusicRecordMapper extends BaseMapper { + + Page selectPages(Page page); + } diff --git a/src/main/java/top/baogutang/music/properties/NetEaseMusicProperties.java b/src/main/java/top/baogutang/music/properties/NetEaseMusicProperties.java index 0007ca8..be90965 100644 --- a/src/main/java/top/baogutang/music/properties/NetEaseMusicProperties.java +++ b/src/main/java/top/baogutang/music/properties/NetEaseMusicProperties.java @@ -28,4 +28,6 @@ public class NetEaseMusicProperties { private String downloadPath; + private String cookie; + } diff --git a/src/main/java/top/baogutang/music/service/AbstractMusicService.java b/src/main/java/top/baogutang/music/service/AbstractMusicService.java index f6670d3..23036f7 100644 --- a/src/main/java/top/baogutang/music/service/AbstractMusicService.java +++ b/src/main/java/top/baogutang/music/service/AbstractMusicService.java @@ -22,12 +22,16 @@ import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.Objects; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -36,7 +40,6 @@ import static top.baogutang.music.constants.BusinessKey.ADMIN_ID_LIST; /** * * @description: - * * @author: N1KO * @date: 2024/12/10 : 17:00 */ @@ -114,6 +117,46 @@ public abstract class AbstractMusicService channelClient = channelClientFactory.getClient(channelEnum); + try { + Path baseDir; + if (StringUtils.isNotBlank(musicRecordEntity.getAlbumName())) { + baseDir = Paths.get(channelClient.getDownloadBasePath(), musicRecordEntity.getArtistName(), musicRecordEntity.getAlbumName()); + } else { + baseDir = Paths.get(channelClient.getDownloadBasePath(), musicRecordEntity.getArtistName()); + } + if (!Files.exists(baseDir)) { + Files.createDirectories(baseDir); + } + boolean hasAnyFile; + try (Stream stream = Files.list(baseDir)) { + hasAnyFile = stream.anyMatch(file -> Files.isRegularFile(file) && file.getFileName().toString().contains(musicRecordEntity.getName())); + } + if (hasAnyFile) { + log.warn("path:{} has file:{}", baseDir.toString(), musicRecordEntity.getName()); + return; + } + MusicDownloadRes download = channelClient.download(musicRecordEntity.getPlatformId()); + if (Objects.isNull(download)) { + log.error("channel:{} music platform id:{} music name:{} download is null!", channelEnum.getDesc(), musicRecordEntity.getPlatformId(), musicRecordEntity.getName()); + return; + } + + log.info(">>>>>>>>>>download file:{} start<<<<<<<<<<", download.getName()); + channelClient.processFile(download); + log.info(">>>>>>>>>>download file:{} success<<<<<<<<<<", download.getName()); + } catch (IOException e) { + log.error(">>>>>>>>>>download error:{}<<<<<<<<<<", e.getMessage(), e); + throw new BusinessException("下载异常"); + } + + } + public String downloads(List idList, Long userId) { if (CollectionUtils.isEmpty(idList)) { throw new BusinessException("请选择下载内容"); @@ -148,6 +191,7 @@ public abstract class AbstractMusicService>>>>>>>>>download file:{} start<<<<<<<<<<", res.getName()); channelClient.processFile(res); log.info(">>>>>>>>>>download file:{} success<<<<<<<<<<", res.getName()); } catch (IOException e) { diff --git a/src/main/java/top/baogutang/music/service/IMusicRecordService.java b/src/main/java/top/baogutang/music/service/IMusicRecordService.java index 40d56ef..ccd75bd 100644 --- a/src/main/java/top/baogutang/music/service/IMusicRecordService.java +++ b/src/main/java/top/baogutang/music/service/IMusicRecordService.java @@ -23,4 +23,7 @@ public interface IMusicRecordService extends IService { List queryByPlatformIdList(List platformIdList); MusicRecordEntity queryByNameOrAlbumOrArtist(String title, String album, String artist); + + void triggerRecall(); + } diff --git a/src/main/java/top/baogutang/music/service/NcmDownloadService.java b/src/main/java/top/baogutang/music/service/NcmDownloadService.java new file mode 100644 index 0000000..278a217 --- /dev/null +++ b/src/main/java/top/baogutang/music/service/NcmDownloadService.java @@ -0,0 +1,368 @@ +package top.baogutang.music.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.*; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.*; + +import org.springframework.stereotype.Service; +import top.baogutang.music.domain.res.download.MusicDownloadRes; +import top.baogutang.music.enums.AudioFileTypeEnum; + +/** + * Netease Cloud Music unified downloader: + * - Fetch song detail to get base info and maxBrLevel + * - Use maxBrLevel to request direct URL via eapi + * - Map to MusicDownloadRes + */ +@Service +public class NcmDownloadService { + + private static final MediaType FORM = MediaType.parse("application/x-www-form-urlencoded"); + private static final ObjectMapper MAPPER = new ObjectMapper(); + private final OkHttpClient http = new OkHttpClient(); + + private static final String UA = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/2.10.2.200154"; + private static final String REFERER = "https://music.163.com/"; + private static final String ORIGIN = "https://music.163.com"; + private static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; + + private static final String SONG_DETAIL_URL = "https://music.163.com/api/v3/song/detail"; + private static final String URL_V1_EAPI = "https://interface3.music.163.com/eapi/song/enhance/player/url/v1"; + private static final String AES_KEY = "e82ckenh8dichen8"; + + /** + * Main entry: provide song id and cookie, returns filled MusicDownloadRes. + */ + public MusicDownloadRes getDownloadRes(String id, String cookie) throws Exception { + DetailResult detail = fetchSongDetail(id, cookie); + + String level = pickLevelOrFallback(detail.maxBrLevel); + + UrlResult urlRes = fetchSongUrl(id, level, cookie); + if ((urlRes.url == null || urlRes.url.isEmpty()) && !"standard".equalsIgnoreCase(level)) { + String fallback = fallbackLevel(level); + if (fallback != null) { + urlRes = fetchSongUrl(id, fallback, cookie); + urlRes.level = fallback; + } + } + + MusicDownloadRes out = new MusicDownloadRes(); + out.setId(String.valueOf(detail.id)); + out.setName(buildDisplayName(detail.name, detail.mainTitle, detail.additionalTitle)); + out.setArtistName(String.join(", ", detail.artists)); + out.setAlbumName(detail.albumName); + out.setPic(detail.picUrl); + + out.setLevel(urlRes.level != null ? urlRes.level : level); + out.setUrl(urlRes.url); + out.setSize(humanSize(urlRes.size)); + + AudioFileTypeEnum fileType = inferFileType(urlRes.type, urlRes.url); + out.setFileType(fileType); + + out.setLyric(null); // 可后续补充歌词接口 + return out; + } + + // ==================== Detail request & parse ==================== + + private DetailResult fetchSongDetail(String id, String cookie) throws Exception { + String cJson = "[{\"id\":" + id + ",\"v\":0}]"; + String body = "c=" + urlEncode(cJson); + + Request request = new Request.Builder() + .url(SONG_DETAIL_URL) + .post(RequestBody.create(FORM, body)) + .header("User-Agent", UA) + .header("Referer", REFERER) + .header("Origin", ORIGIN) + .header("Content-Type", CONTENT_TYPE) + .header("Cookie", cookie == null ? "" : cookie) + .build(); + + try (Response resp = http.newCall(request).execute()) { + if (!resp.isSuccessful()) throw new RuntimeException("HTTP " + resp.code()); + String text = resp.body().string(); + + String firstJson = extractFirstJsonObject(text); + JsonNode root = MAPPER.readTree(firstJson != null ? firstJson : text); + + if (root.path("code").asInt() != 200) { + throw new RuntimeException("API error: code=" + root.path("code").asInt()); + } + + JsonNode songNode = root.path("songs").isArray() && root.path("songs").size() > 0 + ? root.path("songs").get(0) : MAPPER.createObjectNode(); + + DetailResult d = new DetailResult(); + d.id = songNode.path("id").asLong(); + d.name = songNode.path("name").asText(); + d.mainTitle = songNode.path("mainTitle").asText(null); + d.additionalTitle = songNode.path("additionalTitle").asText(null); + d.durationMs = songNode.path("dt").asLong(0); + + d.artists = new ArrayList<>(); + JsonNode ar = songNode.path("ar"); + if (ar.isArray()) { + for (JsonNode a : ar) d.artists.add(a.path("name").asText()); + } + + JsonNode al = songNode.path("al"); + d.albumId = al.path("id").asLong(); + d.albumName = al.path("name").asText(); + d.picUrl = al.path("picUrl").asText(); + + String maxBrLevel = null; + JsonNode privileges = root.path("privileges"); + if (privileges.isArray() && privileges.size() > 0) { + maxBrLevel = privileges.get(0).path("maxBrLevel").asText(null); + } + d.maxBrLevel = maxBrLevel; + return d; + } + } + + private static class DetailResult { + long id; + String name; + String mainTitle; + String additionalTitle; + long durationMs; + List artists; + long albumId; + String albumName; + String picUrl; + String maxBrLevel; + } + + // ==================== URL request (eapi) ==================== + + private UrlResult fetchSongUrl(String id, String level, String rawCookie) throws Exception { + Map headerCfg = new LinkedHashMap<>(); + headerCfg.put("os", "pc"); + headerCfg.put("appver", ""); + headerCfg.put("osver", ""); + headerCfg.put("deviceId", "pyncm!"); + headerCfg.put("requestId", String.valueOf(randomInt(20_000_000, 30_000_000))); + + Map payload = new LinkedHashMap<>(); + payload.put("ids", Collections.singletonList(id)); + payload.put("level", level); + payload.put("encodeType", "flac"); + payload.put("header", MAPPER.writeValueAsString(headerCfg)); + if ("sky".equalsIgnoreCase(level)) payload.put("immerseType", "c51"); + + String apiPath = eapiToApiPath(URL_V1_EAPI); + String payloadJson = MAPPER.writeValueAsString(payload); + String digest = md5Hex("nobody" + apiPath + "use" + payloadJson + "md5forencrypt"); + String paramsPlain = apiPath + "-36cd479b6b5-" + payloadJson + "-36cd479b6b5-" + digest; + String encryptedHex = aes128EcbHex(paramsPlain, AES_KEY); + + String cookieHeader = mergeCookies(rawCookie, headerCfg); + + Request request = new Request.Builder() + .url(URL_V1_EAPI) + .post(RequestBody.create(FORM, "params=" + urlEncode(encryptedHex))) + .header("User-Agent", UA) + .header("Content-Type", CONTENT_TYPE) + .header("Cookie", cookieHeader) + .build(); + + UrlResult out = new UrlResult(); + out.level = level; + + try (Response resp = http.newCall(request).execute()) { + if (!resp.isSuccessful()) { + out.code = resp.code(); + return out; + } + String body = resp.body().string(); + JsonNode root = MAPPER.readTree(body); + + out.code = root.path("code").asInt(); + + JsonNode data0 = root.path("data").isArray() && root.path("data").size() > 0 + ? root.path("data").get(0) : MAPPER.createObjectNode(); + + out.url = data0.path("url").asText(null); + out.size = data0.path("size").asLong(0); + out.type = data0.path("type").asText(null); + out.time = data0.path("time").asLong(0); + return out; + } + } + + private static class UrlResult { + int code; + String url; + long size; + String type; + long time; + String level; + } + + // ==================== Mapping & helpers ==================== + + private static String buildDisplayName(String name, String mainTitle, String additionalTitle) { + if (mainTitle != null && !mainTitle.isEmpty()) { + if (additionalTitle != null && !additionalTitle.isEmpty()) { + return (mainTitle + additionalTitle).trim(); + } + return mainTitle.trim(); + } + return name != null ? name.trim() : ""; + } + + private static AudioFileTypeEnum inferFileType(String type, String url) { + String t = (type != null ? type.toLowerCase() : ""); + if (t.contains("flac")) return AudioFileTypeEnum.FLAC; + if (t.contains("mp3")) return AudioFileTypeEnum.MP3; + if (t.contains("aac") || t.contains("m4a")) return AudioFileTypeEnum.OTHERS; + + if (url != null) { + String lower = url.toLowerCase(); + if (lower.contains(".flac")) return AudioFileTypeEnum.FLAC; + if (lower.contains(".mp3")) return AudioFileTypeEnum.MP3; + if (lower.contains(".m4a") || lower.contains(".aac")) return AudioFileTypeEnum.OTHERS; + } + return AudioFileTypeEnum.FLAC; // 兜底 + } + + private static String humanSize(long bytes) { + if (bytes <= 0) return "0 B"; + String[] units = {"B", "KB", "MB", "GB"}; + int idx = (int) Math.min(units.length - 1, Math.floor(Math.log10(bytes) / Math.log10(1024))); + double val = bytes / Math.pow(1024, idx); + return String.format(Locale.ROOT, "%.2f %s", val, units[idx]); + } + + private static String eapiToApiPath(String eapiUrl) { + HttpUrl httpUrl = HttpUrl.parse(eapiUrl); + String path = httpUrl != null ? httpUrl.encodedPath() : ""; + return path.replace("/eapi/", "/api/"); + } + + private static String md5Hex(String text) throws Exception { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] digest = md.digest(text.getBytes(StandardCharsets.UTF_8)); + return toHex(digest); + } + + private static String aes128EcbHex(String plain, String key) throws Exception { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + SecretKeySpec spec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES"); + cipher.init(Cipher.ENCRYPT_MODE, spec); + byte[] enc = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8)); + return toHex(enc); + } + + private static String toHex(byte[] data) { + StringBuilder sb = new StringBuilder(data.length * 2); + for (byte b : data) sb.append(String.format("%02x", b)); + return sb.toString(); + } + + private static String urlEncode(String s) { + return URLEncoder.encode(s, StandardCharsets.UTF_8); + } + + private static int randomInt(int minInclusive, int maxExclusive) { + return new Random().nextInt(maxExclusive - minInclusive) + minInclusive; + } + + private static String mergeCookies(String rawCookieString, Map headerCfg) { + Map cookies = parseCookie(rawCookieString); + cookies.put("os", String.valueOf(headerCfg.get("os"))); + cookies.put("appver", String.valueOf(headerCfg.get("appver"))); + cookies.put("osver", String.valueOf(headerCfg.get("osver"))); + cookies.put("deviceId", String.valueOf(headerCfg.get("deviceId"))); + StringBuilder sb = new StringBuilder(); + for (Map.Entry e : cookies.entrySet()) { + if (sb.length() > 0) sb.append("; "); + sb.append(e.getKey()).append("=").append(e.getValue() == null ? "" : e.getValue()); + } + return sb.toString(); + } + + private static Map parseCookie(String text) { + Map map = new LinkedHashMap<>(); + if (text == null || text.isEmpty()) return map; + String[] parts = text.split(";"); + for (String part : parts) { + String[] kv = part.trim().split("=", 2); + if (kv.length >= 1 && !kv[0].isEmpty()) { + map.put(kv[0], kv.length == 2 ? kv[1] : ""); + } + } + return map; + } + + private static String extractFirstJsonObject(String text) { + if (text == null) return null; + int start = text.indexOf('{'); + if (start < 0) return null; + int depth = 0; + for (int i = start; i < text.length(); i++) { + char ch = text.charAt(i); + if (ch == '{') depth++; + else if (ch == '}') { + depth--; + if (depth == 0) return text.substring(start, i + 1); + } + } + return null; + } + + /** + * 选择最佳音质等级;当详情未提供或为空时回退到默认值。 + * 可选枚举通常为:standard、exhigh、lossless、hires、sky。 + */ + private static String pickLevelOrFallback(String max) { + if (max == null || max.isEmpty()) { + return "lossless"; // 默认首选,可按需改为 "exhigh" 或 "standard" + } + // 规范化为小写,并过滤未知值 + String lvl = max.toLowerCase(Locale.ROOT); + switch (lvl) { + case "standard": + case "exhigh": + case "lossless": + case "hires": + case "sky": + return lvl; + default: + // 如果返回了未知字符串,回退到较稳妥的 "lossless" + return "lossless"; + } + } + + /** + * 简单降级链:sky → hires → lossless → exhigh → standard。 + * 当当前等级无法获取直链时调用该方法获取下一档。 + * 返回 null 表示已无法继续降级。 + */ + private static String fallbackLevel(String level) { + if (level == null || level.isEmpty()) return null; + switch (level.toLowerCase(Locale.ROOT)) { + case "sky": + return "hires"; + case "hires": + return "lossless"; + case "lossless": + return "exhigh"; + case "exhigh": + return "standard"; + default: + return null; // "standard" 或未知值时不再降级 + } + } + +} diff --git a/src/main/java/top/baogutang/music/service/impl/MusicInfoServiceImpl.java b/src/main/java/top/baogutang/music/service/impl/MusicInfoServiceImpl.java index b3fec1a..7b1d06f 100644 --- a/src/main/java/top/baogutang/music/service/impl/MusicInfoServiceImpl.java +++ b/src/main/java/top/baogutang/music/service/impl/MusicInfoServiceImpl.java @@ -86,13 +86,7 @@ public class MusicInfoServiceImpl implements IMusicInfoService { } // 获取三方token - String token = CacheUtil.cacheOrSupply(KEY_MUSIC_TAG_TOKEN_PREFIX, - 1L, - TimeUnit.DAYS, - redisTemplate, - this::geneToken, - new TypeReference<>() { - }); + String token = geneToken(); Map headers = new HashMap<>(); headers.put("authorization", "jwt " + token); headers.put("Content-Type", "application/json"); diff --git a/src/main/java/top/baogutang/music/service/impl/MusicRecordServiceImpl.java b/src/main/java/top/baogutang/music/service/impl/MusicRecordServiceImpl.java index 9715b73..9504266 100644 --- a/src/main/java/top/baogutang/music/service/impl/MusicRecordServiceImpl.java +++ b/src/main/java/top/baogutang/music/service/impl/MusicRecordServiceImpl.java @@ -1,25 +1,31 @@ package top.baogutang.music.service.impl; +import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; import top.baogutang.music.dao.entity.MusicRecordEntity; import top.baogutang.music.dao.mapper.MusicRecordMapper; import top.baogutang.music.domain.res.download.MusicDownloadRes; import top.baogutang.music.enums.ChannelEnum; import top.baogutang.music.enums.MusicQualityEnum; import top.baogutang.music.service.IMusicRecordService; +import top.baogutang.music.service.IMusicService; +import top.baogutang.music.utils.PageUtil; +import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.List; import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.function.Function; /** * * @description: - * * @author: N1KO * @date: 2024/12/11 : 15:38 */ @@ -27,6 +33,12 @@ import java.util.Objects; @Service public class MusicRecordServiceImpl extends ServiceImpl implements IMusicRecordService { + @Resource(name = "commonExecutor") + private Executor commonExecutor; + + @Resource + private IMusicService musicService; + @Override public void save(MusicDownloadRes res, ChannelEnum channel) { MusicRecordEntity entity = new LambdaQueryChainWrapper<>(baseMapper) @@ -88,4 +100,33 @@ public class MusicRecordServiceImpl extends ServiceImpl { + int pageNo = 1; + int pageSize = 50; + while (true) { + IPage pages = PageUtil.page(page -> baseMapper.selectPages(page), + pageNo, + pageSize, + Function.identity()); + if (CollectionUtils.isEmpty(pages.getRecords())) { + log.info("pageNo:{} has not records", pageNo); + break; + } + pages.getRecords().forEach(entity -> { + musicService.getMusicService(entity.getChannel().getCode()) + .processFile(entity); + }); + if (pages.getRecords().size() >= pageSize) { + pageNo = pageNo + 1; + } else { + break; + } + } + log.info("process success"); + + }); + } + } diff --git a/src/main/java/top/baogutang/music/service/impl/MusicServiceImpl.java b/src/main/java/top/baogutang/music/service/impl/MusicServiceImpl.java index 0b4ecf6..082a274 100644 --- a/src/main/java/top/baogutang/music/service/impl/MusicServiceImpl.java +++ b/src/main/java/top/baogutang/music/service/impl/MusicServiceImpl.java @@ -6,7 +6,6 @@ import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Service; import top.baogutang.music.domain.req.AbstractMusicReq; import top.baogutang.music.domain.res.AbstractMusicRes; -import top.baogutang.music.enums.ChannelEnum; import top.baogutang.music.exceptions.BusinessException; import top.baogutang.music.service.AbstractMusicService; import top.baogutang.music.service.IMusicService; diff --git a/src/main/java/top/baogutang/music/utils/PageRequest.java b/src/main/java/top/baogutang/music/utils/PageRequest.java new file mode 100644 index 0000000..0c2e631 --- /dev/null +++ b/src/main/java/top/baogutang/music/utils/PageRequest.java @@ -0,0 +1,33 @@ +package top.baogutang.music.utils; + +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * @description: + * @author: nikooh + * @date: 2024/03/07 : 20:17 + */ +@Data +public abstract class PageRequest { + + private Integer pageSize = 20; + + private Integer pageNum = 1; + + private List orders = new ArrayList<>(); + + @Data + public static class OrderBy implements Serializable { + + private static final long serialVersionUID = -2936335557980068706L; + + private String columnName; + + private boolean desc; + + } +} diff --git a/src/main/java/top/baogutang/music/utils/PageUtil.java b/src/main/java/top/baogutang/music/utils/PageUtil.java new file mode 100644 index 0000000..d1456ce --- /dev/null +++ b/src/main/java/top/baogutang/music/utils/PageUtil.java @@ -0,0 +1,108 @@ +package top.baogutang.music.utils; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Collectors; + + +/** + * @description: + * @author: nikooh + * @date: 2024/03/07 : 20:17 + */ +public class PageUtil { + + private PageUtil() { + + // private empty constructor + + } + + /** + * general method of paging query + * + * @param queryFunction page query function + * @param pageParam page param + * @param beanCastFunction bean cast function + * @param entity class + * @param query parameter + * @param return bean + * @return Page + */ + public static IPage page(UnaryOperator> queryFunction, U pageParam, Function beanCastFunction) { + + return doPage(queryFunction, new Page<>(pageParam.getPageNum(), pageParam.getPageSize()), beanCastFunction); + + } + + public static IPage page(UnaryOperator> queryFunction, Integer pageNum, Integer pageSize, Function beanCastFunction) { + + return doPage(queryFunction, new Page<>(pageNum, pageSize), beanCastFunction); + + } + + public static IPage doPage(UnaryOperator> queryFunction, Page pageParam, Function beanCastFunction) { + + Page pageRes = queryFunction.apply(pageParam); + List rPageRes = pageRes.getRecords().stream() + .map(beanCastFunction) + .collect(Collectors.toList()); + Page page = new Page<>(pageRes.getCurrent(), pageRes.getSize(), pageRes.getTotal()); + page.setRecords(rPageRes); + return page; + + } + + /** + * default order set + * + * @param orders orders for order + * @param queryWrapper queryWrapper for query + * @param defaultColumn defaultColumn default sort column + * @param T + */ + public static void defaultOrder(List orders, QueryWrapper queryWrapper, SFunction defaultColumn) { + + if (CollectionUtils.isEmpty(orders)) { + defaultOrderProcess(queryWrapper, defaultColumn); + return; + } + orders.forEach(orderBy -> { + // + if (StringUtils.isBlank(orderBy.getColumnName())) { + defaultOrderProcess(queryWrapper, defaultColumn); + } else { + if (orderBy.isDesc()) { + queryWrapper.orderByDesc(orderBy.getColumnName()); + } else { + queryWrapper.orderByAsc(orderBy.getColumnName()); + } + } + }); + + } + + /** + * @param queryWrapper queryWrapper for query + * @param defaultColumn defaultColumn default sort column + * @param t + * @param r + */ + private static void defaultOrderProcess(QueryWrapper queryWrapper, SFunction defaultColumn) { + + if (defaultColumn != null) { + queryWrapper.lambda() + .orderByDesc(defaultColumn); + } + + } + +} diff --git a/src/main/resources/mapper/MusicRecordMapper.xml b/src/main/resources/mapper/MusicRecordMapper.xml new file mode 100644 index 0000000..8a02721 --- /dev/null +++ b/src/main/resources/mapper/MusicRecordMapper.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file