main
This commit is contained in:
parent
ba272e4b1b
commit
8c557fe65f
@ -328,4 +328,5 @@ public class KuGouMusicClient implements ChannelClient<MusicSearchReq, MusicSear
|
|||||||
public List<MusicRecordEntity> queryMusicByPlatformIdList(List<String> platformIdList) {
|
public List<MusicRecordEntity> queryMusicByPlatformIdList(List<String> platformIdList) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import top.baogutang.music.processor.AbstractAudioProcessor;
|
|||||||
import top.baogutang.music.properties.NetEaseMusicProperties;
|
import top.baogutang.music.properties.NetEaseMusicProperties;
|
||||||
import top.baogutang.music.service.IMusicDownloadRecordService;
|
import top.baogutang.music.service.IMusicDownloadRecordService;
|
||||||
import top.baogutang.music.service.IMusicRecordService;
|
import top.baogutang.music.service.IMusicRecordService;
|
||||||
|
import top.baogutang.music.service.NcmDownloadService;
|
||||||
import top.baogutang.music.utils.OkHttpUtil;
|
import top.baogutang.music.utils.OkHttpUtil;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@ -28,11 +29,11 @@ import java.nio.file.Paths;
|
|||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @description:
|
* @description:
|
||||||
*
|
|
||||||
* @author: N1KO
|
* @author: N1KO
|
||||||
* @date: 2024/12/10 : 17:57
|
* @date: 2024/12/10 : 17:57
|
||||||
*/
|
*/
|
||||||
@ -51,6 +52,9 @@ public class NetEaseMusicClient implements ChannelClient<MusicSearchReq, MusicSe
|
|||||||
@Resource
|
@Resource
|
||||||
private IMusicDownloadRecordService musicDownloadRecordService;
|
private IMusicDownloadRecordService musicDownloadRecordService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private NcmDownloadService ncmDownloadService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MusicSearchRes search(MusicSearchReq req) {
|
public MusicSearchRes search(MusicSearchReq req) {
|
||||||
String searchUrl = String.format(netEaseMusicProperties.getQueryBaseUrl(), req.getKeywords(), req.getLimit(), req.getOffset(), req.getType());
|
String searchUrl = String.format(netEaseMusicProperties.getQueryBaseUrl(), req.getKeywords(), req.getLimit(), req.getOffset(), req.getType());
|
||||||
@ -88,18 +92,14 @@ public class NetEaseMusicClient implements ChannelClient<MusicSearchReq, MusicSe
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MusicDownloadRes download(String id) {
|
public MusicDownloadRes download(String id) {
|
||||||
String downloadUrl = String.format(netEaseMusicProperties.getDownloadBaseUrl(), Long.valueOf(id));
|
|
||||||
MusicDownloadRes res = OkHttpUtil.get(downloadUrl, null, null, new TypeReference<>() {
|
|
||||||
});
|
|
||||||
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
|
@Override
|
||||||
@ -121,6 +121,14 @@ public class NetEaseMusicClient implements ChannelClient<MusicSearchReq, MusicSe
|
|||||||
if (!Files.exists(baseDir)) {
|
if (!Files.exists(baseDir)) {
|
||||||
Files.createDirectories(baseDir);
|
Files.createDirectories(baseDir);
|
||||||
}
|
}
|
||||||
|
boolean hasAnyFile;
|
||||||
|
try (Stream<Path> 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());
|
AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(res.getFileType());
|
||||||
try (InputStream musicIn = audioProcessor.processAudioTags(new URL(res.getUrl()).openStream(), res)) {
|
try (InputStream musicIn = audioProcessor.processAudioTags(new URL(res.getUrl()).openStream(), res)) {
|
||||||
if (Objects.nonNull(musicIn)) {
|
if (Objects.nonNull(musicIn)) {
|
||||||
|
|||||||
@ -36,7 +36,6 @@ import java.util.stream.Collectors;
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @description:
|
* @description:
|
||||||
*
|
|
||||||
* @author: N1KO
|
* @author: N1KO
|
||||||
* @date: 2024/12/12 : 15:00
|
* @date: 2024/12/12 : 15:00
|
||||||
*/
|
*/
|
||||||
@ -268,12 +267,21 @@ public class QQMusicClient implements ChannelClient<MusicSearchReq, MusicSearchR
|
|||||||
@Override
|
@Override
|
||||||
public MusicDownloadRes download(String id) {
|
public MusicDownloadRes download(String id) {
|
||||||
String downloadUrl = String.format(qqMusicProperties.getDownloadBaseUrl1(), id);
|
String downloadUrl = String.format(qqMusicProperties.getDownloadBaseUrl1(), id);
|
||||||
QQMusicDownloadRes qqMusicDownloadRes = OkHttpUtil.get(downloadUrl, null, null, new TypeReference<>() {
|
QQMusicDownloadRes qqMusicDownloadRes = null;
|
||||||
});
|
try {
|
||||||
if (Objects.isNull(qqMusicDownloadRes) || Objects.isNull(qqMusicDownloadRes.getMusicUrlInfo())) {
|
|
||||||
downloadUrl = String.format(qqMusicProperties.getDownloadBaseUrl2(), id);
|
|
||||||
qqMusicDownloadRes = OkHttpUtil.get(downloadUrl, null, null, new TypeReference<>() {
|
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())) {
|
if (Objects.isNull(qqMusicDownloadRes) || Objects.isNull(qqMusicDownloadRes.getMusicUrlInfo())) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -307,7 +315,7 @@ public class QQMusicClient implements ChannelClient<MusicSearchReq, MusicSearchR
|
|||||||
musicDownloadRes.setUrl(musicUrlInfo.getUrlInfo128().getUrl());
|
musicDownloadRes.setUrl(musicUrlInfo.getUrlInfo128().getUrl());
|
||||||
musicDownloadRes.setLevel(musicUrlInfo.getUrlInfo128().getBitrate());
|
musicDownloadRes.setLevel(musicUrlInfo.getUrlInfo128().getBitrate());
|
||||||
}
|
}
|
||||||
if(Objects.isNull(musicDownloadRes.getUrl())) {
|
if (Objects.isNull(musicDownloadRes.getUrl())) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
QQMusicDownloadRes.Lyrics lyric = qqMusicDownloadRes.getLyric();
|
QQMusicDownloadRes.Lyrics lyric = qqMusicDownloadRes.getLyric();
|
||||||
@ -350,10 +358,15 @@ public class QQMusicClient implements ChannelClient<MusicSearchReq, MusicSearchR
|
|||||||
if (Objects.isNull(musicQualityEnum)) {
|
if (Objects.isNull(musicQualityEnum)) {
|
||||||
throw new BusinessException("不支持的文件格式");
|
throw new BusinessException("不支持的文件格式");
|
||||||
}
|
}
|
||||||
|
String fileName = res.getName();
|
||||||
|
String[] split = res.getName().split("/");
|
||||||
|
if (split.length > 1) {
|
||||||
|
fileName = split[1];
|
||||||
|
}
|
||||||
AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(res.getFileType());
|
AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(res.getFileType());
|
||||||
try (InputStream musicIn = audioProcessor.processAudioTags(new URL(res.getUrl()).openStream(), res)) {
|
try (InputStream musicIn = audioProcessor.processAudioTags(new URL(res.getUrl()).openStream(), res)) {
|
||||||
if (Objects.nonNull(musicIn)) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import top.baogutang.music.domain.Results;
|
import top.baogutang.music.domain.Results;
|
||||||
import top.baogutang.music.schedule.QQDailyMusicHandler;
|
import top.baogutang.music.schedule.QQDailyMusicHandler;
|
||||||
|
import top.baogutang.music.service.IMusicRecordService;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @description:
|
* @description:
|
||||||
*
|
|
||||||
* @author: N1KO
|
* @author: N1KO
|
||||||
* @date: 2024/12/23 : 15:26
|
* @date: 2024/12/23 : 15:26
|
||||||
*/
|
*/
|
||||||
@ -24,9 +24,19 @@ public class MusicTriggerController {
|
|||||||
@Resource
|
@Resource
|
||||||
private QQDailyMusicHandler qqDailyMusicHandler;
|
private QQDailyMusicHandler qqDailyMusicHandler;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IMusicRecordService musicRecordService;
|
||||||
|
|
||||||
@GetMapping("/daily")
|
@GetMapping("/daily")
|
||||||
public Results<Void> triggerDaily() {
|
public Results<Void> triggerDaily() {
|
||||||
qqDailyMusicHandler.fetchDailyAndSave();
|
qqDailyMusicHandler.fetchDailyAndSave();
|
||||||
return Results.ok();
|
return Results.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/recall")
|
||||||
|
public Results<Void> triggerRecall() {
|
||||||
|
musicRecordService.triggerRecall();
|
||||||
|
return Results.ok();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package top.baogutang.music.dao.mapper;
|
package top.baogutang.music.dao.mapper;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import top.baogutang.music.dao.entity.MusicRecordEntity;
|
import top.baogutang.music.dao.entity.MusicRecordEntity;
|
||||||
|
|
||||||
@ -13,4 +14,7 @@ import top.baogutang.music.dao.entity.MusicRecordEntity;
|
|||||||
*/
|
*/
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface MusicRecordMapper extends BaseMapper<MusicRecordEntity> {
|
public interface MusicRecordMapper extends BaseMapper<MusicRecordEntity> {
|
||||||
|
|
||||||
|
Page<MusicRecordEntity> selectPages(Page<MusicRecordEntity> page);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,4 +28,6 @@ public class NetEaseMusicProperties {
|
|||||||
|
|
||||||
private String downloadPath;
|
private String downloadPath;
|
||||||
|
|
||||||
|
private String cookie;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,12 +22,16 @@ import javax.servlet.http.HttpServletResponse;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
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.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
@ -36,7 +40,6 @@ import static top.baogutang.music.constants.BusinessKey.ADMIN_ID_LIST;
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @description:
|
* @description:
|
||||||
*
|
|
||||||
* @author: N1KO
|
* @author: N1KO
|
||||||
* @date: 2024/12/10 : 17:00
|
* @date: 2024/12/10 : 17:00
|
||||||
*/
|
*/
|
||||||
@ -114,6 +117,46 @@ public abstract class AbstractMusicService<Q extends AbstractMusicReq, S extends
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void processFile(MusicRecordEntity musicRecordEntity) {
|
||||||
|
if (Objects.isNull(musicRecordEntity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ChannelEnum channelEnum = getChannelEnum();
|
||||||
|
ChannelClient<Q, S> 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<Path> 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<String> idList, Long userId) {
|
public String downloads(List<String> idList, Long userId) {
|
||||||
if (CollectionUtils.isEmpty(idList)) {
|
if (CollectionUtils.isEmpty(idList)) {
|
||||||
throw new BusinessException("请选择下载内容");
|
throw new BusinessException("请选择下载内容");
|
||||||
@ -148,6 +191,7 @@ public abstract class AbstractMusicService<Q extends AbstractMusicReq, S extends
|
|||||||
channelClient.saveMusic(res);
|
channelClient.saveMusic(res);
|
||||||
channelClient.saveDownloadRecord(userId, batchNo, id);
|
channelClient.saveDownloadRecord(userId, batchNo, id);
|
||||||
try {
|
try {
|
||||||
|
log.info(">>>>>>>>>>download file:{} start<<<<<<<<<<", res.getName());
|
||||||
channelClient.processFile(res);
|
channelClient.processFile(res);
|
||||||
log.info(">>>>>>>>>>download file:{} success<<<<<<<<<<", res.getName());
|
log.info(">>>>>>>>>>download file:{} success<<<<<<<<<<", res.getName());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@ -23,4 +23,7 @@ public interface IMusicRecordService extends IService<MusicRecordEntity> {
|
|||||||
List<MusicRecordEntity> queryByPlatformIdList(List<String> platformIdList);
|
List<MusicRecordEntity> queryByPlatformIdList(List<String> platformIdList);
|
||||||
|
|
||||||
MusicRecordEntity queryByNameOrAlbumOrArtist(String title, String album, String artist);
|
MusicRecordEntity queryByNameOrAlbumOrArtist(String title, String album, String artist);
|
||||||
|
|
||||||
|
void triggerRecall();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<String> artists;
|
||||||
|
long albumId;
|
||||||
|
String albumName;
|
||||||
|
String picUrl;
|
||||||
|
String maxBrLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== URL request (eapi) ====================
|
||||||
|
|
||||||
|
private UrlResult fetchSongUrl(String id, String level, String rawCookie) throws Exception {
|
||||||
|
Map<String, Object> 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<String, Object> 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<String, Object> headerCfg) {
|
||||||
|
Map<String, String> 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<String, String> 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<String, String> parseCookie(String text) {
|
||||||
|
Map<String, String> 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" 或未知值时不再降级
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -86,13 +86,7 @@ public class MusicInfoServiceImpl implements IMusicInfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取三方token
|
// 获取三方token
|
||||||
String token = CacheUtil.cacheOrSupply(KEY_MUSIC_TAG_TOKEN_PREFIX,
|
String token = geneToken();
|
||||||
1L,
|
|
||||||
TimeUnit.DAYS,
|
|
||||||
redisTemplate,
|
|
||||||
this::geneToken,
|
|
||||||
new TypeReference<>() {
|
|
||||||
});
|
|
||||||
Map<String, String> headers = new HashMap<>();
|
Map<String, String> headers = new HashMap<>();
|
||||||
headers.put("authorization", "jwt " + token);
|
headers.put("authorization", "jwt " + token);
|
||||||
headers.put("Content-Type", "application/json");
|
headers.put("Content-Type", "application/json");
|
||||||
|
|||||||
@ -1,25 +1,31 @@
|
|||||||
package top.baogutang.music.service.impl;
|
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.conditions.query.LambdaQueryChainWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
import top.baogutang.music.dao.entity.MusicRecordEntity;
|
import top.baogutang.music.dao.entity.MusicRecordEntity;
|
||||||
import top.baogutang.music.dao.mapper.MusicRecordMapper;
|
import top.baogutang.music.dao.mapper.MusicRecordMapper;
|
||||||
import top.baogutang.music.domain.res.download.MusicDownloadRes;
|
import top.baogutang.music.domain.res.download.MusicDownloadRes;
|
||||||
import top.baogutang.music.enums.ChannelEnum;
|
import top.baogutang.music.enums.ChannelEnum;
|
||||||
import top.baogutang.music.enums.MusicQualityEnum;
|
import top.baogutang.music.enums.MusicQualityEnum;
|
||||||
import top.baogutang.music.service.IMusicRecordService;
|
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.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @description:
|
* @description:
|
||||||
*
|
|
||||||
* @author: N1KO
|
* @author: N1KO
|
||||||
* @date: 2024/12/11 : 15:38
|
* @date: 2024/12/11 : 15:38
|
||||||
*/
|
*/
|
||||||
@ -27,6 +33,12 @@ import java.util.Objects;
|
|||||||
@Service
|
@Service
|
||||||
public class MusicRecordServiceImpl extends ServiceImpl<MusicRecordMapper, MusicRecordEntity> implements IMusicRecordService {
|
public class MusicRecordServiceImpl extends ServiceImpl<MusicRecordMapper, MusicRecordEntity> implements IMusicRecordService {
|
||||||
|
|
||||||
|
@Resource(name = "commonExecutor")
|
||||||
|
private Executor commonExecutor;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IMusicService musicService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save(MusicDownloadRes res, ChannelEnum channel) {
|
public void save(MusicDownloadRes res, ChannelEnum channel) {
|
||||||
MusicRecordEntity entity = new LambdaQueryChainWrapper<>(baseMapper)
|
MusicRecordEntity entity = new LambdaQueryChainWrapper<>(baseMapper)
|
||||||
@ -88,4 +100,33 @@ public class MusicRecordServiceImpl extends ServiceImpl<MusicRecordMapper, Music
|
|||||||
.one();
|
.one();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void triggerRecall() {
|
||||||
|
commonExecutor.execute(() -> {
|
||||||
|
int pageNo = 1;
|
||||||
|
int pageSize = 50;
|
||||||
|
while (true) {
|
||||||
|
IPage<MusicRecordEntity> 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");
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import top.baogutang.music.domain.req.AbstractMusicReq;
|
import top.baogutang.music.domain.req.AbstractMusicReq;
|
||||||
import top.baogutang.music.domain.res.AbstractMusicRes;
|
import top.baogutang.music.domain.res.AbstractMusicRes;
|
||||||
import top.baogutang.music.enums.ChannelEnum;
|
|
||||||
import top.baogutang.music.exceptions.BusinessException;
|
import top.baogutang.music.exceptions.BusinessException;
|
||||||
import top.baogutang.music.service.AbstractMusicService;
|
import top.baogutang.music.service.AbstractMusicService;
|
||||||
import top.baogutang.music.service.IMusicService;
|
import top.baogutang.music.service.IMusicService;
|
||||||
|
|||||||
33
src/main/java/top/baogutang/music/utils/PageRequest.java
Normal file
33
src/main/java/top/baogutang/music/utils/PageRequest.java
Normal file
@ -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<OrderBy> orders = new ArrayList<>();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class OrderBy implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -2936335557980068706L;
|
||||||
|
|
||||||
|
private String columnName;
|
||||||
|
|
||||||
|
private boolean desc;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/main/java/top/baogutang/music/utils/PageUtil.java
Normal file
108
src/main/java/top/baogutang/music/utils/PageUtil.java
Normal file
@ -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 <T> entity class
|
||||||
|
* @param <U> query parameter
|
||||||
|
* @param <R> return bean
|
||||||
|
* @return Page
|
||||||
|
*/
|
||||||
|
public static <T, U extends PageRequest, R> IPage<R> page(UnaryOperator<Page<T>> queryFunction, U pageParam, Function<T, R> beanCastFunction) {
|
||||||
|
|
||||||
|
return doPage(queryFunction, new Page<>(pageParam.getPageNum(), pageParam.getPageSize()), beanCastFunction);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T, R> IPage<R> page(UnaryOperator<Page<T>> queryFunction, Integer pageNum, Integer pageSize, Function<T, R> beanCastFunction) {
|
||||||
|
|
||||||
|
return doPage(queryFunction, new Page<>(pageNum, pageSize), beanCastFunction);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T, R> IPage<R> doPage(UnaryOperator<Page<T>> queryFunction, Page<T> pageParam, Function<T, R> beanCastFunction) {
|
||||||
|
|
||||||
|
Page<T> pageRes = queryFunction.apply(pageParam);
|
||||||
|
List<R> rPageRes = pageRes.getRecords().stream()
|
||||||
|
.map(beanCastFunction)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Page<R> 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> T
|
||||||
|
*/
|
||||||
|
public static <T, R> void defaultOrder(List<PageRequest.OrderBy> orders, QueryWrapper<T> queryWrapper, SFunction<T, R> 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> t
|
||||||
|
* @param <R> r
|
||||||
|
*/
|
||||||
|
private static <T, R> void defaultOrderProcess(QueryWrapper<T> queryWrapper, SFunction<T, R> defaultColumn) {
|
||||||
|
|
||||||
|
if (defaultColumn != null) {
|
||||||
|
queryWrapper.lambda()
|
||||||
|
.orderByDesc(defaultColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
11
src/main/resources/mapper/MusicRecordMapper.xml
Normal file
11
src/main/resources/mapper/MusicRecordMapper.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||||
|
<mapper namespace="top.baogutang.music.dao.mapper.MusicRecordMapper">
|
||||||
|
|
||||||
|
<select id="selectPages" resultType="top.baogutang.music.dao.entity.MusicRecordEntity">
|
||||||
|
select *
|
||||||
|
from t_music_record
|
||||||
|
where deleted = false
|
||||||
|
order by id desc
|
||||||
|
</select>
|
||||||
|
</mapper>
|
||||||
Loading…
Reference in New Issue
Block a user