[merge] Merge branch 'dev-task'
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -56,4 +56,5 @@ nunit-*.xml
|
|||||||
# DB
|
# DB
|
||||||
*.db
|
*.db
|
||||||
|
|
||||||
appsettings.json
|
appsettings.json
|
||||||
|
appsettings.Development.json
|
||||||
98
.idea/.idea.Abyss/.idea/workspace.xml
generated
98
.idea/.idea.Abyss/.idea/workspace.xml
generated
@@ -10,10 +10,21 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="">
|
<list default="true" id="bf317275-3039-49bb-a475-725a800a0cce" name="Changes" comment="">
|
||||||
|
<change afterPath="$PROJECT_DIR$/Abyss/Toolkits/image-creator.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/Abyss/Toolkits/image-sum.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/Abyss/Toolkits/update-tags.py" afterDir="false" />
|
||||||
|
<change afterPath="$PROJECT_DIR$/Abyss/Toolkits/update-video.py" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.Abyss/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Abyss.sln.DotSettings.user" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss.sln.DotSettings.user" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/ImageController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/ImageController.cs" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/VideoController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/VideoController.cs" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/VideoController.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Controllers/Media/VideoController.cs" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/ConfigureService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/ConfigureService.cs" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Abyss/Components/Tools/NaturalStringComparer.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Components/Tools/NaturalStringComparer.cs" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Abyss/Model/Bookmark.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Model/Bookmark.cs" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Abyss/Model/Comic.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Model/Comic.cs" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/Abyss/Properties/launchSettings.json" beforeDir="false" afterPath="$PROJECT_DIR$/Abyss/Properties/launchSettings.json" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -27,27 +38,39 @@
|
|||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="HighlightingSettingsPerFile">
|
<component name="HighlightingSettingsPerFile">
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/011a191356a243438f987de3ec3d6c6230800/04/8419ff35/ServiceProvider.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/4c/4b962087/Monitor.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/4c/4b962087/Monitor.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/457530be4752476295767457c3639889d1a000/d0/3b166e9e/String.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/5df2accb46d040ccbbbe8331bf4d24b61daa00/df/93debd37/ControllerBase.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/5df2accb46d040ccbbbe8331bf4d24b61daa00/df/93debd37/ControllerBase.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/7598e47d5cdf4107ba88f8220720fdc89000/a6/79d67871/xxHash128.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/7598e47d5cdf4107ba88f8220720fdc89000/a6/79d67871/xxHash128.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/f09ccaeb94c34c2299acd3efee0facee1a400/81/137b58b4/Key.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/f09ccaeb94c34c2299acd3efee0facee1a400/81/137b58b4/Key.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/AbyssController.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/AbyssController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Controllers/Media/VideoController.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Controllers/Media/VideoController.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Security/UserController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Controllers/Task/TaskController.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ConfigureService.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ConfigureService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/ResourceService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/TaskService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Services/UserService.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Services/UserService.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="mock:///home/acite/embd/WebProjects/Abyss/Abyss/Components/Services/UserService.cs" root0="SKIP_HIGHLIGHTING" />
|
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Components/Static/Helpers.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/Bookmark.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Model/ChallengeResponse.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/ChallengeResponse.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/Chip.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/Comic.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/Comment.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Model/ResourceAttribute.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/ResourceAttribute.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/Task.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/TaskCreation.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Model/User.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/User.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/Abyss/Model/UserCreating.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/UserCreating.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/Abyss/Model/Video.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
<setting file="file://$PROJECT_DIR$/AbyssCli/Program.cs" root0="FORCE_HIGHLIGHTING" />
|
<setting file="file://$PROJECT_DIR$/AbyssCli/Program.cs" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file:///storage/Images/31/summary.json" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
<setting file="file:///usr/lib/dotnet/sdk/9.0.109/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.targets" root0="FORCE_HIGHLIGHTING" />
|
||||||
</component>
|
</component>
|
||||||
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
|
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
|
||||||
|
<component name="ProblemsViewState">
|
||||||
|
<option name="selectedTabId" value="CurrentFile" />
|
||||||
|
</component>
|
||||||
<component name="ProjectColorInfo">{
|
<component name="ProjectColorInfo">{
|
||||||
"associatedIndex": 3
|
"associatedIndex": 3
|
||||||
}</component>
|
}</component>
|
||||||
@@ -56,31 +79,31 @@
|
|||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
".NET Launch Settings Profile.Abyss: http.executor": "Run",
|
".NET Launch Settings Profile.Abyss: http.executor": "Run",
|
||||||
".NET Launch Settings Profile.Abyss: https.executor": "Run",
|
".NET Launch Settings Profile.Abyss: https.executor": "Debug",
|
||||||
".NET Project.AbyssCli.executor": "Run",
|
".NET Project.AbyssCli.executor": "Run",
|
||||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
"Publish to folder.Publish Abyss to folder x86.executor": "Run",
|
"Publish to folder.Publish Abyss to folder x86.executor": "Run",
|
||||||
"Publish to folder.Publish Abyss to folder.executor": "Run",
|
"Publish to folder.Publish Abyss to folder.executor": "Run",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"XThreadsFramesViewSplitterKey": "0.30266345",
|
"XThreadsFramesViewSplitterKey": "0.30266345",
|
||||||
"git-widget-placeholder": "main",
|
"git-widget-placeholder": "dev-task",
|
||||||
"last_opened_file_path": "/opt/security/https/server",
|
"last_opened_file_path": "/storage/Images/31/summary.json",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"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"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}</component>
|
}]]></component>
|
||||||
<component name="RunManager" selected="Publish to folder.Publish Abyss to folder x86">
|
<component name="RunManager" selected="Publish to folder.Publish Abyss to folder">
|
||||||
<configuration name="Publish Abyss to folder x86" type="DotNetFolderPublish" factoryName="Publish to folder">
|
<configuration name="Publish Abyss to folder x86" type="DotNetFolderPublish" factoryName="Publish to folder">
|
||||||
<riderPublish configuration="Release" platform="Any CPU" produce_single_file="true" ready_to_run="true" self_contained="true" target_folder="/opt/security/https/server" target_framework="net9.0" uuid_high="3690631506471504162" uuid_low="-4858628519588143325">
|
<riderPublish configuration="Release" platform="Any CPU" produce_single_file="true" ready_to_run="true" self_contained="true" target_folder="/opt/security/https/server" target_framework="net9.0" uuid_high="3690631506471504162" uuid_low="-4858628519588143325">
|
||||||
<runtimes>
|
<runtimes>
|
||||||
@@ -174,7 +197,20 @@
|
|||||||
<workItem from="1756121403390" duration="7316000" />
|
<workItem from="1756121403390" duration="7316000" />
|
||||||
<workItem from="1756145197559" duration="780000" />
|
<workItem from="1756145197559" duration="780000" />
|
||||||
<workItem from="1756205686118" duration="5398000" />
|
<workItem from="1756205686118" duration="5398000" />
|
||||||
<workItem from="1756277940361" duration="862000" />
|
<workItem from="1756277940361" duration="1097000" />
|
||||||
|
<workItem from="1756293105406" duration="18336000" />
|
||||||
|
<workItem from="1756364194123" duration="748000" />
|
||||||
|
<workItem from="1756553959939" duration="2071000" />
|
||||||
|
<workItem from="1756611257955" duration="11070000" />
|
||||||
|
<workItem from="1756693065091" duration="645000" />
|
||||||
|
<workItem from="1756879449291" duration="630000" />
|
||||||
|
<workItem from="1756905732385" duration="955000" />
|
||||||
|
<workItem from="1756953389550" duration="29000" />
|
||||||
|
<workItem from="1756995048032" duration="3919000" />
|
||||||
|
<workItem from="1757064153985" duration="1107000" />
|
||||||
|
<workItem from="1757076719875" duration="601000" />
|
||||||
|
<workItem from="1757219779961" duration="112000" />
|
||||||
|
<workItem from="1757386288260" duration="3634000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
@@ -194,7 +230,7 @@
|
|||||||
<entry key="branch">
|
<entry key="branch">
|
||||||
<value>
|
<value>
|
||||||
<list>
|
<list>
|
||||||
<option value="main" />
|
<option value="dev-task" />
|
||||||
</list>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</entry>
|
</entry>
|
||||||
|
|||||||
@@ -2,4 +2,6 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5df2accb46d040ccbbbe8331bf4d24b61daa00_003Fdf_003F93debd37_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AControllerBase_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F5df2accb46d040ccbbbe8331bf4d24b61daa00_003Fdf_003F93debd37_003FControllerBase_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKey_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff09ccaeb94c34c2299acd3efee0facee1a400_003F81_003F137b58b4_003FKey_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AKey_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff09ccaeb94c34c2299acd3efee0facee1a400_003F81_003F137b58b4_003FKey_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMonitor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003F4c_003F4b962087_003FMonitor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMonitor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003F4c_003F4b962087_003FMonitor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceProvider_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F011a191356a243438f987de3ec3d6c6230800_003F04_003F8419ff35_003FServiceProvider_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F457530be4752476295767457c3639889d1a000_003Fd0_003F3b166e9e_003FString_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AxxHash128_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7598e47d5cdf4107ba88f8220720fdc89000_003Fa6_003F79d67871_003FxxHash128_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AxxHash128_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F7598e47d5cdf4107ba88f8220720fdc89000_003Fa6_003F79d67871_003FxxHash128_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
using Abyss.Components.Services;
|
using Abyss.Components.Services;
|
||||||
using Abyss.Components.Static;
|
using Abyss.Components.Static;
|
||||||
|
using Abyss.Components.Tools;
|
||||||
|
using Abyss.Model;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Abyss.Components.Controllers.Media;
|
namespace Abyss.Components.Controllers.Media;
|
||||||
@@ -28,7 +31,7 @@ public class ImageController(ILogger<ImageController> logger, ResourceService rs
|
|||||||
if(r == null)
|
if(r == null)
|
||||||
return StatusCode(401, new { message = "Unauthorized" });
|
return StatusCode(401, new { message = "Unauthorized" });
|
||||||
|
|
||||||
return Ok(r);
|
return Ok(r.NaturalSort(x => x));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@@ -42,6 +45,27 @@ public class ImageController(ILogger<ImageController> logger, ResourceService rs
|
|||||||
|
|
||||||
return Ok(await System.IO.File.ReadAllTextAsync(d));
|
return Ok(await System.IO.File.ReadAllTextAsync(d));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}/bookmark")]
|
||||||
|
public async Task<IActionResult> 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<Comic>(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}")]
|
[HttpGet("{id}/{file}")]
|
||||||
public async Task<IActionResult> Get(string id, string file, string token)
|
public async Task<IActionResult> Get(string id, string file, string token)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Abyss.Components.Services;
|
using Abyss.Components.Services;
|
||||||
using Abyss.Components.Static;
|
using Abyss.Components.Static;
|
||||||
|
using Abyss.Components.Tools;
|
||||||
|
using Abyss.Model;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Abyss.Components.Controllers.Media;
|
namespace Abyss.Components.Controllers.Media;
|
||||||
|
|
||||||
@@ -38,10 +41,24 @@ public class VideoController(ILogger<VideoController> logger, ResourceService rs
|
|||||||
var d = Helpers.SafePathCombine(VideoFolder, klass);
|
var d = Helpers.SafePathCombine(VideoFolder, klass);
|
||||||
if (d == null) return StatusCode(403, new { message = "403 Denied" });
|
if (d == null) return StatusCode(403, new { message = "403 Denied" });
|
||||||
var r = await rs.Query(d, token, Ip);
|
var r = await rs.Query(d, token, Ip);
|
||||||
|
|
||||||
if (r == null) return StatusCode(401, new { message = "Unauthorized" });
|
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<Video>(x)).ToArray();
|
||||||
|
|
||||||
|
|
||||||
return Ok(r);
|
return Ok(sv.Zip(r, (x, y) => (x, y)).NaturalSort(x => x.x.name).Select(x => x.y).ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{klass}/{id}")]
|
[HttpGet("{klass}/{id}")]
|
||||||
@@ -93,16 +110,5 @@ public class VideoController(ILogger<VideoController> logger, ResourceService rs
|
|||||||
return PhysicalFile(d, "video/mp4", enableRangeProcessing: true);
|
return PhysicalFile(d, "video/mp4", enableRangeProcessing: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{klass}/{id}/nv")]
|
|
||||||
public async Task<IActionResult> 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";
|
private string Ip => HttpContext.Connection.RemoteIpAddress?.ToString() ?? "127.0.0.1";
|
||||||
}
|
}
|
||||||
60
Abyss/Components/Controllers/Task/TaskController.cs
Normal file
60
Abyss/Components/Controllers/Task/TaskController.cs
Normal file
@@ -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<TaskController> logger, ConfigureService config, TaskService taskService) : Controller
|
||||||
|
{
|
||||||
|
public readonly string TaskFolder = Path.Combine(config.MediaRoot, "Tasks");
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> GetTask(string id)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch("{id}")]
|
||||||
|
public async Task<IActionResult> PutChip(string id)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}")]
|
||||||
|
public async Task<IActionResult> VerifyChip(string id)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<IActionResult> DeleteTask(string id)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Ip => HttpContext.Connection.RemoteIpAddress?.ToString() ?? "127.0.0.1";
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ namespace Abyss.Components.Services;
|
|||||||
public class ConfigureService
|
public class ConfigureService
|
||||||
{
|
{
|
||||||
public string MediaRoot { get; set; } = Environment.GetEnvironmentVariable("MEDIA_ROOT") ?? "/opt";
|
public string MediaRoot { get; set; } = Environment.GetEnvironmentVariable("MEDIA_ROOT") ?? "/opt";
|
||||||
|
public string DebugMode { get; set; } = Environment.GetEnvironmentVariable("DEBUG_MODE") ?? "Production";
|
||||||
public string Version { get; } = "Alpha v0.1";
|
public string Version { get; } = "Alpha v0.1";
|
||||||
public string UserDatabase { get; set; } = "user.db";
|
public string UserDatabase { get; set; } = "user.db";
|
||||||
public string RaDatabase { get; set; } = "ra.db";
|
public string RaDatabase { get; set; } = "ra.db";
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public class ResourceService
|
|||||||
private readonly SQLiteAsyncConnection _database;
|
private readonly SQLiteAsyncConnection _database;
|
||||||
|
|
||||||
private static readonly Regex PermissionRegex =
|
private static readonly Regex PermissionRegex =
|
||||||
new Regex(@"^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled);
|
new(@"^([r-][w-]),([r-][w-]),([r-][w-])$", RegexOptions.Compiled);
|
||||||
|
|
||||||
public ResourceService(ILogger<ResourceService> logger, ConfigureService config, IMemoryCache cache,
|
public ResourceService(ILogger<ResourceService> logger, ConfigureService config, IMemoryCache cache,
|
||||||
UserService user)
|
UserService user)
|
||||||
@@ -39,10 +39,15 @@ public class ResourceService
|
|||||||
|
|
||||||
_database = new SQLiteAsyncConnection(config.RaDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
|
_database = new SQLiteAsyncConnection(config.RaDatabase, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create);
|
||||||
_database.CreateTableAsync<ResourceAttribute>().Wait();
|
_database.CreateTableAsync<ResourceAttribute>().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
|
// Create UID only for resources, without considering advanced hash security such as adding salt
|
||||||
private string Uid(string path)
|
private static string Uid(string path)
|
||||||
{
|
{
|
||||||
var b = Encoding.UTF8.GetBytes(path);
|
var b = Encoding.UTF8.GetBytes(path);
|
||||||
var r = XxHash128.Hash(b, 0x11451419);
|
var r = XxHash128.Hash(b, 0x11451419);
|
||||||
@@ -164,8 +169,16 @@ public class ResourceService
|
|||||||
return await Valid(path, token, OperationType.Read, ip);
|
return await Valid(path, token, OperationType.Read, ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Update(string path, string token, string ip)
|
||||||
|
{
|
||||||
|
return await Valid(path, token, OperationType.Write, ip);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> Initialize(string path, string token, string username, string ip)
|
public async Task<bool> Initialize(string path, string token, string username, string ip)
|
||||||
{
|
{
|
||||||
|
// TODO: Use a more elegant Debug mode
|
||||||
|
if (_config.DebugMode == "Debug")
|
||||||
|
goto debug;
|
||||||
// 1. Authorization: Verify the operation is performed by 'root'
|
// 1. Authorization: Verify the operation is performed by 'root'
|
||||||
var requester = _user.Validate(token, ip);
|
var requester = _user.Validate(token, ip);
|
||||||
if (requester != "root")
|
if (requester != "root")
|
||||||
@@ -173,7 +186,7 @@ public class ResourceService
|
|||||||
_logger.LogWarning($"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to initialize resources.");
|
_logger.LogWarning($"Permission denied: Non-root user '{requester ?? "unknown"}' attempted to initialize resources.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
debug:
|
||||||
// 2. Validation: Ensure the target path and owner are valid
|
// 2. Validation: Ensure the target path and owner are valid
|
||||||
if (!Directory.Exists(path))
|
if (!Directory.Exists(path))
|
||||||
{
|
{
|
||||||
@@ -335,4 +348,34 @@ public class ResourceService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
247
Abyss/Components/Services/TaskService.cs
Normal file
247
Abyss/Components/Services/TaskService.cs
Normal file
@@ -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<TaskService> 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<List<String>> Query(string token, string ip)
|
||||||
|
{
|
||||||
|
var r = await rs.Query(TaskFolder, token, ip);
|
||||||
|
var u = user.Validate(token, ip);
|
||||||
|
|
||||||
|
List<string> s = new();
|
||||||
|
foreach (var i in r ?? [])
|
||||||
|
{
|
||||||
|
var p = Helpers.SafePathCombine(TaskFolder, [i, "task.json"]);
|
||||||
|
var c = JsonConvert.DeserializeObject<Model.Task>(await System.IO.File.ReadAllTextAsync(p ?? ""));
|
||||||
|
|
||||||
|
if(c?.Owner == u) s.Add(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TaskCreationResponse?> 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<TaskCreationResponse?> 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<Chip>();
|
||||||
|
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<TaskCreationResponse?> CreateImageTask(string token, string ip, TaskCreation creation)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint GenerateUniqueId(string parentDirectory)
|
||||||
|
{
|
||||||
|
string[] directories = Directory.GetDirectories(parentDirectory);
|
||||||
|
HashSet<uint> existingIds = new HashSet<uint>();
|
||||||
|
|
||||||
|
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<ChipDesc> SliceFile(ulong fileSize)
|
||||||
|
{
|
||||||
|
var tasks = new List<ChipDesc>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ using Abyss.Model;
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using NSec.Cryptography;
|
using NSec.Cryptography;
|
||||||
using SQLite;
|
using SQLite;
|
||||||
|
using Task = System.Threading.Tasks.Task;
|
||||||
|
|
||||||
namespace Abyss.Components.Services;
|
namespace Abyss.Components.Services;
|
||||||
|
|
||||||
@@ -27,6 +28,10 @@ public class UserService
|
|||||||
_database.CreateTableAsync<User>().Wait();
|
_database.CreateTableAsync<User>().Wait();
|
||||||
var rootUser = _database.Table<User>().Where(x => x.Name == "root").FirstOrDefaultAsync().Result;
|
var rootUser = _database.Table<User>().Where(x => x.Name == "root").FirstOrDefaultAsync().Result;
|
||||||
|
|
||||||
|
if (_config.DebugMode == "Debug")
|
||||||
|
_cache.Set("root", $"root@127.0.0.1", DateTimeOffset.Now.AddHours(1));
|
||||||
|
// Test token, can only be used locally. Will be destroyed in one hour.
|
||||||
|
|
||||||
if (rootUser == null)
|
if (rootUser == null)
|
||||||
{
|
{
|
||||||
var key = GenerateKeyPair();
|
var key = GenerateKeyPair();
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ public static class StringArrayExtensions
|
|||||||
{
|
{
|
||||||
public static string[] SortLikeWindows(this string[] array)
|
public static string[] SortLikeWindows(this string[] array)
|
||||||
{
|
{
|
||||||
if (array == null) return null;
|
|
||||||
if (array.Length == 0) return array;
|
if (array.Length == 0) return array;
|
||||||
|
|
||||||
Array.Sort(array, new WindowsFileNameComparer());
|
Array.Sort(array, new WindowsFileNameComparer());
|
||||||
@@ -76,7 +75,6 @@ public static class StringArrayExtensions
|
|||||||
|
|
||||||
public static string[] SortLikeWindowsDescending(this string[] array)
|
public static string[] SortLikeWindowsDescending(this string[] array)
|
||||||
{
|
{
|
||||||
if (array == null) return null;
|
|
||||||
if (array.Length == 0) return array;
|
if (array.Length == 0) return array;
|
||||||
|
|
||||||
Array.Sort(array, new WindowsFileNameComparerDescending());
|
Array.Sort(array, new WindowsFileNameComparerDescending());
|
||||||
@@ -85,14 +83,14 @@ public static class StringArrayExtensions
|
|||||||
|
|
||||||
public static void SortLikeWindowsInPlace(this string[] array)
|
public static void SortLikeWindowsInPlace(this string[] array)
|
||||||
{
|
{
|
||||||
if (array == null || array.Length == 0) return;
|
if (array.Length == 0) return;
|
||||||
|
|
||||||
Array.Sort(array, new WindowsFileNameComparer());
|
Array.Sort(array, new WindowsFileNameComparer());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SortLikeWindowsDescendingInPlace(this string[] array)
|
public static void SortLikeWindowsDescendingInPlace(this string[] array)
|
||||||
{
|
{
|
||||||
if (array == null || array.Length == 0) return;
|
if (array.Length == 0) return;
|
||||||
|
|
||||||
Array.Sort(array, new WindowsFileNameComparerDescending());
|
Array.Sort(array, new WindowsFileNameComparerDescending());
|
||||||
}
|
}
|
||||||
@@ -100,8 +98,8 @@ public static class StringArrayExtensions
|
|||||||
|
|
||||||
public class WindowsFileNameComparer : IComparer<string>
|
public class WindowsFileNameComparer : IComparer<string>
|
||||||
{
|
{
|
||||||
private static readonly Regex _regex = new Regex(@"(\d+|\D+)", RegexOptions.Compiled);
|
private static readonly Regex Regex = new Regex(@"(\d+|\D+)", RegexOptions.Compiled);
|
||||||
private static readonly CompareInfo _compareInfo = CultureInfo.InvariantCulture.CompareInfo;
|
private static readonly CompareInfo CompareInfo = CultureInfo.InvariantCulture.CompareInfo;
|
||||||
|
|
||||||
public int Compare(string? x, string? y)
|
public int Compare(string? x, string? y)
|
||||||
{
|
{
|
||||||
@@ -110,8 +108,8 @@ public class WindowsFileNameComparer : IComparer<string>
|
|||||||
if (y == null) return 1;
|
if (y == null) return 1;
|
||||||
if (ReferenceEquals(x, y)) return 0;
|
if (ReferenceEquals(x, y)) return 0;
|
||||||
|
|
||||||
var partsX = _regex.Matches(x);
|
var partsX = Regex.Matches(x);
|
||||||
var partsY = _regex.Matches(y);
|
var partsY = Regex.Matches(y);
|
||||||
|
|
||||||
int minLength = Math.Min(partsX.Count, partsY.Count);
|
int minLength = Math.Min(partsX.Count, partsY.Count);
|
||||||
|
|
||||||
@@ -130,7 +128,7 @@ public class WindowsFileNameComparer : IComparer<string>
|
|||||||
int comparison;
|
int comparison;
|
||||||
if (ContainsChinese(partX) || ContainsChinese(partY))
|
if (ContainsChinese(partX) || ContainsChinese(partY))
|
||||||
{
|
{
|
||||||
comparison = _compareInfo.Compare(partX, partY, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace);
|
comparison = CompareInfo.Compare(partX, partY, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -161,20 +159,20 @@ public class WindowsFileNameComparer : IComparer<string>
|
|||||||
|
|
||||||
public class WindowsFileNameComparerDescending : IComparer<string>
|
public class WindowsFileNameComparerDescending : IComparer<string>
|
||||||
{
|
{
|
||||||
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
|
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)
|
public static int Compare(string x, string y)
|
||||||
{
|
{
|
||||||
return _comparer.Compare(x, y);
|
return Comparer.Compare(x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
60
Abyss/Components/Tools/NaturalStringComparer.cs
Normal file
60
Abyss/Components/Tools/NaturalStringComparer.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
namespace Abyss.Components.Tools;
|
||||||
|
|
||||||
|
public static class NaturalSortExtensions
|
||||||
|
{
|
||||||
|
public static IOrderedEnumerable<T> NaturalSort<T>(this IEnumerable<T> source, Func<T, string> keySelector)
|
||||||
|
{
|
||||||
|
return source.OrderBy(keySelector, new NaturalStringComparer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NaturalStringComparer : IComparer<string>
|
||||||
|
{
|
||||||
|
public int Compare(string? a, string? b)
|
||||||
|
{
|
||||||
|
if (a == null && b == null) return 0;
|
||||||
|
if (a == null) return -1;
|
||||||
|
if (b == null) return 1;
|
||||||
|
|
||||||
|
int aIndex = 0;
|
||||||
|
int bIndex = 0;
|
||||||
|
|
||||||
|
while (aIndex < a.Length && bIndex < b.Length)
|
||||||
|
{
|
||||||
|
if (char.IsDigit(a[aIndex]) && char.IsDigit(b[bIndex]))
|
||||||
|
{
|
||||||
|
long aNum = 0;
|
||||||
|
long bNum = 0;
|
||||||
|
|
||||||
|
while (aIndex < a.Length && char.IsDigit(a[aIndex]))
|
||||||
|
{
|
||||||
|
aNum = aNum * 10 + (a[aIndex] - '0');
|
||||||
|
aIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (bIndex < b.Length && char.IsDigit(b[bIndex]))
|
||||||
|
{
|
||||||
|
bNum = bNum * 10 + (b[bIndex] - '0');
|
||||||
|
bIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aNum != bNum)
|
||||||
|
{
|
||||||
|
return aNum.CompareTo(bNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int charCompare = a[aIndex].CompareTo(b[bIndex]);
|
||||||
|
if (charCompare != 0)
|
||||||
|
{
|
||||||
|
return charCompare;
|
||||||
|
}
|
||||||
|
aIndex++;
|
||||||
|
bIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.Length.CompareTo(b.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Abyss/Model/Bookmark.cs
Normal file
11
Abyss/Model/Bookmark.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Abyss.Model;
|
||||||
|
|
||||||
|
public class Bookmark
|
||||||
|
{
|
||||||
|
[JsonProperty("page")]
|
||||||
|
public string Page { get; set; } = "";
|
||||||
|
[JsonProperty("name")]
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
}
|
||||||
17
Abyss/Model/Chip.cs
Normal file
17
Abyss/Model/Chip.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
17
Abyss/Model/Comic.cs
Normal file
17
Abyss/Model/Comic.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Abyss.Model;
|
||||||
|
|
||||||
|
public class Comic
|
||||||
|
{
|
||||||
|
[JsonProperty("comic_name")]
|
||||||
|
public string ComicName { get; set; } = "";
|
||||||
|
[JsonProperty("page_count")]
|
||||||
|
public int PageCount { get; set; }
|
||||||
|
[JsonProperty("bookmarks")]
|
||||||
|
public List<Bookmark> Bookmarks { get; set; } = new();
|
||||||
|
[JsonProperty("author")]
|
||||||
|
public string Author { get; set; } = "";
|
||||||
|
[JsonProperty("list")]
|
||||||
|
public List<string> List { get; set; } = new();
|
||||||
|
}
|
||||||
7
Abyss/Model/Comment.cs
Normal file
7
Abyss/Model/Comment.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Abyss.Model;
|
||||||
|
|
||||||
|
public class Comment
|
||||||
|
{
|
||||||
|
public string username = "";
|
||||||
|
public string text = "";
|
||||||
|
}
|
||||||
16
Abyss/Model/Task.cs
Normal file
16
Abyss/Model/Task.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
23
Abyss/Model/TaskCreation.cs
Normal file
23
Abyss/Model/TaskCreation.cs
Normal file
@@ -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<ChipDesc> Chips = new();
|
||||||
|
}
|
||||||
12
Abyss/Model/Video.cs
Normal file
12
Abyss/Model/Video.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Abyss.Model;
|
||||||
|
|
||||||
|
public class Video
|
||||||
|
{
|
||||||
|
public string name;
|
||||||
|
public ulong duration;
|
||||||
|
public List<string> gallery = new();
|
||||||
|
public List<Comment> comment = new();
|
||||||
|
public bool star;
|
||||||
|
public uint like;
|
||||||
|
public string author;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Threading.RateLimiting;
|
using System.Threading.RateLimiting;
|
||||||
|
using Abyss.Components.Controllers.Task;
|
||||||
using Abyss.Components.Services;
|
using Abyss.Components.Services;
|
||||||
using Microsoft.AspNetCore.RateLimiting;
|
using Microsoft.AspNetCore.RateLimiting;
|
||||||
|
|
||||||
@@ -17,6 +18,8 @@ public class Program
|
|||||||
builder.Services.AddSingleton<ConfigureService>();
|
builder.Services.AddSingleton<ConfigureService>();
|
||||||
builder.Services.AddSingleton<UserService>();
|
builder.Services.AddSingleton<UserService>();
|
||||||
builder.Services.AddSingleton<ResourceService>();
|
builder.Services.AddSingleton<ResourceService>();
|
||||||
|
builder.Services.AddSingleton<TaskController>();
|
||||||
|
builder.Services.AddSingleton<TaskService>();
|
||||||
|
|
||||||
builder.Services.AddRateLimiter(options =>
|
builder.Services.AddRateLimiter(options =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
"launchBrowser": false,
|
"launchBrowser": false,
|
||||||
"applicationUrl": "https://localhost:7013;http://localhost:5198",
|
"applicationUrl": "https://localhost:7013;http://localhost:5198",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||||
|
"MEDIA_ROOT" : "/storage",
|
||||||
|
"DEBUG_MODE" : "Debug"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
318
Abyss/Toolkits/image-creator.py
Normal file
318
Abyss/Toolkits/image-creator.py
Normal file
@@ -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(
|
||||||
|
"<Configure>",
|
||||||
|
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('<MouseWheel>', self._on_mousewheel)
|
||||||
|
self.top.bind('<Button-4>', self._on_mousewheel)
|
||||||
|
self.top.bind('<Button-5>', self._on_mousewheel)
|
||||||
|
# Bind zoom keys
|
||||||
|
self.top.bind('<KeyPress-plus>', self._zoom_in)
|
||||||
|
self.top.bind('<KeyPress-equal>', self._zoom_in) # For keyboards where + is shift+=
|
||||||
|
self.top.bind('<KeyPress-minus>', 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("<Button-1>", lambda e, f=filename: self.add_bookmark(f))
|
||||||
|
img_label.bind("<Button-1>", lambda e, f=filename: self.add_bookmark(f))
|
||||||
|
text_label.bind("<Button-1>", 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 <directory_path>")
|
||||||
|
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()
|
||||||
78
Abyss/Toolkits/image-sum.py
Normal file
78
Abyss/Toolkits/image-sum.py
Normal file
@@ -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()
|
||||||
72
Abyss/Toolkits/update-tags.py
Normal file
72
Abyss/Toolkits/update-tags.py
Normal file
@@ -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()
|
||||||
206
Abyss/Toolkits/update-video.py
Normal file
206
Abyss/Toolkits/update-video.py
Normal file
@@ -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 <command> [arguments]")
|
||||||
|
print("Commands:")
|
||||||
|
print(" -u <path> Update the summary.json in the specified path.")
|
||||||
|
print(" -a <video_file> <path> 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 <path>")
|
||||||
|
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 <video_file> <path>")
|
||||||
|
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 <command> [arguments]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user