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
@@ -0,0 +1,134 @@
using dy.net.model.dto;
using dy.net.model.entity;
using dy.net.model.response;
using dy.net.storage;
using dy.net.utils;
namespace dy.net.Tests;
public class StorageMigrationPathPolicyTests
{
private readonly DouyinCookie _cookie = new()
{
Id = "cookie",
WebDavCollectPath = "/账号/收藏",
WebDavFavoritePath = "/账号/喜欢",
WebDavFollowPath = "/账号/关注",
WebDavMixPath = "/账号/合集",
WebDavSeriesPath = "/账号/短剧"
};
[Theory]
[InlineData(VideoTypeEnum.dy_collects, "/账号/收藏")]
[InlineData(VideoTypeEnum.dy_favorite, "/账号/喜欢")]
public void Build_CollectionTypes_UseAuthorTitleAndStableAwemeSuffix(VideoTypeEnum type, string root)
{
var plan = StorageMigrationPathPolicy.Build(Video(type), _cookie, null, null, new AppConfig());
Assert.StartsWith(root + "/测试博主/Unicode标题123_aweme-1/", plan.VideoPath);
Assert.EndsWith("/aweme-1.mp4", plan.VideoPath);
Assert.EndsWith("/aweme-1-poster.jpg", plan.CoverPath);
}
[Fact]
public void Build_CustomCollection_UsesCurrentCategoryFolderRule()
{
var video = Video(VideoTypeEnum.dy_custom_collect);
var category = new DouyinCollectCate { SaveFolder = "专题 / 一", Name = "fallback" };
var plan = StorageMigrationPathPolicy.Build(video, _cookie, category, null, new AppConfig());
Assert.StartsWith("/账号/收藏/专题一/Unicode标题123_aweme-1/", plan.VideoPath);
}
[Theory]
[InlineData(VideoTypeEnum.dy_mix, "/账号/合集")]
[InlineData(VideoTypeEnum.dy_series, "/账号/短剧")]
public void Build_SeriesTypes_PreserveExistingEpisodeNumber(VideoTypeEnum type, string root)
{
var video = Video(type);
video.VideoSavePath = "/old/S01E07.mp4";
var category = new DouyinCollectCate { SaveFolder = "第一季", Name = "剧名" };
var plan = StorageMigrationPathPolicy.Build(video, _cookie, category, null, new AppConfig(), 2);
Assert.Equal(root + "/第一季/S01E07.mp4", plan.VideoPath);
Assert.Equal(root + "/第一季/poster.jpg", plan.CoverPath);
}
[Fact]
public void Build_Followed_UsesCustomFolderAndCurrentTitleTemplate()
{
var video = Video(VideoTypeEnum.dy_follows);
var followed = new DouyinFollowed { SavePath = "自定义博主目录" };
var config = new AppConfig { FullFollowedTitleTemplate = "{ReleaseTime}-{Author}-{Id}" };
var plan = StorageMigrationPathPolicy.Build(video, _cookie, null, followed, config);
Assert.Contains("/账号/关注/自定义博主目录/Unicode标题123/", plan.VideoPath);
Assert.EndsWith("/20260728-测试博主-aweme-1.mp4", plan.VideoPath);
}
[Theory]
[InlineData(VideoTypeEnum.dy_collects)]
[InlineData(VideoTypeEnum.dy_favorite)]
[InlineData(VideoTypeEnum.dy_follows)]
[InlineData(VideoTypeEnum.dy_custom_collect)]
[InlineData(VideoTypeEnum.dy_mix)]
[InlineData(VideoTypeEnum.dy_series)]
public void Build_CurrentSyncAndMigrationUseTheSameRemotePolicy(VideoTypeEnum type)
{
var timestamp = new DateTimeOffset(2026, 7, 28, 0, 0, 0, TimeSpan.Zero).ToUnixTimeSeconds();
var aweme = new Aweme
{
AwemeId = "aweme-1",
Desc = "Unicode 标题 / 123",
CreateTime = timestamp,
Author = new Author { Uid = "author-1", Nickname = "测试 博主" },
MixInfo = new MixInfo { Statis = new MixStatis { CurrentEpisode = 7 } },
Video = new Video
{
BitRate = new List<VideoBitRate>
{
new() { Format = "mp4", PlayAddr = new PlayAddr { FileHash = "hash", Width = 1080, Height = 1920 } }
}
}
};
var category = new DouyinCollectCate { SaveFolder = "第一季", Name = "剧名", CateType = type };
var followed = new DouyinFollowed { SavePath = "自定义博主目录" };
var config = new AppConfig { FullFollowedTitleTemplate = "{ReleaseTime}-{Author}-{Id}" };
var currentSync = StorageMigrationPathPolicy.Build(aweme, type, _cookie, category, followed, config);
var persisted = new DouyinVideo
{
Id = "record-1",
AwemeId = aweme.AwemeId,
ViedoType = type,
VideoTitle = aweme.Desc,
VideoSavePath = currentSync.VideoPath,
Author = aweme.Author.Nickname,
AuthorId = aweme.Author.Uid,
CreateTime = DateTimeUtil.Convert10BitTimestamp(timestamp),
FileHash = "hash",
Resolution = "1080×1920"
};
var migration = StorageMigrationPathPolicy.Build(persisted, _cookie, category, followed, config, 7);
Assert.Equal(currentSync.VideoPath, migration.VideoPath);
Assert.Equal(currentSync.CoverPath, migration.CoverPath);
}
private static DouyinVideo Video(VideoTypeEnum type) => new()
{
Id = "record-1",
AwemeId = "aweme-1",
ViedoType = type,
VideoTitle = "Unicode 标题 / 123",
VideoSavePath = "/old/video.mp4",
Author = "测试 博主",
AuthorId = "author-1",
CreateTime = new DateTime(2026, 7, 28),
Resolution = "1080×1920"
};
}