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 类是一个音乐管理服务,主要提供音乐文件的上传、编辑、查询和删除等功能。以下是其主要功能的详细解释:

核心功能

  1. 音乐上传与保存 (SaveAsync)

    • 接收音乐文件、封面图片和歌词文件
    • 解析音乐文件的元数据(如标题、艺术家、时长等)
    • 上传音乐文件到对象存储
    • 处理封面图片(优先使用上传的封面,其次使用音乐文件内嵌的封面,最后使用默认图片)
    • 保存歌词内容
    • 将音乐信息存入数据库
  2. 音乐信息编辑 (EditMusicInformationAsync)

    • 更新音乐的基本信息
    • 更新歌词内容
    • 更新封面图片
    • 更新分组信息
  3. 音乐查询功能

    • 分页查询 (GetPagesAsync)
    • 按ID查询单个音乐 (GetAsync)
    • 获取热门音乐 (GetTopMusicsAsync)
    • 随机获取分组音乐 (GetRandomGroupMusicAsync)
  4. 音乐删除 (DeleteAsync)

    • 从数据库删除音乐记录
    • 从对象存储删除相关文件
  5. 分组管理 (GetGroupsAsync)

    • 获取所有音乐分组列表

技术特点

  1. 缓存机制

    • 继承自 AbstractCacheService,使用 IFusionCache 实现缓存
    • 查询结果自动缓存,减少数据库访问
    • 数据变更时自动清除相关缓存
  2. 文件处理

    • 使用 TagLib 库解析音乐文件元数据
    • 使用 ImageSharp 处理图片格式检测
    • 支持多种文件上传(音乐、封面、歌词)
  3. 依赖注入

    • 使用构造函数注入依赖项(数据库访问、映射、日志、缓存、配置等)
  4. 异常处理

    • 使用 BusinessException 处理业务逻辑异常
    • 对关键操作进行参数校验
  5. 调试支持

    • 通过条件编译支持调试模式下的数据库访问方式

业务逻辑

  1. 音乐信息处理

    • 自动从音乐文件中提取标题、艺术家、时长等信息
    • 处理分组信息(去重)
  2. 封面处理逻辑

    • 优先使用上传的封面
    • 其次使用音乐文件内嵌的封面
    • 最后使用默认封面图片
  3. 缓存策略

    • 默认缓存过期时间为1天
    • 数据变更时清除相关前缀的缓存

这个服务类是一个完整的音乐管理解决方案,涵盖了音乐文件从上传到展示的全生命周期管理。

评论加载中...