using Dpz.Core.Public.ViewModel.Request;
using Dpz.Core.Public.ViewModel.Response;
namespace Dpz.Core.Service.RepositoryServiceImpl;
public class MusicService(
// #if DEBUG
// Func<IRepository<Music>> func,
// #else
IRepository<Music> repository,
//#endif
IMapper mapper,
ILogger<MusicService> logger,
IFusionCache fusionCache,
IConfiguration configuration
) : AbstractCacheService(fusionCache), IMusicService
{
protected override TimeSpan CacheDefaultExpiration => TimeSpan.FromDays(1);
// #if DEBUG
// // ReSharper disable once InconsistentNaming
// private readonly IRepository<Music> repository = func();
// #endif
/// <summary>
/// 获取图片扩展名
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
private async Task<string?> GetImageFormatAsync(byte[] bytes)
{
try
{
using var stream = new MemoryStream(bytes);
using var image = await Image.LoadAsync(stream);
return image.Metadata.DecodedImageFormat?.FileExtensions.FirstOrDefault();
}
catch (Exception e)
{
logger.LogError(e, "get image format fail");
return null;
}
}
public async Task<MusicResponse?> SaveAsync(MusicUploadActionRequest uploadActionRequest)
{
ArgumentNullException.ThrowIfNull(uploadActionRequest);
ArgumentNullException.ThrowIfNull(uploadActionRequest.Music);
ArgumentNullException.ThrowIfNull(uploadActionRequest.UploadMusic);
var dbCheck = await repository
.SearchFor(x => x.FileName == uploadActionRequest.Music.FileName)
.FirstOrDefaultAsync();
if (dbCheck != null)
{
throw new BusinessException($"《{uploadActionRequest.Music.FileName}》已存在");
}
using var memoryStream = new MemoryStream();
await uploadActionRequest.Music.CopyToAsync(memoryStream);
var bytes = memoryStream.ToArray();
var stream = new MemoryStream(bytes);
var tagFile = TagLib.File.Create(
new TaglibFileForStream(stream, uploadActionRequest.Music.FileName)
);
var id = ObjectId.GenerateNewId();
var musicUrl = await uploadActionRequest.UploadMusic(
bytes,
$"{id}{uploadActionRequest.Music.FileName[uploadActionRequest.Music.FileName.LastIndexOf('.')..]}"
);
if (musicUrl == null)
{
throw new BusinessException("上传音乐失败");
}
var entity = new Music
{
Id = id,
Duration = tagFile.Properties.Duration,
FileName = uploadActionRequest.Music.FileName,
Title = tagFile.Tag.Title,
MusicLength = bytes.Length,
UploadTime = DateTime.Now,
Artist = tagFile.Tag.JoinedPerformers,
LastUpdateTime = DateTime.Now,
From = uploadActionRequest.From,
Group = uploadActionRequest.Group.GroupBy(x => x).Select(x => x.Key).ToList(),
MusicUrl = musicUrl,
};
var coverUrl = await UploadCoverAsync(
uploadActionRequest.Cover,
uploadActionRequest.UploadCover,
entity.Id,
tagFile
);
entity.CoverUrl = coverUrl;
if (uploadActionRequest.Lyric != null)
{
using var lrcStream = new MemoryStream();
await uploadActionRequest.Lyric.CopyToAsync(lrcStream);
lrcStream.Position = 0;
using var reader = new StreamReader(lrcStream);
entity.LyricContent = await reader.ReadToEndAsync();
lrcStream.Position = 0;
var lyricBytes = lrcStream.ToArray();
var lyricUrl = await uploadActionRequest.UploadLyric?.Invoke(
lyricBytes,
$"{entity.Id}.lrc"
)!;
entity.LyricUrl = lyricUrl;
}
await repository.InsertAsync(entity);
await RemoveByPrefixAsync();
return mapper.Map<MusicResponse>(entity);
}
private async Task<string?> UploadCoverAsync(
IFormFile? cover,
UploadCoverAsync? upload,
ObjectId id,
TagLib.File file
)
{
if (upload == null)
{
return null;
}
// 如果上传了封面,则直接设置为封面
if (cover != null)
{
var coverStream = new MemoryStream();
await cover.OpenReadStream().CopyToAsync(coverStream);
var coverBytes = coverStream.ToArray();
var coverExt = await GetImageFormatAsync(coverBytes) ?? "jpg";
var coverUrl = await upload(coverBytes, $"{id}.{coverExt}");
return coverUrl;
}
else
{
// 如果没有上传封面,则获取 tag file 中的封面
var coverBytes = file.Tag.Pictures?.FirstOrDefault()?.Data?.Data;
if (coverBytes != null)
{
var coverExt = await GetImageFormatAsync(coverBytes) ?? "jpg";
var coverUrl = await upload(coverBytes, $"{id}.{coverExt}");
return coverUrl;
}
// 如果 tag file 中没有封面,设置一个默认图片为封面
return $"{configuration["CDNHost"]}/images/music.png";
}
}
public async Task<List<string>> GetGroupsAsync()
{
return await GetOrSetCacheAsync<List<string>>(
nameof(GetGroupsAsync),
async (_, cancellationToken) =>
{
return await repository
.MongodbQueryable.SelectMany(x => x.Group)
.GroupBy(x => x)
.Select(x => x.Key)
.Where(x => !string.IsNullOrWhiteSpace(x))
.OrderBy(x => x)
.ToListAsync(cancellationToken: cancellationToken);
}
);
}
public async Task<MusicResponse> EditMusicInformationAsync(EditMusicInformationRequest? information)
{
if (information == null)
{
throw new ArgumentNullException(nameof(information));
}
var entity = await repository.TryGetAsync(information.Id);
if (entity == null)
{
throw new ArgumentException("not found music by property Id", nameof(information));
}
var vmMusic = mapper.Map<MusicResponse>(entity);
if (information.Lyric != null)
{
using var stream = new MemoryStream();
await information.Lyric.CopyToAsync(stream);
if (information.UploadLyric == null)
{
throw new ArgumentNullException(
nameof(information),
"lyric is not null,upload lyric is null"
);
}
stream.Position = 0;
using var reader = new StreamReader(stream);
entity.LyricContent = await reader.ReadToEndAsync();
stream.Position = 0;
var lyricBytes = stream.ToArray();
entity.LyricUrl = await information.UploadLyric.Invoke(lyricBytes, vmMusic);
}
if (information.Cover != null)
{
if (information.UploadCover == null)
{
throw new ArgumentNullException(
nameof(information),
"cover is not null,upload cover is null"
);
}
using var stream = new MemoryStream();
await information.Cover.CopyToAsync(stream);
var bytes = stream.ToArray();
entity.CoverUrl = await information.UploadCover.Invoke(bytes, vmMusic);
}
entity.Group = information.Group?.GroupBy(x => x).Select(x => x.Key).ToList() ?? [];
entity.LastUpdateTime = DateTime.Now;
await repository.UpdateAsync(entity);
await RemoveByPrefixAsync();
return mapper.Map<MusicResponse>(entity);
}
public async Task<IPagedList<MusicResponse>> GetPagesAsync(
int pageIndex,
int pageSize,
string? title = null
)
{
return await GetOrSetPagedListAsync<MusicResponse>(
nameof(GetPagesAsync),
async _ =>
{
var predicate = repository.MongodbQueryable;
if (!string.IsNullOrEmpty(title))
{
predicate = predicate.Where(x =>
x.FileName.Contains(title) || (x.Title != null && x.Title.Contains(title))
);
}
return await predicate
.OrderByDescending(x => x.UploadTime)
.ToPagedListAsync<Music, MusicResponse>(pageIndex, pageSize);
},
new
{
pageIndex,
pageSize,
title,
}
);
}
public async Task<MusicResponse?> GetAsync(string id)
{
return await GetOrSetCacheAsync<MusicResponse?>(
nameof(GetAsync),
async (_, _) =>
{
var entity = await repository.TryGetAsync(id);
return entity == null ? null : mapper.Map<MusicResponse>(entity);
},
new { id }
);
}
public async Task DeleteAsync(string id, Func<MusicResponse, Task> deleteObjectStorage)
{
var music = await GetAsync(id);
if (music == null)
{
return;
}
var oid = ObjectId.Parse(id);
await repository.DeleteAsync(x => x.Id == oid);
await deleteObjectStorage(music);
await RemoveByPrefixAsync();
}
public async Task<ICollection<MusicResponse>> GetTopMusicsAsync(uint top = 200)
{
return await GetOrSetCacheAsync<List<MusicResponse>>(
nameof(GetTopMusicsAsync),
async (_, cancellationToken) =>
{
var iTop = (int)top;
var list = await repository
.MongodbQueryable.OrderByDescending(x => x.UploadTime)
.Take(iTop)
.ToListAsync(cancellationToken);
list.ForEach(SetEmptyMusic);
return mapper.Map<List<MusicResponse>>(list);
},
new { top }
);
}
private void SetEmptyMusic(Music item)
{
if (string.IsNullOrEmpty(item.CoverUrl))
{
item.CoverUrl = configuration["upyun:Host"] + "/images/music.png";
}
if (string.IsNullOrEmpty(item.LyricUrl))
{
item.LyricUrl = configuration["upyun:Host"] + "/music/lyric/null.lrc";
}
}
public async Task<ICollection<MusicResponse>> GetRandomGroupMusicAsync(int top = 200)
{
if (top <= 0)
{
top = 1;
}
return await GetOrSetCacheAsync<List<MusicResponse>>(
nameof(GetGroupsAsync),
async (_, cancellationToken) =>
{
var group = (await GetGroupsAsync()).MinBy(_ => Guid.NewGuid()) ?? "";
var list = await repository
.SearchFor(x => x.Group.Contains(group))
.OrderByDescending(x => x.UploadTime)
.Take(top)
.ToListAsync(cancellationToken);
list.ForEach(SetEmptyMusic);
return mapper.Map<List<MusicResponse>>(list);
},
new { top }
);
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
MusicService 功能解析
这个 MusicService 类是一个音乐管理服务,主要提供音乐文件的上传、编辑、查询和删除等功能。以下是其主要功能的详细解释:
核心功能
音乐上传与保存 (
SaveAsync)- 接收音乐文件、封面图片和歌词文件
- 解析音乐文件的元数据(如标题、艺术家、时长等)
- 上传音乐文件到对象存储
- 处理封面图片(优先使用上传的封面,其次使用音乐文件内嵌的封面,最后使用默认图片)
- 保存歌词内容
- 将音乐信息存入数据库
音乐信息编辑 (
EditMusicInformationAsync)- 更新音乐的基本信息
- 更新歌词内容
- 更新封面图片
- 更新分组信息
音乐查询功能
- 分页查询 (
GetPagesAsync) - 按ID查询单个音乐 (
GetAsync) - 获取热门音乐 (
GetTopMusicsAsync) - 随机获取分组音乐 (
GetRandomGroupMusicAsync)
- 分页查询 (
音乐删除 (
DeleteAsync)- 从数据库删除音乐记录
- 从对象存储删除相关文件
分组管理 (
GetGroupsAsync)- 获取所有音乐分组列表
技术特点
缓存机制
- 继承自
AbstractCacheService,使用IFusionCache实现缓存 - 查询结果自动缓存,减少数据库访问
- 数据变更时自动清除相关缓存
- 继承自
文件处理
- 使用
TagLib库解析音乐文件元数据 - 使用
ImageSharp处理图片格式检测 - 支持多种文件上传(音乐、封面、歌词)
- 使用
依赖注入
- 使用构造函数注入依赖项(数据库访问、映射、日志、缓存、配置等)
异常处理
- 使用
BusinessException处理业务逻辑异常 - 对关键操作进行参数校验
- 使用
调试支持
- 通过条件编译支持调试模式下的数据库访问方式
业务逻辑
音乐信息处理
- 自动从音乐文件中提取标题、艺术家、时长等信息
- 处理分组信息(去重)
封面处理逻辑
- 优先使用上传的封面
- 其次使用音乐文件内嵌的封面
- 最后使用默认封面图片
缓存策略
- 默认缓存过期时间为1天
- 数据变更时清除相关前缀的缓存
这个服务类是一个完整的音乐管理解决方案,涵盖了音乐文件从上传到展示的全生命周期管理。
评论加载中...