diff --git a/Attributes/AutoInitAttribute.cs b/Attributes/AutoInitAttribute.cs
new file mode 100644
index 0000000..9ee8fc4
--- /dev/null
+++ b/Attributes/AutoInitAttribute.cs
@@ -0,0 +1,25 @@
+namespace MegghysAPI.Attributes
+{
+ [AttributeUsage(AttributeTargets.Method)]
+ public class AutoInitAttribute : Attribute
+ {
+ public AutoInitAttribute()
+ {
+ }
+ public AutoInitAttribute(Action postInit)
+ {
+ PostInit = postInit;
+ }
+ public AutoInitAttribute(string log)
+ {
+ LogMessage = log;
+ }
+ ///
+ /// 越低越优先
+ ///
+ public int Order { get; set; } = 10;
+ public string LogMessage { get; set; }
+ public Action PostInit { get; set; }
+ public bool Async { get; set; } = false;
+ }
+}
diff --git a/Attributes/AutoTimerAttribute.cs b/Attributes/AutoTimerAttribute.cs
new file mode 100644
index 0000000..69e4ff5
--- /dev/null
+++ b/Attributes/AutoTimerAttribute.cs
@@ -0,0 +1,13 @@
+namespace MegghysAPI.Attributes
+{
+ [AttributeUsage(AttributeTargets.Method)]
+ public class AutoTimerAttribute : Attribute
+ {
+ ///
+ /// 单位为s
+ ///
+ public int Time { get; set; } = 30;
+ public bool CallOnRegister { get; set; } = false;
+ public bool Log { get; set; } = true;
+ }
+}
diff --git a/Components/App.razor b/Components/App.razor
new file mode 100644
index 0000000..346ac69
--- /dev/null
+++ b/Components/App.razor
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Components/Layout/MainLayout.razor b/Components/Layout/MainLayout.razor
new file mode 100644
index 0000000..302772b
--- /dev/null
+++ b/Components/Layout/MainLayout.razor
@@ -0,0 +1,26 @@
+@inherits LayoutComponentBase
+
+
+
+ MegghysAPI
+
+
+
+
+
+ @Body
+
+
+
+
+ Documentation and demos
+
+ About Blazor
+
+
+
+
+ An unhandled error has occurred.
+
Reload
+
🗙
+
diff --git a/Components/Layout/NavMenu.razor b/Components/Layout/NavMenu.razor
new file mode 100644
index 0000000..fd44fea
--- /dev/null
+++ b/Components/Layout/NavMenu.razor
@@ -0,0 +1,20 @@
+@rendermode InteractiveServer
+
+
+
+@code {
+ private bool expanded = true;
+ public DesignThemeModes Mode { get; set; }
+ public OfficeColor? OfficeColor { get; set; }
+}
diff --git a/Components/Pages/Counter.razor b/Components/Pages/Counter.razor
new file mode 100644
index 0000000..bc892df
--- /dev/null
+++ b/Components/Pages/Counter.razor
@@ -0,0 +1,21 @@
+@page "/counter"
+@rendermode InteractiveServer
+
+Counter
+
+Counter
+
+
+ Current count: @currentCount
+
+
+Click me
+
+@code {
+ private int currentCount = 0;
+
+ private void IncrementCount()
+ {
+ currentCount++;
+ }
+}
diff --git a/Components/Pages/Error.razor b/Components/Pages/Error.razor
new file mode 100644
index 0000000..576cc2d
--- /dev/null
+++ b/Components/Pages/Error.razor
@@ -0,0 +1,36 @@
+@page "/Error"
+@using System.Diagnostics
+
+Error
+
+Error.
+An error occurred while processing your request.
+
+@if (ShowRequestId)
+{
+
+ Request ID: @RequestId
+
+}
+
+Development Mode
+
+ Swapping to Development environment will display more detailed information about the error that occurred.
+
+
+ The Development environment shouldn't be enabled for deployed applications.
+ It can result in displaying sensitive information from exceptions to end users.
+ For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
+ and restarting the app.
+
+
+@code{
+ [CascadingParameter]
+ private HttpContext? HttpContext { get; set; }
+
+ private string? RequestId { get; set; }
+ private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+
+ protected override void OnInitialized() =>
+ RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
+}
diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor
new file mode 100644
index 0000000..96714a2
--- /dev/null
+++ b/Components/Pages/Home.razor
@@ -0,0 +1,7 @@
+@page "/"
+
+Home
+
+Hello, world!
+
+Welcome to your new Fluent Blazor app.
\ No newline at end of file
diff --git a/Components/Pages/Pixiv.razor b/Components/Pages/Pixiv.razor
new file mode 100644
index 0000000..f0775ef
--- /dev/null
+++ b/Components/Pages/Pixiv.razor
@@ -0,0 +1,43 @@
+@page "/pixiv"
+@rendermode InteractiveServer
+
+Pixiv
+
+
+ 随机获取
+
+
+
+
+ @foreach (var (index, img) in CurrentImgs.S3URL.Index())
+ {
+ @if(index == 0)
+ {
+
+ }
+ else
+ {
+
+ }
+ }
+
+
+@code {
+ public Modules.PixivFavoriteDownloader.Pixiv.PixivImgInfo CurrentImgs;
+
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+ RandomGet();
+ }
+
+ public void FirstImgLoaded()
+ {
+
+ }
+
+ public void RandomGet()
+ {
+ CurrentImgs = Modules.PixivFavoriteDownloader.Favorites.OrderByRandom().First();
+ }
+}
diff --git a/Components/Pages/Weather.razor b/Components/Pages/Weather.razor
new file mode 100644
index 0000000..0506bba
--- /dev/null
+++ b/Components/Pages/Weather.razor
@@ -0,0 +1,43 @@
+@page "/weather"
+@attribute [StreamRendering]
+
+Weather
+
+Weather
+
+This component demonstrates showing data.
+
+
+
+
+
+
+
+
+
+@code {
+ private IQueryable? forecasts;
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Simulate asynchronous loading to demonstrate streaming rendering
+ await Task.Delay(500);
+
+ var startDate = DateOnly.FromDateTime(DateTime.Now);
+ var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
+ forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
+ {
+ Date = startDate.AddDays(index),
+ TemperatureC = Random.Shared.Next(-20, 55),
+ Summary = summaries[Random.Shared.Next(summaries.Length)]
+ }).AsQueryable();
+ }
+
+ private class WeatherForecast
+ {
+ public DateOnly Date { get; set; }
+ public int TemperatureC { get; set; }
+ public string? Summary { get; set; }
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+ }
+}
diff --git a/Components/Routes.razor b/Components/Routes.razor
new file mode 100644
index 0000000..f756e19
--- /dev/null
+++ b/Components/Routes.razor
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/Components/_Imports.razor b/Components/_Imports.razor
new file mode 100644
index 0000000..8c17df0
--- /dev/null
+++ b/Components/_Imports.razor
@@ -0,0 +1,15 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.FluentUI.AspNetCore.Components
+@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons
+@using Microsoft.JSInterop
+@using MegghysAPI
+@using MegghysAPI.Components
+@using MegghysAPI.Entities
+@using static MegghysAPI.Datas
+@using Masuit.Tools
diff --git a/Config.cs b/Config.cs
new file mode 100644
index 0000000..9fdfed1
--- /dev/null
+++ b/Config.cs
@@ -0,0 +1,67 @@
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Unicode;
+
+namespace MegghysAPI
+{
+ public class Config
+ {
+ public static readonly JsonSerializerOptions DefaultSerializerOptions = new()
+ {
+ WriteIndented = true,
+ Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
+ };
+ private static Config _oldInstance = new();
+ private static Config? _instance;
+ public static Config Instance { get { _instance ??= Load(); return _instance; } }
+ public static string ConfigPath => Path.Combine(Datas.DataPath, "Config.json");
+ static bool _first = true;
+ public static Config Load()
+ {
+ if (File.Exists(ConfigPath))
+ {
+ try
+ {
+ var config = Newtonsoft.Json.JsonConvert.DeserializeObject(File.ReadAllText(ConfigPath))!;
+ _oldInstance = config;
+ if (_first)
+ config.Save();
+ return _oldInstance;
+ }
+ catch
+ {
+ Console.WriteLine($"配置文件读取失败");
+ return _oldInstance!;
+ }
+ }
+ else
+ {
+ var config = new Config();
+ config.Save();
+ return config;
+ }
+ }
+ public static void Reload()
+ {
+ _oldInstance = _instance;
+ _instance = null;
+ }
+ public void Save()
+ {
+ if (!Directory.Exists(Datas.DataPath))
+ Directory.CreateDirectory(Datas.DataPath);
+ File.WriteAllText(ConfigPath, JsonSerializer.Serialize(this, DefaultSerializerOptions));
+ }
+
+
+ public bool LogDanmakuError { get; set; } = false;
+ public string ListenHost { get; set; } = "*";
+ public int ListenPort { get; set; } = 4050;
+ public string DBConnectString { get; set; } = $"Host=localhost;Port=5432;Username=postgres;Password=!Hzy05152484; Database=MegghysAPI;";
+ public string MinIOEndpoint { get; set; } = "eternalland.top:9000";
+ public string MinIOAccessKey { get; set; } = "RBzbElm21lf7sy7wK7wG";
+ public string MinIOSecretKey { get; set; } = "Nko5azOSUiYgOUeLsj8hLxGz4cKC8XOcH0VS7lWq";
+ public string MinIORegion { get; set; } = "cn-main";
+ public string MinIOBucket { get; set; } = "general";
+ }
+}
diff --git a/Controllers/MControllerBase.cs b/Controllers/MControllerBase.cs
new file mode 100644
index 0000000..bcda191
--- /dev/null
+++ b/Controllers/MControllerBase.cs
@@ -0,0 +1,91 @@
+using Microsoft.AspNetCore.Mvc;
+using Minio.DataModel.Result;
+
+namespace MegghysAPI.Controllers
+{
+ public interface IResponseResult
+ {
+ public string Message { get; set; }
+ public int Code { get; set; }
+ public object? Data { get; set; }
+ }
+ public class ResponseResult : IResponseResult
+ {
+ public ResponseResult()
+ {
+
+ }
+ public ResponseResult(object? data, int code = 200, string msg = "成功")
+ {
+ Message = msg;
+ Code = code;
+ Data = data;
+ }
+ public string Message { get; set; } = "";
+ public int Code { get; set; } = 0;
+ public object? Data { get; set; } = new();
+ }
+ public class ResponseResult
+ {
+ public ResponseResult()
+ {
+
+ }
+ public ResponseResult(T? data, int code = 200, string msg = "成功")
+ {
+ Message = msg;
+ Code = code;
+ Data = data;
+ }
+ public string Message { get; set; } = "";
+ public int Code { get; set; } = 0;
+ public T? Data { get; set; } = default;
+ public static implicit operator ResponseResult(T value)
+ {
+ return new(value);
+ }
+ public static implicit operator ResponseResult(ResponseResult value)
+ {
+ return new(value.Data, value.Code, value.Message);
+ }
+ public static implicit operator ResponseResult(ResponseResult result)
+ => new(result.Data is null ? default : (T)result.Data, result.Code, result.Message);
+ }
+ public class PaginationResponse : ResponseResult
+ {
+ public PaginationResponse(T data, int pn, int ps, long total)
+ {
+ Code = 200;
+ Message = "成功";
+ Data = data;
+ Ps = ps;
+ Pn = pn;
+ Total = total;
+ More = ps * pn < total;
+ }
+ public int Ps { get; set; }
+ public int Pn { get; set; }
+ public long Total { get; set; }
+ public bool More { get; set; }
+ public static implicit operator PaginationResponse(ResponseResult result)
+ => new(result.Data is null ? default : (T)result.Data, -1, -1, -1);
+ }
+ public class MControllerBase : Controller
+ {
+ protected HttpContext context => ControllerContext.HttpContext;
+ protected ResponseResult ResponseOK(T? data, string msg = "成功")
+ => new(data, 200, msg);
+ protected ResponseResult ResponseOKWithoutData(string msg = "成功")
+ => new(null, 200, msg);
+ protected ResponseResult ResponseUnauthorized(string msg = "未认证")
+ => new(null, 401, msg);
+ protected ResponseResult ResponseForbidden(string msg = "无权限")
+ => new(null, 403, msg);
+ protected ResponseResult ResponseNotFound(string msg = "未找到")
+ => new(null, 404, msg);
+ protected ResponseResult ResponseInternalError(string msg = "内部错误")
+ => new(null, 500, msg);
+ protected ResponseResult ResponseBadRequest(string msg = "无效操作")
+ => new(null, 400, msg);
+ }
+}
diff --git a/Controllers/PublicController.cs b/Controllers/PublicController.cs
new file mode 100644
index 0000000..c729b74
--- /dev/null
+++ b/Controllers/PublicController.cs
@@ -0,0 +1,15 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace MegghysAPI.Controllers
+{
+ [Route("api/public")]
+ [ApiController]
+ public class PublicController : MControllerBase
+ {
+ [Route("header")]
+ public IActionResult Header()
+ {
+ return Ok(string.Join(",", context.Request.Headers.Keys));
+ }
+ }
+}
diff --git a/Core/FileManager.cs b/Core/FileManager.cs
new file mode 100644
index 0000000..e655efa
--- /dev/null
+++ b/Core/FileManager.cs
@@ -0,0 +1,141 @@
+using MegghysAPI.Attributes;
+using Minio;
+using Minio.DataModel.Args;
+
+namespace MegghysAPI.Core
+{
+ internal class FileManager
+ {
+
+ private static IMinioClient minio = new MinioClient()
+ .WithEndpoint(Config.Instance.MinIOEndpoint)
+ .WithRegion(Config.Instance.MinIORegion)
+ .WithCredentials(Config.Instance.MinIOAccessKey, Config.Instance.MinIOSecretKey)
+ .WithSSL(false)
+ .Build();
+ public const string FavoritePath = "/Files/Favorite/";
+ [AutoInit("初始化文件服务")]
+ public static void Init()
+ {
+ }
+ public static async Task DownloadFileAsync(string fileName, string path)
+ {
+ try
+ {
+ var temp = Path.Combine(Datas.TempFilePath, fileName);
+ if (File.Exists(temp))
+ return temp;
+ var result = await minio.GetObjectAsync(new Minio.DataModel.Args.GetObjectArgs()
+ .WithBucket(Config.Instance.MinIOBucket)
+ .WithObject(fileName)
+ .WithCallbackStream(stream =>
+ {
+ // 将下载流保存为本地文件
+ using var fileStream = File.Create(temp);
+ stream.CopyTo(fileStream);
+ }));
+
+ return temp;
+ }
+ catch (Exception ex)
+ {
+ Logs.Warn($"未能从储存空间下载文件: {path}{fileName}\r\n{ex}");
+ return null;
+ }
+ }
+ public static bool UploadBytes(byte[] data, string remotePath, string contentType = "application/octet-stream", bool overwrite = false)
+ {
+ using var stream = new MemoryStream(data);
+ return UploadStream(stream, remotePath, contentType, overwrite: overwrite);
+ }
+ public static async Task UploadStreamAsync(Stream stream, string remotePath, string contentType = "application/octet-stream", bool overwrite = false)
+ {
+ try
+ {
+ if (!overwrite && await ContainsFileAsync(remotePath))
+ return true;
+ // Upload a file to bucket.
+ var putObjectArgs = new PutObjectArgs()
+ .WithBucket(Config.Instance.MinIOBucket)
+ .WithStreamData(stream)
+ .WithObjectSize(stream.Length)
+ .WithObject(remotePath)
+ .WithContentType(contentType);
+ await minio.PutObjectAsync(putObjectArgs);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Logs.Warn($"未能向储存空间路径上传文件流 {remotePath}\r\n{ex}");
+ return false;
+ }
+ }
+ public static bool UploadStream(Stream stream, string remotePath, string contentType = "application/octet-stream", bool overwrite = false)
+ => UploadStreamAsync(stream, remotePath, contentType, overwrite).Result;
+ /*public static async Task UploadFileAsync(string localFilePath, string remotePath = SetuPath, bool overwrite = false)
+ {
+ {
+ try
+ {
+ if (!File.Exists(localFilePath))
+ return false;
+ return await FTPConnection.UploadFile(localFilePath, remotePath, overwrite ? FtpRemoteExists.Overwrite : FtpRemoteExists.Skip);
+ }
+ catch (Exception ex)
+ {
+ Logs.Warn($"未能向储存空间路径上传文件 {localFilePath} => {remotePath}\r\n{ex}");
+ return FtpStatus.Failed;
+ }
+ }
+ }
+ public static FtpStatus UploadFile(string localFilePath, string remotePath = SetuPath, bool overwrite = false)
+ => UploadFileAsync(localFilePath, remotePath, overwrite).Result;*/
+ public static async Task ListFilesNameAsync(string path)
+ {
+ return (await ListFilesAsync(path)).Select(x => x.Name).ToArray();
+ }
+
+ public class MItem(Minio.DataModel.Item item) : Minio.DataModel.Item
+ {
+ public string Name { get; set; } = item.Key.Contains('/') ? item.Key[(item.Key.LastIndexOf('/') + 1)..] : item.Key;
+ }
+ public static async Task ListFilesAsync(string path, bool recursive = false, bool includeUserMetadata = false)
+ {
+ try
+ {
+ var result = minio.ListObjectsEnumAsync(new ListObjectsArgs()
+ .WithBucket(Config.Instance.MinIOBucket)
+ .WithRecursive(recursive)
+ .WithIncludeUserMetadata(includeUserMetadata));
+
+ var fileList = new List();
+ await foreach (var item in result)
+ {
+ fileList.Add(item);
+ }
+ return [.. fileList.Select(x => new MItem(x))];
+ }
+ catch (Exception ex)
+ {
+ Logs.Warn($"未能列出储存空间中 {path} 的文件\r\n{ex}");
+ return null;
+ }
+ }
+ public static async Task ContainsFileAsync(string path)
+ {
+ try
+ {
+ var stat = await minio.StatObjectAsync(
+ new StatObjectArgs()
+ .WithBucket(Config.Instance.MinIOBucket)
+ .WithObject(path)
+ );
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Core/TimerManager.cs b/Core/TimerManager.cs
new file mode 100644
index 0000000..ce3b368
--- /dev/null
+++ b/Core/TimerManager.cs
@@ -0,0 +1,50 @@
+using System.Diagnostics;
+using System.Reflection;
+using MegghysAPI.Attributes;
+
+namespace MegghysAPI.Core
+{
+ public static class TimerManager
+ {
+
+ private static Dictionary timers = new();
+ private static long time = 0;
+ [AutoInit(Order = 100)]
+ public static void RegisterAll()
+ {
+ System.Timers.Timer temp = new()
+ {
+ Interval = 1000,
+ AutoReset = true,
+ };
+ temp.Elapsed += (_, _) =>
+ {
+ if (time != 0)
+ timers.Where(timer => time % timer.Value.Time == 0)
+ .ForEach(timer =>
+ {
+ Task.Run(() =>
+ {
+ var sw = new Stopwatch();
+ sw.Start();
+ timer.Key.Invoke(null, null);
+ sw.Stop();
+ if (timer.Value.Log)
+ Logs.Text($"[{sw.ElapsedMilliseconds} ms] [{timer.Key.DeclaringType.Name}.{timer.Key.Name}] 计时器触发 [{timer.Value.Time}-{time}]");
+ });
+ });
+ time++;
+ };
+ AppDomain.CurrentDomain.GetAssemblies()
+ .ForEach(a => a.GetTypes().Where(t => t.IsClass).ForEach(t => t.GetMethods().Where(m => m.GetCustomAttributes(true).FirstOrDefault(a => a is AutoTimerAttribute) != null)
+ .ForEach(m =>
+ {
+ var attr = m.GetCustomAttributes(true).FirstOrDefault(a => a is AutoTimerAttribute) as AutoTimerAttribute;
+ timers.Add(m, attr);
+ Logs.Info($"注册自动计时器 {m.DeclaringType.Name}.{m.Name} <{attr.Time} s>");
+ })));
+ temp.Start();
+ timers.Where(timer => timer.Value.CallOnRegister).ForEach(timer => Task.Run(() => timer.Key.Invoke(null, null)));
+ }
+ }
+}
diff --git a/DB.cs b/DB.cs
new file mode 100644
index 0000000..cc1a95c
--- /dev/null
+++ b/DB.cs
@@ -0,0 +1,19 @@
+using FreeSql;
+using MegghysAPI.Attributes;
+
+namespace MegghysAPI
+{
+ public static class DB
+ {
+ public static IFreeSql SQL { get; private set; }
+ [AutoInit(Order = 0)]
+ internal static void InitDB()
+ {
+ SQL = new FreeSqlBuilder()
+ .UseConnectionString(DataType.PostgreSQL, Config.Instance.DBConnectString)
+ .UseAutoSyncStructure(true)
+ .Build();
+ SQL.UseJsonMap();
+ }
+ }
+}
diff --git a/Data/Config.json b/Data/Config.json
new file mode 100644
index 0000000..ca98819
--- /dev/null
+++ b/Data/Config.json
@@ -0,0 +1,11 @@
+{
+ "LogDanmakuError": false,
+ "ListenHost": "*",
+ "ListenPort": 4050,
+ "DBConnectString": "Host=42.193.20.136;Port=5432;Username=postgres;Password=!Hzy05152484; Database=MegghysAPI;",
+ "MinIOEndpoint": "eternalland.top:9000",
+ "MinIOAccessKey": "RBzbElm21lf7sy7wK7wG",
+ "MinIOSecretKey": "Nko5azOSUiYgOUeLsj8hLxGz4cKC8XOcH0VS7lWq",
+ "MinIORegion": "cn-main",
+ "MinIOBucket": "general"
+}
\ No newline at end of file
diff --git a/Datas.cs b/Datas.cs
new file mode 100644
index 0000000..4046a17
--- /dev/null
+++ b/Datas.cs
@@ -0,0 +1,26 @@
+using MegghysAPI.Attributes;
+
+namespace MegghysAPI
+{
+ public static class Datas
+ {
+#if DEBUG
+ public const bool IsDebug = true;
+#else
+ public const bool IsDebug = false;
+#endif
+ public const string HIBI_URL = "https://hibi.suki.club/";
+
+ public static string DataPath => Path.Combine(Environment.CurrentDirectory, "Data");
+ public static string TempFilePath => Path.Combine(DataPath, "Temp");
+
+ [AutoInit]
+ public static void Init()
+ {
+ if (!Directory.Exists(DataPath))
+ Directory.CreateDirectory(DataPath);
+ if (!Directory.Exists(TempFilePath))
+ Directory.CreateDirectory(TempFilePath);
+ }
+ }
+}
diff --git a/Entities/LogString.cs b/Entities/LogString.cs
new file mode 100644
index 0000000..dc88048
--- /dev/null
+++ b/Entities/LogString.cs
@@ -0,0 +1,19 @@
+
+
+namespace MegghysAPI.Entities
+{
+ public struct LogString(object text, ConsoleColor? color = null)
+ {
+ public string Text { get; set; } = text?.ToString() ?? string.Empty;
+ public ConsoleColor? Color { get; set; } = color;
+
+ public static implicit operator string(LogString d) => d.Text;
+ public static implicit operator LogString(string d) => new(d);
+ public static implicit operator LogString(int d) => new(d.ToString());
+ public static implicit operator LogString(long d) => new(d.ToString());
+ public static implicit operator LogString((string? text, ConsoleColor color) d) => new(d.text, d.color);
+ public static implicit operator LogString((object? text, ConsoleColor color) d) => new(d.text, d.color);
+ public static implicit operator (string text, ConsoleColor color)(LogString d) => (d.Text, d.Color ?? ConsoleColor.Gray);
+ public static implicit operator (object text, ConsoleColor color)(LogString d) => (d.Text, d.Color ?? ConsoleColor.Gray);
+ }
+}
diff --git a/Logs.cs b/Logs.cs
new file mode 100644
index 0000000..0684300
--- /dev/null
+++ b/Logs.cs
@@ -0,0 +1,173 @@
+using System.Collections.Concurrent;
+using System.Text;
+using MegghysAPI.Attributes;
+using MegghysAPI.Entities;
+
+namespace MegghysAPI
+{
+ public class Logs
+ {
+ public readonly static string SavePath = Path.Combine(Environment.CurrentDirectory, "Logs");
+ public static string LogPath => Path.Combine(Environment.CurrentDirectory, "Logs");
+ public static string LogName => Path.Combine(SavePath, DateTime.Now.ToString("yyyy-MM-dd") + ".log");
+ public const ConsoleColor DefaultColor = ConsoleColor.Gray;
+ public static void Text(object text)
+ {
+ LogAndSave("[Normal]", ConsoleColor.Gray, true, text.ToString());
+ }
+ public static void Info(object text)
+ {
+ LogAndSave("[Info]", ConsoleColor.Yellow, true, text.ToString());
+ }
+ public static void Error(object text)
+ {
+ LogAndSave("[Error]", ConsoleColor.Red, true, text.ToString());
+ }
+ public static void Warn(object text)
+ {
+ LogAndSave("[Warn]", ConsoleColor.DarkYellow, true, text.ToString());
+ }
+ public static void Success(object text)
+ {
+ LogAndSave("[Success]", ConsoleColor.Green, true, text.ToString());
+ }
+ public static void Text(params LogString[] text)
+ {
+ LogAndSave("[Normal]", ConsoleColor.Gray, true, text);
+ }
+ public static void Info(params LogString[] text)
+ {
+ LogAndSave("[Info]", ConsoleColor.Yellow, true, text);
+ }
+ public static void Error(params LogString[] text)
+ {
+ LogAndSave("[Error]", ConsoleColor.Red, true, text);
+ }
+ public static void Warn(params LogString[] text)
+ {
+ LogAndSave("[Warn]", ConsoleColor.DarkYellow, true, text);
+ }
+ public static void Success(params LogString[] text)
+ {
+ LogAndSave("[Success]", ConsoleColor.Green, true, text);
+ }
+ public static void TextCondition(Func condition, params LogString[] text)
+ {
+ if (condition?.Invoke() ?? true)
+ LogAndSave("[Normal]", ConsoleColor.Gray, true, text);
+ }
+ public static void InfoCondition(Func condition, params LogString[] text)
+ {
+ if (condition?.Invoke() ?? true)
+ LogAndSave("[Info]", ConsoleColor.Yellow, true, text);
+ }
+ public static void ErrorCondition(Func condition, params LogString[] text)
+ {
+ if (condition?.Invoke() ?? true)
+ LogAndSave("[Error]", ConsoleColor.Red, true, text);
+ }
+ public static void WarnCondition(Func condition, params LogString[] text)
+ {
+ if (condition?.Invoke() ?? true)
+ LogAndSave("[Warn]", ConsoleColor.DarkYellow, true, text);
+ }
+ public static void SuccesCondition(Func condition, params LogString[] text)
+ {
+ if (condition?.Invoke() ?? true)
+ LogAndSave("[Success]", ConsoleColor.Green, true, text);
+ }
+ public static void Info(params (object message, ConsoleColor color)[] text)
+ {
+ LogAndSave("[Info]", ConsoleColor.Yellow, true, text.Select(t => new LogString(t.message, t.color)).ToArray());
+ }
+ public static void Text(params (object message, ConsoleColor color)[] text)
+ {
+ LogAndSave("[Normal]", ConsoleColor.Gray, true, text.Select(t => new LogString(t.message, t.color)).ToArray());
+ }
+ public static void Error(params (object message, ConsoleColor color)[] text)
+ {
+ LogAndSave("[Error]", ConsoleColor.Red, true, text.Select(t => new LogString(t.message, t.color)).ToArray());
+ }
+ public static void Warn(params (object message, ConsoleColor color)[] text)
+ {
+ LogAndSave("[Warn]", ConsoleColor.DarkYellow, true, text.Select(t => new LogString(t.message, t.color)).ToArray());
+ }
+ public static void Success(params (object message, ConsoleColor color)[] text)
+ {
+ LogAndSave("[Success]", ConsoleColor.Green, true, text.Select(t => new LogString(t.message, t.color)).ToArray());
+ }
+ internal static void Init()
+ {
+ if (!Directory.Exists(LogPath))
+ Directory.CreateDirectory(LogPath);
+ _ = Task.Run(LogLoop);
+ }
+ public static void LogNotDisplay(object message, string prefix = "[NotDisplay]")
+ {
+ File.AppendAllText(LogName, $"{DateTime.Now:yyyy-MM-dd-HH:mm:ss} - {prefix} {message}{Environment.NewLine}", Encoding.UTF8);
+ }
+ public static BlockingCollection<(string prefix, ConsoleColor color, bool save, LogString[] msg)> logQueue = new();
+ [AutoInit(Async = true)]
+ public static void LogLoop()
+ {
+ if (!Directory.Exists(LogPath))
+ {
+ Directory.CreateDirectory(LogPath);
+ }
+ while (true)
+ {
+ try
+ {
+ if (logQueue.TryTake(out var log))
+ {
+ var (prefix, color, save, message) = log;
+ if (save)
+ {
+ File.AppendAllText(LogName, $"{DateTime.Now:yyyy-MM-dd-HH:mm:ss} - {prefix} {string.Join("", message.Select(m => m.Text))}{Environment.NewLine}", Encoding.UTF8);
+ }
+ Console.ForegroundColor = ConsoleColor.DarkGray;
+ Console.Write($"{DateTime.Now:HH:mm:ss} ");
+ lastColor = color;
+ Console.ForegroundColor = color;
+ Console.Write($"{prefix} ");
+ foreach (var item in message)
+ {
+ if (item.Color.HasValue)
+ {
+ if (lastColor != item.Color)
+ {
+ lastColor = item.Color;
+ Console.ForegroundColor = item.Color.Value;
+ }
+ }
+ else if (lastColor != color)
+ {
+ lastColor = color;
+ Console.ForegroundColor = color;
+ }
+
+ Console.Write($"{item.Text}");
+ }
+
+ Console.WriteLine();
+ }
+ else
+ Thread.Sleep(1);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ }
+ }
+ }
+ static ConsoleColor? lastColor = ConsoleColor.Gray;
+ public static void LogAndSave(string prefix = "[Log]", ConsoleColor color = DefaultColor, bool save = true, params LogString[] message)
+ {
+ try
+ {
+ logQueue.TryAdd((prefix, color, save, message));
+ }
+ catch (Exception ex) { Console.WriteLine(ex); }
+ }
+ }
+}
diff --git a/MegghysAPI.csproj b/MegghysAPI.csproj
new file mode 100644
index 0000000..7972e42
--- /dev/null
+++ b/MegghysAPI.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net9.0
+ disable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MegghysAPI.sln b/MegghysAPI.sln
new file mode 100644
index 0000000..291d928
--- /dev/null
+++ b/MegghysAPI.sln
@@ -0,0 +1,24 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.13.35507.96
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MegghysAPI", "MegghysAPI.csproj", "{A01A05D9-A1FF-632A-4B5F-149B79FADB39}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {A01A05D9-A1FF-632A-4B5F-149B79FADB39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A01A05D9-A1FF-632A-4B5F-149B79FADB39}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A01A05D9-A1FF-632A-4B5F-149B79FADB39}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A01A05D9-A1FF-632A-4B5F-149B79FADB39}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {4923B96A-4C69-48C8-8F8F-0372B9E8FE4D}
+ EndGlobalSection
+EndGlobal
diff --git a/Modules/PixivFavoriteDownloader.cs b/Modules/PixivFavoriteDownloader.cs
new file mode 100644
index 0000000..353845b
--- /dev/null
+++ b/Modules/PixivFavoriteDownloader.cs
@@ -0,0 +1,246 @@
+using System.Text.Json.Nodes;
+using System.Web;
+using FreeSql.DataAnnotations;
+using MegghysAPI.Attributes;
+using static MegghysAPI.Modules.PixivFavoriteDownloader.Pixiv;
+
+namespace MegghysAPI.Modules
+{
+ public static class PixivFavoriteDownloader
+ {
+ [AutoInit]
+ public static void Init()
+ {
+ Favorites = DB.SQL.Select().ToList();
+
+ Favorites.ForEach(f =>
+ {
+ f.S3URL = f.URL.Select(u => new PixivURLInfo()
+ {
+ Origin = $"http://eternalland.top:9000/{Config.Instance.MinIOBucket}{FileManager.FavoritePath}{f.Id}_{f.URL.IndexOf(u)}.{u.Extension}",
+ Large = $"http://194.104.147.128:880/{u.Large?.Replace("i.pixiv.re", "i.pximg.net")}?referer=https://www.pixiv.net/",
+ Medium = $"http://194.104.147.128:880/{u.Medium?.Replace("i.pixiv.re", "i.pximg.net")}?referer=https://www.pixiv.net/",
+ Small = $"http://194.104.147.128:880/{u.Small?.Replace("i.pixiv.re", "i.pximg.net")}?referer=https://www.pixiv.net/",
+ }).ToList();
+ });
+
+ Logs.Success($"Pixiv 收藏图片总数: {Favorites.Count}");
+ }
+
+ public static readonly string PIXIV_FAVORITE_URL = Datas.HIBI_URL + "api/pixiv/favorite?id=19045447";
+ public const string PixivFavoritePath = "/files/favorite/";
+
+ public static List Favorites = [];
+
+ [AutoTimer(Time = 5 * 60, CallOnRegister = true)]
+ public static void UpdateMyFavorite()
+ {
+ Logs.Info("正在更新收藏图片");
+ var url = PIXIV_FAVORITE_URL;
+ var num = 0;
+ while (num < 5)
+ {
+ try
+ {
+ var result = Utils.RequestString(url);
+ var json = JsonNode.Parse(result);
+ if (json is null)
+ return;
+ json["illusts"]?.AsArray().ForEach(i =>
+ {
+ if (!Favorites.Any(f => f.Id == (long)i["id"]))
+ {
+ var img = new PixivImgInfo(i, PixivImgType.Favorite);
+ Favorites.Add(img);
+ DB.SQL.InsertOrUpdate()
+ .SetSource(img)
+ .ExecuteAffrows();
+ Logs.Success($"[{Favorites.Count}] 发现新增收藏: {Favorites.Last().Title}, 类型: {Favorites.Last().Restrict}");
+ }
+ });
+ var next = (string)json["next_url"];
+ if (next is null)
+ break;
+ var list = next.Split("max_bookmark_id=", StringSplitOptions.RemoveEmptyEntries);
+ if (list.Length != 0)
+ url = PIXIV_FAVORITE_URL + $"&max_bookmark_id={list[1]}";
+ else
+ break;
+ }
+ catch (Exception ex)
+ {
+ Logs.Error(ex);
+ num++;
+ }
+ }
+ Logs.Success($"收藏图片更新完成. 当前共 {Favorites.Count} 个");
+ }
+ static bool uploading = false;
+ [AutoTimer(Time = 60 * 5, CallOnRegister = true)]
+ public static async Task DownloadFavoriteToBucket()
+ {
+ if (uploading)
+ {
+ Logs.Info($"正在下载收藏, 忽略");
+ return;
+ }
+ uploading = true;
+ var files = await FileManager.ListFilesAsync(FileManager.FavoritePath, true);
+ if (files is null)
+ return;
+ Logs.Info($"FTP 总收藏数: {files.Length}");
+ int num = 0;
+ for (int i = 0; i < Favorites.Count; i++)
+ {
+ var s = Favorites[i];
+ try
+ {
+ num = 0;
+ foreach (var u in s?.URL)
+ {
+ var url = u.Origin.Replace("i.pixiv.re", "i.pximg.net");
+ url = $"http://194.104.147.128:880/{url}?referer=https://www.pixiv.net/";
+ var fileName = $"{s.Id}_{num}.{u.Extension}";
+ if (!files.Any(f => f.Name == fileName))
+ {
+ var data = await Utils.DownloadBytesAsync(url);
+ if (data.code == System.Net.HttpStatusCode.OK)
+ {
+ using var stream = new MemoryStream(data.data);
+ if (await FileManager.UploadStreamAsync(stream, FileManager.FavoritePath + fileName))
+ Logs.Success($"已上传收藏图片 {s.Title}({s.Id})[{num + 1}-{s.URL.Count}]:{Favorites.IndexOf(s)} 至储存空间 [{(double)data.data.Length / 1024 / 1024:0.00} Mb].");
+ else
+ Logs.Warn($"{s.Title} 上传失败");
+ }
+ num++;
+ }
+ }
+ }
+ catch (Exception ex) { Logs.Error(ex); }
+ }
+ uploading = false;
+ }
+
+ public enum PixivImgType
+ {
+ None,
+ Setu,
+ Favorite
+ }
+ public partial class Pixiv
+ {
+ public class PixivImgInfo
+ {
+ public PixivImgInfo() { Type = PixivImgType.None; }
+ public PixivImgInfo(PixivImgType t) { Type = t; }
+ public PixivImgInfo(JsonNode node, PixivImgType t)
+ {
+ Type = t;
+ ArgumentNullException.ThrowIfNull(node);
+ var tag = new List();
+ node["tags"].AsArray().ForEach(t => tag.Add(new((string)t["name"], (string)t["translated_name"])));
+ Id = (long)node["id"];
+ Author = new()
+ {
+ UID = ((long)node["user"]["id"]).ToString(),
+ Name = (string)node["user"]["name"],
+ Account = (string)node["user"]["account"],
+ HeadURL = (string)node["user"]["profile_image_urls"]["medium"]
+ };
+ Title = (string)node["title"];
+ Restrict = (PixivRestrictInfo)(int)node["x_restrict"];
+ Description = (string)node["caption"];
+ Tags = tag;
+ var url = new List();
+ if ((int)node["page_count"] == 1)
+ url.Add(new() { Origin = ((string)node["meta_single_page"]["original_image_url"]).Replace("i.pximg.net", "i.pixiv.cat") });
+ else
+ node["meta_pages"].AsArray().ForEach(p =>
+ {
+ var imgs = p["image_urls"];
+ url.Add(new()
+ {
+ Origin = ((string)imgs["original"] ?? "")
+ .Replace("i.pximg.net", "i.pixiv.re"),
+ Large = ((string)imgs["large"] ?? "")
+ .Replace("i.pximg.net", "i.pixiv.re"),
+ Medium = ((string)imgs["medium"] ?? "")
+ .Replace("i.pximg.net", "i.pixiv.re"),
+ Small = ((string)imgs["square_medium"] ?? "")
+ .Replace("i.pximg.net", "i.pixiv.re")
+ });
+ });
+ URL = url;
+ Width = (int)node["width"];
+ Height = (int)node["height"];
+ UploadDate = DateTime.TryParse((string)node["create_date"], out var date) ? date.Ticks : 0;
+ ViewCount = (long)node["total_view"];
+ FavoriteCount = (long)node["total_bookmarks"];
+ }
+ public long Id { get; set; }
+ public PixivImgType Type { get; set; }
+ public string Title { get; set; }
+ [JsonMap]
+ public PixivRestrictInfo Restrict { get; set; }
+ [Column(DbType = "text")]
+ public string Description { get; set; }
+ [JsonMap]
+ public PixivAuthorInfo Author { get; set; }
+ public bool R18 => Restrict != PixivRestrictInfo.Normal;
+ [JsonMap]
+ public List Tags { get; set; } = [];
+ [JsonMap]
+ public List URL { get; set; } = [];
+
+ [JsonMap]
+ public List S3URL { get; set; } = [];
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public long UploadDate { set; get; }
+ public long ViewCount { get; set; }
+ public long FavoriteCount { get; set; }
+ }
+ public enum PixivRestrictInfo
+ {
+ Normal,
+ R18,
+ R18G
+ }
+ public record PixivAuthorInfo
+ {
+ public string Name { get; set; }
+ public string UID { get; set; }
+ public string Account { get; set; }
+ public string HeadURL { get; set; }
+ }
+ public record PixivURLInfo
+ {
+ public override string ToString()
+ {
+ return Origin;
+ }
+ public string Extension => Path.GetExtension(HttpUtility.UrlDecode(new Uri(Origin).Segments.Last()))?.Replace(".", "");
+ public string Name => HttpUtility.UrlDecode(new Uri(Origin).Segments.Last()) is { } url ? $"{url}" : "";
+ public string Origin { get; set; }
+ public string Large { get; set; }
+ public string Medium { get; set; }
+ public string Small { get; set; }
+ }
+ public record PixivTagInfo
+ {
+ public override string ToString() => $"{Name} {(TranslateName is null ? "" : $"<{TranslateName}>")}";
+ public PixivTagInfo(string name, string translateName)
+ {
+ Name = name;
+ TranslateName = translateName;
+ }
+ public bool Contains(string tag)
+ {
+ return Name.ToLower().Contains(tag.ToLower()) || (TranslateName != null && TranslateName.ToLower().Contains(tag.ToLower()));
+ }
+ public string Name { get; set; }
+ public string TranslateName { get; set; }
+ }
+ }
+ }
+}
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..fa1c672
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,68 @@
+using System.Diagnostics;
+using System.Reflection;
+using MegghysAPI.Attributes;
+using MegghysAPI.Components;
+using Microsoft.FluentUI.AspNetCore.Components;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+builder.Services.AddRazorComponents()
+ .AddInteractiveServerComponents();
+builder.Services.AddFluentUIComponents();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+if (!app.Environment.IsDevelopment())
+{
+ app.UseExceptionHandler("/Error", createScopeForErrors: true);
+ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+ app.UseHsts();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAntiforgery();
+
+app.MapStaticAssets();
+app.MapRazorComponents()
+ .AddInteractiveServerRenderMode();
+
+app.MapControllers();
+app.MapBlazorHub();
+app.MapFallbackToPage("/_Host");
+
+// AutoInitAttribute
+// ȡǰAssembly
+var inits = new List();
+Assembly.GetExecutingAssembly()
+ .GetTypes()
+ .ForEach(t => t.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)
+ .Where(m => m.GetCustomAttribute() is { }).ForEach(m => inits.Add(m)));
+inits = [.. inits.OrderBy(m => m.GetCustomAttribute().Order)];
+inits.ForEach(m =>
+{
+ var sw = new Stopwatch();
+ sw.Start();
+ var attr = m.GetCustomAttribute();
+ if (attr.LogMessage is not null)
+ Logs.Info(attr.LogMessage);
+ if (attr.Async)
+ {
+ Task.Run(() =>
+ {
+ m.Invoke(null, null);
+ attr.PostInit?.Invoke();
+ Logs.Info($"[{sw.ElapsedMilliseconds} ms] Async <{m.DeclaringType.Name}.{m.Name}> => Inited.");
+ });
+ }
+ else
+ {
+ m.Invoke(null, null);
+ attr.PostInit?.Invoke();
+ Logs.Info($"[{sw.ElapsedMilliseconds} ms] <{m.DeclaringType.Name}.{m.Name}> => Inited.");
+ }
+});
+
+app.Run();
diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json
new file mode 100644
index 0000000..b45214b
--- /dev/null
+++ b/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5024",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7009;http://localhost:5024",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+ }
diff --git a/Utils.cs b/Utils.cs
new file mode 100644
index 0000000..5f77d69
--- /dev/null
+++ b/Utils.cs
@@ -0,0 +1,106 @@
+using System.Net;
+
+namespace MegghysAPI
+{
+ public static class Utils
+ {
+ public static readonly HttpClient _client = new(new HttpClientHandler()
+ {
+ AutomaticDecompression = DecompressionMethods.All,
+ })
+ {
+ //Timeout = new TimeSpan(0, 0, 15),
+ };
+
+ public static HttpResponseMessage Request(string address) => RequestAsync(address).Result;
+ public static async Task RequestAsync(string address, Dictionary header = null)
+ {
+ try
+ {
+ var uri = new Uri(address);
+ var request = new HttpRequestMessage(HttpMethod.Get, uri);
+ if (header?.Count > 0)
+ {
+ foreach (var h in header)
+ {
+ request.Headers.TryAddWithoutValidation(h.Key, h.Value);
+ }
+ }
+ return await _client.SendAsync(request);
+ }
+ catch (Exception ex)
+ {
+ Logs.Error((ex.InnerException ?? ex).Message);
+ return null;
+ }
+ }
+ public static async Task RequestStringAsync(string address, Dictionary header = null)
+ {
+ try
+ {
+ var uri = new Uri(address);
+ if (uri.Host.EndsWith("bilibili.com"))
+ {
+ header ??= [];
+ header.TryAdd("Referer", "https://" + uri.Host);
+ }
+ var result = await RequestAsync(address, header);
+ var content = await result?.Content.ReadAsStringAsync();
+ if (result?.IsSuccessStatusCode != true)
+ {
+ Logs.Warn($"请求失败: {address} {result?.StatusCode}: {content[..100]}");
+ }
+ return content;
+ }
+ catch (Exception ex)
+ {
+ Logs.Error(ex);
+ return null;
+ }
+ }
+ public static string RequestString(string address, Dictionary header = null) => RequestStringAsync(address, header).Result;
+ public static async Task<(HttpStatusCode code, byte[] data)> DownloadBytesAsync(string uri)
+ {
+ try
+ {
+ return (HttpStatusCode.OK, await _client.GetByteArrayAsync(uri));
+ }
+ catch (HttpRequestException ex)
+ {
+ return (ex.StatusCode ?? HttpStatusCode.BadRequest, null);
+ }
+ catch (Exception ex)
+ {
+ Logs.Error(ex); // 返回false下载失败
+ return (HttpStatusCode.BadRequest, null);
+ }
+ }
+ public static async Task<(HttpStatusCode code, string path)> DownloadFileAsync(string url, string path = null, bool resume = true)
+ {
+ path ??= Datas.TempFilePath;
+ var dire = Path.GetDirectoryName(path);
+ if (!Directory.Exists(dire))
+ Directory.CreateDirectory(dire);
+ try
+ {
+ var result = await DownloadBytesAsync(url);
+ if (result.code == HttpStatusCode.OK)
+ {
+ await File.WriteAllBytesAsync(path, result.data);
+ return (HttpStatusCode.OK, path);
+ }
+ else
+ return (result.code, null);
+ }
+ catch (HttpRequestException webex)
+ {
+ return (webex.StatusCode ?? HttpStatusCode.BadRequest, null);
+ }
+ catch (Exception ex)
+ {
+ Logs.Error(ex); // 返回false下载失败
+ return (HttpStatusCode.BadRequest, null);
+ }
+ }
+ }
+}
diff --git a/appsettings.Development.json b/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/appsettings.json b/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/wwwroot/app.css b/wwwroot/app.css
new file mode 100644
index 0000000..469e373
--- /dev/null
+++ b/wwwroot/app.css
@@ -0,0 +1,203 @@
+@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css';
+
+body {
+ --body-font: "Segoe UI Variable", "Segoe UI", sans-serif;
+ font-family: var(--body-font);
+ font-size: var(--type-ramp-base-font-size);
+ line-height: var(--type-ramp-base-line-height);
+ margin: 0;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ height: 100vh;
+ font-family: var(--body-font);
+ font-size: var(--type-ramp-base-font-size);
+ line-height: var(--type-ramp-base-line-height);
+ font-weight: var(--font-weight);
+ color: var(--neutral-foreground-rest);
+ background: var(--neutral-fill-layer-rest);
+}
+
+.navmenu-icon {
+ display: none;
+}
+
+.main {
+ min-height: calc(100dvh - 86px);
+ color: var(--neutral-foreground-rest);
+ align-items: stretch !important;
+}
+
+.body-content {
+ align-self: stretch;
+ height: calc(100dvh - 86px) !important;
+ display: flex;
+}
+
+.content {
+ padding: 0.5rem 1.5rem;
+ align-self: stretch !important;
+ width: 100%;
+}
+
+.manage {
+ width: 100dvw;
+}
+
+footer {
+ background: var(--neutral-layer-4);
+ color: var(--neutral-foreground-rest);
+ align-items: center;
+ padding: 10px 10px;
+}
+
+ footer a {
+ color: var(--neutral-foreground-rest);
+ text-decoration: none;
+ }
+
+ footer a:focus {
+ outline: 1px dashed;
+ outline-offset: 3px;
+ }
+
+ footer a:hover {
+ text-decoration: underline;
+ }
+
+.alert {
+ border: 1px dashed var(--accent-fill-rest);
+ padding: 5px;
+}
+
+
+#blazor-error-ui {
+ background: lightyellow;
+ bottom: 0;
+ box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
+ display: none;
+ left: 0;
+ padding: 0.6rem 1.25rem 0.7rem 1.25rem;
+ position: fixed;
+ width: 100%;
+ z-index: 1000;
+ margin: 20px 0;
+}
+
+ #blazor-error-ui .dismiss {
+ cursor: pointer;
+ position: absolute;
+ right: 0.75rem;
+ top: 0.5rem;
+ }
+
+.blazor-error-boundary {
+ background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
+ padding: 1rem 1rem 1rem 3.7rem;
+ color: white;
+}
+
+ .blazor-error-boundary::before {
+ content: "An error has occurred. "
+ }
+
+.loading-progress {
+ position: relative;
+ display: block;
+ width: 8rem;
+ height: 8rem;
+ margin: 20vh auto 1rem auto;
+}
+
+ .loading-progress circle {
+ fill: none;
+ stroke: #e0e0e0;
+ stroke-width: 0.6rem;
+ transform-origin: 50% 50%;
+ transform: rotate(-90deg);
+ }
+
+ .loading-progress circle:last-child {
+ stroke: #1b6ec2;
+ stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
+ transition: stroke-dasharray 0.05s ease-in-out;
+ }
+
+.loading-progress-text {
+ position: absolute;
+ text-align: center;
+ font-weight: bold;
+ inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
+}
+
+ .loading-progress-text:after {
+ content: var(--blazor-load-percentage-text, "Loading");
+ }
+
+code {
+ color: #c02d76;
+}
+
+@media (max-width: 600px) {
+ .header-gutters {
+ margin: 0.5rem 3rem 0.5rem 1.5rem !important;
+ }
+
+ [dir="rtl"] .header-gutters {
+ margin: 0.5rem 1.5rem 0.5rem 3rem !important;
+ }
+
+ .main {
+ flex-direction: column !important;
+ row-gap: 0 !important;
+ }
+
+ nav.sitenav {
+ width: 100%;
+ height: 100%;
+ }
+
+ #main-menu {
+ width: 100% !important;
+ }
+
+ #main-menu > div:first-child:is(.expander) {
+ display: none;
+ }
+
+ .navmenu {
+ width: 100%;
+ }
+
+ #navmenu-toggle {
+ appearance: none;
+ }
+
+ #navmenu-toggle ~ nav {
+ display: none;
+ }
+
+ #navmenu-toggle:checked ~ nav {
+ display: block;
+ }
+
+ .navmenu-icon {
+ cursor: pointer;
+ z-index: 10;
+ display: block;
+ position: absolute;
+ top: 15px;
+ left: unset;
+ right: 20px;
+ width: 20px;
+ height: 20px;
+ border: none;
+ }
+
+ [dir="rtl"] .navmenu-icon {
+ left: 20px;
+ right: unset;
+ }
+}
diff --git a/wwwroot/favicon.ico b/wwwroot/favicon.ico
new file mode 100644
index 0000000..e189d8e
Binary files /dev/null and b/wwwroot/favicon.ico differ