feat: add fnOS packaging, storage workflows and release pipeline

This commit is contained in:
2026-08-11 18:05:49 +08:00
parent c5922f9b08
commit 95932f0199
181 changed files with 24024 additions and 1164 deletions
+8
View File
@@ -8,8 +8,16 @@
public string Key { get; set; }
public string CookieId { get; set; }
public int Total { get; set; }
public string Name { get; set; }
public int Status { get; set; }
public int StatusCode { get; set; }
public string StatusMessage { get; set; }
}
}
+43
View File
@@ -0,0 +1,43 @@
namespace dy.net.model.dto
{
public sealed class DouyinFollowLookupRequest
{
public string CookieId { get; set; }
public string DouyinNo { get; set; }
}
public sealed class DouyinFollowLookupResult
{
public string Query { get; set; }
public List<DouyinFollowCandidate> Candidates { get; set; } = new();
}
public sealed class DouyinFollowCandidate
{
public string SecUid { get; set; }
public string UperId { get; set; }
public string DouyinNo { get; set; }
public string UniqueId { get; set; }
public string ShortId { get; set; }
public string UperName { get; set; }
public string UperAvatar { get; set; }
public string Signature { get; set; }
public string Enterprise { get; set; }
public long FollowerCount { get; set; }
public bool ExactMatch { get; set; }
public bool AlreadyExists { get; set; }
}
}
+53
View File
@@ -0,0 +1,53 @@
namespace dy.net.model.dto
{
public enum DouyinLiveStatusState
{
Unknown = 0,
Offline = 1,
Live = 2
}
public sealed class FollowLiveMonitorUpdateDto
{
public string Id { get; set; }
public bool Enabled { get; set; }
}
public sealed class FollowLiveStatusRefreshDto
{
public string Id { get; set; }
}
public sealed class FollowLiveStatusQueryDto
{
public List<string> Ids { get; set; } = new();
}
public sealed class FollowLiveStatusDto
{
public string Id { get; set; }
public bool LiveMonitorEnabled { get; set; }
public DouyinLiveStatusState LiveStatus { get; set; }
public string LiveRoomId { get; set; }
public string LiveWebRid { get; set; }
public string LiveTitle { get; set; }
public string LiveRoomUrl { get; set; }
public DateTime? LiveCheckedAt { get; set; }
public DateTime? LiveStatusUpdatedAt { get; set; }
public DateTime? LiveStartedAt { get; set; }
public string LiveCheckError { get; set; }
public bool LiveStatusStale { get; set; }
public bool LiveEmailNotificationEnabled { get; set; }
public DateTime? LastLiveNotifiedAt { get; set; }
public string LastLiveNotificationError { get; set; }
}
public sealed class DouyinLiveStatusProbe
{
public DouyinLiveStatusState Status { get; set; }
public string RoomId { get; set; }
public string WebRid { get; set; }
public string Title { get; set; }
public DateTime? StartedAt { get; set; }
}
}
+5
View File
@@ -8,6 +8,11 @@
public string? Title { get; set; }
public string? Author { get; set; }
/// <summary>
/// 博主 UID 精确筛选。用于从关注列表直达该博主的已同步视频。
/// </summary>
public string? AuthorId { get; set; }
//public string? Name { get; set; }
public string? ViedoType { get; set; }
+31
View File
@@ -0,0 +1,31 @@
namespace dy.net.model.dto
{
public enum EmailSecurityMode
{
None = 0,
StartTls = 1,
SslOnConnect = 2
}
public sealed class EmailNotificationSettingsDto
{
public bool Enabled { get; set; }
public string Host { get; set; }
public int Port { get; set; } = 465;
public EmailSecurityMode SecurityMode { get; set; } = EmailSecurityMode.SslOnConnect;
public string UserName { get; set; }
public string Password { get; set; }
public bool HasPassword { get; set; }
public string FromAddress { get; set; }
public string FromName { get; set; }
public string Recipients { get; set; }
public DateTime? LastTestedAt { get; set; }
public string LastTestMessage { get; set; }
}
public sealed class FollowLiveEmailUpdateDto
{
public string Id { get; set; }
public bool Enabled { get; set; }
}
}
+99
View File
@@ -0,0 +1,99 @@
using System.Net;
namespace dy.net.model.dto
{
public enum MediaDownloadFailureKind
{
None = 0,
SourceUnavailable = 1,
SourceForbidden = 2,
SourceUnauthorized = 3,
SourceNotFound = 4,
SourceRateLimited = 5,
SourceInvalidContent = 6,
StorageUnavailable = 7,
IntegrityCheckFailed = 8,
Cancelled = 9
}
public sealed class MediaDownloadResult
{
public bool Success { get; init; }
public string ActualSavePath { get; init; }
public MediaDownloadFailureKind FailureKind { get; init; }
public int? HttpStatusCode { get; init; }
public string SourceHost { get; init; }
public int AttemptedUrlCount { get; init; }
public DateTime? RetryAfter { get; init; }
public string Message { get; init; }
public void Deconstruct(out bool success, out string actualSavePath)
{
success = Success;
actualSavePath = ActualSavePath;
}
public Exception ToException() => FailureKind switch
{
MediaDownloadFailureKind.StorageUnavailable => new MediaStorageException(Message ?? "媒体存储写入失败。"),
MediaDownloadFailureKind.IntegrityCheckFailed => new MediaIntegrityException(Message ?? "媒体完整性校验失败。"),
MediaDownloadFailureKind.Cancelled => new OperationCanceledException(Message ?? "媒体下载已取消。"),
_ => new MediaSourceException(FailureKind, Message ?? "媒体来源不可用。", HttpStatusCode, SourceHost, RetryAfter)
};
public static MediaDownloadResult Succeeded(string path, string host, int attempted) => new()
{
Success = true,
ActualSavePath = path,
SourceHost = host,
AttemptedUrlCount = attempted
};
}
public sealed class MediaSourceException : Exception
{
public MediaSourceException(
MediaDownloadFailureKind kind,
string message,
int? statusCode = null,
string sourceHost = null,
DateTime? retryAfter = null,
Exception innerException = null) : base(message, innerException)
{
FailureKind = kind;
StatusCode = statusCode;
SourceHost = sourceHost;
RetryAfter = retryAfter;
}
public MediaDownloadFailureKind FailureKind { get; }
public int? StatusCode { get; }
public string SourceHost { get; }
public DateTime? RetryAfter { get; }
}
public sealed class MediaStorageException : IOException
{
public MediaStorageException(string message, Exception innerException = null) : base(message, innerException) { }
}
public sealed class MediaIntegrityException : IOException
{
public MediaIntegrityException(string message, Exception innerException = null) : base(message, innerException) { }
}
public enum SourceFailureDisposition
{
Continue = 0,
WaitForSource = 1,
RequiresAuthorization = 2
}
public sealed class SourceAccessDecision
{
public bool Allowed { get; init; }
public bool IsProbe { get; init; }
public string Message { get; init; }
public DateTime? RetryAt { get; init; }
}
}
@@ -0,0 +1,81 @@
using dy.net.model.entity;
namespace dy.net.model.dto
{
public enum OpenListDirectoryRepairStatus
{
Queued = 0,
Scanning = 1,
AwaitingConfirmation = 2,
Cleaning = 3,
Completed = 4,
PartiallyFailed = 5,
Cancelled = 6,
Paused = 7
}
public enum OpenListDirectoryRepairItemStatus
{
Pending = 0,
Inspecting = 1,
EmptyConfirmed = 2,
SkippedNonEmpty = 3,
Deleting = 4,
Deleted = 5,
Failed = 6,
Missing = 7
}
public sealed class OpenListDirectoryRepairPreflightRequest
{
public string LogicalPath { get; set; } = "/collect/Kk";
}
public sealed class OpenListDirectoryRepairPreflightResult
{
public bool CanStart { get; set; }
public string RequestedPath { get; set; }
public string CanonicalPath { get; set; }
public string CandidatePattern { get; set; }
public string ConfigurationFingerprint { get; set; }
public int CandidateCount { get; set; }
public List<string> Errors { get; set; } = new();
public List<string> Warnings { get; set; } = new();
}
public sealed class CreateOpenListDirectoryRepairRequest
{
public string LogicalPath { get; set; } = "/collect/Kk";
public string ConfigurationFingerprint { get; set; }
}
public sealed class ConfirmOpenListDirectoryRepairRequest
{
public string ConfirmationToken { get; set; }
}
public sealed class OpenListDirectoryRepairTaskDetail
{
public OpenListDirectoryRepairTask Task { get; set; }
public string StatusText { get; set; }
public string ConfirmationToken { get; set; }
public bool CanConfirmCleanup { get; set; }
public bool CanCancel { get; set; }
public bool CanResume { get; set; }
public bool CanRetryFailed { get; set; }
}
public sealed class OpenListDirectoryRepairItemPageRequest
{
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 20;
public OpenListDirectoryRepairItemStatus? Status { get; set; }
public string Keyword { get; set; }
}
public sealed class OpenListDirectoryRepairItemPage
{
public int TotalCount { get; set; }
public List<OpenListDirectoryRepairItem> Items { get; set; } = new();
}
}
+47
View File
@@ -0,0 +1,47 @@
namespace dy.net.model.dto
{
public class OpenListSettingsDto
{
public StorageType StorageType { get; set; }
public string Endpoint { get; set; }
public string BasePath { get; set; }
public string LocalStagingPath { get; set; }
public string SourcePath { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public bool HasPassword { get; set; }
public DateTime? LastTestedAt { get; set; }
public string LastTestMessage { get; set; }
public bool RequiresCutover { get; set; }
public int LegacyWebDavRecordCount { get; set; }
public bool SuggestedFromLegacy { get; set; }
}
public class OpenListTestRequest
{
public string Endpoint { get; set; }
public string BasePath { get; set; }
public string LocalStagingPath { get; set; }
public string SourcePath { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
public sealed class OpenListDirectoryRequest : OpenListTestRequest
{
public string Path { get; set; }
}
public sealed class OpenListDirectoryItemDto
{
public string Name { get; set; }
public string Path { get; set; }
}
public sealed class OpenListDirectoryListDto
{
public string Path { get; set; }
public bool CanWrite { get; set; }
public List<OpenListDirectoryItemDto> Directories { get; set; } = new();
}
}
+15
View File
@@ -0,0 +1,15 @@
namespace dy.net.model.dto
{
public enum OpenListTransferStatus
{
Queued = 0,
WaitingForSource = 1,
Copying = 2,
Verifying = 3,
Promoting = 4,
WaitingRetry = 5,
Succeeded = 6,
Failed = 7,
Cancelled = 8
}
}
+144
View File
@@ -0,0 +1,144 @@
using dy.net.model.entity;
namespace dy.net.model.dto
{
public enum StorageMigrationTaskStatus
{
Queued = 0,
Running = 1,
Paused = 2,
Completed = 3,
PartiallyFailed = 4,
Cancelled = 5,
Cleaning = 6,
Cleaned = 7,
RolledBack = 8
}
public enum StorageMigrationItemStage
{
Pending = 0,
Uploading = 1,
Verifying = 2,
Committing = 3,
Succeeded = 4,
SucceededWithWarnings = 5,
Failed = 6,
Cleaned = 7,
RolledBack = 8,
RecordRemoved = 9
}
public sealed class StorageMigrationPreflightRequest
{
public bool VerifyCapabilities { get; set; } = true;
}
public sealed class StorageMigrationPreflightResult
{
public bool CanStart { get; set; }
public string ConfigurationFingerprint { get; set; }
public int RecordCount { get; set; }
public int LocalRecordCount { get; set; }
public int LegacyWebDavRecordCount { get; set; }
public int AdoptableCount { get; set; }
public int TransferRequiredCount { get; set; }
public int ReadableFileCount { get; set; }
public int MissingFileCount { get; set; }
public long TotalBytes { get; set; }
public int InvalidCookieCount { get; set; }
public int MissingTargetPathCount { get; set; }
public int ConflictCount { get; set; }
public string Capacity { get; set; } = "unknown";
public List<string> Errors { get; set; } = new();
public List<string> Warnings { get; set; } = new();
}
public sealed class CreateStorageMigrationRequest
{
public int Concurrency { get; set; } = 1;
public string ConfigurationFingerprint { get; set; }
}
public sealed class StorageMigrationItemPageRequest
{
public int PageIndex { get; set; } = 1;
public int PageSize { get; set; } = 20;
public StorageMigrationItemStage? Stage { get; set; }
}
public sealed class StorageMigrationTaskDetail
{
public StorageMigrationTask Task { get; set; }
public string StatusText { get; set; }
public string CurrentFile { get; set; }
public bool CanPause { get; set; }
public bool CanResume { get; set; }
public bool CanCancel { get; set; }
public bool CanRetryFailed { get; set; }
public bool CanCleanup { get; set; }
public bool CanRollback { get; set; }
public bool CanArchive { get; set; }
}
public sealed class StorageRecordInventory
{
public StorageType CurrentStorageType { get; set; }
public int TotalRecordCount { get; set; }
public int LocalRecordCount { get; set; }
public int WebDavRecordCount { get; set; }
public int OpenListRecordCount { get; set; }
public int CurrentStorageRecordCount { get; set; }
public int OtherStorageRecordCount { get; set; }
public long LocalDeclaredBytes { get; set; }
public long WebDavDeclaredBytes { get; set; }
public long OpenListDeclaredBytes { get; set; }
public bool AllRecordsOnCurrentStorage => TotalRecordCount == CurrentStorageRecordCount;
}
public sealed class StorageMigrationArchiveResult
{
public int ArchivedCount { get; set; }
public int RequiresCleanupCount { get; set; }
public string Message { get; set; }
}
public sealed class StorageMigrationItemPage
{
public int TotalCount { get; set; }
public List<StorageMigrationItem> Items { get; set; } = new();
}
public sealed class FailedMigrationRecordPreview
{
public bool CanExecute { get; set; }
public int ActiveMigrationCount { get; set; }
public int FailedItemCount { get; set; }
public int DistinctVideoCount { get; set; }
public int EligibleRecordCount { get; set; }
public int AlreadyMissingCount { get; set; }
public int ChangedRecordCount { get; set; }
public int PermanentlyExcludedCount { get; set; }
public int InvalidSnapshotCount { get; set; }
public string ConfirmationToken { get; set; }
public List<string> Errors { get; set; } = new();
public List<string> Warnings { get; set; } = new();
}
public sealed class RemoveFailedMigrationRecordsRequest
{
public string ConfirmationToken { get; set; }
}
public sealed class RemoveFailedMigrationRecordsResult
{
public int DeletedRecordCount { get; set; }
public int AlreadyMissingCount { get; set; }
public int RemovedItemCount { get; set; }
public int AffectedTaskCount { get; set; }
public int SkippedChangedCount { get; set; }
public int SkippedExcludedCount { get; set; }
public int InvalidSnapshotCount { get; set; }
public string Message { get; set; }
}
}
+24
View File
@@ -0,0 +1,24 @@
namespace dy.net.model.dto
{
/// <summary>
/// 媒体实际保存的位置。数值 0 必须保留为本地存储,以兼容历史数据库记录。
/// </summary>
public enum StorageType
{
Local = 0,
WebDav = 1,
OpenList = 2
}
/// <summary>
/// StorageType 扩展方法。
/// </summary>
public static class StorageTypeExtensions
{
/// <summary>
/// 判断是否为远程存储(WebDAV 或 OpenList),与本地存储相对。
/// </summary>
public static bool IsRemote(this StorageType type) =>
type is StorageType.WebDav or StorageType.OpenList;
}
}
+233
View File
@@ -0,0 +1,233 @@
using dy.net.model.entity;
namespace dy.net.model.dto
{
public enum VideoTaskType
{
Sync = 0,
Redownload = 1,
ExclusionRestore = 2,
StorageMigration = 3,
StorageMaintenance = 4
}
public enum VideoTaskTrigger
{
Scheduled = 0,
Manual = 1,
UserAction = 2,
UpgradeRecovery = 3
}
public enum VideoTaskStatus
{
Queued = 0,
Running = 1,
WaitingForStorage = 2,
Completed = 3,
PartiallyFailed = 4,
Failed = 5,
Interrupted = 6,
Paused = 7,
Cancelled = 8,
Cleaning = 9,
Cleaned = 10,
RolledBack = 11,
WaitingForSource = 12,
Scanning = 13,
AwaitingConfirmation = 14
}
public enum VideoTaskItemStage
{
Pending = 0,
Downloading = 1,
Verifying = 2,
Committing = 3,
Succeeded = 4,
SucceededWithWarnings = 5,
Failed = 6,
Skipped = 7,
WaitingForStorage = 8,
Cleaned = 9,
RolledBack = 10,
Cancelled = 11,
WaitingForSource = 12,
RecordRemoved = 13,
Inspecting = 14,
EmptyConfirmed = 15,
SkippedNonEmpty = 16
}
public enum VideoTaskErrorType
{
None = 0,
SourceUnavailable = 1,
CookieInvalid = 2,
StorageUnavailable = 3,
IntegrityCheckFailed = 4,
DatabaseCommitFailed = 5,
Interrupted = 6,
ConfigurationChanged = 7,
Excluded = 8,
Unknown = 9,
SourceForbidden = 10,
SourceRateLimited = 11
}
public enum VideoTaskSkipReason
{
None = 0,
AlreadyExists = 1,
Deduplicated = 2,
PermanentlyExcluded = 3,
ConfigurationExcluded = 4,
NoMediaSource = 5
}
public enum MediaStorageHealthStatus
{
Healthy = 0,
Unavailable = 1
}
public sealed class VideoTaskPageRequest : PageRequestDto
{
public VideoTaskType? Type { get; set; }
public VideoTaskStatus? Status { get; set; }
public VideoTaskTrigger? Trigger { get; set; }
public string Keyword { get; set; }
public DateTime? From { get; set; }
public DateTime? To { get; set; }
}
public sealed class VideoTaskItemPageRequest : PageRequestDto
{
public VideoTaskItemStage? Stage { get; set; }
public VideoTaskErrorType? ErrorType { get; set; }
public string Keyword { get; set; }
public bool IncludeSkipped { get; set; }
}
public sealed class VideoTaskListPage
{
public int TotalCount { get; set; }
public List<UnifiedVideoTaskDto> Items { get; set; } = new();
}
public sealed class VideoTaskItemPage
{
public int TotalCount { get; set; }
public List<UnifiedVideoTaskItemDto> Items { get; set; } = new();
}
public sealed class UnifiedVideoTaskDto
{
public string Id { get; set; }
public VideoTaskType Type { get; set; }
public VideoTaskTrigger Trigger { get; set; }
public VideoTaskStatus Status { get; set; }
public string Title { get; set; }
public VideoTypeEnum? VideoType { get; set; }
public StorageType StorageType { get; set; }
public int TotalCount { get; set; }
public int PendingCount { get; set; }
public int RunningCount { get; set; }
public int SuccessCount { get; set; }
public int WarningCount { get; set; }
public int FailedCount { get; set; }
public int SkippedCount { get; set; }
public int RemovedCount { get; set; }
public string CurrentFile { get; set; }
public string ErrorMessage { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public List<string> AvailableActions { get; set; } = new();
}
public sealed class UnifiedVideoTaskItemDto
{
public string Id { get; set; }
public string TaskId { get; set; }
public string VideoId { get; set; }
public string AwemeId { get; set; }
public string CookieName { get; set; }
public VideoTypeEnum? VideoType { get; set; }
public string VideoTitle { get; set; }
public string Author { get; set; }
public string TargetPath { get; set; }
public VideoTaskItemStage Stage { get; set; }
public VideoTaskErrorType ErrorType { get; set; }
public VideoTaskSkipReason SkipReason { get; set; }
public int Attempts { get; set; }
public long ExpectedLength { get; set; }
public long ActualLength { get; set; }
public string ErrorMessage { get; set; }
public string WarningMessage { get; set; }
public string SourceHost { get; set; }
public int? HttpStatusCode { get; set; }
public DateTime? RetryAfter { get; set; }
public string ExclusionId { get; set; }
public DateTime? ExclusionReleasedAt { get; set; }
public string RelatedTaskId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public bool CanRetry { get; set; }
public bool CanRetryCleanup { get; set; }
public string CleanupError { get; set; }
public bool CanUnexclude { get; set; }
}
public sealed class VideoTaskSummaryDto
{
public int Queued { get; set; }
public int Running { get; set; }
public int WaitingForStorage { get; set; }
public int WaitingForSource { get; set; }
public int Failed { get; set; }
public int Completed { get; set; }
public MediaStorageHealth StorageHealth { get; set; }
public List<DouyinSourceHealthDto> SourceHealth { get; set; } = new();
}
public sealed class DouyinSourceHealthDto
{
public string CookieId { get; set; }
public string CookieName { get; set; }
public int ConsecutiveForbidden { get; set; }
public DateTime? CooldownUntil { get; set; }
public bool RequiresAuthorization { get; set; }
public bool ProbePending { get; set; }
public int? LastStatusCode { get; set; }
public string LastError { get; set; }
public DateTime? UpdatedAt { get; set; }
}
public sealed class UnexcludeVideosRequest
{
public List<string> Ids { get; set; } = new();
public bool CreateDownloadTask { get; set; }
}
public sealed class UnexcludeVideosResult
{
public int ReleasedCount { get; set; }
public int QueuedCount { get; set; }
public string TaskId { get; set; }
public List<string> CannotQueueIds { get; set; } = new();
public string Message { get; set; }
}
public sealed class VideoExclusionPageRequest : PageRequestDto
{
public string Keyword { get; set; }
}
public sealed class VideoExclusionPage
{
public int TotalCount { get; set; }
public List<DouyinVideoDelete> Items { get; set; } = new();
}
}
+5
View File
@@ -47,6 +47,11 @@
/// </summary>
dy_followuser_once = 2000,
/// <summary>
/// 独立直播状态监测任务,不属于视频下载任务。
/// </summary>
dy_live_monitor = 3000,
}
}
+50
View File
@@ -0,0 +1,50 @@
namespace dy.net.model.dto
{
public class WebDavSettingsDto
{
public StorageType StorageType { get; set; }
public string Endpoint { get; set; }
public string BasePath { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public bool HasPassword { get; set; }
public bool AllowInvalidCertificate { get; set; }
public DateTime? LastTestedAt { get; set; }
public string LastTestMessage { get; set; }
}
public class WebDavTestRequest
{
public string Endpoint { get; set; }
public string BasePath { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public bool AllowInvalidCertificate { get; set; }
}
/// <summary>统一的存储连接测试请求;新远端存储仅支持 OpenList。</summary>
public class StorageTestRequest
{
public StorageType StorageType { get; set; }
public string Endpoint { get; set; }
public string BasePath { get; set; }
public string LocalStagingPath { get; set; }
public string SourcePath { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public bool AllowInvalidCertificate { get; set; }
}
/// <summary>统一的存储配置保存请求;WebDAV 字段仅为数据库升级兼容保留。</summary>
public class StorageSettingsDto
{
public StorageType StorageType { get; set; }
public string Endpoint { get; set; }
public string BasePath { get; set; }
public string LocalStagingPath { get; set; }
public string SourcePath { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public bool AllowInvalidCertificate { get; set; }
}
}