feat: add fnOS packaging, storage workflows and release pipeline
This commit is contained in:
@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using System.Net;
|
||||
using System.Text.RegularExpressions;
|
||||
using dy.net.storage;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
@@ -25,10 +26,12 @@ namespace dy.net.Controllers
|
||||
private readonly DouyinFollowService douyinFollowService;
|
||||
private readonly DouyinCookieService douyinCookieService;
|
||||
private readonly DouyinHttpClientService httpClientService;
|
||||
private readonly WebDavSettingsService _webDavSettingsService;
|
||||
private readonly VideoTaskService _videoTaskService;
|
||||
|
||||
|
||||
|
||||
public ConfigController(DouyinCookieService dyCookieService, DouyinCommonService commonService, DouyinQuartzJobService quartzJobService, DouyinFollowService douyinFollowService, DouyinCookieService douyinCookieService, DouyinHttpClientService httpClientService)
|
||||
public ConfigController(DouyinCookieService dyCookieService, DouyinCommonService commonService, DouyinQuartzJobService quartzJobService, DouyinFollowService douyinFollowService, DouyinCookieService douyinCookieService, DouyinHttpClientService httpClientService, WebDavSettingsService webDavSettingsService, VideoTaskService videoTaskService)
|
||||
{
|
||||
this.dyCookieService = dyCookieService;
|
||||
this.commonService = commonService;
|
||||
@@ -36,6 +39,8 @@ namespace dy.net.Controllers
|
||||
this.douyinFollowService = douyinFollowService;
|
||||
this.douyinCookieService = douyinCookieService;
|
||||
this.httpClientService = httpClientService;
|
||||
_webDavSettingsService = webDavSettingsService;
|
||||
_videoTaskService = videoTaskService;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +107,11 @@ namespace dy.net.Controllers
|
||||
{
|
||||
if (cookies?.Count > 0)
|
||||
{
|
||||
foreach (var cookie in cookies) ClearImportedSourceHealth(cookie);
|
||||
if (commonService.GetConfig()?.StorageType.IsRemote() == true)
|
||||
{
|
||||
foreach (var cookie in cookies) _webDavSettingsService.ApplyDefaultCookiePaths(cookie);
|
||||
}
|
||||
var imported = await douyinCookieService.ImportCookies(cookies);
|
||||
if (imported)
|
||||
Serilog.Log.Debug("抖音Cookie配置导入成功");
|
||||
@@ -164,6 +174,12 @@ namespace dy.net.Controllers
|
||||
if (!cookieValid)
|
||||
return ApiResult.Fail("Cookie无效或已过期,请按照文档提示重新获取有效Cookie,不要使用插件获取cookie");
|
||||
var result = await dyCookieService.FastResetCookie(dto.id, dto.cookie);
|
||||
if (result)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(dto.id)) await _videoTaskService.ResetSourceHealthAsync(dto.id);
|
||||
else
|
||||
foreach (var item in await dyCookieService.GetAllAsync()) await _videoTaskService.ResetSourceHealthAsync(item.Id);
|
||||
}
|
||||
|
||||
return result ? ApiResult.Success() : ApiResult.Fail("cookie设置失败");
|
||||
}
|
||||
@@ -193,8 +209,12 @@ namespace dy.net.Controllers
|
||||
// 1. 基础赋值
|
||||
dyUserCookies.Id = IdGener.GetLong().ToString();
|
||||
|
||||
var storageType = commonService.GetConfig()?.StorageType ?? StorageType.Local;
|
||||
if (storageType.IsRemote())
|
||||
_webDavSettingsService.ApplyDefaultCookiePaths(dyUserCookies);
|
||||
|
||||
// 2. 路径权限校验
|
||||
var (Success, Message) = ValidatePaths(dyUserCookies);
|
||||
var (Success, Message) = ValidatePaths(dyUserCookies, storageType);
|
||||
if (!Success)
|
||||
return ApiResult.Fail(Message);
|
||||
|
||||
@@ -212,8 +232,28 @@ namespace dy.net.Controllers
|
||||
return saved ? ApiResult.Success() : ApiResult.Fail("添加失败");
|
||||
}
|
||||
|
||||
private static (bool Success, string Message) ValidatePaths(DouyinCookie cookie)
|
||||
private static (bool Success, string Message) ValidatePaths(DouyinCookie cookie, StorageType storageType)
|
||||
{
|
||||
if (storageType.IsRemote())
|
||||
{
|
||||
var remotePaths = new Dictionary<string, string>
|
||||
{
|
||||
{ "收藏 OpenList 相对路径", cookie.WebDavCollectPath },
|
||||
{ "喜欢 OpenList 相对路径", cookie.WebDavFavoritePath },
|
||||
{ "关注 OpenList 相对路径", cookie.WebDavFollowPath },
|
||||
{ "合集 OpenList 相对路径", cookie.WebDavMixPath },
|
||||
{ "短剧 OpenList 相对路径", cookie.WebDavSeriesPath }
|
||||
};
|
||||
|
||||
foreach (var (label, path) in remotePaths)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path)) return (false, $"{label}不能为空");
|
||||
try { StoragePath.NormalizeRemote(path); }
|
||||
catch (ArgumentException ex) { return (false, $"{label}无效:{ex.Message}"); }
|
||||
}
|
||||
return (true, string.Empty);
|
||||
}
|
||||
|
||||
var pathsToCheck = new Dictionary<string, string>
|
||||
{
|
||||
{ "收藏存储路径", cookie.SavePath },
|
||||
@@ -262,6 +302,19 @@ namespace dy.net.Controllers
|
||||
[HttpPost("update")]
|
||||
public async Task<IActionResult> AddOrUpdateAsync([FromBody] DouyinCookie dyUserCookies)
|
||||
{
|
||||
if (dyUserCookies == null) return ApiResult.Fail("配置不能为空");
|
||||
|
||||
var isNewCookie = string.IsNullOrWhiteSpace(dyUserCookies.Id) || dyUserCookies.Id == "0";
|
||||
var storageType = commonService.GetConfig()?.StorageType ?? StorageType.Local;
|
||||
if (storageType.IsRemote())
|
||||
{
|
||||
if (isNewCookie)
|
||||
dyUserCookies.Id = IdGener.GetLong().ToString();
|
||||
_webDavSettingsService.ApplyDefaultCookiePaths(dyUserCookies);
|
||||
var (success, message) = ValidatePaths(dyUserCookies, storageType);
|
||||
if (!success) return ApiResult.Fail(message);
|
||||
}
|
||||
|
||||
RemoveCookieLineString(dyUserCookies);
|
||||
|
||||
var checkCk = await httpClientService.CheckCookie(dyUserCookies);
|
||||
@@ -271,9 +324,10 @@ namespace dy.net.Controllers
|
||||
}
|
||||
dyUserCookies.StatusCode = 0;
|
||||
dyUserCookies.StatusMsg = "正常";
|
||||
if (dyUserCookies.Id == "0")
|
||||
if (isNewCookie)
|
||||
{
|
||||
dyUserCookies.Id = IdGener.GetLong().ToString();
|
||||
if (string.IsNullOrWhiteSpace(dyUserCookies.Id) || dyUserCookies.Id == "0")
|
||||
dyUserCookies.Id = IdGener.GetLong().ToString();
|
||||
var result = await dyCookieService.Add(dyUserCookies);
|
||||
if (result)
|
||||
{
|
||||
@@ -287,6 +341,7 @@ namespace dy.net.Controllers
|
||||
var result = await dyCookieService.UpdateCookieAsync(dyUserCookies);
|
||||
if (result)
|
||||
{
|
||||
await _videoTaskService.ResetSourceHealthAsync(dyUserCookies.Id);
|
||||
ReStartJob();
|
||||
return ApiResult.Success();
|
||||
}
|
||||
@@ -305,6 +360,18 @@ namespace dy.net.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearImportedSourceHealth(DouyinCookie cookie)
|
||||
{
|
||||
cookie.ConsecutiveSourceForbidden = 0;
|
||||
cookie.SourceCooldownUntil = null;
|
||||
cookie.SourceRequiresAuthorization = false;
|
||||
cookie.SourceProbePending = false;
|
||||
cookie.SourceProbeInProgress = false;
|
||||
cookie.LastSourceStatusCode = null;
|
||||
cookie.LastSourceError = null;
|
||||
cookie.SourceHealthUpdatedAt = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量删除用户Cookie
|
||||
/// </summary>
|
||||
@@ -351,10 +418,15 @@ namespace dy.net.Controllers
|
||||
//[Authorize]
|
||||
public async Task<IActionResult> ExecuteJobNow()
|
||||
{
|
||||
var config = commonService.GetConfig();
|
||||
if (config != null)
|
||||
await quartzJobService.InitOrReStartAllJobs(config.Cron.ToString());
|
||||
return ApiResult.Success();
|
||||
try
|
||||
{
|
||||
var tasks = await quartzJobService.TriggerVideoJobsNowAsync();
|
||||
return ApiResult.Success(new { taskIds = tasks.Select(x => x.Id).ToList(), count = tasks.Count });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return ApiResult.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using dy.net.model.dto;
|
||||
using dy.net.service;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
[Route("api/email")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public sealed class EmailController : ControllerBase
|
||||
{
|
||||
private readonly EmailNotificationSettingsService _settings;
|
||||
|
||||
public EmailController(EmailNotificationSettingsService settings) => _settings = settings;
|
||||
|
||||
[HttpGet("config")]
|
||||
public async Task<IActionResult> GetConfig()
|
||||
{
|
||||
var settings = await _settings.GetAsync();
|
||||
return ApiResult.Success(_settings.ToDto(settings));
|
||||
}
|
||||
|
||||
[HttpPut("config")]
|
||||
public async Task<IActionResult> UpdateConfig(EmailNotificationSettingsDto dto)
|
||||
{
|
||||
try { return ApiResult.Success(await _settings.SaveAsync(dto)); }
|
||||
catch (InvalidOperationException ex) { return ApiResult.Fail(ex.Message); }
|
||||
}
|
||||
|
||||
[HttpPost("test")]
|
||||
public async Task<IActionResult> Test(EmailNotificationSettingsDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = await _settings.TestAsync(dto, cancellationToken);
|
||||
return ApiResult.Success(new { message });
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
var message = ex.GetBaseException().Message;
|
||||
Serilog.Log.Warning(ex, "SMTP 测试邮件发送失败");
|
||||
return ApiResult.Fail(string.IsNullOrWhiteSpace(message) ? "测试邮件发送失败" : message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Serilog;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
@@ -14,11 +15,19 @@ namespace dy.net.Controllers
|
||||
{
|
||||
private readonly DouyinFollowService _douyinFollowService;
|
||||
private readonly DouyinQuartzJobService _douyinQuartzJobService;
|
||||
private readonly DouyinUserLookupService _douyinUserLookupService;
|
||||
private readonly DouyinLiveStatusService _douyinLiveStatusService;
|
||||
|
||||
public FollowController(DouyinFollowService douyinFollowService, DouyinQuartzJobService douyinQuartzJobService)
|
||||
public FollowController(
|
||||
DouyinFollowService douyinFollowService,
|
||||
DouyinQuartzJobService douyinQuartzJobService,
|
||||
DouyinUserLookupService douyinUserLookupService,
|
||||
DouyinLiveStatusService douyinLiveStatusService)
|
||||
{
|
||||
this._douyinFollowService = douyinFollowService;
|
||||
_douyinQuartzJobService = douyinQuartzJobService;
|
||||
_douyinUserLookupService = douyinUserLookupService;
|
||||
_douyinLiveStatusService = douyinLiveStatusService;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,20 +61,107 @@ namespace dy.net.Controllers
|
||||
[HttpGet("sync")]
|
||||
public async Task<IActionResult> SyncFollowList()
|
||||
{
|
||||
//后台异步
|
||||
_douyinQuartzJobService.StartFollowJobOnceAsync();
|
||||
await Task.Delay(1000);
|
||||
return ApiResult.Success();
|
||||
try
|
||||
{
|
||||
var started = await _douyinQuartzJobService.StartFollowJobOnceAsync();
|
||||
return started
|
||||
? ApiResult.Success(new { message = "关注列表更新任务已启动" })
|
||||
: ApiResult.Fail("关注列表更新任务启动失败,请查看错误日志。");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "手动启动关注列表更新任务失败");
|
||||
return ApiResult.Fail("关注列表更新任务启动失败:" + ex.GetBaseException().Message);
|
||||
}
|
||||
}
|
||||
[HttpPost("add")]
|
||||
public async Task<IActionResult> AddFollow(DouyinFollowed followed)
|
||||
{
|
||||
if (followed == null)
|
||||
{
|
||||
return ApiResult.Fail("新增信息不能为空");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(followed.mySelfId))
|
||||
{
|
||||
return ApiResult.Fail("请先配置抖音授权信息,抖音授权配置里面填写你的uid");
|
||||
}
|
||||
var res = await _douyinFollowService.AddAsync(followed);
|
||||
return ApiResult.SuccOrFail(res, "", res ? "" : "添加失败,或者已存在相同secuid和uid");
|
||||
if (string.IsNullOrWhiteSpace(followed.SecUid))
|
||||
{
|
||||
return ApiResult.Fail("博主 SecUid 不能为空");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(followed.UperId))
|
||||
{
|
||||
return ApiResult.Fail("博主 UID 不能为空");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(followed.UperName))
|
||||
{
|
||||
return ApiResult.Fail("博主姓名不能为空");
|
||||
}
|
||||
|
||||
followed.mySelfId = followed.mySelfId.Trim();
|
||||
followed.SecUid = followed.SecUid.Trim();
|
||||
followed.UperId = followed.UperId.Trim();
|
||||
followed.UperName = followed.UperName.Trim();
|
||||
followed.DouyinNo = followed.DouyinNo?.Trim();
|
||||
followed.SavePath = followed.SavePath?.Trim();
|
||||
followed.Signature = Limit(followed.Signature, 500);
|
||||
followed.UperAvatar = Limit(followed.UperAvatar, 1000);
|
||||
followed.Enterprise = Limit(followed.Enterprise, 200);
|
||||
followed.FullSync = followed.OpenSync && followed.FullSync;
|
||||
|
||||
if (followed.mySelfId.Length > 200 || followed.SecUid.Length > 500 ||
|
||||
followed.UperId.Length > 100 || followed.UperName.Length > 200)
|
||||
{
|
||||
return ApiResult.Fail("新增信息长度超出限制,请检查 UID、SecUid 和博主姓名");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(followed.DouyinNo) && followed.DouyinNo.Length > 100)
|
||||
{
|
||||
return ApiResult.Fail("抖音号长度不能超过 100 个字符");
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(followed.SavePath))
|
||||
{
|
||||
if (followed.SavePath.Length > 20)
|
||||
{
|
||||
return ApiResult.Fail("保存文件夹名称最长 20 个字符");
|
||||
}
|
||||
if (!DouyinFileNameHelper.IsValidWithoutSpecialChars(followed.SavePath))
|
||||
{
|
||||
return ApiResult.Fail("保存文件夹名称只能包含字母、数字或中文");
|
||||
}
|
||||
}
|
||||
|
||||
var result = await _douyinFollowService.TryAddAsync(followed);
|
||||
return result switch
|
||||
{
|
||||
AddFollowResult.Added => ApiResult.Success("新增非关注博主成功"),
|
||||
AddFollowResult.AlreadyExists => ApiResult.Fail("该博主已存在,无需重复添加"),
|
||||
_ => ApiResult.Fail("添加失败,请稍后重试")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用展示在抖音资料页上的抖音号查找博主。结果需要用户确认后再调用 add。
|
||||
/// </summary>
|
||||
[HttpPost("resolve-by-douyin-no")]
|
||||
public async Task<IActionResult> ResolveByDouyinNo(
|
||||
DouyinFollowLookupRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _douyinUserLookupService.ResolveAsync(request, cancellationToken);
|
||||
return ApiResult.Success(result);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return ApiResult.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Limit(string value, int maxLength)
|
||||
{
|
||||
value = value?.Trim();
|
||||
return value != null && value.Length > maxLength ? value[..maxLength] : value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -107,6 +203,59 @@ namespace dy.net.Controllers
|
||||
return await OpenOrCloseSync(dto);
|
||||
}
|
||||
|
||||
[HttpPost("live-monitor")]
|
||||
public async Task<IActionResult> UpdateLiveMonitor(
|
||||
FollowLiveMonitorUpdateDto dto,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ApiResult.Success(await _douyinLiveStatusService.SetMonitorAsync(dto, cancellationToken));
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException or KeyNotFoundException)
|
||||
{
|
||||
return ApiResult.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("live-status/refresh")]
|
||||
public async Task<IActionResult> RefreshLiveStatus(
|
||||
FollowLiveStatusRefreshDto dto,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (dto == null || string.IsNullOrWhiteSpace(dto.Id))
|
||||
return ApiResult.Fail("博主记录不能为空");
|
||||
return ApiResult.Success(await _douyinLiveStatusService.RefreshAsync(dto.Id, cancellationToken));
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException or KeyNotFoundException)
|
||||
{
|
||||
return ApiResult.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("live-status/query")]
|
||||
public async Task<IActionResult> QueryLiveStatus(FollowLiveStatusQueryDto dto)
|
||||
{
|
||||
if (dto?.Ids == null || dto.Ids.Count == 0)
|
||||
return ApiResult.Success(Array.Empty<FollowLiveStatusDto>());
|
||||
return ApiResult.Success(await _douyinLiveStatusService.QueryAsync(dto.Ids));
|
||||
}
|
||||
|
||||
[HttpPost("live-email")]
|
||||
public async Task<IActionResult> UpdateLiveEmail(FollowLiveEmailUpdateDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ApiResult.Success(await _douyinLiveStatusService.SetEmailNotificationAsync(dto));
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException or KeyNotFoundException)
|
||||
{
|
||||
return ApiResult.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除关注对象
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using dy.net.model.dto;
|
||||
using dy.net.service;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
[Route("api/storage/openlist/directory-repairs")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public sealed class OpenListDirectoryRepairsController : ControllerBase
|
||||
{
|
||||
private readonly OpenListDirectoryRepairService _service;
|
||||
|
||||
public OpenListDirectoryRepairsController(OpenListDirectoryRepairService service) => _service = service;
|
||||
|
||||
[HttpPost("preflight")]
|
||||
public Task<IActionResult> Preflight(
|
||||
[FromBody] OpenListDirectoryRepairPreflightRequest request,
|
||||
CancellationToken cancellationToken) => ExecuteAsync(async () =>
|
||||
ApiResult.Success(await _service.PreflightAsync(request, cancellationToken)));
|
||||
|
||||
[HttpPost]
|
||||
public Task<IActionResult> Create(
|
||||
[FromBody] CreateOpenListDirectoryRepairRequest request,
|
||||
CancellationToken cancellationToken) => ExecuteAsync(async () =>
|
||||
ApiResult.Success(await _service.CreateAsync(request, cancellationToken)));
|
||||
|
||||
[HttpGet("latest")]
|
||||
public Task<IActionResult> Latest() => ExecuteAsync(async () =>
|
||||
ApiResult.Success(await _service.GetLatestAsync()));
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public Task<IActionResult> Get(string id) => ExecuteAsync(async () =>
|
||||
ApiResult.Success(await _service.GetAsync(id)));
|
||||
|
||||
[HttpGet("{id}/items")]
|
||||
public Task<IActionResult> Items(string id, [FromQuery] OpenListDirectoryRepairItemPageRequest request) =>
|
||||
ExecuteAsync(async () => ApiResult.Success(await _service.GetItemsAsync(id, request)));
|
||||
|
||||
[HttpPost("{id}/confirm-cleanup")]
|
||||
public Task<IActionResult> Confirm(
|
||||
string id,
|
||||
[FromBody] ConfirmOpenListDirectoryRepairRequest request) => ExecuteAsync(async () =>
|
||||
{
|
||||
await _service.ConfirmCleanupAsync(id, request);
|
||||
return ApiResult.Success("清理任务已提交");
|
||||
});
|
||||
|
||||
[HttpPost("{id}/cancel")]
|
||||
public Task<IActionResult> Cancel(string id) => ExecuteActionAsync(() => _service.CancelAsync(id));
|
||||
|
||||
[HttpPost("{id}/resume")]
|
||||
public Task<IActionResult> Resume(string id) => ExecuteActionAsync(() => _service.ResumeAsync(id));
|
||||
|
||||
[HttpPost("{id}/retry-failed")]
|
||||
public Task<IActionResult> RetryFailed(string id) => ExecuteActionAsync(() => _service.RetryFailedAsync(id));
|
||||
|
||||
private Task<IActionResult> ExecuteActionAsync(Func<Task> action) => ExecuteAsync(async () =>
|
||||
{
|
||||
await action();
|
||||
return ApiResult.Success("操作已提交");
|
||||
});
|
||||
|
||||
private static async Task<IActionResult> ExecuteAsync(Func<Task<IActionResult>> action)
|
||||
{
|
||||
try { return await action(); }
|
||||
catch (Exception ex) when (ex is InvalidOperationException or KeyNotFoundException
|
||||
or HttpRequestException or TimeoutException)
|
||||
{
|
||||
return ApiResult.Fail(ex.GetBaseException().Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using dy.net.model.dto;
|
||||
using dy.net.service;
|
||||
using dy.net.storage;
|
||||
using jzc.http.lib;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class StorageController : ControllerBase
|
||||
{
|
||||
private readonly DouyinCommonService _commonService;
|
||||
private readonly OpenListSettingsService _openListSettingsService;
|
||||
private readonly OpenListMediaStorage _openListStorage;
|
||||
private readonly DouyinQuartzJobService _quartzJobService;
|
||||
private readonly DouyinVideoService _videoService;
|
||||
|
||||
public StorageController(
|
||||
DouyinCommonService commonService,
|
||||
OpenListSettingsService openListSettingsService,
|
||||
OpenListMediaStorage openListStorage,
|
||||
DouyinQuartzJobService quartzJobService,
|
||||
DouyinVideoService videoService)
|
||||
{
|
||||
_commonService = commonService;
|
||||
_openListSettingsService = openListSettingsService;
|
||||
_openListStorage = openListStorage;
|
||||
_quartzJobService = quartzJobService;
|
||||
_videoService = videoService;
|
||||
}
|
||||
|
||||
[HttpGet("inventory")]
|
||||
public async Task<IActionResult> GetInventory()
|
||||
{
|
||||
var type = _commonService.GetConfig()?.StorageType ?? StorageType.Local;
|
||||
return ApiResult.Success(await _videoService.GetStorageInventoryAsync(type));
|
||||
}
|
||||
|
||||
[HttpGet("config")]
|
||||
public async Task<IActionResult> GetConfig()
|
||||
{
|
||||
var type = _commonService.GetConfig()?.StorageType ?? StorageType.Local;
|
||||
var inventory = await _videoService.GetStorageInventoryAsync(type);
|
||||
var saved = await _openListSettingsService.HasSavedSettingsAsync();
|
||||
var dto = await _openListSettingsService.ToDtoAsync(type);
|
||||
dto.RequiresCutover = type == StorageType.WebDav || inventory.WebDavRecordCount > 0;
|
||||
dto.LegacyWebDavRecordCount = inventory.WebDavRecordCount;
|
||||
dto.SuggestedFromLegacy = !saved;
|
||||
return ApiResult.Success(dto);
|
||||
}
|
||||
|
||||
[HttpPost("test")]
|
||||
public async Task<IActionResult> Test(
|
||||
[FromBody] StorageTestRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (request == null) return ApiResult.Fail("测试请求不能为空");
|
||||
|
||||
if (request.StorageType != StorageType.OpenList)
|
||||
return ApiResult.Fail("新远端存储只支持 OpenList 原生 API。");
|
||||
var candidate = await _openListSettingsService.BuildCandidateAsync(
|
||||
new OpenListTestRequest
|
||||
{
|
||||
Endpoint = request.Endpoint,
|
||||
BasePath = request.BasePath,
|
||||
LocalStagingPath = request.LocalStagingPath,
|
||||
SourcePath = request.SourcePath,
|
||||
UserName = request.UserName,
|
||||
Password = request.Password
|
||||
});
|
||||
var result = await _openListStorage.ProbeAsync(candidate, cancellationToken);
|
||||
return result.Success
|
||||
? ApiResult.Success(new { message = result.Message })
|
||||
: ApiResult.Fail(result.Message);
|
||||
}
|
||||
|
||||
[HttpPost("openlist/directories")]
|
||||
public async Task<IActionResult> ListOpenListDirectories(
|
||||
[FromBody] OpenListDirectoryRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (request == null) return ApiResult.Fail("目录请求不能为空");
|
||||
try
|
||||
{
|
||||
var candidate = await _openListSettingsService.BuildCandidateAsync(request);
|
||||
return ApiResult.Success(await _openListStorage.ListDirectoriesAsync(
|
||||
candidate, request.Path, cancellationToken));
|
||||
}
|
||||
catch (Exception ex) when (ex is InvalidOperationException or HttpRequestException)
|
||||
{ return ApiResult.Fail(ex.GetBaseException().Message); }
|
||||
}
|
||||
|
||||
[HttpPut("config")]
|
||||
public async Task<IActionResult> Update(
|
||||
[FromBody] StorageSettingsDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
if (dto == null) return ApiResult.Fail("配置不能为空");
|
||||
|
||||
if (dto.StorageType == StorageType.WebDav)
|
||||
return ApiResult.Fail("WebDAV 已停止作为新存储目标,请改用 OpenList 原生 API。");
|
||||
|
||||
if (dto.StorageType == StorageType.OpenList)
|
||||
{
|
||||
var candidate = await _openListSettingsService.BuildCandidateAsync(
|
||||
new OpenListTestRequest
|
||||
{
|
||||
Endpoint = dto.Endpoint,
|
||||
BasePath = dto.BasePath,
|
||||
LocalStagingPath = dto.LocalStagingPath,
|
||||
SourcePath = dto.SourcePath,
|
||||
UserName = dto.UserName,
|
||||
Password = dto.Password
|
||||
});
|
||||
|
||||
var result = await _openListStorage.ProbeAsync(candidate, cancellationToken);
|
||||
if (!result.Success) return ApiResult.Fail("无法启用 OpenList:" + result.Message);
|
||||
|
||||
await _openListSettingsService.SaveAsync(candidate, true, result.Message);
|
||||
await _openListSettingsService.InitializeCookiePathsAsync();
|
||||
|
||||
var config = _commonService.GetConfig();
|
||||
if (config == null) return ApiResult.Fail("系统配置不存在");
|
||||
config.StorageType = dto.StorageType;
|
||||
if (!await _commonService.UpdateConfig(config))
|
||||
return ApiResult.Fail("存储模式保存失败");
|
||||
await _quartzJobService.InitOrReStartAllJobs(config.Cron.ToString());
|
||||
|
||||
var response = await _openListSettingsService.ToDtoAsync(config.StorageType);
|
||||
var inventory = await _videoService.GetStorageInventoryAsync(config.StorageType);
|
||||
response.RequiresCutover = inventory.WebDavRecordCount > 0;
|
||||
response.LegacyWebDavRecordCount = inventory.WebDavRecordCount;
|
||||
return ApiResult.Success(response);
|
||||
}
|
||||
|
||||
if (dto.StorageType == StorageType.Local)
|
||||
{
|
||||
var config = _commonService.GetConfig();
|
||||
if (config == null) return ApiResult.Fail("系统配置不存在");
|
||||
config.StorageType = StorageType.Local;
|
||||
if (!await _commonService.UpdateConfig(config))
|
||||
return ApiResult.Fail("存储模式保存失败");
|
||||
await _quartzJobService.InitOrReStartAllJobs(config.Cron.ToString());
|
||||
var response = await _openListSettingsService.ToDtoAsync(config.StorageType);
|
||||
var inventory = await _videoService.GetStorageInventoryAsync(config.StorageType);
|
||||
response.RequiresCutover = inventory.WebDavRecordCount > 0;
|
||||
response.LegacyWebDavRecordCount = inventory.WebDavRecordCount;
|
||||
return ApiResult.Success(response);
|
||||
}
|
||||
return ApiResult.Fail("不支持的存储类型");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using dy.net.model.dto;
|
||||
using dy.net.service;
|
||||
using jzc.http.lib;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
[Route("api/storage/migrations")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class StorageMigrationsController : ControllerBase
|
||||
{
|
||||
private readonly StorageMigrationService _service;
|
||||
|
||||
public StorageMigrationsController(StorageMigrationService service) => _service = service;
|
||||
|
||||
[HttpPost("preflight")]
|
||||
public async Task<IActionResult> Preflight([FromBody] StorageMigrationPreflightRequest request, CancellationToken cancellationToken) =>
|
||||
ApiResult.Success(await _service.PreflightAsync(request?.VerifyCapabilities ?? true, cancellationToken));
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create([FromBody] CreateStorageMigrationRequest request, CancellationToken cancellationToken) =>
|
||||
await ExecuteAsync(async () => ApiResult.Success(await _service.CreateAsync(request, cancellationToken)));
|
||||
|
||||
[HttpGet("latest")]
|
||||
public async Task<IActionResult> Latest() => ApiResult.Success(await _service.GetLatestAsync());
|
||||
|
||||
[HttpGet("failed-records/preview")]
|
||||
public async Task<IActionResult> FailedRecordsPreview() =>
|
||||
ApiResult.Success(await _service.PreviewFailedRecordRemovalAsync());
|
||||
|
||||
[HttpPost("failed-records/remove")]
|
||||
public async Task<IActionResult> RemoveFailedRecords([FromBody] RemoveFailedMigrationRecordsRequest request) =>
|
||||
await ExecuteAsync(async () => ApiResult.Success(await _service.RemoveFailedRecordsAsync(request)));
|
||||
|
||||
[HttpPost("history/archive")]
|
||||
public async Task<IActionResult> ArchiveHistory() =>
|
||||
await ExecuteAsync(async () => ApiResult.Success(await _service.ArchiveAllSettledAsync()));
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<IActionResult> Get(string id) =>
|
||||
await ExecuteAsync(async () => ApiResult.Success(await _service.GetAsync(id)));
|
||||
|
||||
[HttpGet("{id}/items")]
|
||||
public async Task<IActionResult> Items(string id, [FromQuery] StorageMigrationItemPageRequest request) =>
|
||||
await ExecuteAsync(async () => ApiResult.Success(await _service.GetItemsAsync(id, request)));
|
||||
|
||||
[HttpPost("{id}/pause")]
|
||||
public Task<IActionResult> Pause(string id) => ExecuteActionAsync(() => _service.PauseAsync(id));
|
||||
|
||||
[HttpPost("{id}/resume")]
|
||||
public Task<IActionResult> Resume(string id) => ExecuteActionAsync(() => _service.ResumeAsync(id));
|
||||
|
||||
[HttpPost("{id}/cancel")]
|
||||
public Task<IActionResult> Cancel(string id) => ExecuteActionAsync(() => _service.CancelAsync(id));
|
||||
|
||||
[HttpPost("{id}/retry-failed")]
|
||||
public Task<IActionResult> RetryFailed(string id) => ExecuteActionAsync(() => _service.RetryFailedAsync(id));
|
||||
|
||||
[HttpPost("{id}/cleanup")]
|
||||
public Task<IActionResult> Cleanup(string id, CancellationToken cancellationToken) =>
|
||||
ExecuteActionAsync(() => _service.CleanupAsync(id, cancellationToken));
|
||||
|
||||
[HttpPost("{id}/rollback")]
|
||||
public Task<IActionResult> Rollback(string id, CancellationToken cancellationToken) =>
|
||||
ExecuteActionAsync(() => _service.RollbackAsync(id, cancellationToken));
|
||||
|
||||
[HttpPost("{id}/archive")]
|
||||
public Task<IActionResult> Archive(string id) => ExecuteActionAsync(() => _service.ArchiveAsync(id));
|
||||
|
||||
private async Task<IActionResult> ExecuteActionAsync(Func<Task> action) =>
|
||||
await ExecuteAsync(async () =>
|
||||
{
|
||||
await action();
|
||||
return ApiResult.Success("操作已提交");
|
||||
});
|
||||
|
||||
private static async Task<IActionResult> ExecuteAsync(Func<Task<IActionResult>> action)
|
||||
{
|
||||
try { return await action(); }
|
||||
catch (Exception ex) when (ex is InvalidOperationException or KeyNotFoundException)
|
||||
{
|
||||
return ApiResult.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using dy.net.model.dto;
|
||||
using dy.net.service;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
[Route("api/tasks")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class TasksController : ControllerBase
|
||||
{
|
||||
private readonly VideoTaskService _tasks;
|
||||
private readonly DouyinQuartzJobService _quartz;
|
||||
private readonly DouyinVideoService _videos;
|
||||
|
||||
public TasksController(VideoTaskService tasks, DouyinQuartzJobService quartz, DouyinVideoService videos)
|
||||
{
|
||||
_tasks = tasks;
|
||||
_quartz = quartz;
|
||||
_videos = videos;
|
||||
}
|
||||
|
||||
[HttpPost("sync")]
|
||||
public async Task<IActionResult> StartSync([FromQuery] VideoTypeEnum? videoType = null) => await ExecuteAsync(async () =>
|
||||
{
|
||||
var tasks = await _quartz.TriggerVideoJobsNowAsync(videoType);
|
||||
return ApiResult.Success(new { taskIds = tasks.Select(x => x.Id).ToList(), count = tasks.Count });
|
||||
});
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery] VideoTaskPageRequest request) =>
|
||||
ApiResult.Success(await _tasks.GetTasksAsync(request));
|
||||
|
||||
[HttpGet("summary")]
|
||||
public async Task<IActionResult> Summary() => ApiResult.Success(await _tasks.GetSummaryAsync());
|
||||
|
||||
[HttpGet("{type}/{id}")]
|
||||
public async Task<IActionResult> Detail(VideoTaskType type, string id) =>
|
||||
await ExecuteAsync(async () => ApiResult.Success(await _tasks.GetTaskAsync(type, id)));
|
||||
|
||||
[HttpGet("{type}/{id}/items")]
|
||||
public async Task<IActionResult> Items(VideoTaskType type, string id, [FromQuery] VideoTaskItemPageRequest request) =>
|
||||
await ExecuteAsync(async () => ApiResult.Success(await _tasks.GetItemsAsync(type, id, request)));
|
||||
|
||||
[HttpPost("{type}/{id}/retry-failed")]
|
||||
public async Task<IActionResult> RetryFailed(VideoTaskType type, string id) =>
|
||||
await ExecuteAsync(async () =>
|
||||
{
|
||||
await _tasks.RetryFailedAsync(type, id);
|
||||
return ApiResult.Success("重试任务已提交");
|
||||
});
|
||||
|
||||
[HttpPost("{type}/{id}/items/{itemId}/retry")]
|
||||
public async Task<IActionResult> RetryItem(VideoTaskType type, string id, string itemId) =>
|
||||
await ExecuteAsync(async () =>
|
||||
{
|
||||
await _tasks.RetryItemAsync(type, id, itemId);
|
||||
return ApiResult.Success("重试任务已提交");
|
||||
});
|
||||
|
||||
[HttpPost("{type}/{id}/items/{itemId}/retry-cleanup")]
|
||||
public async Task<IActionResult> RetryCleanup(VideoTaskType type, string id, string itemId) =>
|
||||
await ExecuteAsync(async () =>
|
||||
{
|
||||
if (type != VideoTaskType.Sync)
|
||||
throw new InvalidOperationException("仅普通同步任务支持重试旧本地文件清理。");
|
||||
var deleted = await _videos.RetryTaskItemLocalCleanupAsync(id, itemId);
|
||||
return ApiResult.Success(new
|
||||
{
|
||||
deletedCount = deleted,
|
||||
message = deleted > 0 ? $"已清理 {deleted} 个旧本地文件。" : "旧本地文件已不存在或仍被共享引用,清理状态已确认。"
|
||||
});
|
||||
});
|
||||
|
||||
[HttpPost("{type}/{id}/actions/{action}")]
|
||||
public async Task<IActionResult> Action(VideoTaskType type, string id, string action, CancellationToken cancellationToken) =>
|
||||
await ExecuteAsync(async () =>
|
||||
{
|
||||
await _tasks.ExecuteTaskActionAsync(type, id, action, cancellationToken);
|
||||
return ApiResult.Success("操作已提交");
|
||||
});
|
||||
|
||||
[HttpPost("storage-health/probe")]
|
||||
public async Task<IActionResult> Probe(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _tasks.ProbeStorageAsync(cancellationToken);
|
||||
return result.Success ? ApiResult.Success(new { message = result.Message }) : ApiResult.Fail(result.Message);
|
||||
}
|
||||
|
||||
private static async Task<IActionResult> ExecuteAsync(Func<Task<IActionResult>> action)
|
||||
{
|
||||
try { return await action(); }
|
||||
catch (Exception ex) when (ex is InvalidOperationException or KeyNotFoundException)
|
||||
{
|
||||
return ApiResult.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using dy.net.service;
|
||||
using dy.net.utils;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using dy.net.storage;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
@@ -13,11 +14,13 @@ namespace dy.net.Controllers
|
||||
{
|
||||
private readonly DouyinVideoService douyinVideoService;
|
||||
private readonly DouyinCommonService douyinCommonService;
|
||||
private readonly MediaStorageRouter _storageRouter;
|
||||
|
||||
public VideoController(DouyinVideoService dyCollectVideoService, DouyinCommonService douyinCommonService)
|
||||
public VideoController(DouyinVideoService dyCollectVideoService, DouyinCommonService douyinCommonService, MediaStorageRouter storageRouter)
|
||||
{
|
||||
this.douyinVideoService = dyCollectVideoService;
|
||||
this.douyinCommonService = douyinCommonService;
|
||||
_storageRouter = storageRouter;
|
||||
}
|
||||
/// <summary>
|
||||
/// 分页查询收藏视频
|
||||
@@ -88,6 +91,9 @@ namespace dy.net.Controllers
|
||||
return ApiResult.Fail("视频信息不能为空");
|
||||
}
|
||||
|
||||
if (video.StorageType.IsRemote())
|
||||
return await PlayRemoteVideoAsync(video);
|
||||
|
||||
// 1. 获取完整物理路径并校验
|
||||
string videoFullPath = video.VideoSavePath;
|
||||
if (string.IsNullOrWhiteSpace(videoFullPath))
|
||||
@@ -153,6 +159,34 @@ namespace dy.net.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> PlayRemoteVideoAsync(DouyinVideo video)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(video.VideoSavePath)) return ApiResult.Fail("远端视频路径不能为空");
|
||||
|
||||
long? from = null;
|
||||
long? to = null;
|
||||
var range = Request.Headers.Range.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(range) && range.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var parts = range[6..].Split('-', 2);
|
||||
if (long.TryParse(parts[0], out var parsedFrom)) from = parsedFrom;
|
||||
if (parts.Length > 1 && long.TryParse(parts[1], out var parsedTo)) to = parsedTo;
|
||||
}
|
||||
|
||||
await using var remote = await _storageRouter.Resolve(video.StorageType)
|
||||
.OpenReadAsync(video.VideoSavePath, from, to, HttpContext.RequestAborted);
|
||||
|
||||
Response.StatusCode = remote.StatusCode;
|
||||
Response.ContentType = remote.ContentType ?? GetContentType(video.VideoSavePath);
|
||||
Response.Headers.AcceptRanges = "bytes";
|
||||
if (remote.ContentLength.HasValue) Response.ContentLength = remote.ContentLength.Value;
|
||||
if (!string.IsNullOrWhiteSpace(remote.ContentRange)) Response.Headers.ContentRange = remote.ContentRange;
|
||||
|
||||
await remote.Stream.CopyToAsync(Response.Body, 81920, HttpContext.RequestAborted);
|
||||
await Response.Body.FlushAsync(HttpContext.RequestAborted);
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理分片请求(Range),异步安全处理流
|
||||
/// </summary>
|
||||
@@ -275,6 +309,7 @@ namespace dy.net.Controllers
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost("redown")]
|
||||
public async Task<IActionResult> ReDownload(ReDownViedoDto dto)
|
||||
{
|
||||
@@ -301,6 +336,7 @@ namespace dy.net.Controllers
|
||||
/// </summary>
|
||||
/// <param name="dto"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost("vdelete/batch")]
|
||||
public async Task<IActionResult> BathRealDelete(ReDownViedoDto dto)
|
||||
{
|
||||
@@ -321,6 +357,7 @@ namespace dy.net.Controllers
|
||||
/// </summary>
|
||||
/// <param name="vid"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet("vdelete/{vid}")]
|
||||
public async Task<IActionResult> DeleteVideo([FromRoute] string vid)
|
||||
{
|
||||
@@ -346,6 +383,7 @@ namespace dy.net.Controllers
|
||||
/// 查询已删除视频列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet("vdelete/get")]
|
||||
public async Task<IActionResult> GetDeleteVideo()
|
||||
{
|
||||
@@ -356,6 +394,7 @@ namespace dy.net.Controllers
|
||||
/// </summary>
|
||||
/// <param name="uperUid"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet("vdelete/byauthor/{uperUid}")]
|
||||
public async Task<IActionResult> DeleteByAuthor([FromRoute] string uperUid)
|
||||
{
|
||||
@@ -396,6 +435,7 @@ namespace dy.net.Controllers
|
||||
/// 删除无效视频记录
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet("removeInvalid")]
|
||||
public async Task<IActionResult> RemoveInvalidVideo()
|
||||
{
|
||||
@@ -463,6 +503,7 @@ namespace dy.net.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("/Move")]
|
||||
public async Task<IActionResult> Move()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using dy.net.model.dto;
|
||||
using dy.net.service;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace dy.net.Controllers
|
||||
{
|
||||
[Route("api/video/exclusions")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class VideoExclusionsController : ControllerBase
|
||||
{
|
||||
private readonly VideoExclusionService _service;
|
||||
|
||||
public VideoExclusionsController(VideoExclusionService service) => _service = service;
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List([FromQuery] VideoExclusionPageRequest request) =>
|
||||
ApiResult.Success(await _service.GetPageAsync(request));
|
||||
|
||||
[HttpPost("unexclude")]
|
||||
public async Task<IActionResult> Unexclude([FromBody] UnexcludeVideosRequest request)
|
||||
{
|
||||
try { return ApiResult.Success(await _service.UnexcludeAsync(request)); }
|
||||
catch (Exception ex) when (ex is InvalidOperationException or KeyNotFoundException)
|
||||
{
|
||||
return ApiResult.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user