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.autoconfigure.SpringBootApplication;
|
||||
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.*"})
|
||||
@MapperScan(basePackages = {"top.baogutang.music.dao.mapper"})
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class BaogutangMusicApplication {
|
||||
|
||||
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.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());
|
||||
UserLoginRes userLoginRes = TokenUtil.verify(token);
|
||||
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)) {
|
||||
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;
|
||||
|
||||
import top.baogutang.music.dao.entity.MusicRecordEntity;
|
||||
import top.baogutang.music.domain.req.AbstractMusicReq;
|
||||
import top.baogutang.music.domain.req.search.MusicSearchReq;
|
||||
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 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 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 org.springframework.stereotype.Component;
|
||||
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.res.download.MusicDownloadRes;
|
||||
import top.baogutang.music.domain.res.search.*;
|
||||
import top.baogutang.music.enums.AudioFileTypeEnum;
|
||||
import top.baogutang.music.enums.ChannelEnum;
|
||||
import top.baogutang.music.processor.AbstractAudioProcessor;
|
||||
import top.baogutang.music.properties.NetEaseMusicProperties;
|
||||
@ -84,7 +86,11 @@ public class NetEaseMusicClient implements ChannelClient<MusicSearchReq, MusicSe
|
||||
});
|
||||
if (Objects.nonNull(res)) {
|
||||
res.setId(id);
|
||||
String[] split = res.getUrl().split("\\.");
|
||||
String fileType = split[split.length - 1];
|
||||
res.setFileType(AudioFileTypeEnum.parse(fileType));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -102,14 +108,22 @@ public class NetEaseMusicClient implements ChannelClient<MusicSearchReq, MusicSe
|
||||
if (!Files.exists(baseDir)) {
|
||||
Files.createDirectories(baseDir);
|
||||
}
|
||||
String[] split = res.getUrl().split("\\.");
|
||||
String fileType = split[split.length - 1];
|
||||
AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(fileType);
|
||||
AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(res.getFileType());
|
||||
try (InputStream musicIn = audioProcessor.processAudioTags(new URL(res.getUrl()).openStream(), res)) {
|
||||
if (Objects.nonNull(musicIn)) {
|
||||
Files.copy(musicIn, baseDir.resolve(res.getName() + "." + 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.springframework.stereotype.Component;
|
||||
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.res.download.MusicDownloadRes;
|
||||
import top.baogutang.music.domain.res.download.QQMusicDownloadRes;
|
||||
@ -302,6 +303,10 @@ public class QQMusicClient implements ChannelClient<MusicSearchReq, MusicSearchR
|
||||
}
|
||||
}
|
||||
musicDownloadRes.setId(qqMusicDownloadRes.getSong().getMid());
|
||||
MusicQualityEnum musicQualityEnum = MusicQualityEnum.parse(musicDownloadRes.getLevel());
|
||||
if (Objects.nonNull(musicQualityEnum)) {
|
||||
musicDownloadRes.setFileType(musicQualityEnum.getType());
|
||||
}
|
||||
return musicDownloadRes;
|
||||
}
|
||||
|
||||
@ -324,13 +329,22 @@ public class QQMusicClient implements ChannelClient<MusicSearchReq, MusicSearchR
|
||||
if (Objects.isNull(musicQualityEnum)) {
|
||||
throw new BusinessException("不支持的文件格式");
|
||||
}
|
||||
String fileType = musicQualityEnum.getType();
|
||||
AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(fileType);
|
||||
AbstractAudioProcessor audioProcessor = AbstractAudioProcessor.getAudioProcessor(res.getFileType());
|
||||
try (InputStream musicIn = audioProcessor.processAudioTags(new URL(res.getUrl()).openStream(), res)) {
|
||||
if (Objects.nonNull(musicIn)) {
|
||||
Files.copy(musicIn, baseDir.resolve(res.getName() + "." + 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.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
@ -46,4 +48,15 @@ public class ExecutorConfig {
|
||||
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 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;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import top.baogutang.music.annos.Login;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import top.baogutang.music.annos.Vip;
|
||||
import top.baogutang.music.domain.Results;
|
||||
import top.baogutang.music.domain.res.download.MusicDownloadRes;
|
||||
import top.baogutang.music.domain.res.search.MusicPlaylistRes;
|
||||
import top.baogutang.music.domain.ZipMetadata;
|
||||
import top.baogutang.music.service.IMusicService;
|
||||
import top.baogutang.music.service.ZipService;
|
||||
import top.baogutang.music.utils.UserThreadLocal;
|
||||
|
||||
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
|
||||
private IMusicService musicService;
|
||||
|
||||
@Resource
|
||||
private ZipService zipService;
|
||||
|
||||
@GetMapping
|
||||
@Login
|
||||
public Results<MusicDownloadRes> download(@RequestParam(name = "channel") Integer channel,
|
||||
@RequestParam(name = "id") String id) {
|
||||
MusicDownloadRes res = musicService.getMusicService(channel).download(id);
|
||||
@Vip
|
||||
public Results<String> download(@RequestParam(name = "channel") Integer channel,
|
||||
@RequestParam(name = "id") List<String> idList) {
|
||||
String res = musicService.getMusicService(channel).download(UserThreadLocal.get(), idList);
|
||||
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 lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import top.baogutang.music.enums.AudioFileTypeEnum;
|
||||
import top.baogutang.music.enums.ChannelEnum;
|
||||
import top.baogutang.music.enums.MusicQualityEnum;
|
||||
|
||||
@ -26,6 +27,8 @@ public class MusicRecordEntity extends BaseEntity {
|
||||
|
||||
private String name;
|
||||
|
||||
private AudioFileTypeEnum fileType;
|
||||
|
||||
private String albumName;
|
||||
|
||||
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.EqualsAndHashCode;
|
||||
import top.baogutang.music.domain.res.AbstractMusicRes;
|
||||
import top.baogutang.music.enums.AudioFileTypeEnum;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@ -40,4 +41,5 @@ public class MusicDownloadRes extends AbstractMusicRes implements Serializable {
|
||||
|
||||
private String lyric;
|
||||
|
||||
private AudioFileTypeEnum fileType;
|
||||
}
|
||||
|
||||
@ -35,20 +35,20 @@ public enum MusicQualityEnum {
|
||||
SKY("沉浸环绕声", null),
|
||||
DOLBY("杜比全景声", null),
|
||||
JYMASTER("超清母带", null),
|
||||
KBPS_128("128kbps", "mp3"),
|
||||
KBPS_320("320kbps", "mp3"),
|
||||
ATMOS_5_1("Atmos 5.1", "flac"),
|
||||
ATMOS_2("Atmos 2", "flac"),
|
||||
MASTER("Master", "flac"),
|
||||
FLAC("flac", "flac"),
|
||||
KBPS_128("128kbps", AudioFileTypeEnum.MP3),
|
||||
KBPS_320("320kbps", AudioFileTypeEnum.MP3),
|
||||
ATMOS_5_1("Atmos 5.1", AudioFileTypeEnum.FLAC),
|
||||
ATMOS_2("Atmos 2", AudioFileTypeEnum.FLAC),
|
||||
MASTER("Master", AudioFileTypeEnum.FLAC),
|
||||
FLAC("flac", AudioFileTypeEnum.FLAC),
|
||||
|
||||
;
|
||||
|
||||
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.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
|
||||
public abstract class AbstractAudioProcessor {
|
||||
|
||||
public static AbstractAudioProcessor getAudioProcessor(String fileType) {
|
||||
AudioFileTypeEnum fileTypeEnum = AudioFileTypeEnum.parse(fileType);
|
||||
switch (fileTypeEnum) {
|
||||
public static AbstractAudioProcessor getAudioProcessor(AudioFileTypeEnum fileType) {
|
||||
switch (fileType) {
|
||||
case FLAC:
|
||||
return new FlacAudioProcessor();
|
||||
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 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.dao.entity.MusicRecordEntity;
|
||||
import top.baogutang.music.domain.req.AbstractMusicReq;
|
||||
import top.baogutang.music.domain.req.search.MusicSearchReq;
|
||||
import top.baogutang.music.domain.res.AbstractMusicRes;
|
||||
@ -13,10 +18,14 @@ import top.baogutang.music.exceptions.BusinessException;
|
||||
import top.baogutang.music.factory.ChannelClientFactory;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
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")
|
||||
private Executor commonExecutor;
|
||||
|
||||
@Resource
|
||||
private ZipService zipService;
|
||||
|
||||
public abstract ChannelEnum getChannelEnum();
|
||||
|
||||
public MusicSearchRes search(MusicSearchReq req) {
|
||||
@ -66,36 +78,65 @@ public abstract class AbstractMusicService<Q extends AbstractMusicReq, S extends
|
||||
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();
|
||||
ChannelClient<Q, S> channelClient = channelClientFactory.getClient(channelEnum);
|
||||
MusicDownloadRes res = channelClient.download(id);
|
||||
if (Objects.isNull(res)) {
|
||||
log.error(">>>>>>>>>>query detail error! channel:{},song id:{}<<<<<<<<<<", channelEnum.getDesc(), id);
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(res.getArtistName())) {
|
||||
String[] split = res.getArtistName().split("/");
|
||||
String artistName = String.join(",", split);
|
||||
res.setArtistName(artistName);
|
||||
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);
|
||||
if (Objects.isNull(res)) {
|
||||
log.error(">>>>>>>>>>query detail error! channel:{},song id:{}<<<<<<<<<<", channelEnum.getDesc(), id);
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isNotBlank(res.getArtistName())) {
|
||||
String[] split = res.getArtistName().split("/");
|
||||
String artistName = String.join(",", split);
|
||||
res.setArtistName(artistName);
|
||||
}
|
||||
|
||||
channelClient.saveMusic(res);
|
||||
try {
|
||||
channelClient.processFile(res);
|
||||
log.info(">>>>>>>>>>download file:{} success<<<<<<<<<<", res.getName());
|
||||
} catch (IOException e) {
|
||||
log.error(">>>>>>>>>>download error:{}<<<<<<<<<<", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
filePath = channelClient.getDownloadBasePath() + File.separator + res.getArtistName()
|
||||
+ File.separator + res.getAlbumName()
|
||||
+ File.separator + res.getName() +
|
||||
"." + res.getFileType().name().toLowerCase();
|
||||
zipEntryPath = res.getArtistName() + "/" + res.getAlbumName() + "/" + res.getName() + "." + res.getFileType().name().toLowerCase();
|
||||
return Pair.of(filePath, zipEntryPath);
|
||||
}, commonExecutor))
|
||||
.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("请稍后重试");
|
||||
}
|
||||
|
||||
channelClient.saveMusic(res);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
channelClient.processFile(res);
|
||||
log.info(">>>>>>>>>>download file:{} success<<<<<<<<<<", res.getName());
|
||||
} catch (IOException e) {
|
||||
log.error(">>>>>>>>>>download error:{}<<<<<<<<<<", e.getMessage(), e);
|
||||
throw new BusinessException("下载异常");
|
||||
}
|
||||
}, commonExecutor);
|
||||
// try {
|
||||
// channelClient.processFile(res);
|
||||
// } catch (IOException e) {
|
||||
// log.error(">>>>>>>>>>download error:{}<<<<<<<<<<", e.getMessage(), e);
|
||||
// throw new BusinessException("下载异常");
|
||||
// }
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,4 +15,6 @@ import top.baogutang.music.enums.ChannelEnum;
|
||||
public interface IMusicRecordService extends IService<MusicRecordEntity> {
|
||||
|
||||
void save(MusicDownloadRes res, ChannelEnum channel);
|
||||
|
||||
MusicRecordEntity queryByChannelAndPlatform(ChannelEnum channel, String platformId);
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
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.domain.req.user.UserRegisterAndLoginReq;
|
||||
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);
|
||||
|
||||
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.setPlatformId(String.valueOf(res.getId()));
|
||||
entity.setName(res.getName());
|
||||
entity.setFileType(res.getFileType());
|
||||
entity.setAlbumName(res.getAlbumName());
|
||||
entity.setArtistName(res.getArtistName());
|
||||
MusicQualityEnum musicQualityEnum = MusicQualityEnum.parse(res.getLevel());
|
||||
@ -55,4 +56,14 @@ public class MusicRecordServiceImpl extends ServiceImpl<MusicRecordMapper, Music
|
||||
entity.setCreateTime(LocalDateTime.now());
|
||||
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.concurrent.TimeUnit;
|
||||
|
||||
import static top.baogutang.music.constants.CacheKey.KEY_LOGIN_PREFIX;
|
||||
import static top.baogutang.music.constants.CacheKey.LOCK_REGISTER_PREFIX;
|
||||
import static top.baogutang.music.constants.CacheKey.KEY_USER_LOGIN_PREFIX;
|
||||
|
||||
/**
|
||||
*
|
||||
@ -76,14 +75,30 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
|
||||
loginRes.setRole(existsUser.getRole());
|
||||
// 生成token
|
||||
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);
|
||||
return loginRes;
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
|
||||
@ -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:
|
||||
music:
|
||||
zip:
|
||||
# base-path: /downloads/music/zips
|
||||
base-path: /Users/nikooh/Desktop/downloads/music/zips
|
||||
net-ease-music:
|
||||
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
|
||||
|
||||
@ -39,19 +39,22 @@
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
/* Dropdown Menu Styles */
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f9f9f9; /* 修改为浅灰色 */
|
||||
border: 1px solid #ccc; /* 较深的边框 */
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
display: none;
|
||||
min-width: 150px;
|
||||
z-index: 1000;
|
||||
transition: opacity 0.3s ease; /* 可选:添加过渡效果 */
|
||||
}
|
||||
.dropdown.visible {
|
||||
display: block;
|
||||
@ -59,14 +62,16 @@
|
||||
.dropdown button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: none;
|
||||
background: #f9f9f9; /* 与下拉菜单背景一致 */
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
color: #333; /* 深色文本 */
|
||||
transition: background-color 0.3s ease; /* 可选:添加过渡效果 */
|
||||
}
|
||||
.dropdown button:hover {
|
||||
background-color: #f0f0f0;
|
||||
background-color: #e0e0e0; /* 悬停时颜色变化 */
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
@ -204,6 +209,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 其他样式保持不变 */
|
||||
|
||||
.album-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -937,7 +944,6 @@
|
||||
if (data.code === 200) {
|
||||
// 清除可访问的cookie,如username
|
||||
deleteCookie('username');
|
||||
deleteCookie('token');
|
||||
// 由于token是HttpOnly,无法通过JavaScript清除,后端已通过响应头清除
|
||||
// 跳转回登录页面
|
||||
window.location.href = '/login.html';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user