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
+155
View File
@@ -0,0 +1,155 @@
using System.Text.RegularExpressions;
using dy.net.model.dto;
using dy.net.model.entity;
using dy.net.model.response;
using dy.net.utils;
namespace dy.net.storage
{
public sealed class StorageMigrationPathPlan
{
public string VideoPath { get; set; }
public string CoverPath { get; set; }
public string AvatarPath { get; set; }
public string DirectoryPath => StoragePath.DirectoryName(VideoPath);
}
public static class StorageMigrationPathPolicy
{
private static readonly Regex EpisodeName = new("^S(?<season>\\d{2})E(?<episode>\\d{2,})$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static StorageMigrationPathPlan Build(
DouyinVideo video,
DouyinCookie cookie,
DouyinCollectCate category,
DouyinFollowed followed,
AppConfig config,
int episodeNumber = 1)
{
if (video == null) throw new ArgumentNullException(nameof(video));
if (cookie == null) throw new InvalidOperationException($"视频 {video.AwemeId} 缺少所属账号");
var root = GetRemoteRoot(cookie, video.ViedoType);
if (string.IsNullOrWhiteSpace(root)) throw new InvalidOperationException($"账号 {cookie.UserName} 未配置 {video.ViedoType} 的远端目标路径");
var extension = NormalizeExtension(Path.GetExtension(video.VideoSavePath));
var awemeId = string.IsNullOrWhiteSpace(video.AwemeId) ? video.Id : video.AwemeId;
var titleFolder = DouyinFileNameHelper.SanitizeLinuxFileName(video.VideoTitle, awemeId, true);
var authorFolder = DouyinFileNameHelper.SanitizeLinuxFileName(video.Author, video.AuthorId ?? "未知博主", true);
string directory;
string fileName;
switch (video.ViedoType)
{
case VideoTypeEnum.dy_custom_collect:
var customCategory = DouyinFileNameHelper.SanitizeLinuxFileName(category?.SaveFolder, category?.Name ?? "未分类", true);
directory = StoragePath.CombineRemote(root, customCategory, titleFolder + "_" + awemeId);
fileName = awemeId + extension;
break;
case VideoTypeEnum.dy_mix:
case VideoTypeEnum.dy_series:
var seriesFolder = DouyinFileNameHelper.SanitizeLinuxFileName(category?.SaveFolder, category?.Name ?? video.CateXId ?? "未分类", true);
directory = StoragePath.CombineRemote(root, seriesFolder);
var oldEpisode = EpisodeName.Match(Path.GetFileNameWithoutExtension(video.VideoSavePath) ?? string.Empty);
fileName = oldEpisode.Success
? oldEpisode.Value.ToUpperInvariant() + extension
: $"S01E{Math.Max(1, episodeNumber):D2}{extension}";
break;
case VideoTypeEnum.dy_follows:
var followedFolder = string.IsNullOrWhiteSpace(followed?.SavePath) ? authorFolder : followed.SavePath;
directory = StoragePath.CombineRemote(root, followedFolder, titleFolder);
fileName = BuildFollowedFileName(video, config, extension, awemeId);
break;
case VideoTypeEnum.dy_favorite:
case VideoTypeEnum.dy_collects:
default:
directory = StoragePath.CombineRemote(root, authorFolder, titleFolder + "_" + awemeId);
fileName = awemeId + extension;
break;
}
var videoPath = StoragePath.CombineRemote(directory, fileName);
var coverName = video.ViedoType is VideoTypeEnum.dy_mix or VideoTypeEnum.dy_series
? "poster.jpg"
: Path.GetFileNameWithoutExtension(fileName) + "-poster.jpg";
return new StorageMigrationPathPlan
{
VideoPath = videoPath,
CoverPath = StoragePath.CombineRemote(directory, coverName),
AvatarPath = string.IsNullOrWhiteSpace(video.AuthorAvatar) && string.IsNullOrWhiteSpace(video.AuthorAvatarUrl)
? string.Empty
: StoragePath.CombineRemote(root, "author", (string.IsNullOrWhiteSpace(video.AuthorId) ? awemeId : video.AuthorId) + ".jpg")
};
}
public static StorageMigrationPathPlan Build(
Aweme item,
VideoTypeEnum type,
DouyinCookie cookie,
DouyinCollectCate category,
DouyinFollowed followed,
AppConfig config)
{
if (item == null) throw new ArgumentNullException(nameof(item));
var bitrate = item.Video?.BitRate?.FirstOrDefault();
var format = string.IsNullOrWhiteSpace(bitrate?.Format) ? "mp4" : bitrate.Format.TrimStart('.');
var createdAt = DateTimeUtil.Convert10BitTimestamp(item.CreateTime);
var title = string.IsNullOrWhiteSpace(item.Desc)
? $"{item.Author?.Nickname}-{item.CreateTime}"
: item.Desc;
var video = new DouyinVideo
{
ViedoType = type,
AwemeId = item.AwemeId,
VideoTitle = title,
VideoSavePath = $"{item.AwemeId}.{format}",
Author = item.Author?.Nickname,
AuthorId = item.Author?.Uid,
AuthorAvatarUrl = item.Author?.AvatarLarger?.UrlList?.FirstOrDefault()
?? item.Author?.AvatarThumb?.UrlList?.FirstOrDefault(),
CreateTime = createdAt,
FileHash = bitrate?.PlayAddr?.FileHash,
Resolution = bitrate?.PlayAddr == null ? string.Empty : $"{bitrate.PlayAddr.Width}×{bitrate.PlayAddr.Height}"
};
var episode = item.MixInfo?.Statis?.CurrentEpisode ?? 1;
return Build(video, cookie, category, followed, config, Math.Max(1, episode));
}
public static string GetRemoteRoot(DouyinCookie cookie, VideoTypeEnum type) => type switch
{
VideoTypeEnum.dy_favorite => cookie.WebDavFavoritePath,
VideoTypeEnum.dy_follows => cookie.WebDavFollowPath,
VideoTypeEnum.dy_mix => cookie.WebDavMixPath,
VideoTypeEnum.dy_series => cookie.WebDavSeriesPath,
_ => cookie.WebDavCollectPath
};
public static IReadOnlyList<string> GetLocalRoots(DouyinCookie cookie)
{
if (cookie == null) return Array.Empty<string>();
return new[] { cookie.SavePath, cookie.FavSavePath, cookie.UpSavePath, cookie.MixPath, cookie.SeriesPath }
.Where(x => !string.IsNullOrWhiteSpace(x))
.Select(Path.GetFullPath)
.Distinct(StringComparer.Ordinal)
.ToArray();
}
private static string BuildFollowedFileName(DouyinVideo video, AppConfig config, string extension, string awemeId)
{
if (string.IsNullOrWhiteSpace(config?.FullFollowedTitleTemplate)) return awemeId + extension;
var generated = VideoTitleGenerator.Generate(config.FullFollowedTitleTemplate, new VideoTitleDataTemplate
{
Id = awemeId,
ReleaseTime = video.CreateTime,
VideoTitle = video.VideoTitle,
Author = video.Author,
FileHash = video.FileHash,
Resolution = video.Resolution
});
generated = DouyinFileNameHelper.SanitizeLinuxFileName(generated, awemeId);
return generated + extension;
}
private static string NormalizeExtension(string extension) => string.IsNullOrWhiteSpace(extension) ? ".mp4" : extension.ToLowerInvariant();
}
}