feat: 视频预览/数据看板/下载配置 三项业务功能
- 新增视频预览:VideoPlayer 组件、VideoController/DyCollectVideoService/Repository、VideoPageRequestDTO 对应后端改动 - 新增数据看板:workplace/statics.vue、RecordTable、相关 store/http/axios 改动 - 新增下载配置:DownloadConfigService、ConfigController、AppSet.vue、AppConfig - 其他:DyHttpClientService / 同步 Job(Collect, Favorit) / Cookie 表 / CommonService / ServiceExtension 的配套调整 - 仓库卫生:.gitignore 追加 feiniu 过滤;运维脚本归档到 docs/scripts/
This commit is contained in:
@@ -21,3 +21,6 @@ videos/
|
||||
/db/*.db-journal
|
||||
/db/*.db-wal
|
||||
/db/*.db-shm
|
||||
|
||||
# 飞牛 NAS 相关(当前只部署群晖,不入库)
|
||||
*feiniu*
|
||||
|
||||
@@ -136,7 +136,7 @@ namespace dy.net.Controllers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// 立即执行同步任务
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("ExecuteJobNow")]
|
||||
@@ -148,6 +148,55 @@ namespace dy.net.Controllers
|
||||
return Ok(new { code = 0 , error = "" });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置指定Cookie的同步状态,强制重新同步
|
||||
/// </summary>
|
||||
/// <param name="id">Cookie ID</param>
|
||||
/// <param name="type">重置类型:collect=收藏, favorite=喜欢, all=全部</param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost("ResetSyncStatus")]
|
||||
public async Task<IActionResult> ResetSyncStatus(string id, string type = "all")
|
||||
{
|
||||
try
|
||||
{
|
||||
var cookie = await dyCookieService.GetByIdAsync(id);
|
||||
if (cookie == null)
|
||||
{
|
||||
return BadRequest(new { code = -1, error = "Cookie不存在" });
|
||||
}
|
||||
|
||||
if (type == "collect" || type == "all")
|
||||
{
|
||||
cookie.CollHasSyncd = 0;
|
||||
}
|
||||
if (type == "favorite" || type == "all")
|
||||
{
|
||||
cookie.FavHasSyncd = 0;
|
||||
}
|
||||
if (type == "all")
|
||||
{
|
||||
cookie.UperSyncd = 0;
|
||||
}
|
||||
|
||||
var result = await dyCookieService.UpdateAsync(cookie);
|
||||
if (result)
|
||||
{
|
||||
Serilog.Log.Information($"重置Cookie[{cookie.UserName}]的同步状态,类型: {type}");
|
||||
return Ok(new { code = 0, message = "同步状态已重置" });
|
||||
}
|
||||
else
|
||||
{
|
||||
return BadRequest(new { code = -1, error = "重置失败" });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "重置同步状态失败");
|
||||
return StatusCode(500, new { code = -1, error = "重置失败:" + ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task ReStartJob() {
|
||||
var config= commonService.GetConfig();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using dy.net.dto;
|
||||
using dy.net.dto;
|
||||
using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -25,7 +25,10 @@ namespace dy.net.Controllers
|
||||
[HttpPost("paged")]
|
||||
public async Task<IActionResult> GetPagedAsync(VideoPageRequestDTO dto)
|
||||
{
|
||||
var (list, totalCount) = await dyCollectVideoService.GetPagedAsync(dto.PageIndex, dto.PageSize, dto.Tag, dto.Author,dto.ViedoType,dto.Dates);
|
||||
// 调试日志:输出接收到的排序参数
|
||||
Console.WriteLine($"排序参数 - SortField: {dto.SortField}, SortOrder: {dto.SortOrder}");
|
||||
|
||||
var (list, totalCount) = await dyCollectVideoService.GetPagedAsync(dto.PageIndex, dto.PageSize, dto.Tag, dto.Tags, dto.Author, dto.ViedoType, dto.Dates, dto.IsDeleted, dto.SortField, dto.SortOrder);
|
||||
return Ok(new
|
||||
{
|
||||
code = 0,
|
||||
@@ -39,6 +42,21 @@ namespace dy.net.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有视频类型列表(用于下拉框)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("categories")]
|
||||
public async Task<IActionResult> GetAllCategories()
|
||||
{
|
||||
var categories = await dyCollectVideoService.GetAllCategoriesAsync();
|
||||
return Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = categories
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询统计数据
|
||||
/// </summary>
|
||||
@@ -53,6 +71,254 @@ namespace dy.net.Controllers
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理数据库中文件不存在的视频记录
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost("cleanup")]
|
||||
public async Task<IActionResult> CleanupMissingFilesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Serilog.Log.Information("开始清理文件不存在的视频记录");
|
||||
var deletedCount = await dyCollectVideoService.CleanupMissingFilesAsync();
|
||||
Serilog.Log.Information($"清理完成,删除了 {deletedCount} 条记录");
|
||||
return Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
deletedCount = deletedCount,
|
||||
message = $"清理完成,删除了 {deletedCount} 条文件不存在的记录"
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "清理文件不存在的记录失败");
|
||||
return StatusCode(500, new { code = -1, error = "清理失败:" + ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除指定作者的所有视频数据
|
||||
/// </summary>
|
||||
/// <param name="authorName">作者名称</param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost("delete-author")]
|
||||
public async Task<IActionResult> DeleteAuthorVideosAsync([FromBody] DeleteAuthorRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request?.AuthorName))
|
||||
{
|
||||
return BadRequest(new { code = -1, error = "作者名称不能为空" });
|
||||
}
|
||||
|
||||
Serilog.Log.Information($"开始删除作者 {request.AuthorName} 的视频数据");
|
||||
var deletedCount = await dyCollectVideoService.DeleteAuthorVideosAsync(request.AuthorName);
|
||||
Serilog.Log.Information($"删除完成,删除了 {deletedCount} 条记录");
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
code = 0,
|
||||
data = new
|
||||
{
|
||||
deletedCount = deletedCount,
|
||||
message = $"已删除作者 {request.AuthorName} 的 {deletedCount} 条视频记录"
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, $"删除作者视频失败: {request?.AuthorName}");
|
||||
return StatusCode(500, new { code = -1, error = "删除失败:" + ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除作者请求DTO
|
||||
/// </summary>
|
||||
public class DeleteAuthorRequest
|
||||
{
|
||||
public string AuthorName { get; set; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 获取视频流(支持Range请求,用于视频播放)
|
||||
/// </summary>
|
||||
/// <param name="id">视频ID</param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet("stream/{id}")]
|
||||
public async Task<IActionResult> GetVideoStream(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var video = await dyCollectVideoService.GetByIdAsync(id);
|
||||
if (video == null || string.IsNullOrWhiteSpace(video.VideoSavePath))
|
||||
{
|
||||
return NotFound(new { code = -1, error = "视频不存在" });
|
||||
}
|
||||
|
||||
if (!System.IO.File.Exists(video.VideoSavePath))
|
||||
{
|
||||
return NotFound(new { code = -1, error = "视频文件不存在" });
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(video.VideoSavePath);
|
||||
var contentType = GetContentType(video.VideoSavePath);
|
||||
|
||||
// 记录日志,便于调试
|
||||
var rangeHeader = Request.Headers["Range"].ToString();
|
||||
Serilog.Log.Debug($"视频流请求: ID={id}, 路径={video.VideoSavePath}, Content-Type={contentType}, Range={rangeHeader}");
|
||||
|
||||
var fileStream = new FileStream(video.VideoSavePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
|
||||
// 使用ASP.NET Core内置的Range支持
|
||||
Response.Headers.Add("Accept-Ranges", "bytes");
|
||||
|
||||
// 添加缓存控制头,确保浏览器可以缓存视频元数据
|
||||
// 但允许 Range 请求,这样浏览器可以快速开始播放
|
||||
Response.Headers.Add("Cache-Control", "public, max-age=3600");
|
||||
|
||||
// 添加 CORS 头(如果需要)
|
||||
Response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||
Response.Headers.Add("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
||||
Response.Headers.Add("Access-Control-Allow-Headers", "Range");
|
||||
Response.Headers.Add("Access-Control-Expose-Headers", "Content-Range, Accept-Ranges, Content-Length");
|
||||
|
||||
return File(fileStream, contentType, enableRangeProcessing: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, $"获取视频流失败: {id}");
|
||||
return StatusCode(500, new { code = -1, error = "获取视频流失败" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取视频封面图片
|
||||
/// </summary>
|
||||
/// <param name="id">视频ID</param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet("cover/{id}")]
|
||||
public async Task<IActionResult> GetVideoCover(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var video = await dyCollectVideoService.GetByIdAsync(id);
|
||||
if (video == null)
|
||||
{
|
||||
return GetDefaultCoverImage();
|
||||
}
|
||||
|
||||
// 优先使用本地保存的封面
|
||||
if (!string.IsNullOrWhiteSpace(video.VideoCoverSavePath) && System.IO.File.Exists(video.VideoCoverSavePath))
|
||||
{
|
||||
var fileInfo = new FileInfo(video.VideoCoverSavePath);
|
||||
var contentType = GetImageContentType(video.VideoCoverSavePath);
|
||||
var fileStream = System.IO.File.OpenRead(video.VideoCoverSavePath);
|
||||
return File(fileStream, contentType);
|
||||
}
|
||||
|
||||
// 如果没有本地封面,返回默认图片
|
||||
return GetDefaultCoverImage();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, $"获取视频封面失败: {id}");
|
||||
return GetDefaultCoverImage();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回默认封面图片(SVG 占位图)
|
||||
/// </summary>
|
||||
private IActionResult GetDefaultCoverImage()
|
||||
{
|
||||
var svgContent = @"<svg width=""400"" height=""225"" xmlns=""http://www.w3.org/2000/svg"">
|
||||
<rect width=""400"" height=""225"" fill=""#f0f0f0""/>
|
||||
<text x=""50%"" y=""50%"" font-family=""Arial, sans-serif"" font-size=""16"" fill=""#999"" text-anchor=""middle"" dy="".3em"">暂无封面</text>
|
||||
</svg>";
|
||||
var svgBytes = System.Text.Encoding.UTF8.GetBytes(svgContent);
|
||||
return File(svgBytes, "image/svg+xml");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据文件扩展名获取图片 Content-Type
|
||||
/// </summary>
|
||||
private string GetImageContentType(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return "image/jpeg";
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
return extension switch
|
||||
{
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".png" => "image/png",
|
||||
".gif" => "image/gif",
|
||||
".webp" => "image/webp",
|
||||
".svg" => "image/svg+xml",
|
||||
".bmp" => "image/bmp",
|
||||
_ => "image/jpeg"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 根据文件扩展名获取Content-Type
|
||||
/// </summary>
|
||||
private string GetContentType(string fileName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
Serilog.Log.Warning($"文件名为空,使用默认 video/mp4");
|
||||
return "video/mp4"; // 抖音视频默认是 MP4
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
|
||||
// 如果没有扩展名,尝试从文件名推断(抖音视频格式通常是 mp4)
|
||||
if (string.IsNullOrEmpty(extension))
|
||||
{
|
||||
Serilog.Log.Warning($"文件 {fileName} 没有扩展名,使用默认 video/mp4");
|
||||
return "video/mp4";
|
||||
}
|
||||
|
||||
var contentType = extension switch
|
||||
{
|
||||
".mp4" => "video/mp4",
|
||||
".webm" => "video/webm",
|
||||
".ogg" => "video/ogg",
|
||||
".mov" => "video/quicktime",
|
||||
".avi" => "video/x-msvideo",
|
||||
".mkv" => "video/x-matroska",
|
||||
".flv" => "video/x-flv",
|
||||
".wmv" => "video/x-ms-wmv",
|
||||
".3gp" => "video/3gpp",
|
||||
".m3u8" => "application/x-mpegURL", // HLS 流
|
||||
_ => null
|
||||
};
|
||||
|
||||
// 如果无法识别扩展名,默认使用 video/mp4(抖音视频通常是 MP4)
|
||||
if (contentType == null)
|
||||
{
|
||||
Serilog.Log.Warning($"无法识别的视频扩展名: {extension},文件: {fileName},使用默认 video/mp4");
|
||||
return "video/mp4";
|
||||
}
|
||||
|
||||
return contentType;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet]
|
||||
public IActionResult Index()
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
VITE_BASE_URL=/
|
||||
VITE_API_URL=http://localhost:10101
|
||||
@@ -0,0 +1,580 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
:width="900"
|
||||
:footer="null"
|
||||
:maskClosable="false"
|
||||
@cancel="handleClose"
|
||||
class="video-player-modal"
|
||||
:destroyOnClose="true"
|
||||
>
|
||||
<template #title>
|
||||
<div class="modal-title-wrapper">
|
||||
<span class="modal-title-text">{{ modalTitle }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="video-player-container" v-if="currentVideoId">
|
||||
<!-- 视频信息 -->
|
||||
<div class="video-info" v-if="currentVideoInfo">
|
||||
<div class="info-row">
|
||||
<span class="label">作者:</span>
|
||||
<span class="value">{{ currentVideoInfo.author || '未知' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">分类:</span>
|
||||
<span class="value">{{ currentVideoInfo.viedoCate || '未分类' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">分辨率:</span>
|
||||
<span class="value">{{ currentVideoInfo.resolution || '未知' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">文件大小:</span>
|
||||
<span class="value">{{ formatFileSize(currentVideoInfo.fileSize || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频播放器 -->
|
||||
<div class="video-wrapper" v-if="currentVideoId">
|
||||
<video
|
||||
ref="videoPlayer"
|
||||
:src="videoUrl"
|
||||
controls
|
||||
preload="auto"
|
||||
class="video-element"
|
||||
@error="handleVideoError"
|
||||
@loadstart="handleLoadStart"
|
||||
@loadedmetadata="handleLoadedMetadata"
|
||||
@canplay="handleCanPlay"
|
||||
@canplaythrough="handleCanPlayThrough"
|
||||
@progress="handleProgress"
|
||||
@waiting="handleWaiting"
|
||||
@playing="handlePlaying"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
<div v-else class="video-wrapper">
|
||||
<a-empty description="视频信息加载中..." />
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<a-spin size="large" tip="加载视频中..." />
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="error" class="error-message">
|
||||
<a-alert
|
||||
:message="error"
|
||||
type="error"
|
||||
show-icon
|
||||
closable
|
||||
@close="error = ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onUnmounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import Cookie from 'js-cookie';
|
||||
|
||||
interface VideoInfo {
|
||||
id: string;
|
||||
videoTitle: string;
|
||||
author: string;
|
||||
viedoCate: string;
|
||||
resolution: string;
|
||||
fileSize: number;
|
||||
videoSavePath?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
videoId?: string;
|
||||
videoInfo?: VideoInfo;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const videoPlayer = ref<HTMLVideoElement | null>(null);
|
||||
const currentVideoId = ref<string>('');
|
||||
const currentVideoInfo = ref<VideoInfo | null>(null);
|
||||
const videoDirectUrl = ref<string>('');
|
||||
|
||||
// 视频URL - 直接使用流式 URL,让浏览器处理 Range 请求
|
||||
const videoUrl = computed(() => videoDirectUrl.value);
|
||||
|
||||
// 标题(完整显示,支持多行)
|
||||
const modalTitle = computed(() => {
|
||||
return currentVideoInfo.value?.videoTitle || '视频预览';
|
||||
});
|
||||
|
||||
// 设置视频流 URL,让浏览器直接处理 Range 请求实现流式播放
|
||||
const loadVideoStream = async (videoId: string) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
|
||||
// 构建视频流 URL,浏览器会自动发送 Range 请求
|
||||
// 添加时间戳避免缓存问题
|
||||
const url = `/api/video/stream/${videoId}?t=${Date.now()}`;
|
||||
console.log('[VideoPlayer] 设置视频流 URL:', url);
|
||||
|
||||
// 直接使用 URL,让浏览器处理 Range 请求
|
||||
// 浏览器会自动发送 Range 请求头,实现边加载边播放
|
||||
videoDirectUrl.value = url;
|
||||
|
||||
// 确保 video 元素存在,并且等待 DOM 更新
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
if (videoPlayer.value) {
|
||||
// 先清空 src,避免使用旧的 URL
|
||||
videoPlayer.value.src = '';
|
||||
|
||||
// 等待一下,确保清空操作完成
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
// 设置新的视频 URL
|
||||
videoPlayer.value.src = videoDirectUrl.value;
|
||||
console.log('[VideoPlayer] 视频 src 已设置:', videoDirectUrl.value);
|
||||
|
||||
// 加载视频元数据(不等待完整下载)
|
||||
// 浏览器会自动发送 Range 请求,只加载必要的部分
|
||||
videoPlayer.value.load();
|
||||
console.log('[VideoPlayer] 调用 video.load(),开始加载视频');
|
||||
|
||||
// 视频元素会自动处理 Range 请求,实现流式播放
|
||||
// 当有足够的数据可以播放时(通常只需要几秒的数据),浏览器就可以开始播放
|
||||
// 不需要等待整个文件下载完成
|
||||
|
||||
// 监听网络状态变化,用于调试
|
||||
const checkNetworkState = () => {
|
||||
const networkState = videoPlayer.value?.networkState;
|
||||
const readyState = videoPlayer.value?.readyState;
|
||||
const buffered = videoPlayer.value?.buffered;
|
||||
|
||||
console.log('[VideoPlayer] 网络状态:', {
|
||||
networkState,
|
||||
readyState,
|
||||
bufferedLength: buffered?.length || 0,
|
||||
bufferedRanges: buffered?.length ? Array.from({ length: buffered.length }, (_, i) => ({
|
||||
start: buffered.start(i),
|
||||
end: buffered.end(i)
|
||||
})) : []
|
||||
});
|
||||
};
|
||||
|
||||
// 定期检查网络状态(用于调试)
|
||||
const networkCheckInterval = setInterval(() => {
|
||||
if (videoPlayer.value && loading.value) {
|
||||
checkNetworkState();
|
||||
} else {
|
||||
clearInterval(networkCheckInterval);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// 5秒后停止检查
|
||||
setTimeout(() => {
|
||||
clearInterval(networkCheckInterval);
|
||||
}, 5000);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[VideoPlayer] 设置视频流 URL 失败:', err);
|
||||
loading.value = false;
|
||||
|
||||
// 根据错误类型设置错误消息
|
||||
if (err.message) {
|
||||
error.value = err.message;
|
||||
} else {
|
||||
error.value = '视频加载失败,请稍后重试';
|
||||
}
|
||||
|
||||
// 清空 URL
|
||||
videoDirectUrl.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 打开播放器
|
||||
const open = async (id?: string, info?: VideoInfo) => {
|
||||
console.log('VideoPlayer.open called', { id, info });
|
||||
if (!id && !info?.id) {
|
||||
message.error('视频ID不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
const videoId = id || info!.id;
|
||||
const videoInfo = info || {
|
||||
id: videoId,
|
||||
videoTitle: '视频预览',
|
||||
author: '未知',
|
||||
viedoCate: '未分类',
|
||||
resolution: '未知',
|
||||
fileSize: 0,
|
||||
};
|
||||
|
||||
console.log('设置视频信息', { videoId, videoInfo });
|
||||
|
||||
// 先清空之前的状态
|
||||
if (videoPlayer.value) {
|
||||
videoPlayer.value.pause();
|
||||
videoPlayer.value.src = '';
|
||||
}
|
||||
videoDirectUrl.value = '';
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
|
||||
// 设置视频信息
|
||||
currentVideoId.value = videoId;
|
||||
currentVideoInfo.value = videoInfo;
|
||||
|
||||
// 先显示模态框
|
||||
visible.value = true;
|
||||
|
||||
// 等待 DOM 渲染完成后再加载视频流
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 加载视频流
|
||||
await loadVideoStream(videoId);
|
||||
};
|
||||
|
||||
// 关闭播放器
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
if (videoPlayer.value) {
|
||||
videoPlayer.value.pause();
|
||||
videoPlayer.value.src = '';
|
||||
}
|
||||
videoDirectUrl.value = '';
|
||||
loading.value = false;
|
||||
error.value = '';
|
||||
currentVideoId.value = '';
|
||||
currentVideoInfo.value = null;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
// 视频加载开始
|
||||
const handleLoadStart = () => {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
console.log('[VideoPlayer] loadstart 事件触发');
|
||||
};
|
||||
|
||||
// 视频元数据加载完成
|
||||
const handleLoadedMetadata = () => {
|
||||
const video = videoPlayer.value;
|
||||
if (video) {
|
||||
console.log('[VideoPlayer] loadedmetadata 事件触发', {
|
||||
duration: video.duration,
|
||||
videoWidth: video.videoWidth,
|
||||
videoHeight: video.videoHeight,
|
||||
readyState: video.readyState,
|
||||
networkState: video.networkState
|
||||
});
|
||||
}
|
||||
// 不在这里关闭 loading,等待 canplay 事件
|
||||
};
|
||||
|
||||
// 视频可以开始播放(有足够的数据)
|
||||
const handleCanPlay = () => {
|
||||
const video = videoPlayer.value;
|
||||
loading.value = false;
|
||||
if (video) {
|
||||
console.log('[VideoPlayer] canplay 事件触发 - 视频可以开始播放', {
|
||||
duration: video.duration,
|
||||
readyState: video.readyState,
|
||||
networkState: video.networkState,
|
||||
bufferedLength: video.buffered?.length || 0
|
||||
});
|
||||
}
|
||||
// 如果视频还没有播放,可以尝试自动播放(需要用户交互)
|
||||
if (video && video.paused) {
|
||||
// 注意:自动播放可能被浏览器阻止,需要用户交互
|
||||
// 这里不自动播放,让用户点击播放按钮
|
||||
}
|
||||
};
|
||||
|
||||
// 视频可以流畅播放(有足够的数据缓冲)
|
||||
const handleCanPlayThrough = () => {
|
||||
const video = videoPlayer.value;
|
||||
loading.value = false;
|
||||
if (video) {
|
||||
console.log('[VideoPlayer] canplaythrough 事件触发 - 视频可以流畅播放', {
|
||||
duration: video.duration,
|
||||
readyState: video.readyState,
|
||||
networkState: video.networkState,
|
||||
bufferedLength: video.buffered?.length || 0
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 视频加载进度
|
||||
const handleProgress = () => {
|
||||
const video = videoPlayer.value;
|
||||
if (video && video.buffered.length > 0) {
|
||||
const bufferedEnd = video.buffered.end(video.buffered.length - 1);
|
||||
const bufferedPercent = video.duration > 0 ? (bufferedEnd / video.duration) * 100 : 0;
|
||||
console.log('[VideoPlayer] progress 事件 - 缓冲进度:', {
|
||||
bufferedPercent: bufferedPercent.toFixed(2) + '%',
|
||||
bufferedEnd,
|
||||
duration: video.duration,
|
||||
readyState: video.readyState
|
||||
});
|
||||
|
||||
// 如果已经有足够的数据可以播放,关闭加载状态
|
||||
if (bufferedEnd > 0 && video.readyState >= 2) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 视频等待数据
|
||||
const handleWaiting = () => {
|
||||
console.log('[VideoPlayer] waiting 事件 - 视频等待数据');
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
// 视频开始播放
|
||||
const handlePlaying = () => {
|
||||
console.log('[VideoPlayer] playing 事件 - 视频开始播放');
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
// 视频加载错误
|
||||
const handleVideoError = (e: Event) => {
|
||||
const video = e.target as HTMLVideoElement;
|
||||
|
||||
// 如果 src 为空或正在加载中,忽略错误(可能是时序问题)
|
||||
if (!video.src || !videoDirectUrl.value || video.networkState === 0) {
|
||||
console.log('忽略视频加载错误(可能是时序问题):', {
|
||||
src: video.src,
|
||||
directUrl: videoDirectUrl.value,
|
||||
networkState: video.networkState,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('视频加载错误详情:', {
|
||||
error: video.error,
|
||||
errorCode: video.error?.code,
|
||||
errorMessage: video.error?.message,
|
||||
networkState: video.networkState,
|
||||
readyState: video.readyState,
|
||||
src: video.src,
|
||||
directUrl: videoDirectUrl.value,
|
||||
});
|
||||
|
||||
loading.value = false;
|
||||
|
||||
if (video.error) {
|
||||
switch (video.error.code) {
|
||||
case 1: // MEDIA_ERR_ABORTED
|
||||
error.value = '视频加载被中止';
|
||||
break;
|
||||
case 2: // MEDIA_ERR_NETWORK
|
||||
// 网络错误可能是认证失败(401)或其他网络问题
|
||||
// 检查是否是认证问题
|
||||
if (video.networkState === 3) { // NETWORK_NO_SOURCE
|
||||
// 可能是认证失败,尝试提示用户
|
||||
error.value = '视频加载失败,可能是认证问题。请检查是否已登录,或刷新页面后重试';
|
||||
} else {
|
||||
error.value = '网络错误,请检查网络连接';
|
||||
}
|
||||
break;
|
||||
case 3: // MEDIA_ERR_DECODE
|
||||
error.value = '视频解码失败,可能是视频格式问题或文件损坏';
|
||||
break;
|
||||
case 4: // MEDIA_ERR_SRC_NOT_SUPPORTED
|
||||
// 尝试重新加载视频
|
||||
if (videoDirectUrl.value && video.src === videoDirectUrl.value) {
|
||||
console.log('尝试重新加载视频...');
|
||||
setTimeout(() => {
|
||||
if (videoPlayer.value && videoDirectUrl.value) {
|
||||
videoPlayer.value.load();
|
||||
}
|
||||
}, 500);
|
||||
return; // 不显示错误,等待重试
|
||||
}
|
||||
error.value = '视频格式不支持。请检查:1) 视频文件是否完整 2) 浏览器是否支持该格式 3) 文件扩展名是否正确';
|
||||
break;
|
||||
default:
|
||||
error.value = `视频加载失败 (错误代码: ${video.error.code})`;
|
||||
}
|
||||
} else {
|
||||
error.value = '视频加载失败,请检查网络连接和视频文件';
|
||||
}
|
||||
};
|
||||
|
||||
// 监听视频加载完成
|
||||
watch(
|
||||
() => videoPlayer.value?.readyState,
|
||||
(readyState) => {
|
||||
if (readyState && readyState >= 2) {
|
||||
// HAVE_CURRENT_DATA 或更高
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (videoPlayer.value) {
|
||||
videoPlayer.value.pause();
|
||||
videoPlayer.value.src = '';
|
||||
}
|
||||
videoDirectUrl.value = '';
|
||||
});
|
||||
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
open,
|
||||
close: handleClose,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.video-player-modal {
|
||||
:deep(.ant-modal-body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
// 确保标题区域不遮挡关闭按钮,支持多行显示
|
||||
:deep(.ant-modal-header) {
|
||||
position: relative;
|
||||
padding-right: 56px !important; // 为关闭按钮留出足够空间
|
||||
min-height: 55px;
|
||||
padding-bottom: 16px; // 增加底部内边距,为多行标题留空间
|
||||
|
||||
.ant-modal-title {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义标题样式
|
||||
.modal-title-wrapper {
|
||||
width: 100%;
|
||||
max-width: calc(100% - 56px); // 减去关闭按钮的宽度
|
||||
padding-right: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-title-text {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
line-height: 1.5;
|
||||
max-height: 80px;
|
||||
overflow-y: auto;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
// 确保关闭按钮始终可见且在最上层
|
||||
:deep(.ant-modal-close) {
|
||||
position: absolute !important;
|
||||
right: 0 !important;
|
||||
top: 0 !important;
|
||||
z-index: 1000 !important;
|
||||
width: 56px !important;
|
||||
height: 55px !important;
|
||||
line-height: 55px !important;
|
||||
text-align: center !important;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: transparent;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.ant-modal-close-x {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-player-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
|
||||
.info-row {
|
||||
display: inline-block;
|
||||
margin-right: 24px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: #000;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
.video-element {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 70vh;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
+114
-111
@@ -13,17 +13,12 @@ const columns = [
|
||||
{
|
||||
title: 'Cookie名',
|
||||
dataIndex: 'userName',
|
||||
width: 100,
|
||||
width: 120,
|
||||
},
|
||||
{ title: '状态', dataIndex: 'status', width: 80 },
|
||||
{ title: '收藏路径', dataIndex: 'savePath', width: 100 },
|
||||
{ title: '喜欢路径', dataIndex: 'favSavePath', width: 100 },
|
||||
{ title: 'Up主路径', dataIndex: 'upSavePath', width: 100 },
|
||||
// { title: 'Cookie', dataIndex: 'cookies' },
|
||||
{ title: 'Up主secUserId', dataIndex: 'upSecUserIds' },
|
||||
{ title: '用户secUserId', dataIndex: 'secUserId' },
|
||||
{ title: '操作', dataIndex: 'edit', width: 200 },
|
||||
// { title: 'id', dataIndex: 'id', width: 200, hiden: false },
|
||||
{ title: '存储路径', dataIndex: 'paths', width: 300, ellipsis: true },
|
||||
{ title: 'SecUserId', dataIndex: 'userIds', width: 200, ellipsis: true },
|
||||
{ title: '操作', dataIndex: 'edit', width: 180, fixed: 'right' },
|
||||
];
|
||||
|
||||
// 定义数组中单个对象的类型:包含uper和uid字段(可选字符串类型,根据实际需求调整是否可选)
|
||||
@@ -47,16 +42,6 @@ type DataItem = {
|
||||
upSavePath?: string;
|
||||
};
|
||||
|
||||
// const dataSource = reactive<DataItem[]>([
|
||||
// {
|
||||
// userName: 'Li Zhi',
|
||||
// cookies: '131231',
|
||||
// savePath: 'x',
|
||||
// status: 1,
|
||||
// id: '1',
|
||||
// },
|
||||
// ]);
|
||||
|
||||
const loading = ref(false);
|
||||
const datas: UnwrapRef<DataItem[]> = reactive([]);
|
||||
const pagination = ref({
|
||||
@@ -153,9 +138,6 @@ function submit() {
|
||||
} else {
|
||||
copyObject(editRecord.value, resData);
|
||||
}
|
||||
// alert(JSON.stringify(form.upSecUserIdsJson));
|
||||
// console.log('1', authors);
|
||||
// console.log('2', res);
|
||||
useApiStore()
|
||||
.UpdateConfig(resData)
|
||||
.then((res) => {
|
||||
@@ -177,17 +159,15 @@ function submit() {
|
||||
|
||||
const editRecord = ref<DataItem>();
|
||||
|
||||
import { Modal } from 'ant-design-vue'; // 假设使用Ant Design Vue的Modal组件
|
||||
import { Modal } from 'ant-design-vue';
|
||||
|
||||
const deleted = (id: string) => {
|
||||
// 显示确认对话框
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除这条记录吗?此操作不可撤销。',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
// 用户确认后执行删除操作
|
||||
useApiStore()
|
||||
.deleteCookie(id)
|
||||
.then((res) => {
|
||||
@@ -200,15 +180,11 @@ const deleted = (id: string) => {
|
||||
});
|
||||
},
|
||||
onCancel: () => {
|
||||
// 用户取消删除,不执行任何操作
|
||||
console.log('已取消删除');
|
||||
},
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 编辑
|
||||
* @param record
|
||||
*/
|
||||
|
||||
function edit(record: DataItem) {
|
||||
editRecord.value = record;
|
||||
copyObject(form, record);
|
||||
@@ -264,7 +240,7 @@ const rowCount = 4;
|
||||
<a-form-item label="喜欢的存储路径" name="favSavePath">
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<a-input v-model:value="form.favSavePath" style="flex: 1;" />
|
||||
<a-tooltip title="同步“我喜欢的”视频时,必填!!!">
|
||||
<a-tooltip title="同步'我喜欢的'视频时,必填!!!">
|
||||
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px;" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
@@ -272,7 +248,7 @@ const rowCount = 4;
|
||||
<a-form-item label="SecUserId" name="secUserId">
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<a-input v-model:value="form.secUserId" style="flex: 1;" />
|
||||
<a-tooltip title="同步“我喜欢的”视频时,必填!!!">
|
||||
<a-tooltip title="同步'我喜欢的'视频时,必填!!!">
|
||||
<ExclamationCircleOutlined style="color: #faad14;font-size: 16px;" />
|
||||
</a-tooltip>
|
||||
</div>
|
||||
@@ -287,8 +263,7 @@ const rowCount = 4;
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="博主信息" name="upSecUserIdsJson">
|
||||
<a-form-item-rest> <!-- 用这个包裹所有辅助元素 -->
|
||||
<!-- 添加行按钮 -->
|
||||
<a-form-item-rest>
|
||||
<a-button type="primary" @click="addRow" style="margin-bottom: 12px">
|
||||
添加抖音Up主
|
||||
<template #icon>
|
||||
@@ -296,23 +271,15 @@ const rowCount = 4;
|
||||
</template>
|
||||
</a-button>
|
||||
|
||||
<!-- 动态渲染所有行 -->
|
||||
<div v-for="(row, index) in form.upSecUserIdsJson" :key="index" style="display: flex; gap: 12px; margin-bottom: 8px; align-items: center">
|
||||
<!-- uper输入框 -->
|
||||
<a-input v-model:value="row.uper" placeholder="博主别名,可自定义" style="flex: 1" />
|
||||
|
||||
<!-- uid输入框 -->
|
||||
<a-input v-model:value="row.uid" placeholder="博主secUserId" style="flex: 3" />
|
||||
|
||||
<!-- syncAll 开关字段 -->
|
||||
<div style="flex: 1; display: flex; align-items: center;">
|
||||
<a-tooltip title="默认关闭,仅同步 UP 主最新一页数据;开启将同步全部作品(量大不建议开启)">
|
||||
<span style="margin-right: 8px; cursor: default;color:#faad14">同步全部作品</span>
|
||||
</a-tooltip>
|
||||
<a-switch v-model:checked="row.syncAll" />
|
||||
</div>
|
||||
|
||||
<!-- 删除当前行按钮 -->
|
||||
<a-button type="text" danger @click="removeRow(index)">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
@@ -330,10 +297,8 @@ const rowCount = 4;
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 成员表格 -->
|
||||
<a-table v-bind="$attrs" :columns="columns" :dataSource="dataSource" :pagination="false">
|
||||
<a-table v-bind="$attrs" :columns="columns" :dataSource="dataSource" :pagination="false" :scroll="{ x: 900 }">
|
||||
<template #title>
|
||||
<!-- 关键修改:将 justify-between 改为 justify-end(使子元素靠右侧对齐) -->
|
||||
<div class="flex justify-end pr-4">
|
||||
<a-button type="primary" @click="GetRecords()" :loading="formLoading" class="mr-2">
|
||||
<template #icon>
|
||||
@@ -359,23 +324,51 @@ const rowCount = 4;
|
||||
</template>
|
||||
</a-badge>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'cookies'">
|
||||
<!-- 触发按钮 -->
|
||||
<a-button @click="showCookies(record)">查看</a-button>
|
||||
<template v-else-if="column.dataIndex === 'paths'">
|
||||
<div class="path-info">
|
||||
<div class="path-row" v-if="record.savePath">
|
||||
<span class="path-label">收藏:</span>
|
||||
<a-tooltip :title="record.savePath">
|
||||
<span class="path-value">{{ record.savePath }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="path-row" v-if="record.favSavePath">
|
||||
<span class="path-label">喜欢:</span>
|
||||
<a-tooltip :title="record.favSavePath">
|
||||
<span class="path-value">{{ record.favSavePath }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="path-row" v-if="record.upSavePath">
|
||||
<span class="path-label">Up主:</span>
|
||||
<a-tooltip :title="record.upSavePath">
|
||||
<span class="path-value">{{ record.upSavePath }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'upSecUserIds'">
|
||||
<!-- 触发按钮 -->
|
||||
<a-button @click="showUpers(record)">查看</a-button>
|
||||
<template v-else-if="column.dataIndex === 'userIds'">
|
||||
<div class="userid-info">
|
||||
<div class="userid-row" v-if="record.secUserId">
|
||||
<span class="userid-label">用户:</span>
|
||||
<a-tooltip :title="record.secUserId">
|
||||
<span class="userid-value">{{ record.secUserId }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div class="userid-row" v-if="record.upSecUserIds">
|
||||
<span class="userid-label">Up主:</span>
|
||||
<a-button size="small" type="link" @click="showUpers(record)" style="padding: 0; height: auto;">查看</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'edit'">
|
||||
<a-button :disabled="showModal" type="link" @click="edit(record)">
|
||||
<a-button :disabled="showModal" type="link" @click="edit(record)" size="small">
|
||||
<template #icon>
|
||||
<EditFilled />
|
||||
</template>
|
||||
编辑
|
||||
</a-button>
|
||||
|
||||
<a-button type="link" @click="deleted(record.id)" danger>
|
||||
<a-button type="link" @click="deleted(record.id)" danger size="small">
|
||||
<template #icon>
|
||||
<DeleteFilled />
|
||||
</template>
|
||||
@@ -388,17 +381,13 @@ const rowCount = 4;
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- Modal弹窗 -->
|
||||
<a-modal title="Cookie 详情" :visible="showCookiesModal" style="width:1200px;" @cancel="showCookiesModal = false" @ok="showCookiesModal = false">
|
||||
<!-- 弹窗内容 -->
|
||||
<div class="cookie-content">
|
||||
{{ showCookiesData || '无Cookie数据' }}
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- Modal弹窗 -->
|
||||
<a-modal title="要同步的Up主信息" :visible="showUpersModal" style="width:1200px;" @cancel="showUpersModal = false" @ok="showUpersModal = false">
|
||||
<!-- 弹窗内容 -->
|
||||
<div class="cookie-content">
|
||||
{{ showUpersData || '未设置' }}
|
||||
</div>
|
||||
@@ -414,100 +403,114 @@ const rowCount = 4;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 透明滚动条样式 - WebKit内核浏览器 */
|
||||
.cookie-content::-webkit-scrollbar {
|
||||
width: 6px; /* 更细的滚动条 */
|
||||
}
|
||||
|
||||
/* 轨道完全透明 */
|
||||
.cookie-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 滑块半透明(默认几乎看不见) */
|
||||
.cookie-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(150, 150, 150, 0.2); /* 浅灰透明 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* hover时稍微显示一点 */
|
||||
.cookie-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(150, 150, 150, 0.4); /* 略深一点的透明 */
|
||||
}
|
||||
|
||||
/* 角落也透明 */
|
||||
.cookie-content::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Firefox 透明滚动条适配 */
|
||||
.cookie-content {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
|
||||
}
|
||||
|
||||
.cookie-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.cookie-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.cookie-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cookie-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.cookie-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(150, 150, 150, 0.4);
|
||||
}
|
||||
|
||||
.cookie-content::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.cookie-content {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
|
||||
}
|
||||
|
||||
/* ---------------------- 新增:a-textarea 透明滚动条 ---------------------- */
|
||||
/* 1. 穿透 scoped,定位 a-textarea 内部的原生 textarea 元素 */
|
||||
:deep(.ant-input-textarea-input) {
|
||||
/* 确保内容超出时显示滚动条(a-textarea 默认已配置,可省略) */
|
||||
overflow-y: auto;
|
||||
/* Firefox 透明滚动条:thin 细滚动条 + 滑块颜色/轨道颜色 */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(150, 150, 150, 0.2) transparent;
|
||||
}
|
||||
|
||||
/* 2. WebKit 浏览器(Chrome/Safari/Edge)透明滚动条 */
|
||||
/* 滚动条宽度 */
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar {
|
||||
width: 6px; /* 与 cookie-content 保持一致的细滚动条 */
|
||||
height: 6px; /* 横向滚动条(如需) */
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
/* 滚动条轨道(完全透明) */
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 滚动条滑块(半透明,hover 时加深) */
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb {
|
||||
background: rgba(150, 150, 150, 0.2); /* 浅灰透明,默认几乎看不见 */
|
||||
border-radius: 3px; /* 圆角优化 */
|
||||
background: rgba(150, 150, 150, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(150, 150, 150, 0.4); /* hover 时略深,提升交互感知 */
|
||||
background: rgba(150, 150, 150, 0.4);
|
||||
}
|
||||
|
||||
/* 滚动条角落(完全透明,避免留白) */
|
||||
:deep(.ant-input-textarea-input)::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.path-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.path-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.path-label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.path-value {
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.userid-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.userid-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.userid-label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.userid-value {
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -16,6 +16,47 @@
|
||||
<a-alert message="每次扫描行数,第一次同步完成后,可以适当调高提高效率" type="success" />
|
||||
</a-form-item>
|
||||
|
||||
<a-divider orientation="left">下载配置</a-divider>
|
||||
<a-alert message="默认情况下,系统不会自动下载视频,避免占用过多存储空间。如需下载,请配置以下选项。" type="info" style="margin-bottom: 16px;" />
|
||||
|
||||
<a-form-item label="启用自动下载">
|
||||
<a-switch v-model:checked="formState.EnableAutoDownload" />
|
||||
<a-alert message="关闭后,系统只同步视频信息到数据库,不会下载视频文件" type="warning" style="margin-top: 8px;" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="下载时间范围(天)" v-if="formState.EnableAutoDownload">
|
||||
<a-input-number v-model:value="formState.DownloadDaysRange" :min="0" :max="3650" style="width: 200px;" />
|
||||
<a-alert message="0表示不限制时间,只下载指定天数内的视频(例如:30表示只下载最近30天的视频)" type="success" style="margin-top: 8px;" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="下载喜欢的视频" v-if="formState.EnableAutoDownload">
|
||||
<a-switch v-model:checked="formState.DownloadFavorite" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="下载收藏的视频" v-if="formState.EnableAutoDownload">
|
||||
<a-switch v-model:checked="formState.DownloadCollect" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="作者白名单" v-if="formState.EnableAutoDownload">
|
||||
<a-textarea
|
||||
v-model:value="formState.AuthorWhitelist"
|
||||
placeholder="每行一个作者名称或ID,留空则不限制 例如: 作者1 作者2 MS4wLjABAAAA..."
|
||||
:rows="4"
|
||||
style="width: 500px;"
|
||||
/>
|
||||
<a-alert message="配置后,只有白名单中的作者视频才会被下载。留空则不限制。" type="success" style="margin-top: 8px;" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="作者黑名单" v-if="formState.EnableAutoDownload">
|
||||
<a-textarea
|
||||
v-model:value="formState.AuthorBlacklist"
|
||||
placeholder="每行一个作者名称或ID,留空则不限制 例如: 作者1 作者2 MS4wLjABAAAA..."
|
||||
:rows="4"
|
||||
style="width: 500px;"
|
||||
/>
|
||||
<a-alert message="配置后,黑名单中的作者视频不会被下载。留空则不限制。" type="success" style="margin-top: 8px;" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :wrapper-col="{ span: 10, offset: 3 }">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="onUpdate" v-if="componentDisabled">修改配置</a-button>
|
||||
@@ -43,11 +84,23 @@ interface FormState {
|
||||
Cron: string;
|
||||
Id: string;
|
||||
BatchCount: number;
|
||||
EnableAutoDownload?: boolean;
|
||||
DownloadDaysRange?: number;
|
||||
AuthorWhitelist?: string;
|
||||
AuthorBlacklist?: string;
|
||||
DownloadFavorite?: boolean;
|
||||
DownloadCollect?: boolean;
|
||||
}
|
||||
const formState: UnwrapRef<FormState> = reactive({
|
||||
Cron: '30',
|
||||
Id: '0',
|
||||
BatchCount: 10,
|
||||
EnableAutoDownload: false,
|
||||
DownloadDaysRange: 0,
|
||||
AuthorWhitelist: '',
|
||||
AuthorBlacklist: '',
|
||||
DownloadFavorite: false,
|
||||
DownloadCollect: false,
|
||||
});
|
||||
|
||||
const rules: Record<string, Rule[]> = {
|
||||
@@ -62,6 +115,21 @@ const getConfig = () => {
|
||||
formState.Cron = res.data.cron;
|
||||
formState.Id = res.data.id;
|
||||
formState.BatchCount = res.data.batchCount;
|
||||
// 下载配置
|
||||
formState.EnableAutoDownload = res.data.enableAutoDownload ?? false;
|
||||
formState.DownloadDaysRange = res.data.downloadDaysRange ?? 0;
|
||||
try {
|
||||
formState.AuthorWhitelist = res.data.authorWhitelist ? JSON.parse(res.data.authorWhitelist).join('\n') : '';
|
||||
} catch {
|
||||
formState.AuthorWhitelist = '';
|
||||
}
|
||||
try {
|
||||
formState.AuthorBlacklist = res.data.authorBlacklist ? JSON.parse(res.data.authorBlacklist).join('\n') : '';
|
||||
} catch {
|
||||
formState.AuthorBlacklist = '';
|
||||
}
|
||||
formState.DownloadFavorite = res.data.downloadFavorite ?? false;
|
||||
formState.DownloadCollect = res.data.downloadCollect ?? false;
|
||||
} else {
|
||||
message.error(res.erro, 8);
|
||||
}
|
||||
@@ -79,12 +147,32 @@ const onSubmit = () => {
|
||||
.validate()
|
||||
.then(() => {
|
||||
console.log('values', formState, toRaw(formState));
|
||||
// 处理作者白名单和黑名单,转换为JSON数组
|
||||
const submitData = { ...toRaw(formState) };
|
||||
if (submitData.AuthorWhitelist) {
|
||||
const whitelist = submitData.AuthorWhitelist.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
submitData.AuthorWhitelist = whitelist.length > 0 ? JSON.stringify(whitelist) : '';
|
||||
} else {
|
||||
submitData.AuthorWhitelist = '';
|
||||
}
|
||||
if (submitData.AuthorBlacklist) {
|
||||
const blacklist = submitData.AuthorBlacklist.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0);
|
||||
submitData.AuthorBlacklist = blacklist.length > 0 ? JSON.stringify(blacklist) : '';
|
||||
} else {
|
||||
submitData.AuthorBlacklist = '';
|
||||
}
|
||||
|
||||
useApiStore()
|
||||
.apiUpdateConfig(toRaw(formState))
|
||||
.apiUpdateConfig(submitData)
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
message.success('配置修改生效,同步任务将按照新的规则执行');
|
||||
componentDisabled.value = true;
|
||||
getConfig(); // 重新加载配置
|
||||
} else {
|
||||
message.error(res.erro, 8);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,19 @@
|
||||
<a-form-item label="抖音作者" ref="author" name="author">
|
||||
<a-input v-model:value="quaryData.author"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="视频类型">
|
||||
<a-select
|
||||
v-model:value="selectedCategories"
|
||||
mode="multiple"
|
||||
placeholder="请选择视频类型(可多选)"
|
||||
style="width: 300px"
|
||||
:options="categoryOptions"
|
||||
:loading="categoryLoading"
|
||||
allow-clear
|
||||
@change="onCategoryChanged"
|
||||
>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-radio-group v-model:value="quaryData.viedoType" button-style="solid" @change="onViedoTypeChanged">
|
||||
<a-radio-button value="*">全部</a-radio-button>
|
||||
@@ -15,69 +28,165 @@
|
||||
<a-radio-button value="3">关注的</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="删除状态">
|
||||
<a-select
|
||||
v-model:value="quaryData.isDeleted"
|
||||
placeholder="全部"
|
||||
style="width: 120px"
|
||||
allow-clear
|
||||
@change="handleQuery"
|
||||
>
|
||||
<a-select-option :value="undefined">全部</a-select-option>
|
||||
<a-select-option :value="false">未删除</a-select-option>
|
||||
<a-select-option :value="true">已删除</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="GetRecords">查询</a-button>
|
||||
<a-button type="primary" @click="handleQuery">查询</a-button>
|
||||
<a-button type="danger" @click="StartNow">立即重新同步</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-table :columns="columns" :data-source="dataSource" bordered :pagination="pagination" @change="handleTableChange" :loading="loading">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
bordered
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<!-- 如果视频文件不存在,显示"已删除" -->
|
||||
<span v-if="!record.videoSavePath" style="color: #999; font-size: 13px;">已删除</span>
|
||||
<!-- 如果视频文件存在,显示预览按钮 -->
|
||||
<a-button v-else type="link" size="small" @click="handlePreview(record)">
|
||||
<template #icon><PlayCircleOutlined /></template>
|
||||
预览
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 视频播放器 -->
|
||||
<VideoPlayer ref="videoPlayerRef" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, reactive, ref } from 'vue';
|
||||
import { defineComponent, reactive, ref, watch } from 'vue';
|
||||
import { useApiStore } from '@/store';
|
||||
import type { UnwrapRef } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import locale from 'ant-design-vue/es/date-picker/locale/zh_CN';
|
||||
import { PlayCircleOutlined } from '@ant-design/icons-vue';
|
||||
import VideoPlayer from '@/components/video/VideoPlayer.vue';
|
||||
type RangeValue = [Dayjs, Dayjs];
|
||||
import 'dayjs/locale/zh-cn';
|
||||
dayjs.locale('zh-cn');
|
||||
const columns = ref([
|
||||
{
|
||||
title: '同步时间',
|
||||
dataIndex: 'syncTimeStr',
|
||||
// sorter: true,
|
||||
align: 'center',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '同步类型',
|
||||
dataIndex: 'viedoTypeStr',
|
||||
// sorter: true,
|
||||
align: 'center',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '视频类型',
|
||||
dataIndex: 'viedoCate',
|
||||
// sorter: true,
|
||||
width: 300,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '作者',
|
||||
dataIndex: 'author',
|
||||
// sorter: true,
|
||||
align: 'center',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '视频名称',
|
||||
dataIndex: 'videoTitle',
|
||||
// sorter: true,
|
||||
align: 'left',
|
||||
},
|
||||
|
||||
const videoPlayerRef = ref<InstanceType<typeof VideoPlayer> | null>(null);
|
||||
|
||||
// 当前排序状态
|
||||
const currentSortField = ref<string | null>(null);
|
||||
const currentSortOrder = ref<'asc' | 'desc' | null>(null);
|
||||
|
||||
// 处理列头点击排序
|
||||
const handleColumnSort = (fieldKey: string) => {
|
||||
console.log('列头点击排序:', fieldKey);
|
||||
|
||||
// 如果点击的是当前排序列,切换排序方向;否则设置为升序
|
||||
if (currentSortField.value === fieldKey) {
|
||||
if (currentSortOrder.value === 'asc') {
|
||||
currentSortOrder.value = 'desc';
|
||||
} else if (currentSortOrder.value === 'desc') {
|
||||
// 第三次点击取消排序
|
||||
currentSortField.value = null;
|
||||
currentSortOrder.value = null;
|
||||
}
|
||||
} else {
|
||||
currentSortField.value = fieldKey;
|
||||
currentSortOrder.value = 'asc';
|
||||
}
|
||||
|
||||
// 更新查询参数
|
||||
quaryData.sortField = currentSortField.value;
|
||||
quaryData.sortOrder = currentSortOrder.value;
|
||||
|
||||
console.log('设置排序参数:', {
|
||||
sortField: quaryData.sortField,
|
||||
sortOrder: quaryData.sortOrder
|
||||
});
|
||||
|
||||
// 更新列配置以反映排序状态
|
||||
updateColumnSortState();
|
||||
|
||||
// 重置到第一页并重新查询
|
||||
pagination.value.current = 1;
|
||||
GetRecords();
|
||||
};
|
||||
|
||||
// 更新列配置的排序状态
|
||||
const updateColumnSortState = () => {
|
||||
columns.value.forEach((col: any) => {
|
||||
if (col.sorter && col.key) {
|
||||
if (currentSortField.value === col.key) {
|
||||
col.sorter.order = currentSortOrder.value === 'asc' ? 'ascend' : currentSortOrder.value === 'desc' ? 'descend' : null;
|
||||
} else {
|
||||
col.sorter.order = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
// 强制触发响应式更新
|
||||
columns.value = [...columns.value];
|
||||
};
|
||||
|
||||
// 监听排序状态变化,自动更新列配置
|
||||
watch([currentSortField, currentSortOrder], () => {
|
||||
updateColumnSortState();
|
||||
}, { immediate: true });
|
||||
|
||||
// 创建可排序列配置
|
||||
const createSortableColumn = (title: string, dataIndex: string, key: string, width?: number, align: string = 'left') => {
|
||||
const col: any = {
|
||||
title,
|
||||
dataIndex,
|
||||
key,
|
||||
width,
|
||||
align,
|
||||
sorter: {
|
||||
compare: () => 0, // 禁用前端排序
|
||||
multiple: false,
|
||||
order: null, // 初始化为 null,后续通过 updateColumnSortState 更新
|
||||
},
|
||||
customHeaderCell: () => {
|
||||
return {
|
||||
onClick: () => {
|
||||
handleColumnSort(key);
|
||||
},
|
||||
style: { cursor: 'pointer' }
|
||||
};
|
||||
},
|
||||
};
|
||||
return col;
|
||||
};
|
||||
|
||||
const columns = ref([
|
||||
createSortableColumn('同步时间', 'syncTimeStr', 'syncTime', 180, 'center'),
|
||||
createSortableColumn('同步类型', 'viedoTypeStr', 'viedoType', 100, 'center'),
|
||||
createSortableColumn('视频类型', 'viedoCate', 'viedoCate', 300, 'center'),
|
||||
createSortableColumn('作者', 'author', 'author', 150, 'center'),
|
||||
createSortableColumn('视频名称', 'videoTitle', 'videoTitle', undefined, 'left'),
|
||||
createSortableColumn('用户', 'dyUser', 'dyUser', 200, 'center'),
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'dyUser',
|
||||
// sorter: true,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
align: 'center',
|
||||
width: 200,
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
},
|
||||
]);
|
||||
const loading = ref(false);
|
||||
@@ -90,7 +199,11 @@ interface QuaryParam {
|
||||
pageSize: number;
|
||||
author: string;
|
||||
tag: string;
|
||||
tags?: string[];
|
||||
viedoType: string;
|
||||
isDeleted?: boolean | null;
|
||||
sortField?: string | null;
|
||||
sortOrder?: string | null;
|
||||
}
|
||||
|
||||
const value1 = ref<RangeValue>();
|
||||
@@ -104,26 +217,127 @@ const quaryData: UnwrapRef<QuaryParam> = reactive({
|
||||
pageSize: 20,
|
||||
author: '',
|
||||
tag: '',
|
||||
tags: undefined,
|
||||
viedoType: '*',
|
||||
isDeleted: undefined,
|
||||
sortField: null,
|
||||
sortOrder: null,
|
||||
});
|
||||
|
||||
// 视频类型相关
|
||||
const selectedCategories = ref<string[]>([]);
|
||||
const categoryOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const categoryLoading = ref(false);
|
||||
|
||||
// 加载视频类型列表
|
||||
const loadCategories = async () => {
|
||||
categoryLoading.value = true;
|
||||
try {
|
||||
console.log('开始加载视频类型列表...');
|
||||
const res = await useApiStore().VideoCategories();
|
||||
console.log('视频类型 API 响应(完整):', JSON.stringify(res, null, 2));
|
||||
console.log('视频类型 API 响应类型:', typeof res);
|
||||
console.log('视频类型 API 响应是否为数组:', Array.isArray(res));
|
||||
|
||||
// 处理不同的响应格式
|
||||
let categories: string[] = [];
|
||||
|
||||
if (Array.isArray(res)) {
|
||||
// 如果直接返回数组
|
||||
categories = res;
|
||||
} else if (res && typeof res === 'object') {
|
||||
// 如果是对象,尝试获取 data 字段
|
||||
if (res.code === 0 && res.data) {
|
||||
categories = Array.isArray(res.data) ? res.data : [];
|
||||
} else if (Array.isArray(res.data)) {
|
||||
categories = res.data;
|
||||
} else if (res.code === 0 && Array.isArray(res)) {
|
||||
categories = res as any;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('提取的视频类型数据:', categories);
|
||||
|
||||
if (categories && categories.length > 0) {
|
||||
categoryOptions.value = categories.map(cat => ({
|
||||
label: cat || '其他',
|
||||
value: cat || '其他',
|
||||
}));
|
||||
console.log('视频类型选项:', categoryOptions.value);
|
||||
} else {
|
||||
console.warn('视频类型数据为空,可能原因:1. 数据库中没有视频数据 2. 所有视频的 Tag1 和 Tag2 都为空');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载视频类型列表失败:', err);
|
||||
} finally {
|
||||
categoryLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 视频类型选择变化
|
||||
const onCategoryChanged = (values: string[]) => {
|
||||
selectedCategories.value = values;
|
||||
if (values && values.length > 0) {
|
||||
quaryData.tags = values;
|
||||
quaryData.tag = ''; // 清空单标签筛选
|
||||
} else {
|
||||
quaryData.tags = undefined;
|
||||
}
|
||||
pagination.value.current = 1;
|
||||
GetRecords();
|
||||
};
|
||||
// 查询处理函数:先刷新类型列表,再查询记录
|
||||
const handleQuery = async () => {
|
||||
// 先刷新视频类型列表(确保包含最新的类型)
|
||||
await loadCategories();
|
||||
// 然后查询记录
|
||||
GetRecords();
|
||||
};
|
||||
|
||||
const GetRecords = () => {
|
||||
loading.value = true;
|
||||
quaryData.pageIndex = pagination.value.current;
|
||||
quaryData.pageSize = pagination.value.defaultPageSize;
|
||||
|
||||
// 构建请求参数,确保排序参数被包含
|
||||
const requestParams = {
|
||||
...quaryData,
|
||||
sortField: quaryData.sortField || null,
|
||||
sortOrder: quaryData.sortOrder || null,
|
||||
};
|
||||
|
||||
console.log('发送查询请求,参数:', JSON.stringify(requestParams, null, 2));
|
||||
|
||||
useApiStore()
|
||||
.VideoPageList(quaryData)
|
||||
.VideoPageList(requestParams)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
console.log('查询响应:', res);
|
||||
if (res.code === 0) {
|
||||
console.log('接收到的数据:', res.data.data);
|
||||
console.log('数据条数:', res.data.data?.length);
|
||||
if (res.data.data && res.data.data.length > 0) {
|
||||
console.log('第一条数据的同步时间:', res.data.data[0].syncTimeStr);
|
||||
console.log('最后一条数据的同步时间:', res.data.data[res.data.data.length - 1].syncTimeStr);
|
||||
}
|
||||
|
||||
dataSource.value = res.data.data;
|
||||
pagination.value.current = res.data.pageIndex;
|
||||
pagination.value.defaultPageSize = res.data.pageSize;
|
||||
pagination.value.total = res.data.total;
|
||||
pagination.value.showTotal = () => `共 ${res.data.total} 条`;
|
||||
|
||||
// 数据加载完成后,确保排序状态正确显示
|
||||
updateColumnSortState();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
loading.value = false;
|
||||
console.error('查询失败:', err);
|
||||
});
|
||||
};
|
||||
onMounted(() => {
|
||||
loadCategories(); // 加载视频类型列表
|
||||
GetRecords();
|
||||
});
|
||||
const pagination = ref({
|
||||
@@ -133,12 +347,88 @@ const pagination = ref({
|
||||
showTotal: () => `共 ${0} 条`,
|
||||
});
|
||||
|
||||
const handleTableChange = (e) => {
|
||||
console.log(e);
|
||||
const handleTableChange = (e: any) => {
|
||||
console.log('表格变化事件:', e);
|
||||
console.log('事件对象键:', Object.keys(e));
|
||||
console.log('sorter 属性:', e.sorter);
|
||||
|
||||
// 处理分页变化
|
||||
if (e.current !== undefined) {
|
||||
pagination.value.current = e.current;
|
||||
pagination.value.defaultPageSize = e.defaultPageSize;
|
||||
}
|
||||
if (e.pageSize !== undefined) {
|
||||
pagination.value.defaultPageSize = e.pageSize;
|
||||
}
|
||||
if (e.total !== undefined) {
|
||||
pagination.value.total = e.total;
|
||||
pagination.value.showTotal = () => `共 ${e.total} 条`;
|
||||
}
|
||||
|
||||
// 处理排序变化(Ant Design Vue 3.x 中排序通过 change 事件的 sorter 属性传递)
|
||||
if (e.sorter) {
|
||||
console.log('检测到排序变化:', e.sorter);
|
||||
handleSorterChange(e.sorter);
|
||||
} else {
|
||||
// 如果没有排序变化,直接查询
|
||||
console.log('无排序变化,直接查询');
|
||||
GetRecords();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理排序变化
|
||||
const handleSorterChange = (sorter: any) => {
|
||||
console.log('排序变化事件:', sorter);
|
||||
console.log('排序变化类型:', typeof sorter, Array.isArray(sorter) ? '数组' : '对象');
|
||||
console.log('当前查询参数:', quaryData);
|
||||
|
||||
// Ant Design Vue 3.x 的 sorterChange 可能传递单个对象或对象数组
|
||||
let activeSorter: any = null;
|
||||
|
||||
if (Array.isArray(sorter)) {
|
||||
// 如果是数组,找到第一个有 order 的排序器
|
||||
activeSorter = sorter.find((s: any) => s && s.order);
|
||||
} else if (sorter && typeof sorter === 'object') {
|
||||
// 如果是单个对象
|
||||
activeSorter = sorter;
|
||||
}
|
||||
|
||||
console.log('活动排序器:', activeSorter);
|
||||
|
||||
if (!activeSorter || !activeSorter.field || !activeSorter.order) {
|
||||
// 没有排序或取消排序
|
||||
quaryData.sortField = null;
|
||||
quaryData.sortOrder = null;
|
||||
console.log('取消排序');
|
||||
} else {
|
||||
// 映射前端字段名(key)到后端字段名
|
||||
const fieldMap: Record<string, string> = {
|
||||
'syncTime': 'syncTime',
|
||||
'viedoType': 'viedoType',
|
||||
'viedoCate': 'viedoCate',
|
||||
'author': 'author',
|
||||
'videoTitle': 'videoTitle',
|
||||
'dyUser': 'dyUser',
|
||||
};
|
||||
|
||||
const field = activeSorter.field;
|
||||
const order = activeSorter.order;
|
||||
const backendField = fieldMap[field] || field;
|
||||
|
||||
// 有排序:ascend -> asc, descend -> desc
|
||||
quaryData.sortField = backendField;
|
||||
quaryData.sortOrder = order === 'ascend' ? 'asc' : 'desc';
|
||||
|
||||
console.log('设置排序参数:', {
|
||||
field: field,
|
||||
backendField: backendField,
|
||||
order: order,
|
||||
sortField: quaryData.sortField,
|
||||
sortOrder: quaryData.sortOrder
|
||||
});
|
||||
}
|
||||
|
||||
// 重置到第一页并重新查询
|
||||
pagination.value.current = 1;
|
||||
GetRecords();
|
||||
};
|
||||
|
||||
@@ -159,6 +449,36 @@ const onViedoTypeChanged = (e) => {
|
||||
pagination.value.current = 1;
|
||||
GetRecords();
|
||||
};
|
||||
|
||||
// 预览视频
|
||||
const handlePreview = (record: any) => {
|
||||
console.log('handlePreview called', record);
|
||||
|
||||
if (!record.id) {
|
||||
console.error('视频记录没有 id', record);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!videoPlayerRef.value) {
|
||||
console.error('videoPlayerRef.value 为 null,组件可能未正确挂载');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('调用 videoPlayerRef.value.open');
|
||||
try {
|
||||
videoPlayerRef.value.open(record.id, {
|
||||
id: record.id,
|
||||
videoTitle: record.videoTitle || '未知标题',
|
||||
author: record.author || '未知作者',
|
||||
viedoCate: record.viedoCate || '未分类',
|
||||
resolution: record.resolution || '未知',
|
||||
fileSize: record.fileSize || 0,
|
||||
videoSavePath: record.videoSavePath,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('打开视频播放器失败:', error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
+1119
-16
File diff suppressed because it is too large
Load Diff
@@ -138,6 +138,34 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// 清理数据库中文件不存在的视频记录
|
||||
async function VideoCleanup() {
|
||||
return http.request<any, Response<any>>('/api/video/cleanup', 'post_json').then(r => {
|
||||
return r.data;
|
||||
}).finally(() => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有视频类型列表
|
||||
async function VideoCategories() {
|
||||
return http.request<any, Response<any>>('/api/video/categories', 'get').then(r => {
|
||||
console.log('VideoCategories 原始响应:', r);
|
||||
// 响应拦截器已经处理了 code === 0 的情况,直接返回 data
|
||||
// 如果 r 本身就是 Response 对象,返回 r.data
|
||||
// 如果 r 已经是 data(拦截器处理后的结果),直接返回
|
||||
if (r && typeof r === 'object' && 'code' in r && 'data' in r) {
|
||||
return r.data;
|
||||
}
|
||||
return r;
|
||||
}).catch(err => {
|
||||
console.error('VideoCategories API 错误:', err);
|
||||
throw err;
|
||||
}).finally(() => {
|
||||
|
||||
});
|
||||
}
|
||||
//cookies
|
||||
async function CookiePageList(param: object) {
|
||||
return http.request<any, Response<any>>('/api/config/paged', 'post_json', param).then(r => {
|
||||
@@ -163,6 +191,15 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
});
|
||||
}
|
||||
|
||||
// 删除指定作者的所有视频数据
|
||||
async function DeleteAuthorVideos(authorName: string) {
|
||||
return http.request<any, Response<any>>('/api/video/delete-author', 'post_json', { authorName }).then(r => {
|
||||
return r.data;
|
||||
}).finally(() => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
deleteCookie,
|
||||
UpdateConfig,
|
||||
@@ -176,6 +213,9 @@ export const useApiStore = defineStore('coreapi', () => {
|
||||
StartJobNow,
|
||||
VideoStatics,
|
||||
VideoPageList,
|
||||
CookiePageList
|
||||
VideoCategories,
|
||||
VideoCleanup,
|
||||
CookiePageList,
|
||||
DeleteAuthorVideos
|
||||
};
|
||||
});
|
||||
|
||||
@@ -41,13 +41,20 @@ http.interceptors.response.use(
|
||||
(error) => {
|
||||
// console.log(error)
|
||||
// debugger
|
||||
if (error.response.status === 401) {
|
||||
useAccountStore().setLogged(false)
|
||||
if (error.response && error.response.status === 401) {
|
||||
useAccountStore().setLogged(false);
|
||||
return Promise.reject({
|
||||
message: '未授权,请重新登录',
|
||||
code: 401,
|
||||
error: 'Unauthorized',
|
||||
data: error.response.data,
|
||||
});
|
||||
} else {
|
||||
if (error.response && isAxiosResponse(error.response)) {
|
||||
return Promise.reject({
|
||||
message: error.response.statusText,
|
||||
code: error.response.status,
|
||||
error: error.response.data?.error || error.response.statusText,
|
||||
data: error.response.data,
|
||||
});
|
||||
}
|
||||
|
||||
+29
-13
@@ -83,20 +83,18 @@ function toUrlencoded(params?: Record<string | number, any>) {
|
||||
function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
|
||||
const _axios = axios.create(config);
|
||||
|
||||
// 添加响应拦截器处理401状态
|
||||
_axios.interceptors.response.use(
|
||||
// 成功响应直接返回
|
||||
response => response,
|
||||
// 错误响应处理
|
||||
error => {
|
||||
// 检查是否是401未授权错误
|
||||
if (error.response && error.response.status === 401) {
|
||||
// 调用removeAuthorization方法清除token
|
||||
http.removeAuthorization();
|
||||
|
||||
// 这里可以添加额外的处理,比如跳转到登录页
|
||||
// 示例: window.location.href = '/login';
|
||||
// 添加请求拦截器,自动添加Authorization header
|
||||
_axios.interceptors.request.use(
|
||||
(config) => {
|
||||
// 从Cookie中读取Authorization token
|
||||
const token = Cookie.get(config.xsrfCookieName || 'Authorization');
|
||||
if (token && config.headers) {
|
||||
// 将token添加到请求头
|
||||
config.headers[config.xsrfHeaderName || 'Authorization'] = token;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
@@ -155,6 +153,24 @@ function createAxiosHttp(config: AxiosRequestConfig): AxiosHttp {
|
||||
},
|
||||
};
|
||||
|
||||
// 添加响应拦截器处理401状态(在http对象创建后)
|
||||
_axios.interceptors.response.use(
|
||||
// 成功响应直接返回
|
||||
response => response,
|
||||
// 错误响应处理
|
||||
error => {
|
||||
// 检查是否是401未授权错误
|
||||
if (error.response && error.response.status === 401) {
|
||||
// 清除token
|
||||
http.removeAuthorization();
|
||||
|
||||
// 这里可以添加额外的处理,比如跳转到登录页
|
||||
// 示例: window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
return http;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -34,10 +34,10 @@ export default ({ command, mode }) => {
|
||||
open: false, //自动打开
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: env.VITE_API_URL,
|
||||
target: env.VITE_API_URL || 'http://localhost:10101',
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\//, ''),
|
||||
// 不重写路径,保持 /api 前缀
|
||||
},
|
||||
},
|
||||
hmr: true,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
DB_PATH="/Users/gaotu/WebstormProjects/dysync.net/db/dy.sqlite"
|
||||
|
||||
echo "=== 数据库检查 ==="
|
||||
echo ""
|
||||
|
||||
echo "1. 视频记录总数:"
|
||||
sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM dy_collect_video;"
|
||||
|
||||
echo ""
|
||||
echo "2. 有保存路径的视频数量:"
|
||||
sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM dy_collect_video WHERE videoSavePath IS NOT NULL AND videoSavePath != '';"
|
||||
|
||||
echo ""
|
||||
echo "3. Cookie配置数量:"
|
||||
sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM dy_user_cookies;"
|
||||
|
||||
echo ""
|
||||
echo "4. Cookie配置详情:"
|
||||
sqlite3 -header -column "$DB_PATH" "SELECT id, userName, savePath, favSavePath, uperSavePath, collHasSyncd FROM dy_user_cookies LIMIT 5;"
|
||||
|
||||
echo ""
|
||||
echo "5. 视频记录示例(前3条):"
|
||||
sqlite3 -header -column "$DB_PATH" "SELECT id, videoTitle, author, videoSavePath, fileSize FROM dy_collect_video LIMIT 3;"
|
||||
|
||||
echo ""
|
||||
echo "6. 检查视频文件是否存在:"
|
||||
sqlite3 "$DB_PATH" "SELECT videoSavePath FROM dy_collect_video WHERE videoSavePath IS NOT NULL AND videoSavePath != '' LIMIT 1;" | while read path; do
|
||||
if [ -n "$path" ]; then
|
||||
echo "检查: $path"
|
||||
if [ -f "$path" ]; then
|
||||
echo "✅ 文件存在"
|
||||
ls -lh "$path" | awk '{print " 大小: " $5}'
|
||||
else
|
||||
echo "❌ 文件不存在"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# 安装 ASP.NET Core 6.0 x64 Runtime
|
||||
|
||||
echo "正在下载并安装 ASP.NET Core 6.0 x64 Runtime..."
|
||||
echo "需要管理员权限,请输入您的密码:"
|
||||
|
||||
curl -sSL https://dot.net/v1/dotnet-install.sh | sudo bash /dev/stdin --runtime aspnetcore --version 6.0.36 --arch x64 --install-dir /usr/local/share/dotnet/x64
|
||||
|
||||
echo ""
|
||||
echo "安装完成!验证安装:"
|
||||
export PATH="/usr/local/share/dotnet/x64:$PATH"
|
||||
dotnet --list-runtimes
|
||||
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# 安装 .NET 6.0 x64 Runtime
|
||||
|
||||
echo "正在下载并安装 .NET 6.0 x64 Runtime..."
|
||||
echo "需要管理员权限,请输入您的密码:"
|
||||
|
||||
curl -sSL https://dot.net/v1/dotnet-install.sh | sudo bash /dev/stdin --runtime dotnet --version 6.0.36 --arch x64 --install-dir /usr/local/share/dotnet/x64
|
||||
|
||||
echo ""
|
||||
echo "安装完成!验证安装:"
|
||||
export PATH="/usr/local/share/dotnet/x64:$PATH"
|
||||
dotnet --list-runtimes
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""检查数据库中的视频数据"""
|
||||
|
||||
import sqlite3
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
db_path = "/Users/gaotu/WebstormProjects/dysync.net/db/dy.sqlite"
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"❌ 数据库文件不存在: {db_path}")
|
||||
exit(1)
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
print("=" * 60)
|
||||
print("数据库检查报告")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 视频总数
|
||||
cursor.execute("SELECT COUNT(*) FROM dy_collect_video")
|
||||
total = cursor.fetchone()[0]
|
||||
print(f"\n1. 视频记录总数: {total}")
|
||||
|
||||
# 2. 有保存路径的视频数量
|
||||
cursor.execute("SELECT COUNT(*) FROM dy_collect_video WHERE videoSavePath IS NOT NULL AND videoSavePath != ''")
|
||||
with_path = cursor.fetchone()[0]
|
||||
print(f"2. 有保存路径的视频数量: {with_path}")
|
||||
|
||||
# 3. 按类型统计
|
||||
cursor.execute("SELECT ViedoType, COUNT(*) FROM dy_collect_video GROUP BY ViedoType")
|
||||
type_stats = cursor.fetchall()
|
||||
print(f"\n3. 按类型统计:")
|
||||
for vtype, count in type_stats:
|
||||
type_name = {"1": "喜欢的", "2": "收藏的", "3": "关注的"}.get(str(vtype), f"未知({vtype})")
|
||||
print(f" {type_name}: {count}")
|
||||
|
||||
# 4. 文件大小统计
|
||||
cursor.execute("SELECT SUM(fileSize) FROM dy_collect_video WHERE fileSize > 0")
|
||||
total_size = cursor.fetchone()[0] or 0
|
||||
print(f"\n4. 总文件大小: {total_size / (1024**3):.2f} GB")
|
||||
|
||||
# 5. 最近的视频记录
|
||||
print(f"\n5. 最近的视频记录(前5条):")
|
||||
cursor.execute("""
|
||||
SELECT id, videoTitle, author, videoSavePath, fileSize, syncTime
|
||||
FROM dy_collect_video
|
||||
ORDER BY syncTime DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
records = cursor.fetchall()
|
||||
if records:
|
||||
for idx, (vid, title, author, path, size, sync_time) in enumerate(records, 1):
|
||||
title_short = (title[:30] + "...") if title and len(title) > 30 else (title or "无标题")
|
||||
size_mb = (size / (1024**2)) if size else 0
|
||||
file_exists = "✅" if path and os.path.exists(path) else "❌"
|
||||
print(f" {idx}. [{file_exists}] {title_short}")
|
||||
print(f" 作者: {author}, 大小: {size_mb:.2f} MB")
|
||||
if path:
|
||||
print(f" 路径: {path}")
|
||||
else:
|
||||
print(" (无记录)")
|
||||
|
||||
# 6. 检查文件是否存在
|
||||
print(f"\n6. 文件存在性检查:")
|
||||
cursor.execute("SELECT videoSavePath FROM dy_collect_video WHERE videoSavePath IS NOT NULL AND videoSavePath != '' LIMIT 10")
|
||||
paths = cursor.fetchall()
|
||||
if paths:
|
||||
exists_count = 0
|
||||
for (path,) in paths:
|
||||
if path and os.path.exists(path):
|
||||
exists_count += 1
|
||||
print(f" 检查了 {len(paths)} 个路径,{exists_count} 个文件存在")
|
||||
else:
|
||||
print(" (无路径记录)")
|
||||
|
||||
conn.close()
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
@@ -7,12 +7,30 @@ namespace dy.net.dto
|
||||
// 3. 引用类型(string)如果允许为null,显式声明为 string?
|
||||
public string? Tag { get; set; }
|
||||
|
||||
// 支持多个标签筛选
|
||||
public List<string>? Tags { get; set; }
|
||||
|
||||
public string? Author { get; set; }
|
||||
|
||||
public string? ViedoType { get; set; }
|
||||
|
||||
// 4. 泛型集合(List<string>)允许为null,声明为 List<string>?
|
||||
public List<string>? Dates { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 删除状态筛选:null=全部,true=已删除(videoSavePath为空),false=未删除(videoSavePath不为空)
|
||||
/// </summary>
|
||||
public bool? IsDeleted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序字段:syncTime-同步时间, viedoType-同步类型, viedoCate-视频类型, author-作者, videoTitle-视频名称, dyUser-用户
|
||||
/// </summary>
|
||||
public string? SortField { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序方向:asc-升序, desc-降序
|
||||
/// </summary>
|
||||
public string? SortOrder { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -95,8 +95,22 @@ namespace dy.net.extension
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
context.Token = context.Request.Headers["Authorization"]
|
||||
// 优先从 Header 读取
|
||||
var token = context.Request.Headers["Authorization"]
|
||||
.FirstOrDefault()?.Split(" ").Last();
|
||||
|
||||
// 如果 Header 中没有,尝试从 Cookie 读取
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = context.Request.Cookies["Authorization"];
|
||||
// Cookie 中可能包含 "Bearer " 前缀,需要移除
|
||||
if (!string.IsNullOrEmpty(token) && token.StartsWith("Bearer "))
|
||||
{
|
||||
token = token.Substring(7);
|
||||
}
|
||||
}
|
||||
|
||||
context.Token = token;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
|
||||
+59
-44
@@ -82,6 +82,7 @@ namespace dy.net.job
|
||||
|
||||
while (hasMore)
|
||||
{
|
||||
Serilog.Log.Debug($"collect-正在获取第 {index + 1} 页数据,cursor: {cursor}");
|
||||
var data = await _douyinService.SyncCollectVideos(cursor, count, cookie.Cookies);
|
||||
hasMore = data != null && data.HasMore == 1 && cookie.CollHasSyncd == 0;
|
||||
if (cookie.CollHasSyncd == 1)
|
||||
@@ -94,11 +95,15 @@ namespace dy.net.job
|
||||
Serilog.Log.Debug($"collect-Cookie[{cookie.UserName}]获取收藏数据失败,请检查一下Cookie");
|
||||
break;
|
||||
}
|
||||
|
||||
Serilog.Log.Debug($"collect-API返回: HasMore={data.HasMore}, AwemeList数量={data.AwemeList?.Count ?? 0}");
|
||||
|
||||
cursor = data != null && !string.IsNullOrWhiteSpace(data.Cursor) ? data.Cursor : "0";
|
||||
|
||||
|
||||
if (data.AwemeList == null || !data.AwemeList.Any())
|
||||
{
|
||||
Serilog.Log.Debug($"collect-第 {index + 1} 页数据为空,结束同步");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -127,29 +132,14 @@ namespace dy.net.job
|
||||
var fileName = $"{item.AwemeId}.{v.Format}"; // 用ID做文件名,避免特殊字符
|
||||
var savePath = Path.Combine(saveFolder, fileName);
|
||||
|
||||
if (!File.Exists(savePath))
|
||||
{
|
||||
Serilog.Log.Debug($"collect-视频[{SanitizePath(item.Desc)}]开始下载");
|
||||
await Task.Delay(_random.Next(1, 4) * 1000);
|
||||
var downVideo = await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies);
|
||||
// 检查是否应该下载(根据配置)
|
||||
var videoCreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime);
|
||||
var downloadConfigService = new DownloadConfigService(commonService);
|
||||
var shouldDownload = downloadConfigService.ShouldDownload("2", item.Author?.Uid, item.Author?.Nickname, videoCreateTime);
|
||||
|
||||
if (downVideo)
|
||||
{
|
||||
Serilog.Log.Debug($"collect-视频[{SanitizePath(item.Desc)}]下载{(downVideo ? "成功" : "失败")}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error($"collect-视频[{SanitizePath(item.Desc)}]下载{(downVideo ? "成功" : "失败")}");
|
||||
|
||||
}
|
||||
if (downVideo)
|
||||
{
|
||||
await DownVideoCover(item, saveFolder, cookie.Cookies);
|
||||
|
||||
// 用AuthorId做文件名,避免昵称特殊字符
|
||||
// 获取作者头像信息(无论是否下载都需要)
|
||||
var avatarImgName = $"{item.Author.Uid}.jpg";
|
||||
var avatarSavePath = Path.Combine(cookie.SavePath, "author", avatarImgName);
|
||||
await DownAuthorAvatar(cookie.SavePath, item, avatarSavePath, cookie.Cookies);
|
||||
var avatarUrl = item.Author?.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
|
||||
? item.Author.AvatarLarger.UrlList[0] : null;
|
||||
// 备用头像链接
|
||||
@@ -159,9 +149,51 @@ namespace dy.net.job
|
||||
? item.Author.AvatarThumb.UrlList[0] : null;
|
||||
}
|
||||
|
||||
// 下载视频和封面(如果配置允许)
|
||||
string finalSavePath = null;
|
||||
string finalCoverSavePath = null;
|
||||
if (!File.Exists(savePath) && shouldDownload)
|
||||
{
|
||||
Serilog.Log.Debug($"collect-视频[{SanitizePath(item.Desc)}]开始下载");
|
||||
await Task.Delay(_random.Next(1, 4) * 1000);
|
||||
var downVideo = await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies);
|
||||
|
||||
if (downVideo)
|
||||
{
|
||||
Serilog.Log.Debug($"collect-视频[{SanitizePath(item.Desc)}]下载成功");
|
||||
await DownVideoCover(item, saveFolder, cookie.Cookies);
|
||||
await DownAuthorAvatar(cookie.SavePath, item, avatarSavePath, cookie.Cookies);
|
||||
finalSavePath = savePath;
|
||||
finalCoverSavePath = Path.Combine(saveFolder, "poster.jpg");
|
||||
|
||||
// 构造视频数据
|
||||
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
|
||||
NfoFileGenerator.GenerateNfoFile(new VideoNfo
|
||||
{
|
||||
Author = item.Author?.Nickname,
|
||||
Poster = Path.Combine(saveFolder, "poster.jpg"),
|
||||
Title = item.Desc,
|
||||
Thumbnail = Path.Combine(saveFolder, "fanart.jpg"),
|
||||
ReleaseDate = videoCreateTime,
|
||||
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
||||
}, nfoPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error($"collect-视频[{SanitizePath(item.Desc)}]下载失败");
|
||||
}
|
||||
}
|
||||
else if (!shouldDownload)
|
||||
{
|
||||
Serilog.Log.Debug($"collect-视频[{SanitizePath(item.Desc)}]不符合下载配置,仅同步信息到数据库");
|
||||
}
|
||||
else if (File.Exists(savePath))
|
||||
{
|
||||
// 文件已存在,使用现有路径
|
||||
finalSavePath = savePath;
|
||||
finalCoverSavePath = Path.Combine(saveFolder, "poster.jpg");
|
||||
}
|
||||
|
||||
// 无论是否下载,都保存视频信息到数据库
|
||||
DyCollectVideo video = new()
|
||||
{
|
||||
ViedoType = "2",
|
||||
@@ -170,7 +202,7 @@ namespace dy.net.job
|
||||
AuthorId = item.Author?.Uid,
|
||||
AuthorAvatar = avatarSavePath,
|
||||
AuthorAvatarUrl = avatarUrl,
|
||||
CreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
|
||||
CreateTime = videoCreateTime,
|
||||
VideoTitle = item.Desc,
|
||||
Id = IdGener.GetLong().ToString(),
|
||||
Resolution = $"{v.PlayAddr.Width}×{v.PlayAddr.Height}",
|
||||
@@ -182,40 +214,23 @@ namespace dy.net.job
|
||||
VideoUrl = videoUrl,
|
||||
VideoCoverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
|
||||
? item.Video.Cover.UrlList[0] : null,
|
||||
VideoSavePath = savePath,
|
||||
VideoCoverSavePath = Path.Combine(saveFolder, "poster.jpg"),
|
||||
VideoSavePath = finalSavePath, // 如果未下载则为 null
|
||||
VideoCoverSavePath = finalCoverSavePath, // 如果未下载则为 null
|
||||
SyncTime = DateTime.Now,
|
||||
DyUserId = data.Uid,
|
||||
CookieId = cookie.Id
|
||||
};
|
||||
videos.Add(video);
|
||||
|
||||
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
|
||||
NfoFileGenerator.GenerateNfoFile(new VideoNfo
|
||||
{
|
||||
Author = video.Author,
|
||||
Poster = Path.Combine(saveFolder, "poster.jpg"),
|
||||
Title = video.VideoTitle,
|
||||
Thumbnail = Path.Combine(saveFolder, "fanart.jpg"),
|
||||
ReleaseDate = video.CreateTime,
|
||||
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
||||
}, nfoPath);
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Serilog.Log.Debug($"视频[{item.AwemeId}]已存在,跳过下载");
|
||||
}
|
||||
}
|
||||
index++;
|
||||
// 批量保存到数据库(减少数据库操作频率)
|
||||
if (videos.Any())
|
||||
{
|
||||
//Serilog.Log.Debug($"处理Cookie[{cookie.UserName}]的第{index}页数据,共{videos.Count}条");
|
||||
Serilog.Log.Debug($"collect-处理Cookie[{cookie.UserName}]的第{index}页数据,共{videos.Count}条,准备保存到数据库");
|
||||
try
|
||||
{
|
||||
await _douyinVideoService.batchInsert(videos);
|
||||
var insertResult = await _douyinVideoService.batchInsert(videos);
|
||||
Serilog.Log.Debug($"collect-batchInsert返回结果: {insertResult}, 本次处理{videos.Count}条视频");
|
||||
syncCount += videos.Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
+60
-44
@@ -79,6 +79,7 @@ namespace dy.net.job
|
||||
string cursor = "0";
|
||||
while (hasMore)
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-正在获取第 {index + 1} 页数据,cursor: {cursor}, secUserId: {cookie.SecUserId}");
|
||||
var data = await _douyinService.SyncFavoriteVideos(count,cursor, cookie.SecUserId, cookie.Cookies);
|
||||
hasMore = data != null && data.HasMore == 1 && cookie.FavHasSyncd == 0;
|
||||
if (cookie.FavHasSyncd == 1)
|
||||
@@ -91,10 +92,14 @@ namespace dy.net.job
|
||||
Serilog.Log.Debug($"favorite-Cookie[{cookie.UserName}]获取喜欢的数据失败,请检查一下Cookie和sec_user_id");
|
||||
break;
|
||||
}
|
||||
|
||||
Serilog.Log.Debug($"favorite-API返回: HasMore={data.HasMore}, AwemeList数量={data.AwemeList?.Count ?? 0}");
|
||||
|
||||
cursor = data != null && !string.IsNullOrWhiteSpace(data.MaxCursor) ? data.MaxCursor : "0";
|
||||
|
||||
if (data.AwemeList == null || !data.AwemeList.Any())
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-第 {index + 1} 页数据为空,结束同步");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -122,28 +127,14 @@ namespace dy.net.job
|
||||
var fileName = $"{item.AwemeId}.{v.Format}"; // 用ID做文件名,避免特殊字符
|
||||
var savePath = Path.Combine(saveFolder, fileName);
|
||||
|
||||
if (!File.Exists(savePath))
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-视频[{SanitizePath(item.Desc)}]开始下载");
|
||||
await Task.Delay(_random.Next(1, 4) * 1000);
|
||||
var downVideo = await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies);
|
||||
// 检查是否应该下载(根据配置)
|
||||
var videoCreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime);
|
||||
var downloadConfigService = new DownloadConfigService(commonService);
|
||||
var shouldDownload = downloadConfigService.ShouldDownload("1", item.Author?.Uid, item.Author?.Nickname, videoCreateTime);
|
||||
|
||||
if (downVideo)
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-视频[{SanitizePath(item.Desc)}]下载{(downVideo ? "成功" : "失败")}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error($"favorite-视频[{SanitizePath(item.Desc)}]下载{(downVideo ? "成功" : "失败")}");
|
||||
}
|
||||
if (downVideo)
|
||||
{
|
||||
await DownVideoCover(item, saveFolder, cookie.Cookies);
|
||||
|
||||
// 用AuthorId做文件名,避免昵称特殊字符
|
||||
// 获取作者头像信息(无论是否下载都需要)
|
||||
var avatarImgName = $"{item.Author.Uid}.jpg";
|
||||
var avatarSavePath = Path.Combine(cookie.FavSavePath, "author", avatarImgName);
|
||||
await DownAuthorAvatar(cookie.FavSavePath, item, avatarSavePath, cookie.Cookies);
|
||||
var avatarUrl = item.Author?.AvatarLarger?.UrlList != null && item.Author.AvatarLarger.UrlList.Any()
|
||||
? item.Author.AvatarLarger.UrlList[0] : null;
|
||||
// 尝试使用AvatarThumb作为备用头像URL
|
||||
@@ -153,18 +144,60 @@ namespace dy.net.job
|
||||
? item.Author.AvatarThumb.UrlList[0] : null;
|
||||
}
|
||||
|
||||
// 下载视频和封面(如果配置允许)
|
||||
string finalSavePath = null;
|
||||
string finalCoverSavePath = null;
|
||||
if (!File.Exists(savePath) && shouldDownload)
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-视频[{SanitizePath(item.Desc)}]开始下载");
|
||||
await Task.Delay(_random.Next(1, 4) * 1000);
|
||||
var downVideo = await _douyinService.DownloadAsync(videoUrl, savePath, cookie.Cookies);
|
||||
|
||||
if (downVideo)
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-视频[{SanitizePath(item.Desc)}]下载成功");
|
||||
await DownVideoCover(item, saveFolder, cookie.Cookies);
|
||||
await DownAuthorAvatar(cookie.FavSavePath, item, avatarSavePath, cookie.Cookies);
|
||||
finalSavePath = savePath;
|
||||
finalCoverSavePath = Path.Combine(saveFolder, "poster.jpg");
|
||||
|
||||
// 构造视频数据
|
||||
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
|
||||
NfoFileGenerator.GenerateNfoFile(new VideoNfo
|
||||
{
|
||||
Author = item.Author?.Nickname,
|
||||
Poster = Path.Combine(saveFolder, "poster.jpg"),
|
||||
Title = item.Desc,
|
||||
Thumbnail = Path.Combine(saveFolder, "fanart.jpg"),
|
||||
ReleaseDate = videoCreateTime,
|
||||
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
||||
}, nfoPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error($"favorite-视频[{SanitizePath(item.Desc)}]下载失败");
|
||||
}
|
||||
}
|
||||
else if (!shouldDownload)
|
||||
{
|
||||
Serilog.Log.Debug($"favorite-视频[{SanitizePath(item.Desc)}]不符合下载配置,仅同步信息到数据库");
|
||||
}
|
||||
else if (File.Exists(savePath))
|
||||
{
|
||||
// 文件已存在,使用现有路径
|
||||
finalSavePath = savePath;
|
||||
finalCoverSavePath = Path.Combine(saveFolder, "poster.jpg");
|
||||
}
|
||||
|
||||
// 无论是否下载,都保存视频信息到数据库
|
||||
DyCollectVideo video = new()
|
||||
{
|
||||
ViedoType="1",
|
||||
ViedoType = "1",
|
||||
AwemeId = item.AwemeId,
|
||||
Author = item.Author?.Nickname,
|
||||
AuthorId = item.Author?.Uid,
|
||||
AuthorAvatar = avatarSavePath,
|
||||
AuthorAvatarUrl = avatarUrl,
|
||||
CreateTime = DateTimeUtil.Convert10BitTimestamp(item.CreateTime),
|
||||
CreateTime = videoCreateTime,
|
||||
VideoTitle = item.Desc,
|
||||
Id = IdGener.GetLong().ToString(),
|
||||
Resolution = $"{v.PlayAddr.Width}×{v.PlayAddr.Height}",
|
||||
@@ -176,40 +209,23 @@ namespace dy.net.job
|
||||
VideoUrl = videoUrl,
|
||||
VideoCoverUrl = item.Video.Cover.UrlList != null && item.Video.Cover.UrlList.Any()
|
||||
? item.Video.Cover.UrlList[0] : null,
|
||||
VideoSavePath = savePath,
|
||||
VideoCoverSavePath = Path.Combine(saveFolder, "poster.jpg"),
|
||||
VideoSavePath = finalSavePath, // 如果未下载则为 null
|
||||
VideoCoverSavePath = finalCoverSavePath, // 如果未下载则为 null
|
||||
SyncTime = DateTime.Now,
|
||||
DyUserId = data.Uid,
|
||||
CookieId = cookie.Id
|
||||
};
|
||||
videos.Add(video);
|
||||
|
||||
var nfoPath = Path.Combine(saveFolder, $"{item.AwemeId}.nfo");
|
||||
NfoFileGenerator.GenerateNfoFile(new VideoNfo
|
||||
{
|
||||
Author = video.Author,
|
||||
Poster = Path.Combine(saveFolder, "poster.jpg"),
|
||||
Title = video.VideoTitle,
|
||||
Thumbnail= Path.Combine(saveFolder, "fanart.jpg"),
|
||||
ReleaseDate = video.CreateTime,
|
||||
Genres = new List<string> { tag1, tag2, tag3 }.Where(t => !string.IsNullOrWhiteSpace(t)).ToList()
|
||||
}, nfoPath);
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Serilog.Log.Debug($"视频[{item.AwemeId}]已存在,跳过下载");
|
||||
}
|
||||
}
|
||||
index++;
|
||||
// 批量保存到数据库(减少数据库操作频率)
|
||||
if (videos.Any())
|
||||
{
|
||||
//Serilog.Log.Debug($"处理Cookie[{cookie.UserName}]的第{index}页数据,共{videos.Count}条");
|
||||
Serilog.Log.Debug($"favorite-处理Cookie[{cookie.UserName}]的第{index}页数据,共{videos.Count}条,准备保存到数据库");
|
||||
try
|
||||
{
|
||||
await _douyinVideoService.batchInsert(videos);
|
||||
var insertResult = await _douyinVideoService.batchInsert(videos);
|
||||
Serilog.Log.Debug($"favorite-batchInsert返回结果: {insertResult}, 本次处理{videos.Count}条视频");
|
||||
syncCount += videos.Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -20,5 +20,41 @@ namespace dy.net.model
|
||||
/// 每次查询数量
|
||||
/// </summary>
|
||||
public int BatchCount { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用自动下载(默认false,不自动下载)
|
||||
/// </summary>
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public bool? EnableAutoDownload { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 下载时间范围(天数,例如:30表示只下载最近30天的视频,0表示不限制)
|
||||
/// </summary>
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public int? DownloadDaysRange { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 作者白名单(JSON数组,例如:["作者1","作者2"],为空则不限制)
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 2000, IsNullable = true)]
|
||||
public string AuthorWhitelist { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 作者黑名单(JSON数组,例如:["作者1","作者2"],为空则不限制)
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 2000, IsNullable = true)]
|
||||
public string AuthorBlacklist { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否下载喜欢的视频(默认false)
|
||||
/// </summary>
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public bool? DownloadFavorite { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 是否下载收藏的视频(默认false)
|
||||
/// </summary>
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public bool? DownloadCollect { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using dy.net.model;
|
||||
using SqlSugar;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace dy.net.repository
|
||||
{
|
||||
@@ -17,10 +18,14 @@ namespace dy.net.repository
|
||||
/// </summary>
|
||||
/// <param name="pageIndex">页码(从1开始)</param>
|
||||
/// <param name="pageSize">每页数量</param>
|
||||
/// <param name="tag">可选标签过滤</param>
|
||||
/// <param name="tag">可选标签过滤(单个,兼容旧版本)</param>
|
||||
/// <param name="tags">可选标签过滤(多个)</param>
|
||||
/// <param name="author">可选作者过滤</param>
|
||||
/// <param name="isDeleted">删除状态筛选:null=全部,true=已删除(videoSavePath为空),false=未删除(videoSavePath不为空)</param>
|
||||
/// <param name="sortField">排序字段:syncTime-同步时间, viedoType-同步类型, viedoCate-视频类型, author-作者, videoTitle-视频名称, dyUser-用户</param>
|
||||
/// <param name="sortOrder">排序方向:asc-升序, desc-降序</param>
|
||||
/// <returns>分页结果(视频列表和总数)</returns>
|
||||
public async Task<(List<DyCollectVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null,string viedoType=null, List<string> dates = null)
|
||||
public async Task<(List<DyCollectVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, List<string> tags = null, string author = null,string viedoType=null, List<string> dates = null, bool? isDeleted = null, string sortField = null, string sortOrder = null)
|
||||
{
|
||||
|
||||
DateTime? start = null;
|
||||
@@ -35,16 +40,206 @@ namespace dy.net.repository
|
||||
start = Convert.ToDateTime(dates[0]);
|
||||
}
|
||||
|
||||
var where = this.Db.Queryable<DyCollectVideo>()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(tag), x => x.Tag1 == tag)
|
||||
var where = this.Db.Queryable<DyCollectVideo>();
|
||||
|
||||
// 支持多个标签筛选(优先使用 tags,如果为空则使用 tag)
|
||||
// 注意:tags 匹配的是 viedoCate(Tag1/Tag2 组合),格式如 "美食/美食展示"
|
||||
// 特殊值 "未分类" 表示 Tag1 和 Tag2 都为空的视频
|
||||
if (tags != null && tags.Any())
|
||||
{
|
||||
// 过滤空值
|
||||
var validTags = tags.Where(t => !string.IsNullOrWhiteSpace(t)).ToList();
|
||||
if (validTags.Any())
|
||||
{
|
||||
// 检查是否包含"未分类"选项
|
||||
var hasEmptyCategory = validTags.Contains("未分类");
|
||||
var otherTags = validTags.Where(t => t != "未分类").ToList();
|
||||
|
||||
// 构建 OR 条件:对于每个 tag,匹配对应的 Tag1/Tag2 组合
|
||||
if (hasEmptyCategory && otherTags.Any())
|
||||
{
|
||||
// 同时包含"未分类"和其他类型
|
||||
// 使用内存过滤的方式,确保逻辑正确
|
||||
var matchingVideoIds = new List<string>();
|
||||
|
||||
// 获取所有视频(先应用其他筛选条件)
|
||||
var allVideos = await this.Db.Queryable<DyCollectVideo>()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(author), x => x.Author == author)
|
||||
.WhereIF(start.HasValue, x => x.SyncTime >= start.Value)
|
||||
.WhereIF(end.HasValue, x => x.SyncTime <= end.Value)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(viedoType) && viedoType != "*", x => x.ViedoType == viedoType);
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(viedoType) && viedoType != "*", x => x.ViedoType == viedoType)
|
||||
.WhereIF(isDeleted.HasValue && isDeleted.Value, x => string.IsNullOrWhiteSpace(x.VideoSavePath))
|
||||
.WhereIF(isDeleted.HasValue && !isDeleted.Value, x => !string.IsNullOrWhiteSpace(x.VideoSavePath))
|
||||
.ToListAsync();
|
||||
|
||||
// 在内存中过滤 viedoCate
|
||||
foreach (var video in allVideos)
|
||||
{
|
||||
var viedoCate = (string.IsNullOrWhiteSpace(video.Tag1) ? "" : video.Tag1) + "/" + (string.IsNullOrWhiteSpace(video.Tag2) ? "" : video.Tag2);
|
||||
|
||||
// 检查是否是"未分类"或匹配其他标签
|
||||
bool isMatch = (string.IsNullOrWhiteSpace(video.Tag1) && string.IsNullOrWhiteSpace(video.Tag2)) || // 未分类
|
||||
otherTags.Contains(viedoCate); // 其他类型
|
||||
|
||||
if (isMatch)
|
||||
{
|
||||
matchingVideoIds.Add(video.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingVideoIds.Any())
|
||||
{
|
||||
// 使用 ID 列表重新构建查询
|
||||
where = this.Db.Queryable<DyCollectVideo>()
|
||||
.Where(x => matchingVideoIds.Contains(x.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果没有匹配的数据,返回空结果
|
||||
where = this.Db.Queryable<DyCollectVideo>().Where(x => false);
|
||||
}
|
||||
}
|
||||
else if (hasEmptyCategory)
|
||||
{
|
||||
// 只选择了"未分类"
|
||||
where = where.Where(x => string.IsNullOrWhiteSpace(x.Tag1) && string.IsNullOrWhiteSpace(x.Tag2));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 只选择了其他类型,构建所有条件的 OR
|
||||
// 使用更简单的方法:先获取所有符合条件的视频 ID,然后使用 In 方法
|
||||
// 这样可以避免复杂的表达式树构建
|
||||
|
||||
var matchingVideoIds = new List<string>();
|
||||
|
||||
// 获取所有视频
|
||||
var allVideos = await this.Db.Queryable<DyCollectVideo>()
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(author), x => x.Author == author)
|
||||
.WhereIF(start.HasValue, x => x.SyncTime >= start.Value)
|
||||
.WhereIF(end.HasValue, x => x.SyncTime <= end.Value)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(viedoType) && viedoType != "*", x => x.ViedoType == viedoType)
|
||||
.WhereIF(isDeleted.HasValue && isDeleted.Value, x => string.IsNullOrWhiteSpace(x.VideoSavePath))
|
||||
.WhereIF(isDeleted.HasValue && !isDeleted.Value, x => !string.IsNullOrWhiteSpace(x.VideoSavePath))
|
||||
.ToListAsync();
|
||||
|
||||
// 在内存中过滤 viedoCate
|
||||
foreach (var video in allVideos)
|
||||
{
|
||||
var viedoCate = (string.IsNullOrWhiteSpace(video.Tag1) ? "" : video.Tag1) + "/" + (string.IsNullOrWhiteSpace(video.Tag2) ? "" : video.Tag2);
|
||||
|
||||
if (otherTags.Contains(viedoCate))
|
||||
{
|
||||
matchingVideoIds.Add(video.Id);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchingVideoIds.Any())
|
||||
{
|
||||
// 使用 ID 列表重新构建查询
|
||||
where = this.Db.Queryable<DyCollectVideo>()
|
||||
.Where(x => matchingVideoIds.Contains(x.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果没有匹配的数据,返回空结果
|
||||
where = this.Db.Queryable<DyCollectVideo>().Where(x => false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(tag))
|
||||
{
|
||||
// 兼容旧版本的单标签筛选
|
||||
if (tag.Contains("/"))
|
||||
{
|
||||
var parts = tag.Split('/', 2);
|
||||
var tag1 = parts[0];
|
||||
var tag2 = parts.Length > 1 ? parts[1] : "";
|
||||
where = where.Where(x => x.Tag1 == tag1 && (string.IsNullOrWhiteSpace(tag2) || x.Tag2 == tag2 || string.IsNullOrWhiteSpace(x.Tag2)));
|
||||
}
|
||||
else
|
||||
{
|
||||
where = where.Where(x => x.Tag1 == tag);
|
||||
}
|
||||
}
|
||||
|
||||
// 应用其他筛选条件
|
||||
where = where
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(author), x => x.Author == author)
|
||||
.WhereIF(start.HasValue, x => x.SyncTime >= start.Value)
|
||||
.WhereIF(end.HasValue, x => x.SyncTime <= end.Value)
|
||||
.WhereIF(!string.IsNullOrWhiteSpace(viedoType) && viedoType != "*", x => x.ViedoType == viedoType)
|
||||
.WhereIF(isDeleted.HasValue && isDeleted.Value, x => string.IsNullOrWhiteSpace(x.VideoSavePath)) // 已删除:videoSavePath为空
|
||||
.WhereIF(isDeleted.HasValue && !isDeleted.Value, x => !string.IsNullOrWhiteSpace(x.VideoSavePath)); // 未删除:videoSavePath不为空
|
||||
|
||||
|
||||
var totalCount = await where.CountAsync();
|
||||
var list = await where.OrderByDescending(x=>x.SyncTime).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
|
||||
|
||||
// 构建排序逻辑
|
||||
var isAsc = !string.IsNullOrWhiteSpace(sortOrder) && sortOrder.ToLower() == "asc";
|
||||
var query = where;
|
||||
|
||||
// 调试日志
|
||||
Console.WriteLine($"Repository排序参数 - sortField: {sortField}, sortOrder: {sortOrder}, isAsc: {isAsc}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(sortField))
|
||||
{
|
||||
switch (sortField.ToLower())
|
||||
{
|
||||
case "synctime":
|
||||
query = isAsc ? query.OrderBy(x => x.SyncTime) : query.OrderByDescending(x => x.SyncTime);
|
||||
Console.WriteLine($"应用排序: SyncTime {(isAsc ? "升序" : "降序")}");
|
||||
break;
|
||||
case "viedotype":
|
||||
query = isAsc ? query.OrderBy(x => x.ViedoType) : query.OrderByDescending(x => x.ViedoType);
|
||||
Console.WriteLine($"应用排序: ViedoType {(isAsc ? "升序" : "降序")}");
|
||||
break;
|
||||
case "viedocate":
|
||||
// 视频类型需要按Tag1和Tag2排序,SqlSugar使用OrderBy链式调用
|
||||
if (isAsc)
|
||||
{
|
||||
query = query.OrderBy(x => x.Tag1).OrderBy(x => x.Tag2);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.OrderByDescending(x => x.Tag1).OrderByDescending(x => x.Tag2);
|
||||
}
|
||||
Console.WriteLine($"应用排序: ViedoCate {(isAsc ? "升序" : "降序")}");
|
||||
break;
|
||||
case "author":
|
||||
query = isAsc ? query.OrderBy(x => x.Author) : query.OrderByDescending(x => x.Author);
|
||||
Console.WriteLine($"应用排序: Author {(isAsc ? "升序" : "降序")}");
|
||||
break;
|
||||
case "videotitle":
|
||||
query = isAsc ? query.OrderBy(x => x.VideoTitle) : query.OrderByDescending(x => x.VideoTitle);
|
||||
Console.WriteLine($"应用排序: VideoTitle {(isAsc ? "升序" : "降序")}");
|
||||
break;
|
||||
case "dyuser":
|
||||
// 用户字段需要先查询用户信息,这里先按CookieId排序,后续会设置DyUser
|
||||
query = isAsc ? query.OrderBy(x => x.CookieId) : query.OrderByDescending(x => x.CookieId);
|
||||
Console.WriteLine($"应用排序: DyUser {(isAsc ? "升序" : "降序")}");
|
||||
break;
|
||||
default:
|
||||
// 默认按同步时间降序
|
||||
query = query.OrderByDescending(x => x.SyncTime);
|
||||
Console.WriteLine($"未识别的排序字段,使用默认排序: SyncTime 降序");
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认按同步时间降序
|
||||
query = query.OrderByDescending(x => x.SyncTime);
|
||||
Console.WriteLine("无排序字段,使用默认排序: SyncTime 降序");
|
||||
}
|
||||
|
||||
var list = await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();
|
||||
Console.WriteLine($"查询完成,返回 {list.Count} 条记录");
|
||||
if (list.Any())
|
||||
{
|
||||
Console.WriteLine($"第一条记录的同步时间: {list.First().SyncTime}");
|
||||
Console.WriteLine($"最后一条记录的同步时间: {list.Last().SyncTime}");
|
||||
}
|
||||
if (list.Any()) {
|
||||
var users= await this.Db.Queryable<DyUserCookies>().ToListAsync();
|
||||
foreach (var item in list)
|
||||
|
||||
@@ -54,6 +54,13 @@ namespace dy.net.service
|
||||
{
|
||||
cc.Cron = config.Cron;
|
||||
cc.BatchCount = config.BatchCount;
|
||||
// 更新下载配置
|
||||
cc.EnableAutoDownload = config.EnableAutoDownload;
|
||||
cc.DownloadDaysRange = config.DownloadDaysRange;
|
||||
cc.AuthorWhitelist = config.AuthorWhitelist;
|
||||
cc.AuthorBlacklist = config.AuthorBlacklist;
|
||||
cc.DownloadFavorite = config.DownloadFavorite;
|
||||
cc.DownloadCollect = config.DownloadCollect;
|
||||
}
|
||||
|
||||
var update = await sqlSugarClient.Updateable<AppConfig>(cc).ExecuteCommandAsync();
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using dy.net.model;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace dy.net.service
|
||||
{
|
||||
/// <summary>
|
||||
/// 下载配置服务,用于判断是否应该下载视频
|
||||
/// </summary>
|
||||
public class DownloadConfigService
|
||||
{
|
||||
private readonly CommonService _commonService;
|
||||
|
||||
public DownloadConfigService(CommonService commonService)
|
||||
{
|
||||
_commonService = commonService;
|
||||
}
|
||||
|
||||
private AppConfig GetConfig()
|
||||
{
|
||||
return _commonService.GetConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否应该下载视频
|
||||
/// </summary>
|
||||
/// <param name="videoType">视频类型:"1"=喜欢,"2"=收藏</param>
|
||||
/// <param name="authorId">作者ID</param>
|
||||
/// <param name="authorName">作者名称</param>
|
||||
/// <param name="createTime">视频创建时间</param>
|
||||
/// <returns>true=应该下载,false=不应该下载</returns>
|
||||
public bool ShouldDownload(string videoType, string authorId, string authorName, DateTime createTime)
|
||||
{
|
||||
var config = GetConfig();
|
||||
if (config == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果未启用自动下载,直接返回false
|
||||
if (config.EnableAutoDownload != true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查视频类型是否允许下载
|
||||
if (videoType == "1" && config.DownloadFavorite != true)
|
||||
{
|
||||
return false; // 喜欢类型但未启用下载
|
||||
}
|
||||
|
||||
if (videoType == "2" && config.DownloadCollect != true)
|
||||
{
|
||||
return false; // 收藏类型但未启用下载
|
||||
}
|
||||
|
||||
// 检查时间范围
|
||||
if (config.DownloadDaysRange.HasValue && config.DownloadDaysRange.Value > 0)
|
||||
{
|
||||
var daysDiff = (DateTime.Now - createTime).TotalDays;
|
||||
if (daysDiff > config.DownloadDaysRange.Value)
|
||||
{
|
||||
return false; // 超出时间范围
|
||||
}
|
||||
}
|
||||
|
||||
// 检查作者白名单
|
||||
if (!string.IsNullOrWhiteSpace(config.AuthorWhitelist))
|
||||
{
|
||||
try
|
||||
{
|
||||
var whitelist = JsonConvert.DeserializeObject<List<string>>(config.AuthorWhitelist);
|
||||
if (whitelist != null && whitelist.Any())
|
||||
{
|
||||
// 如果配置了白名单,只有白名单中的作者才下载
|
||||
var isInWhitelist = whitelist.Any(a =>
|
||||
(!string.IsNullOrWhiteSpace(authorId) && a.Equals(authorId, StringComparison.OrdinalIgnoreCase)) ||
|
||||
(!string.IsNullOrWhiteSpace(authorName) && a.Equals(authorName, StringComparison.OrdinalIgnoreCase))
|
||||
);
|
||||
if (!isInWhitelist)
|
||||
{
|
||||
return false; // 不在白名单中
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Warning($"解析作者白名单失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查作者黑名单
|
||||
if (!string.IsNullOrWhiteSpace(config.AuthorBlacklist))
|
||||
{
|
||||
try
|
||||
{
|
||||
var blacklist = JsonConvert.DeserializeObject<List<string>>(config.AuthorBlacklist);
|
||||
if (blacklist != null && blacklist.Any())
|
||||
{
|
||||
// 如果配置了黑名单,黑名单中的作者不下载
|
||||
var isInBlacklist = blacklist.Any(a =>
|
||||
(!string.IsNullOrWhiteSpace(authorId) && a.Equals(authorId, StringComparison.OrdinalIgnoreCase)) ||
|
||||
(!string.IsNullOrWhiteSpace(authorName) && a.Equals(authorName, StringComparison.OrdinalIgnoreCase))
|
||||
);
|
||||
if (isInBlacklist)
|
||||
{
|
||||
return false; // 在黑名单中
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Warning($"解析作者黑名单失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 所有检查都通过,可以下载
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,12 +41,18 @@ namespace dy.net.service
|
||||
.Where(video => !existingAwemeIds.Contains(video.AwemeId))
|
||||
.ToList();
|
||||
|
||||
Serilog.Log.Debug($"batchInsert: 待插入{videos.Count}条,已存在{existingAwemeIds.Count}条,需要插入{videosToInsert.Count}条");
|
||||
|
||||
// 4. 如果没有需要插入的新记录,直接返回成功
|
||||
if (!videosToInsert.Any())
|
||||
{
|
||||
Serilog.Log.Debug($"batchInsert: 所有视频都已存在,无需插入");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5. 批量插入过滤后的新记录
|
||||
var insertedCount = await _dyCollectVideoRepository.InsertRangeAsync(videosToInsert);
|
||||
Serilog.Log.Debug($"batchInsert: 实际插入{insertedCount}条记录");
|
||||
|
||||
// 返回是否插入成功(至少插入一条)
|
||||
return insertedCount > 0;
|
||||
@@ -55,16 +61,47 @@ namespace dy.net.service
|
||||
|
||||
public async Task<VideoStaticsDto> GetStatics()
|
||||
{
|
||||
List<DyCollectVideo> allList = await this._dyCollectVideoRepository.GetAllAsync();
|
||||
if (!allList.Any())
|
||||
return new VideoStaticsDto();
|
||||
|
||||
// 只统计文件实际存在的视频
|
||||
var list = allList.Where(x =>
|
||||
!string.IsNullOrWhiteSpace(x.VideoSavePath) &&
|
||||
System.IO.File.Exists(x.VideoSavePath)
|
||||
).ToList();
|
||||
|
||||
Serilog.Log.Debug($"统计视频:数据库记录 {allList.Count} 条,文件存在 {list.Count} 条");
|
||||
|
||||
List<DyCollectVideo> list = await this._dyCollectVideoRepository.GetAllAsync();
|
||||
if (!list.Any())
|
||||
return new VideoStaticsDto();
|
||||
var Categories = list.GroupBy(x => x.Tag1).Select(x => new VideoStaticsItemDto { Name = x.Key, Count = x.LongCount() }).OrderByDescending(p => p.Count).ToList();
|
||||
Categories.Where(x => string.IsNullOrWhiteSpace(x.Name)).ToList().ForEach(x => x.Name = "其他");
|
||||
|
||||
// 按 viedoCate(Tag1/Tag2 组合)分组统计分类
|
||||
var Categories = list
|
||||
.GroupBy(x =>
|
||||
{
|
||||
// 使用与模型 ViedoCate 属性相同的逻辑:Tag1 + "/" + Tag2
|
||||
var tag1 = string.IsNullOrWhiteSpace(x.Tag1) ? "" : x.Tag1;
|
||||
var tag2 = string.IsNullOrWhiteSpace(x.Tag2) ? "" : x.Tag2;
|
||||
var viedoCate = tag1 + "/" + tag2;
|
||||
// 如果 Tag1 和 Tag2 都为空,标记为"未分类"
|
||||
if (string.IsNullOrWhiteSpace(tag1) && string.IsNullOrWhiteSpace(tag2))
|
||||
{
|
||||
return "未分类";
|
||||
}
|
||||
return viedoCate;
|
||||
})
|
||||
.Select(x => new VideoStaticsItemDto
|
||||
{
|
||||
Name = x.Key,
|
||||
Count = x.LongCount()
|
||||
})
|
||||
.OrderByDescending(p => p.Count)
|
||||
.ToList();
|
||||
var data = new VideoStaticsDto
|
||||
{
|
||||
AuthorCount = list.Select(x => x.AuthorId).Distinct().Count(),
|
||||
CategoryCount = list.Select(x => x.Tag1).Distinct().Count(),
|
||||
CategoryCount = Categories.Count, // 使用实际分类数量
|
||||
VideoCount = list.Count,
|
||||
Categories = Categories,
|
||||
FavoriteCount = list.Count(x => x.ViedoType == "1"),
|
||||
@@ -85,9 +122,154 @@ namespace dy.net.service
|
||||
|
||||
//分页查询
|
||||
|
||||
public async Task<(List<DyCollectVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, string author = null, string viedoType = null, List<string>? dates = null)
|
||||
public async Task<(List<DyCollectVideo> list, int totalCount)> GetPagedAsync(int pageIndex, int pageSize, string tag = null, List<string> tags = null, string author = null, string viedoType = null, List<string>? dates = null, bool? isDeleted = null, string sortField = null, string sortOrder = null)
|
||||
{
|
||||
return await _dyCollectVideoRepository.GetPagedAsync(pageIndex, pageSize, tag, author, viedoType, dates);
|
||||
return await _dyCollectVideoRepository.GetPagedAsync(pageIndex, pageSize, tag, tags, author, viedoType, dates, isDeleted, sortField, sortOrder);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有视频类型列表(用于下拉框)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<List<string>> GetAllCategoriesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取所有视频记录
|
||||
var allVideos = await _dyCollectVideoRepository.GetAllAsync();
|
||||
Serilog.Log.Debug($"获取视频类型列表:共 {allVideos?.Count ?? 0} 条视频记录");
|
||||
|
||||
// 提取所有非空的 viedoCate(Tag1/Tag2 组合),去重并排序
|
||||
// 使用与模型 ViedoCate 属性相同的逻辑:Tag1 + "/" + Tag2
|
||||
var categories = allVideos
|
||||
.Select(x =>
|
||||
{
|
||||
// 使用与模型 ViedoCate 属性相同的逻辑
|
||||
var tag1 = string.IsNullOrWhiteSpace(x.Tag1) ? "" : x.Tag1;
|
||||
var tag2 = string.IsNullOrWhiteSpace(x.Tag2) ? "" : x.Tag2;
|
||||
var viedoCate = tag1 + "/" + tag2;
|
||||
return viedoCate;
|
||||
})
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x) && x != "/") // 过滤空值和只有 "/" 的情况
|
||||
.Distinct()
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
|
||||
// 检查是否存在空类型的视频(Tag1 和 Tag2 都为空)
|
||||
var hasEmptyCategory = allVideos.Any(x => string.IsNullOrWhiteSpace(x.Tag1) && string.IsNullOrWhiteSpace(x.Tag2));
|
||||
if (hasEmptyCategory)
|
||||
{
|
||||
// 在列表开头添加"未分类"选项
|
||||
categories.Insert(0, "未分类");
|
||||
}
|
||||
|
||||
Serilog.Log.Debug($"提取到的视频类型数量:{categories.Count},类型列表:{string.Join(", ", categories)}");
|
||||
return categories;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "获取视频类型列表失败");
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取视频信息
|
||||
/// </summary>
|
||||
/// <param name="id">视频ID</param>
|
||||
/// <returns></returns>
|
||||
public async Task<DyCollectVideo> GetByIdAsync(string id)
|
||||
{
|
||||
return await _dyCollectVideoRepository.GetByIdAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理数据库中文件不存在的视频记录
|
||||
/// </summary>
|
||||
/// <returns>清理的记录数量</returns>
|
||||
public async Task<int> CleanupMissingFilesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var allVideos = await _dyCollectVideoRepository.GetAllAsync();
|
||||
if (!allVideos.Any())
|
||||
return 0;
|
||||
|
||||
// 找出需要清理的视频ID:
|
||||
// 1. VideoSavePath 不为空但文件不存在的记录
|
||||
// 2. VideoSavePath 为空或null的记录(这些记录没有视频文件,也应该被清理)
|
||||
var missingFileIds = allVideos
|
||||
.Where(x =>
|
||||
// 情况1:有路径但文件不存在
|
||||
(!string.IsNullOrWhiteSpace(x.VideoSavePath) && !System.IO.File.Exists(x.VideoSavePath)) ||
|
||||
// 情况2:路径为空(这些记录没有视频文件,也应该清理)
|
||||
string.IsNullOrWhiteSpace(x.VideoSavePath)
|
||||
)
|
||||
.Select(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
if (!missingFileIds.Any())
|
||||
{
|
||||
Serilog.Log.Debug("清理检查:所有视频文件都存在,无需清理");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var pathEmptyCount = allVideos.Count(x => string.IsNullOrWhiteSpace(x.VideoSavePath));
|
||||
var fileMissingCount = allVideos.Count(x => !string.IsNullOrWhiteSpace(x.VideoSavePath) && !System.IO.File.Exists(x.VideoSavePath));
|
||||
|
||||
Serilog.Log.Debug($"清理检查:发现 {missingFileIds.Count} 条需要清理的记录(路径为空:{pathEmptyCount} 条,文件不存在:{fileMissingCount} 条)");
|
||||
|
||||
// 批量删除
|
||||
var deletedCount = await _dyCollectVideoRepository.DeleteByIdsAsync(missingFileIds);
|
||||
Serilog.Log.Debug($"清理完成:删除了 {deletedCount} 条记录");
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, "清理文件不存在的记录失败");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除指定作者的所有视频数据
|
||||
/// </summary>
|
||||
/// <param name="authorName">作者名称</param>
|
||||
/// <returns>删除的记录数量</returns>
|
||||
public async Task<int> DeleteAuthorVideosAsync(string authorName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allVideos = await _dyCollectVideoRepository.GetAllAsync();
|
||||
if (!allVideos.Any())
|
||||
return 0;
|
||||
|
||||
// 找出该作者的所有视频ID
|
||||
var authorVideoIds = allVideos
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.Author) && x.Author.Equals(authorName, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
if (!authorVideoIds.Any())
|
||||
{
|
||||
Serilog.Log.Debug($"删除作者视频:未找到作者 {authorName} 的视频记录");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Serilog.Log.Debug($"删除作者视频:发现作者 {authorName} 的 {authorVideoIds.Count} 条视频记录");
|
||||
|
||||
// 批量删除
|
||||
var deletedCount = await _dyCollectVideoRepository.DeleteByIdsAsync(authorVideoIds);
|
||||
Serilog.Log.Debug($"删除完成:删除了 {deletedCount} 条记录");
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Serilog.Log.Error(ex, $"删除作者 {authorName} 的视频失败");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -68,11 +68,24 @@ namespace dy.net.service
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
|
||||
Serilog.Log.Debug($"SyncCollectVideos API响应长度: {data?.Length ?? 0}");
|
||||
var result = JsonConvert.DeserializeObject<CollectVideoInfo>(data);
|
||||
if (result != null)
|
||||
{
|
||||
Serilog.Log.Debug($"SyncCollectVideos 解析结果: HasMore={result.HasMore}, AwemeList数量={result.AwemeList?.Count ?? 0}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error($"SyncCollectVideos fail: {respose.StatusCode}");
|
||||
var preview = data != null && data.Length > 0 ? data.Substring(0, Math.Min(500, data.Length)) : "空响应";
|
||||
Serilog.Log.Warning($"SyncCollectVideos 解析失败,响应内容前500字符: {preview}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
var errorContent = await respose.Content.ReadAsStringAsync();
|
||||
var errorPreview = errorContent != null && errorContent.Length > 0 ? errorContent.Substring(0, Math.Min(500, errorContent.Length)) : "空响应";
|
||||
Serilog.Log.Error($"SyncCollectVideos fail: {respose.StatusCode}, 响应内容: {errorPreview}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -129,15 +142,29 @@ namespace dy.net.service
|
||||
// 构建请求URL
|
||||
var queryString = new FormUrlEncodedContent(dics);
|
||||
string fullUrl = $"{DouYinApi}/favorite?{await queryString.ReadAsStringAsync()}";
|
||||
Serilog.Log.Debug($"SyncFavoriteVideos 请求URL: {fullUrl.Substring(0, Math.Min(200, fullUrl.Length))}...");
|
||||
var respose = await httpClient.GetAsync(fullUrl);
|
||||
if (respose.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await respose.Content.ReadAsStringAsync();
|
||||
return JsonConvert.DeserializeObject<CollectVideoInfo>(data);
|
||||
Serilog.Log.Debug($"SyncFavoriteVideos API响应长度: {data?.Length ?? 0}");
|
||||
var result = JsonConvert.DeserializeObject<CollectVideoInfo>(data);
|
||||
if (result != null)
|
||||
{
|
||||
Serilog.Log.Debug($"SyncFavoriteVideos 解析结果: HasMore={result.HasMore}, AwemeList数量={result.AwemeList?.Count ?? 0}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Serilog.Log.Error($"SyncFavoriteVideos fail: {respose.StatusCode}");
|
||||
var preview = data != null && data.Length > 0 ? data.Substring(0, Math.Min(500, data.Length)) : "空响应";
|
||||
Serilog.Log.Warning($"SyncFavoriteVideos 解析失败,响应内容前500字符: {preview}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
var errorContent = await respose.Content.ReadAsStringAsync();
|
||||
var errorPreview = errorContent != null && errorContent.Length > 0 ? errorContent.Substring(0, Math.Min(500, errorContent.Length)) : "空响应";
|
||||
Serilog.Log.Error($"SyncFavoriteVideos fail: {respose.StatusCode}, 响应内容: {errorPreview}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user