feat: add fnOS packaging, storage workflows and release pipeline
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user