download
This commit is contained in:
parent
7ed681b078
commit
d55ab34c46
@ -5,6 +5,7 @@ import org.mybatis.spring.annotation.MapperScan;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.scheduling.annotation.EnableAsync;
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -17,6 +18,7 @@ import org.springframework.scheduling.annotation.EnableAsync;
|
|||||||
@SpringBootApplication(scanBasePackages = {"top.baogutang.music.*"})
|
@SpringBootApplication(scanBasePackages = {"top.baogutang.music.*"})
|
||||||
@MapperScan(basePackages = {"top.baogutang.music.dao.mapper"})
|
@MapperScan(basePackages = {"top.baogutang.music.dao.mapper"})
|
||||||
@EnableAsync
|
@EnableAsync
|
||||||
|
@EnableScheduling
|
||||||
public class BaogutangMusicApplication {
|
public class BaogutangMusicApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
19
src/main/java/top/baogutang/music/annos/Vip.java
Normal file
19
src/main/java/top/baogutang/music/annos/Vip.java
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package top.baogutang.music.annos;
|
||||||
|
|
||||||
|
import top.baogutang.music.enums.UserLevel;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @description:
|
||||||
|
*
|
||||||
|
* @author: N1KO
|
||||||
|
* @date: 2024/12/13 : 18:34
|
||||||
|
*/
|
||||||
|
@Documented
|
||||||
|
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface Vip {
|
||||||
|
UserLevel latestLevel() default UserLevel.NORMAL;
|
||||||
|
}
|
||||||
@ -22,7 +22,7 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import static top.baogutang.music.constants.CacheKey.KEY_LOGIN_PREFIX;
|
import static top.baogutang.music.constants.CacheKey.KEY_USER_LOGIN_PREFIX;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -106,7 +106,7 @@ public class LoginAspect {
|
|||||||
log.info("request url:{},method:{}", request.getRequestURL(), request.getMethod());
|
log.info("request url:{},method:{}", request.getRequestURL(), request.getMethod());
|
||||||
UserLoginRes userLoginRes = TokenUtil.verify(token);
|
UserLoginRes userLoginRes = TokenUtil.verify(token);
|
||||||
Long userId = userLoginRes.getId();
|
Long userId = userLoginRes.getId();
|
||||||
Object val = redisTemplate.opsForValue().get(KEY_LOGIN_PREFIX + userId);
|
Object val = redisTemplate.opsForValue().get(KEY_USER_LOGIN_PREFIX + userId);
|
||||||
if (Objects.isNull(val)) {
|
if (Objects.isNull(val)) {
|
||||||
throw new LoginException("登录已失效");
|
throw new LoginException("登录已失效");
|
||||||
}
|
}
|
||||||
|
|||||||
105
src/main/java/top/baogutang/music/aspect/VipAspect.java
Normal file
105
src/main/java/top/baogutang/music/aspect/VipAspect.java
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package top.baogutang.music.aspect;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.*;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
import top.baogutang.music.annos.Vip;
|
||||||
|
import top.baogutang.music.domain.res.user.UserLoginRes;
|
||||||
|
import top.baogutang.music.enums.UserLevel;
|
||||||
|
import top.baogutang.music.exceptions.LoginException;
|
||||||
|
import top.baogutang.music.exceptions.VipException;
|
||||||
|
import top.baogutang.music.service.IUserService;
|
||||||
|
import top.baogutang.music.utils.CacheUtil;
|
||||||
|
import top.baogutang.music.utils.TokenUtil;
|
||||||
|
import top.baogutang.music.utils.UserLevelThreadLocal;
|
||||||
|
import top.baogutang.music.utils.UserThreadLocal;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import javax.servlet.http.Cookie;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static top.baogutang.music.aspect.LoginAspect.AUTHORIZATION;
|
||||||
|
import static top.baogutang.music.constants.CacheKey.KEY_USER_LEVEL_PREFIX;
|
||||||
|
import static top.baogutang.music.constants.CacheKey.KEY_USER_LOGIN_PREFIX;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @description:
|
||||||
|
*
|
||||||
|
* @author: N1KO
|
||||||
|
* @date: 2024/12/13 : 18:36
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class VipAspect {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IUserService userService;
|
||||||
|
|
||||||
|
@Pointcut("@annotation(top.baogutang.music.annos.Vip)")
|
||||||
|
public void vip() {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Around("vip()")
|
||||||
|
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||||
|
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
|
||||||
|
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||||
|
Method method = signature.getMethod();
|
||||||
|
Vip vip = method.getAnnotation(Vip.class);
|
||||||
|
if (vip != null) {
|
||||||
|
Cookie[] cookies = request.getCookies();
|
||||||
|
if (Objects.isNull(cookies) || cookies.length == 0) {
|
||||||
|
throw new LoginException("请登录后操作");
|
||||||
|
}
|
||||||
|
Cookie tokenCookie = Arrays.stream(cookies)
|
||||||
|
.filter(cookie -> AUTHORIZATION.equalsIgnoreCase(cookie.getName()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (Objects.isNull(tokenCookie) || StringUtils.isBlank(tokenCookie.getValue())) {
|
||||||
|
throw new LoginException("请登录后操作");
|
||||||
|
}
|
||||||
|
String token = tokenCookie.getValue();
|
||||||
|
UserLoginRes userLoginRes = TokenUtil.verify(token);
|
||||||
|
Long userId = userLoginRes.getId();
|
||||||
|
Object val = redisTemplate.opsForValue().get(KEY_USER_LOGIN_PREFIX + userId);
|
||||||
|
if (Objects.isNull(val)) {
|
||||||
|
throw new LoginException("登录已失效");
|
||||||
|
}
|
||||||
|
if (!token.equals(val.toString())) {
|
||||||
|
throw new LoginException("登录已失效");
|
||||||
|
}
|
||||||
|
UserThreadLocal.set(userId);
|
||||||
|
String userLevelCacheKey = KEY_USER_LEVEL_PREFIX + userId;
|
||||||
|
UserLevel userLevel = CacheUtil.cacheOrSupply(userLevelCacheKey,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
redisTemplate,
|
||||||
|
() -> userService.queryUserLevel(userId));
|
||||||
|
UserLevelThreadLocal.set(userLevel);
|
||||||
|
if (Objects.equals(UserLevel.NORMAL, userLevel)) {
|
||||||
|
throw new VipException("等级不够,请升级");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return point.proceed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After("vip()")
|
||||||
|
public void after() {
|
||||||
|
UserLevelThreadLocal.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package top.baogutang.music.client;
|
package top.baogutang.music.client;
|
||||||
|
|
||||||
|
import top.baogutang.music.dao.entity.MusicRecordEntity;
|
||||||
import top.baogutang.music.domain.req.AbstractMusicReq;
|
import top.baogutang.music.domain.req.AbstractMusicReq;
|
||||||
import top.baogutang.music.domain.req.search.MusicSearchReq;
|
import top.baogutang.music.domain.req.search.MusicSearchReq;
|
||||||
import top.baogutang.music.domain.res.AbstractMusicRes;
|
import top.baogutang.music.domain.res.AbstractMusicRes;
|
||||||
@ -7,6 +8,7 @@ import top.baogutang.music.domain.res.download.MusicDownloadRes;
|
|||||||
import top.baogutang.music.domain.res.search.*;
|
import top.baogutang.music.domain.res.search.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -32,4 +34,9 @@ public interface ChannelClient<Q extends AbstractMusicReq, S extends AbstractMus
|
|||||||
void saveMusic(MusicDownloadRes res);
|
void saveMusic(MusicDownloadRes res);
|
||||||
|
|
||||||
void processFile(MusicDownloadRes res) throws IOException;
|
void processFile(MusicDownloadRes res) throws IOException;
|
||||||
|
|
||||||
|
MusicRecordEntity queryByPlatform(String id);
|
||||||
|
|
||||||
|
String getDownloadBasePath();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,11 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import top.baogutang.music.annos.ChannelInfo;
|
import top.baogutang.music.annos.ChannelInfo;
|
||||||
|
import top.baogutang.music.dao.entity.MusicRecordEntity;
|
||||||
import top.baogutang.music.domain.req.search.MusicSearchReq;
|
import top.baogutang.music.domain.req.search.MusicSearchReq;
|
||||||
import top.baogutang.music.domain.res.download.MusicDownloadRes;
|
import top.baogutang.music.domain.res.download.MusicDownloadRes;
|
||||||
import top.baogutang.music.domain.res.search.*;
|
import top.baogutang.music.domain.res.search.*;
|
||||||
|
import top.baogutang.music.enums.AudioFileTypeEnum;
|
||||||
import top.baogutang.music.enums.ChannelEnum;
|
import top.baogutang.music.enums.ChannelEnum;
|
||||||
import top.baogutang.music.processor.AbstractAudioProcessor;
|
import top.baogutang.music.processor.AbstractAudioProcessor;
|
||||||
import top.baogutang.music.properties.NetEaseMusicProperties;
|
import top.baogutang.music.properties.NetEaseMusicProperties;
|
||||||
@ -84,7 +86,11 @@ public class NetEaseMusicClient implements ChannelClient<MusicSearchReq, MusicSe
|
|||||||
});
|
});
|
||||||
if (Objects.nonNull(res)) {
|
if (Objects.nonNull(res)) {
|
||||||
res.setId(id);
|
res.setId(id);
|
||||||
|
String[] split = res.getUrl().split("\\.");
|
||||||
|
String fileType = split[split.length - 1];
|
||||||
|
res.setFileType(AudioFileTypeEnum.parse(fileType));
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,14 +108,22 @@ public class NetEaseMusicClient implements ChannelClient<MusicSearchReq, MusicSe
|
|||||||
if (!Files.exists(baseDir)) {
|
if (!Files.exists(baseDir)) {
|
||||||
Files.createDirectories(baseDir);
|
Files.createDirectories(baseDir);
|
||||||
}
|
}
|
||||||
String[] split = res.getUrl().split("\\.");
|
AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(res.getFileType());
|
||||||
String fileType = split[split.length - 1];
|
|
||||||
AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(fileType);
|
|
||||||
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() + "." + fileType), StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(musicIn, baseDir.resolve(res.getName() + "." + res.getFileType().name().toLowerCase()), StandardCopyOption.REPLACE_EXISTING);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MusicRecordEntity queryByPlatform(String id) {
|
||||||
|
return musicRecordService.queryByChannelAndPlatform(ChannelEnum.NET_EASE_MUSIC, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDownloadBasePath() {
|
||||||
|
return netEaseMusicProperties.getDownloadPath();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import top.baogutang.music.annos.ChannelInfo;
|
import top.baogutang.music.annos.ChannelInfo;
|
||||||
|
import top.baogutang.music.dao.entity.MusicRecordEntity;
|
||||||
import top.baogutang.music.domain.req.search.MusicSearchReq;
|
import top.baogutang.music.domain.req.search.MusicSearchReq;
|
||||||
import top.baogutang.music.domain.res.download.MusicDownloadRes;
|
import top.baogutang.music.domain.res.download.MusicDownloadRes;
|
||||||
import top.baogutang.music.domain.res.download.QQMusicDownloadRes;
|
import top.baogutang.music.domain.res.download.QQMusicDownloadRes;
|
||||||
@ -302,6 +303,10 @@ public class QQMusicClient implements ChannelClient<MusicSearchReq, MusicSearchR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
musicDownloadRes.setId(qqMusicDownloadRes.getSong().getMid());
|
musicDownloadRes.setId(qqMusicDownloadRes.getSong().getMid());
|
||||||
|
MusicQualityEnum musicQualityEnum = MusicQualityEnum.parse(musicDownloadRes.getLevel());
|
||||||
|
if (Objects.nonNull(musicQualityEnum)) {
|
||||||
|
musicDownloadRes.setFileType(musicQualityEnum.getType());
|
||||||
|
}
|
||||||
return musicDownloadRes;
|
return musicDownloadRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,13 +329,22 @@ public class QQMusicClient implements ChannelClient<MusicSearchReq, MusicSearchR
|
|||||||
if (Objects.isNull(musicQualityEnum)) {
|
if (Objects.isNull(musicQualityEnum)) {
|
||||||
throw new BusinessException("不支持的文件格式");
|
throw new BusinessException("不支持的文件格式");
|
||||||
}
|
}
|
||||||
String fileType = musicQualityEnum.getType();
|
AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(res.getFileType());
|
||||||
AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(fileType);
|
|
||||||
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() + "." + fileType), StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(musicIn, baseDir.resolve(res.getName() + "." + res.getFileType().name().toLowerCase()), StandardCopyOption.REPLACE_EXISTING);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MusicRecordEntity queryByPlatform(String id) {
|
||||||
|
return musicRecordService.queryByChannelAndPlatform(ChannelEnum.QQ_MUSIC, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDownloadBasePath() {
|
||||||
|
return qqMusicProperties.getDownloadPath();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.TaskScheduler;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
|
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
|
||||||
@ -46,4 +48,15 @@ public class ExecutorConfig {
|
|||||||
return executor;
|
return executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean("taskExecutor")
|
||||||
|
public TaskScheduler taskScheduler() {
|
||||||
|
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
|
||||||
|
taskScheduler.setPoolSize(maxPoolSize);
|
||||||
|
taskScheduler.setThreadNamePrefix(appName + "-schedule-");
|
||||||
|
taskScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||||
|
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||||
|
taskScheduler.setPoolSize(10);
|
||||||
|
return taskScheduler;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/main/java/top/baogutang/music/config/ScheduleConfig.java
Normal file
27
src/main/java/top/baogutang/music/config/ScheduleConfig.java
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package top.baogutang.music.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.TaskScheduler;
|
||||||
|
import org.springframework.scheduling.annotation.SchedulingConfigurer;
|
||||||
|
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: 定时调度任务线程配置
|
||||||
|
* @author: nikooh
|
||||||
|
* @date: 2024/01/09 : 17:56
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class ScheduleConfig implements SchedulingConfigurer {
|
||||||
|
|
||||||
|
@Resource(name = "taskExecutor")
|
||||||
|
private TaskScheduler taskScheduler;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
|
||||||
|
|
||||||
|
scheduledTaskRegistrar.setScheduler(taskScheduler);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,7 +15,9 @@ public class CacheKey {
|
|||||||
|
|
||||||
public static final String LOCK_REGISTER_PREFIX = "baogutang-music:lock:register:username:";
|
public static final String LOCK_REGISTER_PREFIX = "baogutang-music:lock:register:username:";
|
||||||
|
|
||||||
public static final String KEY_LOGIN_PREFIX = "baogutang-music:user:token:id:";
|
public static final String KEY_USER_LOGIN_PREFIX = "baogutang-music:user:token:id:";
|
||||||
|
|
||||||
|
public static final String KEY_USER_LEVEL_PREFIX = "baogutang-music:user:level:id:";
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,21 @@
|
|||||||
package top.baogutang.music.controller;
|
package top.baogutang.music.controller;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.http.*;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import top.baogutang.music.annos.Vip;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import top.baogutang.music.annos.Login;
|
|
||||||
import top.baogutang.music.domain.Results;
|
import top.baogutang.music.domain.Results;
|
||||||
import top.baogutang.music.domain.res.download.MusicDownloadRes;
|
import top.baogutang.music.domain.ZipMetadata;
|
||||||
import top.baogutang.music.domain.res.search.MusicPlaylistRes;
|
|
||||||
import top.baogutang.music.service.IMusicService;
|
import top.baogutang.music.service.IMusicService;
|
||||||
|
import top.baogutang.music.service.ZipService;
|
||||||
|
import top.baogutang.music.utils.UserThreadLocal;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -28,13 +32,59 @@ public class MusicDownloadController {
|
|||||||
@Resource
|
@Resource
|
||||||
private IMusicService musicService;
|
private IMusicService musicService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ZipService zipService;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@Login
|
@Vip
|
||||||
public Results<MusicDownloadRes> download(@RequestParam(name = "channel") Integer channel,
|
public Results<String> download(@RequestParam(name = "channel") Integer channel,
|
||||||
@RequestParam(name = "id") String id) {
|
@RequestParam(name = "id") List<String> idList) {
|
||||||
MusicDownloadRes res = musicService.getMusicService(channel).download(id);
|
String res = musicService.getMusicService(channel).download(UserThreadLocal.get(), idList);
|
||||||
return Results.ok(res);
|
return Results.ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提供一个接口,让前端可以通过URL下载ZIP文件。
|
||||||
|
*
|
||||||
|
* @param zipFileName ZIP文件名
|
||||||
|
* @return ZIP文件流
|
||||||
|
*/
|
||||||
|
@GetMapping("/downloads/{zipFileName}")
|
||||||
|
public ResponseEntity<?> downloadZip(@PathVariable String zipFileName) {
|
||||||
|
ZipMetadata metadata = zipService.getZipMetadata(zipFileName);
|
||||||
|
if (metadata == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("ZIP文件不存在或已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (LocalDateTime.now().isAfter(metadata.getExpirationTime())) {
|
||||||
|
// 删除过期的ZIP文件
|
||||||
|
File expiredZip = new File(metadata.getZipFilePath());
|
||||||
|
if (expiredZip.exists()) {
|
||||||
|
expiredZip.delete();
|
||||||
|
}
|
||||||
|
// 从映射中移除
|
||||||
|
zipService.removeZip(zipFileName);
|
||||||
|
return ResponseEntity.status(HttpStatus.GONE).body("下载链接已过期");
|
||||||
|
}
|
||||||
|
|
||||||
|
File zipFile = new File(metadata.getZipFilePath());
|
||||||
|
if (!zipFile.exists()) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("ZIP文件不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
headers.setContentDisposition(ContentDisposition.builder("attachment").filename(zipFileName).build());
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] fileContent = Files.readAllBytes(zipFile.toPath());
|
||||||
|
return new ResponseEntity<>(fileContent, headers, HttpStatus.OK);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error(">>>>>>>>>>读取zip文件失败:{}<<<<<<<<<<", e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("读取ZIP文件失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
package top.baogutang.music.controller;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @description:
|
||||||
|
*
|
||||||
|
* @author: N1KO
|
||||||
|
* @date: 2024/12/15 : 11:52
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/music/order")
|
||||||
|
public class OrderController {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package top.baogutang.music.dao.entity;
|
|||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import top.baogutang.music.enums.AudioFileTypeEnum;
|
||||||
import top.baogutang.music.enums.ChannelEnum;
|
import top.baogutang.music.enums.ChannelEnum;
|
||||||
import top.baogutang.music.enums.MusicQualityEnum;
|
import top.baogutang.music.enums.MusicQualityEnum;
|
||||||
|
|
||||||
@ -26,6 +27,8 @@ public class MusicRecordEntity extends BaseEntity {
|
|||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
private AudioFileTypeEnum fileType;
|
||||||
|
|
||||||
private String albumName;
|
private String albumName;
|
||||||
|
|
||||||
private String artistName;
|
private String artistName;
|
||||||
|
|||||||
23
src/main/java/top/baogutang/music/domain/ZipMetadata.java
Normal file
23
src/main/java/top/baogutang/music/domain/ZipMetadata.java
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package top.baogutang.music.domain;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class ZipMetadata {
|
||||||
|
|
||||||
|
private final String zipFilePath;
|
||||||
|
|
||||||
|
private final LocalDateTime expirationTime;
|
||||||
|
|
||||||
|
public ZipMetadata(String zipFilePath, LocalDateTime expirationTime) {
|
||||||
|
this.zipFilePath = zipFilePath;
|
||||||
|
this.expirationTime = expirationTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getZipFilePath() {
|
||||||
|
return zipFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getExpirationTime() {
|
||||||
|
return expirationTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import top.baogutang.music.domain.res.AbstractMusicRes;
|
import top.baogutang.music.domain.res.AbstractMusicRes;
|
||||||
|
import top.baogutang.music.enums.AudioFileTypeEnum;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
@ -40,4 +41,5 @@ public class MusicDownloadRes extends AbstractMusicRes implements Serializable {
|
|||||||
|
|
||||||
private String lyric;
|
private String lyric;
|
||||||
|
|
||||||
|
private AudioFileTypeEnum fileType;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,20 +35,20 @@ public enum MusicQualityEnum {
|
|||||||
SKY("沉浸环绕声", null),
|
SKY("沉浸环绕声", null),
|
||||||
DOLBY("杜比全景声", null),
|
DOLBY("杜比全景声", null),
|
||||||
JYMASTER("超清母带", null),
|
JYMASTER("超清母带", null),
|
||||||
KBPS_128("128kbps", "mp3"),
|
KBPS_128("128kbps", AudioFileTypeEnum.MP3),
|
||||||
KBPS_320("320kbps", "mp3"),
|
KBPS_320("320kbps", AudioFileTypeEnum.MP3),
|
||||||
ATMOS_5_1("Atmos 5.1", "flac"),
|
ATMOS_5_1("Atmos 5.1", AudioFileTypeEnum.FLAC),
|
||||||
ATMOS_2("Atmos 2", "flac"),
|
ATMOS_2("Atmos 2", AudioFileTypeEnum.FLAC),
|
||||||
MASTER("Master", "flac"),
|
MASTER("Master", AudioFileTypeEnum.FLAC),
|
||||||
FLAC("flac", "flac"),
|
FLAC("flac", AudioFileTypeEnum.FLAC),
|
||||||
|
|
||||||
;
|
;
|
||||||
|
|
||||||
private final String desc;
|
private final String desc;
|
||||||
|
|
||||||
private final String type;
|
private final AudioFileTypeEnum type;
|
||||||
|
|
||||||
MusicQualityEnum(String desc, String type) {
|
MusicQualityEnum(String desc, AudioFileTypeEnum type) {
|
||||||
this.desc = desc;
|
this.desc = desc;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
package top.baogutang.music.exceptions;
|
||||||
|
|
||||||
|
import top.baogutang.music.domain.Results;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @description:
|
||||||
|
*
|
||||||
|
* @author: N1KO
|
||||||
|
* @date: 2024/12/15 : 11:36
|
||||||
|
*/
|
||||||
|
public class VipException extends RuntimeException {
|
||||||
|
|
||||||
|
|
||||||
|
private int code = Results.FAIL_CODE;
|
||||||
|
|
||||||
|
public VipException(int code, String message) {
|
||||||
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public VipException(String message) {
|
||||||
|
super(message);
|
||||||
|
this.code = -300;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCode(int code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,9 +23,8 @@ import java.net.URL;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class AbstractAudioProcessor {
|
public abstract class AbstractAudioProcessor {
|
||||||
|
|
||||||
public static AbstractAudioProcessor getAudioProcessor(String fileType) {
|
public static AbstractAudioProcessor getAudioProcessor(AudioFileTypeEnum fileType) {
|
||||||
AudioFileTypeEnum fileTypeEnum = AudioFileTypeEnum.parse(fileType);
|
switch (fileType) {
|
||||||
switch (fileTypeEnum) {
|
|
||||||
case FLAC:
|
case FLAC:
|
||||||
return new FlacAudioProcessor();
|
return new FlacAudioProcessor();
|
||||||
case MP3:
|
case MP3:
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
package top.baogutang.music.schedules;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import top.baogutang.music.domain.ZipMetadata;
|
||||||
|
import top.baogutang.music.service.ZipService;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.io.File;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class ZipCleanupScheduler {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ZipService zipService;
|
||||||
|
|
||||||
|
// 每小时执行一次
|
||||||
|
@Scheduled(cron = "0 0 * * * ?")
|
||||||
|
public void cleanupOldZips() {
|
||||||
|
Map<String, ZipMetadata> zipStore = zipService.getZipStore();
|
||||||
|
|
||||||
|
Iterator<Map.Entry<String, ZipMetadata>> iterator = zipStore.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<String, ZipMetadata> entry = iterator.next();
|
||||||
|
ZipMetadata metadata = entry.getValue();
|
||||||
|
|
||||||
|
if (LocalDateTime.now().isAfter(metadata.getExpirationTime())) {
|
||||||
|
// 删除ZIP文件
|
||||||
|
File zipFile = new File(metadata.getZipFilePath());
|
||||||
|
if (zipFile.exists()) {
|
||||||
|
boolean deleted = zipFile.delete();
|
||||||
|
if (deleted) {
|
||||||
|
log.info(">>>>>>>>>>Deleted expired ZIP{}<<<<<<<<<<", metadata.getZipFilePath());
|
||||||
|
} else {
|
||||||
|
log.error(">>>>>>>>>>Failed to delete ZIP:{}<<<<<<<<<<", metadata.getZipFilePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从映射中移除
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,12 @@ package top.baogutang.music.service;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
import top.baogutang.music.client.ChannelClient;
|
import top.baogutang.music.client.ChannelClient;
|
||||||
|
import top.baogutang.music.dao.entity.MusicRecordEntity;
|
||||||
import top.baogutang.music.domain.req.AbstractMusicReq;
|
import top.baogutang.music.domain.req.AbstractMusicReq;
|
||||||
import top.baogutang.music.domain.req.search.MusicSearchReq;
|
import top.baogutang.music.domain.req.search.MusicSearchReq;
|
||||||
import top.baogutang.music.domain.res.AbstractMusicRes;
|
import top.baogutang.music.domain.res.AbstractMusicRes;
|
||||||
@ -13,10 +18,14 @@ import top.baogutang.music.exceptions.BusinessException;
|
|||||||
import top.baogutang.music.factory.ChannelClientFactory;
|
import top.baogutang.music.factory.ChannelClientFactory;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -34,6 +43,9 @@ public abstract class AbstractMusicService<Q extends AbstractMusicReq, S extends
|
|||||||
@Resource(name = "commonExecutor")
|
@Resource(name = "commonExecutor")
|
||||||
private Executor commonExecutor;
|
private Executor commonExecutor;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ZipService zipService;
|
||||||
|
|
||||||
public abstract ChannelEnum getChannelEnum();
|
public abstract ChannelEnum getChannelEnum();
|
||||||
|
|
||||||
public MusicSearchRes search(MusicSearchReq req) {
|
public MusicSearchRes search(MusicSearchReq req) {
|
||||||
@ -66,9 +78,29 @@ public abstract class AbstractMusicService<Q extends AbstractMusicReq, S extends
|
|||||||
return channelClient.artist(id);
|
return channelClient.artist(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MusicDownloadRes download(String id) {
|
public String download(Long userId, List<String> idList) {
|
||||||
|
if (CollectionUtils.isEmpty(idList)) {
|
||||||
|
throw new BusinessException("请选择文件进行下载");
|
||||||
|
}
|
||||||
ChannelEnum channelEnum = getChannelEnum();
|
ChannelEnum channelEnum = getChannelEnum();
|
||||||
ChannelClient<Q, S> channelClient = channelClientFactory.getClient(channelEnum);
|
ChannelClient<Q, S> channelClient = channelClientFactory.getClient(channelEnum);
|
||||||
|
Map<String, String> filesWithPaths = idList.stream()
|
||||||
|
.map(id ->
|
||||||
|
CompletableFuture.supplyAsync(() -> {
|
||||||
|
MusicRecordEntity musicRecord = channelClient.queryByPlatform(id);
|
||||||
|
String filePath;
|
||||||
|
String zipEntryPath;
|
||||||
|
if (Objects.nonNull(musicRecord)) {
|
||||||
|
// 当前歌曲已经下载过,读取本地文件
|
||||||
|
zipEntryPath = musicRecord.getArtistName() + "/" + musicRecord.getAlbumName() + "/" + musicRecord.getName() + "." + musicRecord.getFileType().name().toLowerCase();
|
||||||
|
filePath = channelClient.getDownloadBasePath() + File.separator + musicRecord.getArtistName()
|
||||||
|
+ File.separator + musicRecord.getAlbumName()
|
||||||
|
+ File.separator + musicRecord.getName() +
|
||||||
|
"." + musicRecord.getFileType().name().toLowerCase();
|
||||||
|
return Pair.of(filePath, zipEntryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不存在,先下载
|
||||||
MusicDownloadRes res = channelClient.download(id);
|
MusicDownloadRes res = channelClient.download(id);
|
||||||
if (Objects.isNull(res)) {
|
if (Objects.isNull(res)) {
|
||||||
log.error(">>>>>>>>>>query detail error! channel:{},song id:{}<<<<<<<<<<", channelEnum.getDesc(), id);
|
log.error(">>>>>>>>>>query detail error! channel:{},song id:{}<<<<<<<<<<", channelEnum.getDesc(), id);
|
||||||
@ -81,21 +113,30 @@ public abstract class AbstractMusicService<Q extends AbstractMusicReq, S extends
|
|||||||
}
|
}
|
||||||
|
|
||||||
channelClient.saveMusic(res);
|
channelClient.saveMusic(res);
|
||||||
CompletableFuture.runAsync(() -> {
|
|
||||||
try {
|
try {
|
||||||
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) {
|
||||||
log.error(">>>>>>>>>>download error:{}<<<<<<<<<<", e.getMessage(), e);
|
log.error(">>>>>>>>>>download error:{}<<<<<<<<<<", e.getMessage(), e);
|
||||||
throw new BusinessException("下载异常");
|
return null;
|
||||||
}
|
}
|
||||||
}, commonExecutor);
|
filePath = channelClient.getDownloadBasePath() + File.separator + res.getArtistName()
|
||||||
// try {
|
+ File.separator + res.getAlbumName()
|
||||||
// channelClient.processFile(res);
|
+ File.separator + res.getName() +
|
||||||
// } catch (IOException e) {
|
"." + res.getFileType().name().toLowerCase();
|
||||||
// log.error(">>>>>>>>>>download error:{}<<<<<<<<<<", e.getMessage(), e);
|
zipEntryPath = res.getArtistName() + "/" + res.getAlbumName() + "/" + res.getName() + "." + res.getFileType().name().toLowerCase();
|
||||||
// throw new BusinessException("下载异常");
|
return Pair.of(filePath, zipEntryPath);
|
||||||
// }
|
}, commonExecutor))
|
||||||
return res;
|
.map(CompletableFuture::join)
|
||||||
|
.filter(pair -> Objects.nonNull(pair.getKey()))
|
||||||
|
.collect(Collectors.toMap(Pair::getKey, Pair::getValue));
|
||||||
|
|
||||||
|
try {
|
||||||
|
return zipService.createZip(filesWithPaths,userId);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error(">>>>>>>>>>gene zip error:{}<<<<<<<<<<", e.getMessage(), e);
|
||||||
|
throw new BusinessException("请稍后重试");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,4 +15,6 @@ import top.baogutang.music.enums.ChannelEnum;
|
|||||||
public interface IMusicRecordService extends IService<MusicRecordEntity> {
|
public interface IMusicRecordService extends IService<MusicRecordEntity> {
|
||||||
|
|
||||||
void save(MusicDownloadRes res, ChannelEnum channel);
|
void save(MusicDownloadRes res, ChannelEnum channel);
|
||||||
|
|
||||||
|
MusicRecordEntity queryByChannelAndPlatform(ChannelEnum channel, String platformId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package top.baogutang.music.service;
|
package top.baogutang.music.service;
|
||||||
|
|
||||||
import top.baogutang.music.enums.ChannelEnum;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
import top.baogutang.music.dao.entity.UserEntity;
|
import top.baogutang.music.dao.entity.UserEntity;
|
||||||
import top.baogutang.music.domain.req.user.UserRegisterAndLoginReq;
|
import top.baogutang.music.domain.req.user.UserRegisterAndLoginReq;
|
||||||
import top.baogutang.music.domain.res.user.UserLoginRes;
|
import top.baogutang.music.domain.res.user.UserLoginRes;
|
||||||
|
import top.baogutang.music.enums.UserLevel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -19,4 +20,6 @@ public interface IUserService extends IService<UserEntity> {
|
|||||||
UserLoginRes login(UserRegisterAndLoginReq req);
|
UserLoginRes login(UserRegisterAndLoginReq req);
|
||||||
|
|
||||||
void logout(Long id);
|
void logout(Long id);
|
||||||
|
|
||||||
|
UserLevel queryUserLevel(Long userId);
|
||||||
}
|
}
|
||||||
|
|||||||
72
src/main/java/top/baogutang/music/service/ZipService.java
Normal file
72
src/main/java/top/baogutang/music/service/ZipService.java
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package top.baogutang.music.service;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import top.baogutang.music.domain.ZipMetadata;
|
||||||
|
import top.baogutang.music.utils.ZipUtil;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class ZipService {
|
||||||
|
|
||||||
|
|
||||||
|
@Value("${baogutang.music.zip.base-path}")
|
||||||
|
private String zipPath;
|
||||||
|
|
||||||
|
// 存储ZIP文件的元数据,键为ZIP文件名
|
||||||
|
private static final ConcurrentMap<String, ZipMetadata> zipStore = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 定义ZIP文件的有效时长(例如,1小时)
|
||||||
|
private static final long ZIP_VALID_DURATION_HOURS = 1;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
File zipDir = new File(zipPath);
|
||||||
|
if (!zipDir.exists()) {
|
||||||
|
boolean created = zipDir.mkdirs();
|
||||||
|
if (created) {
|
||||||
|
log.info(">>>>>>>>>>ZIP目录创建成功:{}<<<<<<<<<<", zipPath);
|
||||||
|
} else {
|
||||||
|
log.info(">>>>>>>>>>无法创建ZIP目录:{}<<<<<<<<<<", zipPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createZip(Map<String, String> filesWithPaths, Long userId) throws IOException {
|
||||||
|
String zipFileName = "music_download_" + UUID.randomUUID() + ".zip";
|
||||||
|
String zipFilePath = zipPath + File.separator + userId + File.separator + zipFileName;
|
||||||
|
|
||||||
|
// 创建ZIP文件
|
||||||
|
ZipUtil.createZip(filesWithPaths, zipFilePath);
|
||||||
|
|
||||||
|
// 计算过期时间
|
||||||
|
LocalDateTime expirationTime = LocalDateTime.now().plusHours(ZIP_VALID_DURATION_HOURS);
|
||||||
|
|
||||||
|
// 存储ZIP元数据
|
||||||
|
zipStore.put(zipFileName, new ZipMetadata(zipFilePath, expirationTime));
|
||||||
|
|
||||||
|
return zipFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ZipMetadata getZipMetadata(String zipFileName) {
|
||||||
|
return zipStore.get(zipFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeZip(String zipFileName) {
|
||||||
|
zipStore.remove(zipFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConcurrentMap<String, ZipMetadata> getZipStore() {
|
||||||
|
return zipStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -40,6 +40,7 @@ public class MusicRecordServiceImpl extends ServiceImpl<MusicRecordMapper, Music
|
|||||||
entity = new MusicRecordEntity();
|
entity = new MusicRecordEntity();
|
||||||
entity.setPlatformId(String.valueOf(res.getId()));
|
entity.setPlatformId(String.valueOf(res.getId()));
|
||||||
entity.setName(res.getName());
|
entity.setName(res.getName());
|
||||||
|
entity.setFileType(res.getFileType());
|
||||||
entity.setAlbumName(res.getAlbumName());
|
entity.setAlbumName(res.getAlbumName());
|
||||||
entity.setArtistName(res.getArtistName());
|
entity.setArtistName(res.getArtistName());
|
||||||
MusicQualityEnum musicQualityEnum = MusicQualityEnum.parse(res.getLevel());
|
MusicQualityEnum musicQualityEnum = MusicQualityEnum.parse(res.getLevel());
|
||||||
@ -55,4 +56,14 @@ public class MusicRecordServiceImpl extends ServiceImpl<MusicRecordMapper, Music
|
|||||||
entity.setCreateTime(LocalDateTime.now());
|
entity.setCreateTime(LocalDateTime.now());
|
||||||
baseMapper.insert(entity);
|
baseMapper.insert(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MusicRecordEntity queryByChannelAndPlatform(ChannelEnum channel, String platformId) {
|
||||||
|
return new LambdaQueryChainWrapper<>(baseMapper)
|
||||||
|
.eq(MusicRecordEntity::getChannel, channel)
|
||||||
|
.eq(MusicRecordEntity::getPlatformId, platformId)
|
||||||
|
.eq(MusicRecordEntity::getDeleted, Boolean.FALSE)
|
||||||
|
.last(" limit 1 ")
|
||||||
|
.one();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,8 +25,7 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static top.baogutang.music.constants.CacheKey.KEY_LOGIN_PREFIX;
|
import static top.baogutang.music.constants.CacheKey.KEY_USER_LOGIN_PREFIX;
|
||||||
import static top.baogutang.music.constants.CacheKey.LOCK_REGISTER_PREFIX;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -76,14 +75,30 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
|
|||||||
loginRes.setRole(existsUser.getRole());
|
loginRes.setRole(existsUser.getRole());
|
||||||
// 生成token
|
// 生成token
|
||||||
String token = TokenUtil.token(existsUser.getId(), loginRes.getUsername());
|
String token = TokenUtil.token(existsUser.getId(), loginRes.getUsername());
|
||||||
redisTemplate.opsForValue().set(KEY_LOGIN_PREFIX + existsUser.getId(), token, 86400000, TimeUnit.MILLISECONDS);
|
redisTemplate.opsForValue().set(KEY_USER_LOGIN_PREFIX + existsUser.getId(), token, 86400000, TimeUnit.MILLISECONDS);
|
||||||
loginRes.setToken(token);
|
loginRes.setToken(token);
|
||||||
return loginRes;
|
return loginRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void logout(Long id) {
|
public void logout(Long id) {
|
||||||
redisTemplate.delete(KEY_LOGIN_PREFIX + id);
|
redisTemplate.delete(KEY_USER_LOGIN_PREFIX + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserLevel queryUserLevel(Long userId) {
|
||||||
|
if (Objects.isNull(userId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
UserEntity user = new LambdaQueryChainWrapper<>(baseMapper)
|
||||||
|
.eq(UserEntity::getId, userId)
|
||||||
|
.eq(UserEntity::getDeleted, Boolean.FALSE)
|
||||||
|
.last(" limit 1 ")
|
||||||
|
.one();
|
||||||
|
if (Objects.nonNull(user)) {
|
||||||
|
return user.getLevel();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserEntity validate(UserRegisterAndLoginReq req) {
|
private UserEntity validate(UserRegisterAndLoginReq req) {
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
package top.baogutang.music.utils;
|
||||||
|
|
||||||
|
import top.baogutang.music.enums.UserLevel;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @description:
|
||||||
|
*
|
||||||
|
* @author: N1KO
|
||||||
|
* @date: 2024/12/15 : 11:20
|
||||||
|
*/
|
||||||
|
public class UserLevelThreadLocal {
|
||||||
|
|
||||||
|
private static final ThreadLocal<UserLevel> levelThread = new ThreadLocal<>();
|
||||||
|
|
||||||
|
public static void set(UserLevel userLevel) {
|
||||||
|
levelThread.set(userLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserLevel get() {
|
||||||
|
return levelThread.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void remove() {
|
||||||
|
levelThread.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/main/java/top/baogutang/music/utils/ZipUtil.java
Normal file
56
src/main/java/top/baogutang/music/utils/ZipUtil.java
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
package top.baogutang.music.utils;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.zip.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description: JacksonUtil
|
||||||
|
* @author: nikooh
|
||||||
|
* @date: 2024/08/06 : 10:50
|
||||||
|
*/
|
||||||
|
public class ZipUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将多个文件按照指定的目录结构打包成一个ZIP文件。
|
||||||
|
*
|
||||||
|
* @param filesWithPaths Map,每个键是文件的绝对路径,值是ZIP中的相对路径(歌手/专辑/歌曲)。
|
||||||
|
* @param zipFilePath 输出ZIP文件的路径。
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static void createZip(Map<String, String> filesWithPaths, String zipFilePath) throws IOException {
|
||||||
|
try (
|
||||||
|
FileOutputStream fos = new FileOutputStream(zipFilePath);
|
||||||
|
BufferedOutputStream bos = new BufferedOutputStream(fos);
|
||||||
|
ZipOutputStream zos = new ZipOutputStream(bos)
|
||||||
|
) {
|
||||||
|
for (Map.Entry<String, String> entry : filesWithPaths.entrySet()) {
|
||||||
|
String filePath = entry.getKey();
|
||||||
|
// 确保使用正斜杠
|
||||||
|
String zipEntryName = entry.getValue().replace("\\", "/");
|
||||||
|
|
||||||
|
File file = new File(filePath);
|
||||||
|
if (!file.exists()) {
|
||||||
|
// 跳过不存在的文件
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FileInputStream fis = new FileInputStream(file);
|
||||||
|
BufferedInputStream bis = new BufferedInputStream(fis)
|
||||||
|
) {
|
||||||
|
ZipEntry zipEntry = new ZipEntry(zipEntryName);
|
||||||
|
zos.putNextEntry(zipEntry);
|
||||||
|
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int len;
|
||||||
|
while ((len = bis.read(buffer)) != -1) {
|
||||||
|
zos.write(buffer, 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
zos.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,6 +39,10 @@ knife4j:
|
|||||||
|
|
||||||
|
|
||||||
baogutang:
|
baogutang:
|
||||||
|
music:
|
||||||
|
zip:
|
||||||
|
# base-path: /downloads/music/zips
|
||||||
|
base-path: /Users/nikooh/Desktop/downloads/music/zips
|
||||||
net-ease-music:
|
net-ease-music:
|
||||||
query-base-url: http://117.72.78.133:5173/search?keywords=%s&limit=%d&offset=%d&type=%d
|
query-base-url: http://117.72.78.133:5173/search?keywords=%s&limit=%d&offset=%d&type=%d
|
||||||
playlist-base-url: http://117.72.78.133:5173/playlist/track/all?id=%d
|
playlist-base-url: http://117.72.78.133:5173/playlist/track/all?id=%d
|
||||||
|
|||||||
@ -39,19 +39,22 @@
|
|||||||
color: #333;
|
color: #333;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
/* Dropdown Menu Styles */
|
/* Dropdown Menu Styles */
|
||||||
.dropdown {
|
.dropdown {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
top: 100%;
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: #fff;
|
background-color: #f9f9f9; /* 修改为浅灰色 */
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ccc; /* 较深的边框 */
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
display: none;
|
display: none;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
transition: opacity 0.3s ease; /* 可选:添加过渡效果 */
|
||||||
}
|
}
|
||||||
.dropdown.visible {
|
.dropdown.visible {
|
||||||
display: block;
|
display: block;
|
||||||
@ -59,14 +62,16 @@
|
|||||||
.dropdown button {
|
.dropdown button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: none;
|
background: #f9f9f9; /* 与下拉菜单背景一致 */
|
||||||
border: none;
|
border: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
color: #333; /* 深色文本 */
|
||||||
|
transition: background-color 0.3s ease; /* 可选:添加过渡效果 */
|
||||||
}
|
}
|
||||||
.dropdown button:hover {
|
.dropdown button:hover {
|
||||||
background-color: #f0f0f0;
|
background-color: #e0e0e0; /* 悬停时颜色变化 */
|
||||||
}
|
}
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
@ -204,6 +209,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 其他样式保持不变 */
|
||||||
|
|
||||||
.album-list {
|
.album-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -937,7 +944,6 @@
|
|||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
// 清除可访问的cookie,如username
|
// 清除可访问的cookie,如username
|
||||||
deleteCookie('username');
|
deleteCookie('username');
|
||||||
deleteCookie('token');
|
|
||||||
// 由于token是HttpOnly,无法通过JavaScript清除,后端已通过响应头清除
|
// 由于token是HttpOnly,无法通过JavaScript清除,后端已通过响应头清除
|
||||||
// 跳转回登录页面
|
// 跳转回登录页面
|
||||||
window.location.href = '/login.html';
|
window.location.href = '/login.html';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user