From d55ab34c46d95d329a565f557268cca10baeea77 Mon Sep 17 00:00:00 2001 From: N1KO Date: Sun, 15 Dec 2024 16:04:33 +0800 Subject: [PATCH] download --- .../music/BaogutangMusicApplication.java | 2 + .../java/top/baogutang/music/annos/Vip.java | 19 ++++ .../baogutang/music/aspect/LoginAspect.java | 4 +- .../top/baogutang/music/aspect/VipAspect.java | 105 ++++++++++++++++++ .../baogutang/music/client/ChannelClient.java | 7 ++ .../music/client/NetEaseMusicClient.java | 22 +++- .../baogutang/music/client/QQMusicClient.java | 20 +++- .../music/config/ExecutorConfig.java | 13 +++ .../music/config/ScheduleConfig.java | 27 +++++ .../baogutang/music/constants/CacheKey.java | 4 +- .../controller/MusicDownloadController.java | 72 ++++++++++-- .../music/controller/OrderController.java | 20 ++++ .../music/dao/entity/MusicRecordEntity.java | 3 + .../baogutang/music/domain/ZipMetadata.java | 23 ++++ .../domain/res/download/MusicDownloadRes.java | 2 + .../music/enums/MusicQualityEnum.java | 16 +-- .../music/exceptions/VipException.java | 36 ++++++ .../processor/AbstractAudioProcessor.java | 5 +- .../music/schedules/ZipCleanupScheduler.java | 49 ++++++++ .../music/service/AbstractMusicService.java | 95 +++++++++++----- .../music/service/IMusicRecordService.java | 2 + .../music/service/IMusicService.java | 1 - .../baogutang/music/service/IUserService.java | 3 + .../baogutang/music/service/ZipService.java | 72 ++++++++++++ .../service/impl/MusicRecordServiceImpl.java | 11 ++ .../music/service/impl/UserServiceImpl.java | 23 +++- .../music/utils/UserLevelThreadLocal.java | 28 +++++ .../top/baogutang/music/utils/ZipUtil.java | 56 ++++++++++ src/main/resources/application.yml | 4 + src/main/resources/templates/music.html | 16 ++- 30 files changed, 691 insertions(+), 69 deletions(-) create mode 100644 src/main/java/top/baogutang/music/annos/Vip.java create mode 100644 src/main/java/top/baogutang/music/aspect/VipAspect.java create mode 100644 src/main/java/top/baogutang/music/config/ScheduleConfig.java create mode 100644 src/main/java/top/baogutang/music/controller/OrderController.java create mode 100644 src/main/java/top/baogutang/music/domain/ZipMetadata.java create mode 100644 src/main/java/top/baogutang/music/exceptions/VipException.java create mode 100644 src/main/java/top/baogutang/music/schedules/ZipCleanupScheduler.java create mode 100644 src/main/java/top/baogutang/music/service/ZipService.java create mode 100644 src/main/java/top/baogutang/music/utils/UserLevelThreadLocal.java create mode 100644 src/main/java/top/baogutang/music/utils/ZipUtil.java diff --git a/src/main/java/top/baogutang/music/BaogutangMusicApplication.java b/src/main/java/top/baogutang/music/BaogutangMusicApplication.java index c3b55ba..ba52097 100644 --- a/src/main/java/top/baogutang/music/BaogutangMusicApplication.java +++ b/src/main/java/top/baogutang/music/BaogutangMusicApplication.java @@ -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) { diff --git a/src/main/java/top/baogutang/music/annos/Vip.java b/src/main/java/top/baogutang/music/annos/Vip.java new file mode 100644 index 0000000..edc9e76 --- /dev/null +++ b/src/main/java/top/baogutang/music/annos/Vip.java @@ -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; +} diff --git a/src/main/java/top/baogutang/music/aspect/LoginAspect.java b/src/main/java/top/baogutang/music/aspect/LoginAspect.java index 2fcd292..36909d1 100644 --- a/src/main/java/top/baogutang/music/aspect/LoginAspect.java +++ b/src/main/java/top/baogutang/music/aspect/LoginAspect.java @@ -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("登录已失效"); } diff --git a/src/main/java/top/baogutang/music/aspect/VipAspect.java b/src/main/java/top/baogutang/music/aspect/VipAspect.java new file mode 100644 index 0000000..7ae22cb --- /dev/null +++ b/src/main/java/top/baogutang/music/aspect/VipAspect.java @@ -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 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(); + } + +} diff --git a/src/main/java/top/baogutang/music/client/ChannelClient.java b/src/main/java/top/baogutang/music/client/ChannelClient.java index b6050c0..f7b3ebe 100644 --- a/src/main/java/top/baogutang/music/client/ChannelClient.java +++ b/src/main/java/top/baogutang/music/client/ChannelClient.java @@ -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 download(@RequestParam(name = "channel") Integer channel, - @RequestParam(name = "id") String id) { - MusicDownloadRes res = musicService.getMusicService(channel).download(id); + @Vip + public Results download(@RequestParam(name = "channel") Integer channel, + @RequestParam(name = "id") List 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文件失败"); + } + } + } diff --git a/src/main/java/top/baogutang/music/controller/OrderController.java b/src/main/java/top/baogutang/music/controller/OrderController.java new file mode 100644 index 0000000..d907821 --- /dev/null +++ b/src/main/java/top/baogutang/music/controller/OrderController.java @@ -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 { + + +} diff --git a/src/main/java/top/baogutang/music/dao/entity/MusicRecordEntity.java b/src/main/java/top/baogutang/music/dao/entity/MusicRecordEntity.java index 87e63b5..53b0693 100644 --- a/src/main/java/top/baogutang/music/dao/entity/MusicRecordEntity.java +++ b/src/main/java/top/baogutang/music/dao/entity/MusicRecordEntity.java @@ -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; diff --git a/src/main/java/top/baogutang/music/domain/ZipMetadata.java b/src/main/java/top/baogutang/music/domain/ZipMetadata.java new file mode 100644 index 0000000..ff7abd9 --- /dev/null +++ b/src/main/java/top/baogutang/music/domain/ZipMetadata.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/top/baogutang/music/domain/res/download/MusicDownloadRes.java b/src/main/java/top/baogutang/music/domain/res/download/MusicDownloadRes.java index 543f5bb..15156a2 100644 --- a/src/main/java/top/baogutang/music/domain/res/download/MusicDownloadRes.java +++ b/src/main/java/top/baogutang/music/domain/res/download/MusicDownloadRes.java @@ -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; } diff --git a/src/main/java/top/baogutang/music/enums/MusicQualityEnum.java b/src/main/java/top/baogutang/music/enums/MusicQualityEnum.java index cd7d6d8..b777186 100644 --- a/src/main/java/top/baogutang/music/enums/MusicQualityEnum.java +++ b/src/main/java/top/baogutang/music/enums/MusicQualityEnum.java @@ -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; } diff --git a/src/main/java/top/baogutang/music/exceptions/VipException.java b/src/main/java/top/baogutang/music/exceptions/VipException.java new file mode 100644 index 0000000..841265d --- /dev/null +++ b/src/main/java/top/baogutang/music/exceptions/VipException.java @@ -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; + } +} diff --git a/src/main/java/top/baogutang/music/processor/AbstractAudioProcessor.java b/src/main/java/top/baogutang/music/processor/AbstractAudioProcessor.java index 1bd1a9b..01f63f8 100644 --- a/src/main/java/top/baogutang/music/processor/AbstractAudioProcessor.java +++ b/src/main/java/top/baogutang/music/processor/AbstractAudioProcessor.java @@ -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: diff --git a/src/main/java/top/baogutang/music/schedules/ZipCleanupScheduler.java b/src/main/java/top/baogutang/music/schedules/ZipCleanupScheduler.java new file mode 100644 index 0000000..f2ec2dc --- /dev/null +++ b/src/main/java/top/baogutang/music/schedules/ZipCleanupScheduler.java @@ -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 zipStore = zipService.getZipStore(); + + Iterator> iterator = zipStore.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry 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(); + } + } + } +} diff --git a/src/main/java/top/baogutang/music/service/AbstractMusicService.java b/src/main/java/top/baogutang/music/service/AbstractMusicService.java index 85801b0..52fd674 100644 --- a/src/main/java/top/baogutang/music/service/AbstractMusicService.java +++ b/src/main/java/top/baogutang/music/service/AbstractMusicService.java @@ -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 idList) { + if (CollectionUtils.isEmpty(idList)) { + throw new BusinessException("请选择文件进行下载"); + } ChannelEnum channelEnum = getChannelEnum(); ChannelClient 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 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; } } diff --git a/src/main/java/top/baogutang/music/service/IMusicRecordService.java b/src/main/java/top/baogutang/music/service/IMusicRecordService.java index 57845c4..6de79ed 100644 --- a/src/main/java/top/baogutang/music/service/IMusicRecordService.java +++ b/src/main/java/top/baogutang/music/service/IMusicRecordService.java @@ -15,4 +15,6 @@ import top.baogutang.music.enums.ChannelEnum; public interface IMusicRecordService extends IService { void save(MusicDownloadRes res, ChannelEnum channel); + + MusicRecordEntity queryByChannelAndPlatform(ChannelEnum channel, String platformId); } diff --git a/src/main/java/top/baogutang/music/service/IMusicService.java b/src/main/java/top/baogutang/music/service/IMusicService.java index d0521a0..e166ed7 100644 --- a/src/main/java/top/baogutang/music/service/IMusicService.java +++ b/src/main/java/top/baogutang/music/service/IMusicService.java @@ -1,6 +1,5 @@ package top.baogutang.music.service; -import top.baogutang.music.enums.ChannelEnum; /** * diff --git a/src/main/java/top/baogutang/music/service/IUserService.java b/src/main/java/top/baogutang/music/service/IUserService.java index 5f016b0..ac55ef3 100644 --- a/src/main/java/top/baogutang/music/service/IUserService.java +++ b/src/main/java/top/baogutang/music/service/IUserService.java @@ -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 { UserLoginRes login(UserRegisterAndLoginReq req); void logout(Long id); + + UserLevel queryUserLevel(Long userId); } diff --git a/src/main/java/top/baogutang/music/service/ZipService.java b/src/main/java/top/baogutang/music/service/ZipService.java new file mode 100644 index 0000000..1d46c89 --- /dev/null +++ b/src/main/java/top/baogutang/music/service/ZipService.java @@ -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 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 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 getZipStore() { + return zipStore; + } +} diff --git a/src/main/java/top/baogutang/music/service/impl/MusicRecordServiceImpl.java b/src/main/java/top/baogutang/music/service/impl/MusicRecordServiceImpl.java index a532d54..eb779dd 100644 --- a/src/main/java/top/baogutang/music/service/impl/MusicRecordServiceImpl.java +++ b/src/main/java/top/baogutang/music/service/impl/MusicRecordServiceImpl.java @@ -40,6 +40,7 @@ public class MusicRecordServiceImpl extends ServiceImpl(baseMapper) + .eq(MusicRecordEntity::getChannel, channel) + .eq(MusicRecordEntity::getPlatformId, platformId) + .eq(MusicRecordEntity::getDeleted, Boolean.FALSE) + .last(" limit 1 ") + .one(); + } } diff --git a/src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java b/src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java index ebca11f..168b4e9 100644 --- a/src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java +++ b/src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java @@ -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 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) { diff --git a/src/main/java/top/baogutang/music/utils/UserLevelThreadLocal.java b/src/main/java/top/baogutang/music/utils/UserLevelThreadLocal.java new file mode 100644 index 0000000..71adca4 --- /dev/null +++ b/src/main/java/top/baogutang/music/utils/UserLevelThreadLocal.java @@ -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 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(); + } +} diff --git a/src/main/java/top/baogutang/music/utils/ZipUtil.java b/src/main/java/top/baogutang/music/utils/ZipUtil.java new file mode 100644 index 0000000..901ff73 --- /dev/null +++ b/src/main/java/top/baogutang/music/utils/ZipUtil.java @@ -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 filesWithPaths, String zipFilePath) throws IOException { + try ( + FileOutputStream fos = new FileOutputStream(zipFilePath); + BufferedOutputStream bos = new BufferedOutputStream(fos); + ZipOutputStream zos = new ZipOutputStream(bos) + ) { + for (Map.Entry 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(); + } + } + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9588f6e..1c2f9f6 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/main/resources/templates/music.html b/src/main/resources/templates/music.html index ea08740..69c865d 100644 --- a/src/main/resources/templates/music.html +++ b/src/main/resources/templates/music.html @@ -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';