From e27058626e8dfe6055fd923e85cfb86351044b55 Mon Sep 17 00:00:00 2001
From: acite <1498045907@qq.com>
Date: Thu, 28 Aug 2025 00:34:26 +0800
Subject: [PATCH 1/4] [feat] Partially implemented transmission system
---
.gitignore | 3 +-
.idea/.idea.Abyss/.idea/workspace.xml | 95 +++++--
Abyss.sln.DotSettings.user | 1 +
.../Controllers/Media/VideoController.cs | 11 -
.../Controllers/Task/TaskController.cs | 60 +++++
Abyss/Components/Services/ResourceService.cs | 35 +++
Abyss/Components/Services/TaskService.cs | 247 ++++++++++++++++++
Abyss/Components/Services/UserService.cs | 3 +
Abyss/Components/Static/Helpers.cs | 26 +-
Abyss/Components/Tools/TemporaryDB.cs | 6 +
Abyss/Model/Chip.cs | 17 ++
Abyss/Model/Comment.cs | 7 +
Abyss/Model/Task.cs | 16 ++
Abyss/Model/TaskCreation.cs | 23 ++
Abyss/Model/Video.cs | 12 +
Abyss/Program.cs | 3 +
Abyss/Properties/launchSettings.json | 3 +-
Abyss/appsettings.Development.json | 8 -
18 files changed, 512 insertions(+), 64 deletions(-)
create mode 100644 Abyss/Components/Controllers/Task/TaskController.cs
create mode 100644 Abyss/Components/Services/TaskService.cs
create mode 100644 Abyss/Components/Tools/TemporaryDB.cs
create mode 100644 Abyss/Model/Chip.cs
create mode 100644 Abyss/Model/Comment.cs
create mode 100644 Abyss/Model/Task.cs
create mode 100644 Abyss/Model/TaskCreation.cs
create mode 100644 Abyss/Model/Video.cs
delete mode 100644 Abyss/appsettings.Development.json
diff --git a/.gitignore b/.gitignore
index 01e899a..b523ad6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,4 +56,5 @@ nunit-*.xml
# DB
*.db
-appsettings.json
\ No newline at end of file
+appsettings.json
+appsettings.Development.json
\ No newline at end of file
diff --git a/.idea/.idea.Abyss/.idea/workspace.xml b/.idea/.idea.Abyss/.idea/workspace.xml
index 59d545e..1ad86c0 100644
--- a/.idea/.idea.Abyss/.idea/workspace.xml
+++ b/.idea/.idea.Abyss/.idea/workspace.xml
@@ -10,10 +10,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -27,27 +41,36 @@
+
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
{
"associatedIndex": 3
}
@@ -56,31 +79,31 @@
- {
- "keyToString": {
- ".NET Launch Settings Profile.Abyss: http.executor": "Run",
- ".NET Launch Settings Profile.Abyss: https.executor": "Run",
- ".NET Project.AbyssCli.executor": "Run",
- "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
- "ModuleVcsDetector.initialDetectionPerformed": "true",
- "Publish to folder.Publish Abyss to folder x86.executor": "Run",
- "Publish to folder.Publish Abyss to folder.executor": "Run",
- "RunOnceActivity.ShowReadmeOnStart": "true",
- "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
- "RunOnceActivity.git.unshallow": "true",
- "XThreadsFramesViewSplitterKey": "0.30266345",
- "git-widget-placeholder": "main",
- "last_opened_file_path": "/opt/security/https/server",
- "node.js.detected.package.eslint": "true",
- "node.js.detected.package.tslint": "true",
- "node.js.selected.package.eslint": "(autodetect)",
- "node.js.selected.package.tslint": "(autodetect)",
- "nodejs_package_manager_path": "npm",
- "settings.editor.selected.configurable": "preferences.pluginManager",
- "vue.rearranger.settings.migration": "true"
+
-
+}]]>
+
@@ -174,7 +197,8 @@
-
+
+
@@ -224,6 +248,19 @@
+
+ file://$PROJECT_DIR$/Abyss/Components/Services/TaskService.cs
+ 60
+
+
+
+
+
+
+
+
+
+
diff --git a/Abyss.sln.DotSettings.user b/Abyss.sln.DotSettings.user
index e567e74..674a5bb 100644
--- a/Abyss.sln.DotSettings.user
+++ b/Abyss.sln.DotSettings.user
@@ -2,4 +2,5 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
\ No newline at end of file
diff --git a/Abyss/Components/Controllers/Media/VideoController.cs b/Abyss/Components/Controllers/Media/VideoController.cs
index 3fccbdd..c04968b 100644
--- a/Abyss/Components/Controllers/Media/VideoController.cs
+++ b/Abyss/Components/Controllers/Media/VideoController.cs
@@ -93,16 +93,5 @@ public class VideoController(ILogger logger, ResourceService rs
return PhysicalFile(d, "video/mp4", enableRangeProcessing: true);
}
- [HttpGet("{klass}/{id}/nv")]
- public async Task Nv(string klass, string id, string token)
- {
- var d = Helpers.SafePathCombine(VideoFolder, [klass, id, "video.a.mp4"]);
- if (d == null) return StatusCode(403, new { message = "403 Denied" });
-
- var r = await rs.Get(d, token, Ip);
- if (!r) return StatusCode(403, new { message = "403 Denied" });
- return PhysicalFile(d, "video/mp4", enableRangeProcessing: true);
- }
-
private string Ip => HttpContext.Connection.RemoteIpAddress?.ToString() ?? "127.0.0.1";
}
\ No newline at end of file
diff --git a/Abyss/Components/Controllers/Task/TaskController.cs b/Abyss/Components/Controllers/Task/TaskController.cs
new file mode 100644
index 0000000..d20c1bb
--- /dev/null
+++ b/Abyss/Components/Controllers/Task/TaskController.cs
@@ -0,0 +1,60 @@
+using Abyss.Components.Services;
+using Abyss.Components.Static;
+using Abyss.Model;
+using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Abyss.Components.Controllers.Task;
+
+
+[ApiController]
+[Route("api/[controller]")]
+public class TaskController(ILogger logger, ConfigureService config, TaskService taskService) : Controller
+{
+ public readonly string TaskFolder = Path.Combine(config.MediaRoot, "Tasks");
+
+ [HttpGet]
+ public async Task Query(string token)
+ {
+ // If the token is invalid, an empty list will be returned, which is part of the design
+ return Json(await taskService.Query(token, Ip));
+ }
+
+ [HttpPost]
+ public async Task Create(string token, [FromBody] TaskCreation creation)
+ {
+ var r = await taskService.Create(token, Ip, creation);
+ if(r == null)
+ {
+ return BadRequest();
+ }
+ return Ok(JsonConvert.SerializeObject(r, Formatting.Indented));
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetTask(string id)
+ {
+ throw new NotImplementedException();
+ }
+
+ [HttpPatch("{id}")]
+ public async Task PutChip(string id)
+ {
+ throw new NotImplementedException();
+ }
+
+ [HttpPost("{id}")]
+ public async Task VerifyChip(string id)
+ {
+ throw new NotImplementedException();
+ }
+
+ [HttpDelete("{id}")]
+ public async Task DeleteTask(string id)
+ {
+ throw new NotImplementedException();
+ }
+
+ private string Ip => HttpContext.Connection.RemoteIpAddress?.ToString() ?? "127.0.0.1";
+}
\ No newline at end of file
diff --git a/Abyss/Components/Services/ResourceService.cs b/Abyss/Components/Services/ResourceService.cs
index 2d578da..f8e2126 100644
--- a/Abyss/Components/Services/ResourceService.cs
+++ b/Abyss/Components/Services/ResourceService.cs
@@ -39,6 +39,11 @@ public class ResourceService
_database = new SQLiteAsyncConnection(config.RaDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
_database.CreateTableAsync().Wait();
+
+ var tasksPath = Helpers.SafePathCombine(_config.MediaRoot, "Tasks");
+ if(tasksPath != null)
+ InsertRaRow(tasksPath, "root", "rw,r-,r-", true).Wait();
+
}
// Create UID only for resources, without considering advanced hash security such as adding salt
@@ -335,4 +340,34 @@ public class ResourceService
return false;
}
}
+
+ private async Task InsertRaRow(string fullPath, string owner, string permission, bool update = false)
+ {
+ if (!PermissionRegex.IsMatch(permission))
+ {
+ _logger.LogError($"Invalid permission format: {permission}");
+ return false;
+ }
+
+ var path = Path.GetRelativePath(_config.MediaRoot, fullPath);
+
+ if (update)
+ return await _database.InsertOrReplaceAsync(new ResourceAttribute()
+ {
+ Uid = Uid(path),
+ Name = path,
+ Owner = owner,
+ Permission = permission,
+ }) == 1;
+ else
+ {
+ return await _database.InsertAsync(new ResourceAttribute()
+ {
+ Uid = Uid(path),
+ Name = path,
+ Owner = owner,
+ Permission = permission,
+ }) == 1;
+ }
+ }
}
\ No newline at end of file
diff --git a/Abyss/Components/Services/TaskService.cs b/Abyss/Components/Services/TaskService.cs
new file mode 100644
index 0000000..3290064
--- /dev/null
+++ b/Abyss/Components/Services/TaskService.cs
@@ -0,0 +1,247 @@
+using Abyss.Components.Static;
+using Abyss.Model;
+using Newtonsoft.Json;
+using SQLite;
+using Task = Abyss.Model.Task;
+
+namespace Abyss.Components.Services;
+
+
+
+public class TaskService(ILogger logger, ConfigureService config, ResourceService rs, UserService user)
+{
+ public readonly string TaskFolder = Path.Combine(config.MediaRoot, "Tasks");
+ public readonly string VideoFolder = Path.Combine(config.MediaRoot, "Videos");
+
+ private const ulong MaxChunkSize = 20 * 1024 * 1024;
+
+ public async Task> Query(string token, string ip)
+ {
+ var r = await rs.Query(TaskFolder, token, ip);
+ var u = user.Validate(token, ip);
+
+ List s = new();
+ foreach (var i in r ?? [])
+ {
+ var p = Helpers.SafePathCombine(TaskFolder, [i, "task.json"]);
+ var c = JsonConvert.DeserializeObject(await System.IO.File.ReadAllTextAsync(p ?? ""));
+
+ if(c?.Owner == u) s.Add(i);
+ }
+
+ return s;
+ }
+
+ public async Task Create(string token, string ip, TaskCreation creation)
+ {
+ if(creation.Name.Length > 64 || creation.Klass.Length > 16 || creation.Size > 10UL * 1024UL * 1024UL * 1024UL || creation.Author.Length > 32)
+ return null;
+ if(creation.Name == "" || creation.Klass == "")
+ return null;
+ if(!IsFileNameSafe(creation.Klass))
+ return null;
+ if (GetAvailableFreeSpace(TaskFolder) - (long)creation.Size < 10 * 1024L * 1024L * 1024L)
+ { // Reserve 10GB of space
+ return null;
+ }
+
+ switch ((TaskType)creation.Type)
+ {
+ case TaskType.Image:
+ return await CreateImageTask(token, ip, creation);
+ case TaskType.Video:
+ return await CreateVideoTask(token, ip, creation);
+ default:
+ return null;
+ }
+ }
+
+ private async Task CreateVideoTask(string token, string ip, TaskCreation creation)
+ {
+ if(!await rs.Valid(VideoFolder, token, OperationType.Write, ip))
+ return null;
+ var u = user.Validate(token, ip);
+ if(u == null)
+ return null;
+
+ var r = new TaskCreationResponse()
+ {
+ Id = GenerateUniqueId(TaskFolder),
+ Chips = SliceFile(creation.Size)
+ };
+
+ Directory.CreateDirectory(Path.Combine(TaskFolder, r.Id.ToString()));
+ Directory.CreateDirectory(Path.Combine(TaskFolder, r.Id.ToString(), "gallery"));
+ // It shouldn't be a problem to spell it directly like this, as all the parameters are generated by myself
+
+ Task v = new Task()
+ {
+ Name = creation.Name,
+ Owner = u,
+ Class = creation.Klass,
+ Id = r.Id,
+ Type = TaskType.Video
+ };
+
+ await System.IO.File.WriteAllTextAsync(
+ Path.Combine(TaskFolder, r.Id.ToString(), "task.json"),
+ JsonConvert.SerializeObject(v, Formatting.Indented));
+
+ using (var connection = new SQLiteConnection(Path.Combine(TaskFolder, r.Id.ToString(), "task.db")))
+ {
+ connection.CreateTable();
+ connection.InsertAll(r.Chips.Select(x => new Chip()
+ {
+ Addr = x.Addr,
+ Hash = "",
+ Id = x.Id,
+ Size = x.Size,
+ State = ChipState.Created
+ }));
+ connection.Close();
+ }
+
+ CreateEmptyFile(Path.Combine(TaskFolder, r.Id.ToString(), "video.mp4"), (long)creation.Size);
+ return r;
+ }
+
+ private async Task CreateImageTask(string token, string ip, TaskCreation creation)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static uint GenerateUniqueId(string parentDirectory)
+ {
+ string[] directories = Directory.GetDirectories(parentDirectory);
+ HashSet existingIds = new HashSet();
+
+ foreach (string dirPath in directories)
+ {
+ string dirName = new DirectoryInfo(dirPath).Name;
+ if (uint.TryParse(dirName, out uint id))
+ {
+ if (id != 0)
+ {
+ existingIds.Add(id);
+ }
+ }
+ }
+
+ uint newId = 1;
+ while (existingIds.Contains(newId))
+ {
+ newId++;
+ if (newId == uint.MaxValue)
+ {
+ return 0;
+ }
+ }
+
+ return newId;
+ }
+
+ public static List SliceFile(ulong fileSize)
+ {
+ var tasks = new List();
+ if (fileSize == 0)
+ {
+ return tasks;
+ }
+
+ ulong remainingSize = fileSize;
+ ulong currentAddr = 0;
+ uint id = 0;
+
+ while (remainingSize > 0)
+ {
+ ulong chunkSize = remainingSize > MaxChunkSize ? MaxChunkSize : remainingSize;
+
+ tasks.Add(new ChipDesc
+ {
+ Id = id,
+ Addr = currentAddr,
+ Size = chunkSize
+ });
+
+ currentAddr += chunkSize;
+ remainingSize -= chunkSize;
+ id++;
+ }
+
+ return tasks;
+ }
+
+ public static bool IsFileNameSafe(string fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ return false;
+ }
+
+ if (fileName.Contains(Path.DirectorySeparatorChar) ||
+ fileName.Contains(Path.AltDirectorySeparatorChar))
+ {
+ return false;
+ }
+
+ char[] invalidChars = Path.GetInvalidFileNameChars();
+ if (fileName.Any(c => invalidChars.Contains(c)))
+ {
+ return false;
+ }
+
+ string[] reservedNames = {
+ "CON", "PRN", "AUX", "NUL",
+ "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
+ "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
+ };
+ string nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName).ToUpperInvariant();
+ if (reservedNames.Contains(nameWithoutExtension))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static void CreateEmptyFile(string filePath, long sizeInBytes)
+ {
+ using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
+ {
+ fs.SetLength(sizeInBytes);
+ }
+ }
+
+ public static long GetAvailableFreeSpace(string directoryPath)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(directoryPath))
+ {
+ return -1;
+ }
+
+ string rootPath = Path.GetPathRoot(directoryPath) ?? "";
+
+ if (string.IsNullOrEmpty(rootPath))
+ {
+ return -1;
+ }
+
+ DriveInfo driveInfo = new DriveInfo(rootPath);
+
+ if (driveInfo.IsReady)
+ {
+ return driveInfo.AvailableFreeSpace;
+ }
+ else
+ {
+ return -1;
+ }
+ }
+ catch (Exception ex)
+ {
+ return -1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Abyss/Components/Services/UserService.cs b/Abyss/Components/Services/UserService.cs
index ad60d98..574c321 100644
--- a/Abyss/Components/Services/UserService.cs
+++ b/Abyss/Components/Services/UserService.cs
@@ -7,6 +7,7 @@ using Abyss.Model;
using Microsoft.Extensions.Caching.Memory;
using NSec.Cryptography;
using SQLite;
+using Task = System.Threading.Tasks.Task;
namespace Abyss.Components.Services;
@@ -27,6 +28,8 @@ public class UserService
_database.CreateTableAsync().Wait();
var rootUser = _database.Table().Where(x => x.Name == "root").FirstOrDefaultAsync().Result;
+ _cache.Set("acite", $"acite@127.0.0.1", DateTimeOffset.Now.AddDays(1));
+
if (rootUser == null)
{
var key = GenerateKeyPair();
diff --git a/Abyss/Components/Static/Helpers.cs b/Abyss/Components/Static/Helpers.cs
index 3781aa5..849be90 100644
--- a/Abyss/Components/Static/Helpers.cs
+++ b/Abyss/Components/Static/Helpers.cs
@@ -67,7 +67,6 @@ public static class StringArrayExtensions
{
public static string[] SortLikeWindows(this string[] array)
{
- if (array == null) return null;
if (array.Length == 0) return array;
Array.Sort(array, new WindowsFileNameComparer());
@@ -76,7 +75,6 @@ public static class StringArrayExtensions
public static string[] SortLikeWindowsDescending(this string[] array)
{
- if (array == null) return null;
if (array.Length == 0) return array;
Array.Sort(array, new WindowsFileNameComparerDescending());
@@ -85,14 +83,14 @@ public static class StringArrayExtensions
public static void SortLikeWindowsInPlace(this string[] array)
{
- if (array == null || array.Length == 0) return;
+ if (array.Length == 0) return;
Array.Sort(array, new WindowsFileNameComparer());
}
public static void SortLikeWindowsDescendingInPlace(this string[] array)
{
- if (array == null || array.Length == 0) return;
+ if (array.Length == 0) return;
Array.Sort(array, new WindowsFileNameComparerDescending());
}
@@ -100,8 +98,8 @@ public static class StringArrayExtensions
public class WindowsFileNameComparer : IComparer
{
- private static readonly Regex _regex = new Regex(@"(\d+|\D+)", RegexOptions.Compiled);
- private static readonly CompareInfo _compareInfo = CultureInfo.InvariantCulture.CompareInfo;
+ private static readonly Regex Regex = new Regex(@"(\d+|\D+)", RegexOptions.Compiled);
+ private static readonly CompareInfo CompareInfo = CultureInfo.InvariantCulture.CompareInfo;
public int Compare(string? x, string? y)
{
@@ -110,8 +108,8 @@ public class WindowsFileNameComparer : IComparer
if (y == null) return 1;
if (ReferenceEquals(x, y)) return 0;
- var partsX = _regex.Matches(x);
- var partsY = _regex.Matches(y);
+ var partsX = Regex.Matches(x);
+ var partsY = Regex.Matches(y);
int minLength = Math.Min(partsX.Count, partsY.Count);
@@ -130,7 +128,7 @@ public class WindowsFileNameComparer : IComparer
int comparison;
if (ContainsChinese(partX) || ContainsChinese(partY))
{
- comparison = _compareInfo.Compare(partX, partY, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace);
+ comparison = CompareInfo.Compare(partX, partY, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace);
}
else
{
@@ -161,20 +159,20 @@ public class WindowsFileNameComparer : IComparer
public class WindowsFileNameComparerDescending : IComparer
{
- private static readonly WindowsFileNameComparer _ascendingComparer = new WindowsFileNameComparer();
+ private static readonly WindowsFileNameComparer AscendingComparer = new WindowsFileNameComparer();
- public int Compare(string x, string y)
+ public int Compare(string? x, string? y)
{
- return _ascendingComparer.Compare(y, x);
+ return AscendingComparer.Compare(y, x);
}
}
public static class StringNaturalCompare
{
- private static readonly WindowsFileNameComparer _comparer = new WindowsFileNameComparer();
+ private static readonly WindowsFileNameComparer Comparer = new WindowsFileNameComparer();
public static int Compare(string x, string y)
{
- return _comparer.Compare(x, y);
+ return Comparer.Compare(x, y);
}
}
\ No newline at end of file
diff --git a/Abyss/Components/Tools/TemporaryDB.cs b/Abyss/Components/Tools/TemporaryDB.cs
new file mode 100644
index 0000000..b7a99a6
--- /dev/null
+++ b/Abyss/Components/Tools/TemporaryDB.cs
@@ -0,0 +1,6 @@
+namespace Abyss.Components.Tools;
+
+public class TemporaryDB
+{
+
+}
\ No newline at end of file
diff --git a/Abyss/Model/Chip.cs b/Abyss/Model/Chip.cs
new file mode 100644
index 0000000..4af46ac
--- /dev/null
+++ b/Abyss/Model/Chip.cs
@@ -0,0 +1,17 @@
+namespace Abyss.Model;
+
+public enum ChipState
+{
+ Created,
+ Uploaded,
+ Verified
+}
+
+public class Chip
+{
+ public uint Id { get; set; }
+ public ulong Addr { get; set; }
+ public ulong Size { get; set; }
+ public string Hash { get; set; } = "";
+ public ChipState State { get; set; }
+}
\ No newline at end of file
diff --git a/Abyss/Model/Comment.cs b/Abyss/Model/Comment.cs
new file mode 100644
index 0000000..48f72d7
--- /dev/null
+++ b/Abyss/Model/Comment.cs
@@ -0,0 +1,7 @@
+namespace Abyss.Model;
+
+public class Comment
+{
+ public string username = "";
+ public string text = "";
+}
\ No newline at end of file
diff --git a/Abyss/Model/Task.cs b/Abyss/Model/Task.cs
new file mode 100644
index 0000000..6292db5
--- /dev/null
+++ b/Abyss/Model/Task.cs
@@ -0,0 +1,16 @@
+namespace Abyss.Model;
+
+public enum TaskType
+{
+ Video = 1,
+ Image = 2,
+}
+
+public class Task
+{
+ public uint Id;
+ public string Owner = "";
+ public string Class = "";
+ public string Name = "";
+ public TaskType Type;
+}
\ No newline at end of file
diff --git a/Abyss/Model/TaskCreation.cs b/Abyss/Model/TaskCreation.cs
new file mode 100644
index 0000000..7d5611b
--- /dev/null
+++ b/Abyss/Model/TaskCreation.cs
@@ -0,0 +1,23 @@
+namespace Abyss.Model;
+
+public class TaskCreation
+{
+ public int Type { get; set; }
+ public ulong Size { get; set; }
+ public string Klass { get; set; } = "";
+ public string Name { get; set; } = "";
+ public string Author { get; set; } = "";
+}
+
+public class ChipDesc
+{
+ public uint Id;
+ public ulong Addr;
+ public ulong Size;
+}
+
+public class TaskCreationResponse // As Array
+{
+ public uint Id;
+ public List Chips = new();
+}
\ No newline at end of file
diff --git a/Abyss/Model/Video.cs b/Abyss/Model/Video.cs
new file mode 100644
index 0000000..6cbc051
--- /dev/null
+++ b/Abyss/Model/Video.cs
@@ -0,0 +1,12 @@
+namespace Abyss.Model;
+
+public class Video
+{
+ public string name;
+ public ulong duration;
+ public List gallery = new();
+ public List comment = new();
+ public bool star;
+ public uint like;
+ public string author;
+}
\ No newline at end of file
diff --git a/Abyss/Program.cs b/Abyss/Program.cs
index 01d9fcc..3b53873 100644
--- a/Abyss/Program.cs
+++ b/Abyss/Program.cs
@@ -1,4 +1,5 @@
using System.Threading.RateLimiting;
+using Abyss.Components.Controllers.Task;
using Abyss.Components.Services;
using Microsoft.AspNetCore.RateLimiting;
@@ -17,6 +18,8 @@ public class Program
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
builder.Services.AddRateLimiter(options =>
{
diff --git a/Abyss/Properties/launchSettings.json b/Abyss/Properties/launchSettings.json
index 7d84003..ba42f88 100644
--- a/Abyss/Properties/launchSettings.json
+++ b/Abyss/Properties/launchSettings.json
@@ -17,7 +17,8 @@
"launchBrowser": false,
"applicationUrl": "https://localhost:7013;http://localhost:5198",
"environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "MEDIA_ROOT" : "/storage"
}
}
}
diff --git a/Abyss/appsettings.Development.json b/Abyss/appsettings.Development.json
deleted file mode 100644
index 0c208ae..0000000
--- a/Abyss/appsettings.Development.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- }
-}
From a2bf8bfcecf95b4ecd056c526fa8524ea3783279 Mon Sep 17 00:00:00 2001
From: acite <1498045907@qq.com>
Date: Sun, 7 Sep 2025 12:37:15 +0800
Subject: [PATCH 2/4] [feat] Comic Bookmarks Post
---
.../Tools/{TemporaryDB.cs => NaturalStringComparer.cs} | 0
Abyss/Model/Bookmark.cs | 6 ++++++
Abyss/Model/Comic.cs | 6 ++++++
3 files changed, 12 insertions(+)
rename Abyss/Components/Tools/{TemporaryDB.cs => NaturalStringComparer.cs} (100%)
create mode 100644 Abyss/Model/Bookmark.cs
create mode 100644 Abyss/Model/Comic.cs
diff --git a/Abyss/Components/Tools/TemporaryDB.cs b/Abyss/Components/Tools/NaturalStringComparer.cs
similarity index 100%
rename from Abyss/Components/Tools/TemporaryDB.cs
rename to Abyss/Components/Tools/NaturalStringComparer.cs
diff --git a/Abyss/Model/Bookmark.cs b/Abyss/Model/Bookmark.cs
new file mode 100644
index 0000000..1411c4e
--- /dev/null
+++ b/Abyss/Model/Bookmark.cs
@@ -0,0 +1,6 @@
+namespace Abyss.Model;
+
+public class Bookmark
+{
+
+}
\ No newline at end of file
diff --git a/Abyss/Model/Comic.cs b/Abyss/Model/Comic.cs
new file mode 100644
index 0000000..e45c675
--- /dev/null
+++ b/Abyss/Model/Comic.cs
@@ -0,0 +1,6 @@
+namespace Abyss.Model;
+
+public class Comic
+{
+
+}
\ No newline at end of file
From 99a5e42d99fb233bede912700f041433aed9022d Mon Sep 17 00:00:00 2001
From: acite <1498045907@qq.com>
Date: Tue, 9 Sep 2025 12:10:00 +0800
Subject: [PATCH 3/4] [feat] Python toolkits
---
Abyss/Toolkits/image-creator.py | 318 ++++++++++++++++++++++++++++++++
Abyss/Toolkits/image-sum.py | 78 ++++++++
Abyss/Toolkits/update-tags.py | 72 ++++++++
Abyss/Toolkits/update-video.py | 206 +++++++++++++++++++++
4 files changed, 674 insertions(+)
create mode 100644 Abyss/Toolkits/image-creator.py
create mode 100644 Abyss/Toolkits/image-sum.py
create mode 100644 Abyss/Toolkits/update-tags.py
create mode 100644 Abyss/Toolkits/update-video.py
diff --git a/Abyss/Toolkits/image-creator.py b/Abyss/Toolkits/image-creator.py
new file mode 100644
index 0000000..77927b2
--- /dev/null
+++ b/Abyss/Toolkits/image-creator.py
@@ -0,0 +1,318 @@
+import os
+import sys
+import json
+import re
+import tkinter as tk
+from tkinter import simpledialog, Toplevel, Canvas, Frame, Scrollbar
+from PIL import Image, ImageTk
+
+# --- Configuration ---
+# Supported image file extensions
+SUPPORTED_EXTENSIONS = ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp')
+# Default thumbnail size for the GUI on first launch
+DEFAULT_THUMBNAIL_SIZE = (300, 300)
+# Number of columns in the GUI grid
+GRID_COLUMNS = 5
+
+
+def natural_sort_key(s):
+ return [int(text) if text.isdigit() else text.lower()
+ for text in re.split('([0-9]+)', s)]
+
+
+class BookmarkApp:
+ """
+ A GUI application for selecting images and creating bookmarks, with zoom functionality.
+ """
+
+ def __init__(self, parent, image_dir, image_files):
+ """
+ Initialize the bookmark creation window.
+ """
+ self.top = Toplevel(parent)
+ self.top.title("Bookmark Creator | Keys: [+] Zoom In, [-] Zoom Out")
+
+ self.top.grid_rowconfigure(0, weight=1)
+ self.top.grid_columnconfigure(0, weight=1)
+
+ self.image_dir = image_dir
+ self.image_files = image_files
+ self.bookmarks = []
+ self._photo_images = [] # To prevent garbage collection
+
+ # --- Zoom Configuration ---
+ self.current_size = DEFAULT_THUMBNAIL_SIZE[0]
+ self.zoom_step = 25
+ self.min_zoom_size = 50
+ self.max_zoom_size = 500
+
+ # --- Create a scrollable frame ---
+ self.canvas = Canvas(self.top)
+ self.scrollbar = Scrollbar(self.top, orient="vertical", command=self.canvas.yview)
+ self.scrollable_frame = Frame(self.canvas)
+
+ self.scrollable_frame.bind(
+ "",
+ lambda e: self.canvas.configure(
+ scrollregion=self.canvas.bbox("all")
+ )
+ )
+
+ self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
+ self.canvas.configure(yscrollcommand=self.scrollbar.set)
+
+ self.canvas.grid(row=0, column=0, sticky="nsew")
+ self.scrollbar.grid(row=0, column=1, sticky="ns")
+
+ # --- Bind Events ---
+ self.top.bind('', self._on_mousewheel)
+ self.top.bind('', self._on_mousewheel)
+ self.top.bind('', self._on_mousewheel)
+ # Bind zoom keys
+ self.top.bind('', self._zoom_in)
+ self.top.bind('', self._zoom_in) # For keyboards where + is shift+=
+ self.top.bind('', self._zoom_out)
+
+ self._repopulate_images()
+
+ def _zoom_in(self, event=None):
+ """Increases the size of the thumbnails."""
+ new_size = self.current_size + self.zoom_step
+ if new_size > self.max_zoom_size:
+ new_size = self.max_zoom_size
+
+ if new_size != self.current_size:
+ self.current_size = new_size
+ print(f"Zoom In. New thumbnail size: {self.current_size}x{self.current_size}")
+ self._repopulate_images()
+
+ def _zoom_out(self, event=None):
+ """Decreases the size of the thumbnails."""
+ new_size = self.current_size - self.zoom_step
+ if new_size < self.min_zoom_size:
+ new_size = self.min_zoom_size
+
+ if new_size != self.current_size:
+ self.current_size = new_size
+ print(f"Zoom Out. New thumbnail size: {self.current_size}x{self.current_size}")
+ self._repopulate_images()
+
+ def _on_mousewheel(self, event):
+ """Handle mouse wheel scrolling."""
+ if sys.platform == "linux":
+ scroll_delta = -1 if event.num == 4 else 1
+ else:
+ scroll_delta = int(-1 * (event.delta / 120))
+ self.canvas.yview_scroll(scroll_delta, "units")
+
+ def _repopulate_images(self):
+ """
+ Clear and redraw all images in the grid with the current size.
+ This is called on initial load and after every zoom action.
+ """
+ # Clear existing widgets
+ for widget in self.scrollable_frame.winfo_children():
+ widget.destroy()
+ self._photo_images.clear() # Clear the photo references
+
+ new_thumbnail_size = (self.current_size, self.current_size)
+
+ for i, filename in enumerate(self.image_files):
+ try:
+ filepath = os.path.join(self.image_dir, filename)
+ with Image.open(filepath) as img:
+ img.thumbnail(new_thumbnail_size, Image.Resampling.LANCZOS)
+ photo = ImageTk.PhotoImage(img)
+ self._photo_images.append(photo)
+
+ container = Frame(self.scrollable_frame, bd=2, relief="groove")
+ img_label = tk.Label(container, image=photo)
+ img_label.pack()
+
+ text_label = tk.Label(container, text=filename)
+ text_label.pack()
+
+ container.bind("", lambda e, f=filename: self.add_bookmark(f))
+ img_label.bind("", lambda e, f=filename: self.add_bookmark(f))
+ text_label.bind("", lambda e, f=filename: self.add_bookmark(f))
+
+ row = i // GRID_COLUMNS
+ col = i % GRID_COLUMNS
+ container.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
+ except Exception as e:
+ print(f"Warning: Could not load image {filename}. Error: {e}")
+
+ def add_bookmark(self, page_filename):
+ """Prompt user for a bookmark name and add it."""
+ bookmark_name = simpledialog.askstring(
+ "Add Bookmark",
+ f"Enter a name for the bookmark on page:\n{page_filename}",
+ parent=self.top
+ )
+ if bookmark_name:
+ self.bookmarks.append({
+ "name": bookmark_name,
+ "page": page_filename
+ })
+ print(f"Success: Bookmark '{bookmark_name}' created for page '{page_filename}'.")
+
+ def wait(self):
+ """Wait for the Toplevel window to be closed."""
+ self.top.wait_window()
+ return self.bookmarks
+
+
+def load_existing_summary(summary_path):
+ """Load existing summary.json if it exists, else return None."""
+ if os.path.exists(summary_path):
+ try:
+ with open(summary_path, 'r', encoding='utf-8') as f:
+ return json.load(f)
+ except (IOError, json.JSONDecodeError) as e:
+ print(f"Warning: Could not read existing summary.json: {e}")
+ return None
+
+
+def get_tags_from_user():
+ """Prompt user to enter tags in the console."""
+ try:
+ tags_input = input("Enter tags (comma-separated): ").strip()
+ if tags_input:
+ return [tag.strip() for tag in tags_input.split(",") if tag.strip()]
+ return []
+ except (KeyboardInterrupt, EOFError):
+ print("\nOperation cancelled by user.")
+ sys.exit(0)
+
+
+def main():
+ """
+ Main function to execute the script.
+ """
+ # --- 1. Get and Validate Directory from Command Line ---
+ if len(sys.argv) != 2:
+ print("Usage: python restructure_comic.py ")
+ sys.exit(1)
+
+ target_dir = sys.argv[1]
+ if not os.path.isdir(target_dir):
+ print(f"Error: The provided path '{target_dir}' is not a valid directory.")
+ sys.exit(1)
+
+ print(f"Processing directory: {target_dir}")
+
+ # --- 2. Check for existing summary.json ---
+ json_filepath = os.path.join(target_dir, "summary.json")
+ existing_summary = load_existing_summary(json_filepath)
+
+ # --- 3. Get User Input for Metadata ---
+ if existing_summary:
+ print("Found existing summary.json. Using existing data where available.")
+ comic_name = existing_summary.get("comic_name", "")
+ author = existing_summary.get("author", "anonymous")
+ tags = existing_summary.get("tags", [])
+ existing_bookmarks = existing_summary.get("bookmarks", [])
+
+ # Only prompt for missing fields
+ if not comic_name:
+ try:
+ comic_name = input("Enter the comic name: ")
+ except (KeyboardInterrupt, EOFError):
+ print("\nOperation cancelled by user.")
+ sys.exit(0)
+ else:
+ try:
+ comic_name = input("Enter the comic name: ")
+ author = input("Enter the author name (or leave blank for 'anonymous'): ")
+ if not author:
+ author = "anonymous"
+ tags = get_tags_from_user()
+ existing_bookmarks = []
+ except (KeyboardInterrupt, EOFError):
+ print("\nOperation cancelled by user.")
+ sys.exit(0)
+
+ # --- 4. Scan, Sort, and Rename Image Files ---
+ try:
+ all_files = os.listdir(target_dir)
+ image_files = sorted(
+ [f for f in all_files if f.lower().endswith(SUPPORTED_EXTENSIONS)],
+ key=natural_sort_key
+ )
+
+ if not image_files:
+ print("Error: No supported image files found in the directory.")
+ sys.exit(1)
+
+ # Only rename files if we don't have an existing summary with a file list
+ if existing_summary and "list" in existing_summary:
+ new_filenames = existing_summary["list"]
+ print("Using existing file list from summary.json")
+ else:
+ page_count = len(image_files)
+ num_digits = len(str(page_count))
+ new_filenames = []
+
+ print("\nRenaming files...")
+ for i, old_filename in enumerate(image_files, start=1):
+ file_ext = os.path.splitext(old_filename)[1]
+ new_filename_base = f"{i:0{num_digits}d}"
+ new_filename = f"{new_filename_base}{file_ext}"
+
+ old_filepath = os.path.join(target_dir, old_filename)
+ new_filepath = os.path.join(target_dir, new_filename)
+
+ if old_filepath != new_filepath:
+ os.rename(old_filepath, new_filepath)
+ print(f" '{old_filename}' -> '{new_filename}'")
+ else:
+ print(f" '{old_filename}' is already correctly named. Skipping.")
+
+ new_filenames.append(new_filename)
+
+ except OSError as e:
+ print(f"\nAn error occurred during file operations: {e}")
+ sys.exit(1)
+
+ print("\nFile operations complete.")
+
+ # --- 5. Launch GUI for Bookmark Creation ---
+ print("Launching bookmark creator GUI...")
+ print("Please click on images in the new window to create bookmarks.")
+ print("Use '+' and '-' keys to zoom in and out. Close the window when finished.")
+
+ root = tk.Tk()
+ root.withdraw()
+
+ gui = BookmarkApp(root, target_dir, new_filenames)
+ new_bookmarks = gui.wait()
+
+ root.destroy()
+ print("Bookmark creation finished.")
+
+ # Combine existing bookmarks with new ones
+ all_bookmarks = existing_bookmarks + new_bookmarks
+
+ # --- 6. Create and Write summary.json ---
+ summary_data = {
+ "comic_name": comic_name,
+ "page_count": len(new_filenames),
+ "bookmarks": all_bookmarks,
+ "author": author,
+ "tags": tags,
+ "list": new_filenames
+ }
+
+ try:
+ with open(json_filepath, 'w', encoding='utf-8') as f:
+ json.dump(summary_data, f, indent=2, ensure_ascii=False)
+ print(f"\nSuccessfully created/updated '{json_filepath}'")
+ except IOError as e:
+ print(f"\nError: Could not write to '{json_filepath}'. Reason: {e}")
+ sys.exit(1)
+
+ print("\nOperation completed successfully!")
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/Abyss/Toolkits/image-sum.py b/Abyss/Toolkits/image-sum.py
new file mode 100644
index 0000000..8b03465
--- /dev/null
+++ b/Abyss/Toolkits/image-sum.py
@@ -0,0 +1,78 @@
+import json
+import os
+import sys
+from pathlib import Path
+
+def process_directory(directory_path):
+ """
+ 处理指定目录,扫描图片文件并更新summary.json
+
+ Args:
+ directory_path (str): 目录路径
+ """
+ try:
+ # 转换为Path对象
+ path = Path(directory_path)
+
+ # 检查目录是否存在
+ if not path.exists() or not path.is_dir():
+ print(f"错误: 目录 '{directory_path}' 不存在或不是目录")
+ return False
+
+ # 支持的图片文件扩展名
+ image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.webp'}
+
+ # 扫描目录中的图片文件
+ image_files = []
+ for file in path.iterdir():
+ if file.is_file() and file.suffix.lower() in image_extensions:
+ image_files.append(file.name)
+
+ # 按文件名排序
+ image_files.sort()
+
+ print(f"找到 {len(image_files)} 个图片文件")
+
+ # 读取或创建summary.json
+ summary_file = path / "summary.json"
+ if summary_file.exists():
+ try:
+ with open(summary_file, 'r', encoding='utf-8') as f:
+ summary_data = json.load(f)
+ except json.JSONDecodeError:
+ print("错误: summary.json 格式不正确")
+ return False
+ else:
+ summary_data = {}
+
+ # 更新列表
+ summary_data['list'] = image_files
+
+ # 写回文件
+ with open(summary_file, 'w', encoding='utf-8') as f:
+ json.dump(summary_data, f, ensure_ascii=False, indent=2)
+
+ print(f"成功更新 {summary_file}")
+ return True
+
+ except Exception as e:
+ print(f"处理过程中发生错误: {e}")
+ return False
+
+def main():
+ # 检查命令行参数
+ if len(sys.argv) != 2:
+ print("用法: python script.py <目录路径>")
+ sys.exit(1)
+
+ directory_path = sys.argv[1]
+
+ # 处理目录
+ if process_directory(directory_path):
+ print("操作完成")
+ else:
+ print("操作失败")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
diff --git a/Abyss/Toolkits/update-tags.py b/Abyss/Toolkits/update-tags.py
new file mode 100644
index 0000000..cb1c79d
--- /dev/null
+++ b/Abyss/Toolkits/update-tags.py
@@ -0,0 +1,72 @@
+import os
+import json
+import sys
+
+def process_directory(base_path):
+ # 检查基础路径是否存在
+ if not os.path.exists(base_path):
+ print(f"错误:路径 '{base_path}' 不存在")
+ return
+
+ # 遍历基础路径下的所有子目录
+ for item in os.listdir(base_path):
+ item_path = os.path.join(base_path, item)
+
+ # 只处理目录,忽略文件
+ if os.path.isdir(item_path):
+ summary_path = os.path.join(item_path, "summary.json")
+
+ # 检查summary.json文件是否存在
+ if os.path.exists(summary_path):
+ try:
+ # 读取JSON文件
+ with open(summary_path, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+
+ # 获取comic_name和tags
+ comic_name = data.get('comic_name', '未知名称')
+ tags = data.get('tags', [])
+
+ # 输出信息
+ print(f"\n漫画名称: {comic_name}")
+ print(f"当前标签: {tags}")
+
+ # 提示用户输入新标签
+ user_input = input("请输入新标签(多个标签用英文逗号分隔,直接回车跳过): ").strip()
+
+ if user_input:
+ # 分割用户输入的标签
+ new_tags = [tag.strip() for tag in user_input.split(',') if tag.strip()]
+
+ if new_tags:
+ # 添加新标签到列表
+ tags.extend(new_tags)
+ data['tags'] = tags
+
+ # 写回文件
+ with open(summary_path, 'w', encoding='utf-8') as f:
+ json.dump(data, f, ensure_ascii=False, indent=4)
+
+ print(f"已添加新标签: {new_tags}")
+ else:
+ print("未输入有效标签,跳过")
+ else:
+ print("未输入标签,跳过")
+
+ except json.JSONDecodeError:
+ print(f"错误:{summary_path} 不是有效的JSON文件")
+ except Exception as e:
+ print(f"处理文件 {summary_path} 时出错: {e}")
+ else:
+ print(f"跳过目录 {item}:未找到summary.json文件")
+
+def main():
+ if len(sys.argv) != 2:
+ print("用法: python script.py <目录路径>")
+ sys.exit(1)
+
+ base_dir = sys.argv[1]
+ process_directory(base_dir)
+
+if __name__ == "__main__":
+ main()
diff --git a/Abyss/Toolkits/update-video.py b/Abyss/Toolkits/update-video.py
new file mode 100644
index 0000000..a72d794
--- /dev/null
+++ b/Abyss/Toolkits/update-video.py
@@ -0,0 +1,206 @@
+import json
+import os
+import sys
+import subprocess
+import shutil
+from pathlib import Path
+
+def get_video_duration(video_path):
+ """Get video duration in milliseconds using ffprobe"""
+ try:
+ cmd = [
+ 'ffprobe',
+ '-v', 'error',
+ '-show_entries', 'format=duration',
+ '-of', 'default=noprint_wrappers=1:nokey=1',
+ str(video_path)
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
+ duration_seconds = float(result.stdout.strip())
+ return int(duration_seconds * 1000)
+ except (subprocess.CalledProcessError, FileNotFoundError, ValueError) as e:
+ print(f"Error getting video duration: {e}")
+ return 0
+
+def create_thumbnails(video_path, gallery_path, num_thumbnails=10):
+ """
+ Extracts thumbnails from a video and saves them to the gallery directory.
+ """
+ try:
+ # Check if ffmpeg is installed
+ subprocess.run(['ffmpeg', '-version'], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ except (subprocess.CalledProcessError, FileNotFoundError):
+ print("Error: ffmpeg is not installed or not in your PATH. Skipping thumbnail creation.")
+ return
+
+ try:
+ # Get video duration using ffprobe
+ duration_cmd = [
+ 'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
+ '-of', 'default=noprint_wrappers=1:nokey=1', str(video_path)
+ ]
+ result = subprocess.run(duration_cmd, capture_output=True, text=True, check=True)
+ duration = float(result.stdout)
+ except (subprocess.CalledProcessError, ValueError) as e:
+ print(f"Could not get duration for '{video_path}': {e}. Skipping thumbnail creation.")
+ return
+
+ if duration <= 0:
+ print(f"Warning: Invalid video duration for '{video_path}'. Skipping thumbnail creation.")
+ return
+
+ interval = duration / (num_thumbnails + 1)
+
+ print(f"Generating {num_thumbnails} thumbnails for {video_path.name}...")
+
+ for i in range(num_thumbnails):
+ timestamp = (i + 1) * interval
+ output_thumbnail_path = gallery_path / f"{i}.jpg"
+
+ ffmpeg_cmd = [
+ 'ffmpeg', '-ss', str(timestamp), '-i', str(video_path),
+ '-vframes', '1', '-q:v', '2', str(output_thumbnail_path), '-y'
+ ]
+
+ try:
+ subprocess.run(ffmpeg_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ print(f" Extracted thumbnail {i}.jpg")
+ except subprocess.CalledProcessError as e:
+ print(f" Error extracting thumbnail {i}.jpg: {e}")
+
+def update_summary(base_path, name_input=None, author_input=None):
+ """
+ Updates the summary.json file for a given path.
+ name_input and author_input are optional, used for the '-a' mode.
+ """
+ summary_path = base_path / "summary.json"
+ video_path = base_path / "video.mp4"
+ gallery_path = base_path / "gallery"
+
+ # Default template
+ default_summary = {
+ "name": name_input if name_input is not None else "null",
+ "duration": 0,
+ "gallery": [],
+ "comment": [],
+ "star": False,
+ "like": 0,
+ "author": author_input if author_input is not None else "anonymous"
+ }
+
+ # Load existing summary if available
+ if summary_path.exists():
+ try:
+ with open(summary_path, 'r', encoding='utf-8') as f:
+ existing_data = json.load(f)
+ # Update default with existing values
+ for key in default_summary:
+ if key in existing_data:
+ default_summary[key] = existing_data[key]
+ except json.JSONDecodeError:
+ print("Warning: Invalid JSON in summary.json, using defaults")
+
+ # Update duration from video file
+ if video_path.exists():
+ default_summary["duration"] = get_video_duration(video_path)
+ else:
+ print(f"Warning: video.mp4 not found at {video_path}")
+
+ # Update gallery from directory
+ if gallery_path.exists() and gallery_path.is_dir():
+ gallery_files = []
+ for file in gallery_path.iterdir():
+ if file.is_file():
+ gallery_files.append(file.name)
+ gallery_files.sort()
+ default_summary["gallery"] = gallery_files
+ else:
+ print(f"Warning: gallery directory not found at {gallery_path}")
+
+ # Write updated summary
+ with open(summary_path, 'w', encoding='utf-8') as f:
+ json.dump(default_summary, f, indent=4, ensure_ascii=False)
+
+ print(f"Summary updated successfully at {summary_path}")
+
+def find_next_directory(base_path):
+ """Find the next available integer directory name."""
+ existing_dirs = set()
+ for item in base_path.iterdir():
+ if item.is_dir() and item.name.isdigit():
+ existing_dirs.add(int(item.name))
+
+ next_num = 1
+ while next_num in existing_dirs:
+ next_num += 1
+ return str(next_num)
+
+def main():
+ if len(sys.argv) < 2:
+ print("Usage: python script.py [arguments]")
+ print("Commands:")
+ print(" -u Update the summary.json in the specified path.")
+ print(" -a Add a new video project in a new directory under the specified path.")
+ sys.exit(1)
+
+ command = sys.argv[1]
+
+ if command == '-u':
+ if len(sys.argv) != 3:
+ print("Usage: python script.py -u ")
+ sys.exit(1)
+ base_path = Path(sys.argv[2])
+ if not base_path.is_dir():
+ print(f"Error: Path not found or is not a directory: {base_path}")
+ sys.exit(1)
+ update_summary(base_path)
+
+ elif command == '-a':
+ if len(sys.argv) != 4:
+ print("Usage: python script.py -a ")
+ sys.exit(1)
+
+ video_source_path = Path(sys.argv[2])
+ base_path = Path(sys.argv[3])
+
+ if not video_source_path.exists() or not video_source_path.is_file():
+ print(f"Error: Video file not found: {video_source_path}")
+ sys.exit(1)
+
+ if not base_path.is_dir():
+ print(f"Error: Base path not found or is not a directory: {base_path}")
+ sys.exit(1)
+
+ # Find a new directory name (e.g., "1", "2", "3")
+ new_dir_name = find_next_directory(base_path)
+ new_project_path = base_path / new_dir_name
+
+ # Create the new project directory and the gallery subdirectory
+ new_project_path.mkdir(exist_ok=True)
+ gallery_path = new_project_path / "gallery"
+ gallery_path.mkdir(exist_ok=True)
+ print(f"New project directory created at {new_project_path}")
+
+ # Copy video file to the new directory
+ shutil.copy(video_source_path, new_project_path / "video.mp4")
+ print(f"Video copied to {new_project_path / 'video.mp4'}")
+
+ # --- 新增功能:自动生成缩略图 ---
+ video_dest_path = new_project_path / "video.mp4"
+ create_thumbnails(video_dest_path, gallery_path)
+ # ------------------------------------
+
+ # Get user input for name and author
+ video_name = input("Enter the video name: ")
+ video_author = input("Enter the author's name: ")
+
+ # Update the summary with user input
+ update_summary(new_project_path, name_input=video_name, author_input=video_author)
+
+ else:
+ print("Invalid command. Use -u or -a.")
+ print("Usage: python script.py [arguments]")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
From 3e03b13d11a67c67f9cb85c971867585b3db5d6d Mon Sep 17 00:00:00 2001
From: acite <1498045907@qq.com>
Date: Tue, 9 Sep 2025 12:11:30 +0800
Subject: [PATCH 4/4] [optimize] code optimize
---
.idea/.idea.Abyss/.idea/workspace.xml | 61 +++++++++----------
Abyss.sln.DotSettings.user | 1 +
.../Controllers/Media/ImageController.cs | 26 +++++++-
.../Controllers/Media/VideoController.cs | 21 ++++++-
Abyss/Components/Services/ConfigureService.cs | 1 +
Abyss/Components/Services/ResourceService.cs | 14 ++++-
Abyss/Components/Services/UserService.cs | 4 +-
.../Components/Tools/NaturalStringComparer.cs | 58 +++++++++++++++++-
Abyss/Model/Bookmark.cs | 7 ++-
Abyss/Model/Comic.cs | 13 +++-
Abyss/Properties/launchSettings.json | 3 +-
11 files changed, 166 insertions(+), 43 deletions(-)
diff --git a/.idea/.idea.Abyss/.idea/workspace.xml b/.idea/.idea.Abyss/.idea/workspace.xml
index 1ad86c0..19883db 100644
--- a/.idea/.idea.Abyss/.idea/workspace.xml
+++ b/.idea/.idea.Abyss/.idea/workspace.xml
@@ -10,24 +10,21 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
+
-
@@ -43,6 +40,7 @@
+
@@ -54,9 +52,10 @@
-
+
+
@@ -65,6 +64,7 @@
+
@@ -93,17 +93,17 @@
"RunOnceActivity.git.unshallow": "true",
"XThreadsFramesViewSplitterKey": "0.30266345",
"git-widget-placeholder": "dev-task",
- "last_opened_file_path": "/home/acite/embd/WebProjects/Abyss/.gitignore",
+ "last_opened_file_path": "/storage/Images/31/summary.json",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
- "settings.editor.selected.configurable": "preferences.pluginManager",
+ "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable",
"vue.rearranger.settings.migration": "true"
}
}]]>
-
+
@@ -198,7 +198,19 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -218,7 +230,7 @@
-
+
@@ -248,19 +260,6 @@
-
- file://$PROJECT_DIR$/Abyss/Components/Services/TaskService.cs
- 60
-
-
-
-
-
-
-
-
-
-
diff --git a/Abyss.sln.DotSettings.user b/Abyss.sln.DotSettings.user
index 674a5bb..152a239 100644
--- a/Abyss.sln.DotSettings.user
+++ b/Abyss.sln.DotSettings.user
@@ -3,4 +3,5 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
\ No newline at end of file
diff --git a/Abyss/Components/Controllers/Media/ImageController.cs b/Abyss/Components/Controllers/Media/ImageController.cs
index 969c42d..c5f94fc 100644
--- a/Abyss/Components/Controllers/Media/ImageController.cs
+++ b/Abyss/Components/Controllers/Media/ImageController.cs
@@ -1,6 +1,9 @@
using Abyss.Components.Services;
using Abyss.Components.Static;
+using Abyss.Components.Tools;
+using Abyss.Model;
using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Abyss.Components.Controllers.Media;
@@ -28,7 +31,7 @@ public class ImageController(ILogger logger, ResourceService rs
if(r == null)
return StatusCode(401, new { message = "Unauthorized" });
- return Ok(r);
+ return Ok(r.NaturalSort(x => x));
}
[HttpGet("{id}")]
@@ -42,6 +45,27 @@ public class ImageController(ILogger logger, ResourceService rs
return Ok(await System.IO.File.ReadAllTextAsync(d));
}
+
+ [HttpPost("{id}/bookmark")]
+ public async Task Bookmark(string id, string token, [FromBody] Bookmark bookmark)
+ {
+ var d = Helpers.SafePathCombine(ImageFolder, [id, "summary.json"]);
+ if (d == null) return StatusCode(403, new { message = "403 Denied" });
+
+ var r = await rs.Update(d, token, Ip);
+ if (!r) return StatusCode(403, new { message = "403 Denied" });
+
+ Comic c = JsonConvert.DeserializeObject(await System.IO.File.ReadAllTextAsync(d))!;
+
+ var bookmarkPage = Helpers.SafePathCombine(ImageFolder, [id, bookmark.Page]);
+ if(!System.IO.File.Exists(bookmarkPage))
+ return BadRequest();
+
+ c.Bookmarks.Add(bookmark);
+ var o = JsonConvert.SerializeObject(c);
+ await System.IO.File.WriteAllTextAsync(d, o);
+ return Ok();
+ }
[HttpGet("{id}/{file}")]
public async Task Get(string id, string file, string token)
diff --git a/Abyss/Components/Controllers/Media/VideoController.cs b/Abyss/Components/Controllers/Media/VideoController.cs
index c04968b..520a995 100644
--- a/Abyss/Components/Controllers/Media/VideoController.cs
+++ b/Abyss/Components/Controllers/Media/VideoController.cs
@@ -1,7 +1,10 @@
using System.Diagnostics;
using Abyss.Components.Services;
using Abyss.Components.Static;
+using Abyss.Components.Tools;
+using Abyss.Model;
using Microsoft.AspNetCore.Mvc;
+using Newtonsoft.Json;
namespace Abyss.Components.Controllers.Media;
@@ -38,10 +41,24 @@ public class VideoController(ILogger logger, ResourceService rs
var d = Helpers.SafePathCombine(VideoFolder, klass);
if (d == null) return StatusCode(403, new { message = "403 Denied" });
var r = await rs.Query(d, token, Ip);
-
if (r == null) return StatusCode(401, new { message = "Unauthorized" });
+
+ var rv = r.Select(x =>
+ {
+ return Helpers.SafePathCombine(VideoFolder, [klass, x, "summary.json"]);
+ }).ToArray();
+
+ for (int i = 0; i < rv.Length; i++)
+ {
+ if(rv[i] == null) continue;
+ rv[i] = await System.IO.File.ReadAllTextAsync(rv[i] ?? "");
+ }
+
+ var sv = rv.Where(x => x!=null).Select(x => x ?? "")
+ .Select(x => JsonConvert.DeserializeObject