cauto 10 місяців тому
коміт
aec86adaaa

+ 21 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,21 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredPackages">
+        <value>
+          <list size="1">
+            <item index="0" class="java.lang.String" itemvalue="typing_extensions" />
+          </list>
+        </value>
+      </option>
+    </inspection_tool>
+    <inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <option name="ignoredErrors">
+        <list>
+          <option value="N806" />
+        </list>
+      </option>
+    </inspection_tool>
+  </profile>
+</component>

+ 6 - 0
.idea/inspectionProfiles/profiles_settings.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>

+ 7 - 0
.idea/misc.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Black">
+    <option name="sdkName" value="Python 3.11 (pythonProject)" />
+  </component>
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (pythonProject)" project-jdk-type="Python SDK" />
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/pythonProject.iml" filepath="$PROJECT_DIR$/.idea/pythonProject.iml" />
+    </modules>
+  </component>
+</project>

+ 10 - 0
.idea/pythonProject.iml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.venv" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 212 - 0
.idea/workspace.xml

@@ -0,0 +1,212 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="AutoImportSettings">
+    <option name="autoReloadType" value="SELECTIVE" />
+  </component>
+  <component name="ChangeListManager">
+    <list default="true" id="e3b9eb38-ee4f-4b25-9417-dca75aa02bfe" name="Changes" comment="">
+      <change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/.idea/pythonProject.iml" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/api/__init__.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/api/alist.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/api/api_clients.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/api/aria2.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/api/nas_tools.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/app.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/config/__init__.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/config/config.ini" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/config/config_manager.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/pikpak.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/services/__init__.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/services/download_manager.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/services/download_service.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/services/nas_sync_service.py" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/templates/files.html" afterDir="false" />
+      <change afterPath="$PROJECT_DIR$/templates/index.html" afterDir="false" />
+    </list>
+    <option name="SHOW_DIALOG" value="false" />
+    <option name="HIGHLIGHT_CONFLICTS" value="true" />
+    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+    <option name="LAST_RESOLUTION" value="IGNORE" />
+  </component>
+  <component name="FileTemplateManagerImpl">
+    <option name="RECENT_TEMPLATES">
+      <list>
+        <option value="Python Script" />
+        <option value="HTML File" />
+      </list>
+    </option>
+  </component>
+  <component name="Git.Settings">
+    <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
+  </component>
+  <component name="ProblemsViewState">
+    <option name="selectedTabId" value="CurrentFile" />
+  </component>
+  <component name="ProjectColorInfo">{
+  &quot;associatedIndex&quot;: 5
+}</component>
+  <component name="ProjectId" id="2fBgXhhpWutLelfura7EYLSeq6R" />
+  <component name="ProjectLevelVcsManager" settingsEditedManually="true" />
+  <component name="ProjectViewState">
+    <option name="hideEmptyMiddlePackages" value="true" />
+    <option name="showLibraryContents" value="true" />
+  </component>
+  <component name="PropertiesComponent"><![CDATA[{
+  "keyToString": {
+    "DefaultHtmlFileTemplate": "HTML File",
+    "Python.app.executor": "Debug",
+    "Python.main.executor": "Debug",
+    "Python.pikpak.executor": "Debug",
+    "RunOnceActivity.OpenProjectViewOnStart": "true",
+    "RunOnceActivity.ShowReadmeOnStart": "true",
+    "git-widget-placeholder": "master",
+    "last_opened_file_path": "/Users/cauto/Desktop/work/python_work/alist_task/pythonProject/config",
+    "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",
+    "vue.rearranger.settings.migration": "true"
+  }
+}]]></component>
+  <component name="RecentsManager">
+    <key name="CopyFile.RECENT_KEYS">
+      <recent name="$PROJECT_DIR$/config" />
+      <recent name="$PROJECT_DIR$/services" />
+    </key>
+    <key name="MoveFile.RECENT_KEYS">
+      <recent name="$PROJECT_DIR$/services" />
+      <recent name="$PROJECT_DIR$/config" />
+      <recent name="$PROJECT_DIR$/api" />
+    </key>
+  </component>
+  <component name="RunManager" selected="Python.app">
+    <configuration name="app" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
+      <module name="pythonProject" />
+      <option name="ENV_FILES" value="" />
+      <option name="INTERPRETER_OPTIONS" value="" />
+      <option name="PARENT_ENVS" value="true" />
+      <envs>
+        <env name="PYTHONUNBUFFERED" value="1" />
+      </envs>
+      <option name="SDK_HOME" value="" />
+      <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
+      <option name="IS_MODULE_SDK" value="true" />
+      <option name="ADD_CONTENT_ROOTS" value="true" />
+      <option name="ADD_SOURCE_ROOTS" value="true" />
+      <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
+      <option name="SCRIPT_NAME" value="$PROJECT_DIR$/app.py" />
+      <option name="PARAMETERS" value="" />
+      <option name="SHOW_COMMAND_LINE" value="false" />
+      <option name="EMULATE_TERMINAL" value="false" />
+      <option name="MODULE_MODE" value="false" />
+      <option name="REDIRECT_INPUT" value="false" />
+      <option name="INPUT_FILE" value="" />
+      <method v="2" />
+    </configuration>
+    <configuration name="main" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
+      <module name="pythonProject" />
+      <option name="ENV_FILES" value="" />
+      <option name="INTERPRETER_OPTIONS" value="" />
+      <option name="PARENT_ENVS" value="true" />
+      <envs>
+        <env name="PYTHONUNBUFFERED" value="1" />
+      </envs>
+      <option name="SDK_HOME" value="" />
+      <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
+      <option name="IS_MODULE_SDK" value="true" />
+      <option name="ADD_CONTENT_ROOTS" value="true" />
+      <option name="ADD_SOURCE_ROOTS" value="true" />
+      <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
+      <option name="SCRIPT_NAME" value="$PROJECT_DIR$/main.py" />
+      <option name="PARAMETERS" value="" />
+      <option name="SHOW_COMMAND_LINE" value="false" />
+      <option name="EMULATE_TERMINAL" value="false" />
+      <option name="MODULE_MODE" value="false" />
+      <option name="REDIRECT_INPUT" value="false" />
+      <option name="INPUT_FILE" value="" />
+      <method v="2" />
+    </configuration>
+    <configuration name="pikpak" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
+      <module name="pythonProject" />
+      <option name="ENV_FILES" value="" />
+      <option name="INTERPRETER_OPTIONS" value="" />
+      <option name="PARENT_ENVS" value="true" />
+      <envs>
+        <env name="PYTHONUNBUFFERED" value="1" />
+      </envs>
+      <option name="SDK_HOME" value="" />
+      <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
+      <option name="IS_MODULE_SDK" value="true" />
+      <option name="ADD_CONTENT_ROOTS" value="true" />
+      <option name="ADD_SOURCE_ROOTS" value="true" />
+      <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
+      <option name="SCRIPT_NAME" value="$PROJECT_DIR$/pikpak.py" />
+      <option name="PARAMETERS" value="" />
+      <option name="SHOW_COMMAND_LINE" value="false" />
+      <option name="EMULATE_TERMINAL" value="false" />
+      <option name="MODULE_MODE" value="false" />
+      <option name="REDIRECT_INPUT" value="false" />
+      <option name="INPUT_FILE" value="" />
+      <method v="2" />
+    </configuration>
+    <recent_temporary>
+      <list>
+        <item itemvalue="Python.app" />
+        <item itemvalue="Python.pikpak" />
+      </list>
+    </recent_temporary>
+  </component>
+  <component name="SharedIndexes">
+    <attachedChunks>
+      <set>
+        <option value="bundled-python-sdk-5a2391486177-2887949eec09-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-233.13763.11" />
+      </set>
+    </attachedChunks>
+  </component>
+  <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
+  <component name="TaskManager">
+    <task active="true" id="Default" summary="Default task">
+      <changelist id="e3b9eb38-ee4f-4b25-9417-dca75aa02bfe" name="Changes" comment="" />
+      <created>1713279927643</created>
+      <option name="number" value="Default" />
+      <option name="presentableId" value="Default" />
+      <updated>1713279927643</updated>
+      <workItem from="1713279929305" duration="6625000" />
+      <workItem from="1713360335152" duration="1834000" />
+      <workItem from="1714757043688" duration="3901000" />
+    </task>
+    <servers />
+  </component>
+  <component name="TypeScriptGeneratedFilesManager">
+    <option name="version" value="3" />
+  </component>
+  <component name="XDebuggerManager">
+    <breakpoint-manager>
+      <breakpoints>
+        <line-breakpoint enabled="true" suspend="THREAD" type="python-line">
+          <url>file://$PROJECT_DIR$/api/alist.py</url>
+          <line>177</line>
+          <option name="timeStamp" value="6" />
+        </line-breakpoint>
+        <line-breakpoint enabled="true" suspend="THREAD" type="python-line">
+          <url>file://$PROJECT_DIR$/api/alist.py</url>
+          <line>180</line>
+          <option name="timeStamp" value="7" />
+        </line-breakpoint>
+      </breakpoints>
+    </breakpoint-manager>
+  </component>
+  <component name="com.intellij.coverage.CoverageDataManagerImpl">
+    <SUITE FILE_PATH="coverage/pythonProject$pikpak.coverage" NAME="pikpak Coverage Results" MODIFIED="1713361137219" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
+    <SUITE FILE_PATH="coverage/pythonProject$app.coverage" NAME="app Coverage Results" MODIFIED="1714760950945" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
+    <SUITE FILE_PATH="coverage/pythonProject$main.coverage" NAME="main Coverage Results" MODIFIED="1713282932589" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="true" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
+  </component>
+</project>

+ 0 - 0
api/__init__.py


+ 281 - 0
api/alist.py

@@ -0,0 +1,281 @@
+import json
+import logging
+import os
+from enum import unique, Enum
+
+import requests
+
+
+class ActionType(Enum):
+    COPY = 1
+    MOVE = 2
+
+
+@unique
+class TaskType(Enum):
+    UPLOAD = 'upload'
+    COPY = 'copy'
+    ARIA2_DOWNLOAD = 'aria2_down'
+    ARIA2_TRANSFER = 'aria2_transfer'
+    QBITTORRENT_DOWNLOAD = 'qbit_down'
+    QBITTORRENT_TRANSFER = 'qbit_transfer'
+
+
+class AlistAPI:
+    def __init__(self, url, username, password):
+        self.url = url
+        self.headers = {
+            'UserAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
+                         'Chrome/87.0.4280.88 Safari/537.36',
+            'Content-Type': 'application/json'
+        }
+        # self.aria2_client = Client(rpc_url, secret=rpc_secret)
+        # self.aria2_api = API(self.aria2_client)
+
+    def login(self, username, password):
+        data = {
+            'username': username,
+            'password': password
+        }
+        response = requests.post(f'{self.url}/auth/login', data=json.dumps(data), headers=self.headers)
+        if response.status_code == 200:
+            token = response.json()
+            self.headers['Authorization'] = token['data']['token']
+        else:
+            raise Exception('Login failed')
+
+    def get_directory(self, path="", password="", page=1, per_page=0, refresh=False):
+        payload = {
+            "path": path,
+            "password": password,
+            "page": page,
+            "per_page": per_page,
+            "refresh": refresh
+        }
+        response = requests.post(f'{self.url}/fs/dirs', data=json.dumps(payload), headers=self.headers)
+        return response.json()
+
+    def copy_file(self, src_dir, dst_dir, names):
+        payload = {
+            "src_dir": src_dir,
+            "dst_dir": dst_dir,
+            "names": names
+        }
+        response = requests.post(f'{self.url}/fs/copy', data=json.dumps(payload), headers=self.headers)
+        return response.json()
+
+    def get_completed_tasks(self, task_type):
+        """获取指定任务类型的已完成任务列表"""
+        if task_type == TaskType.UPLOAD:
+            api_endpoint = '/admin/task/upload/done'
+        elif task_type == TaskType.COPY:
+            api_endpoint = '/admin/task/copy/done'
+        elif task_type == TaskType.ARIA2_DOWNLOAD:
+            api_endpoint = '/admin/task/aria2_down/done'
+        elif task_type == TaskType.ARIA2_TRANSFER:
+            api_endpoint = '/admin/task/aria2_transfer/done'
+        elif task_type == TaskType.QBITTORRENT_DOWNLOAD:
+            api_endpoint = '/admin/task/qbit_down/done'
+        elif task_type == TaskType.QBITTORRENT_TRANSFER:
+            api_endpoint = '/admin/task/qbit_transfer/done'
+        else:
+            raise ValueError("Invalid task type")
+
+        response = requests.get(
+            f'{self.url}{api_endpoint}',
+            headers=self.headers
+        )
+        return response.json()
+
+    def copy_directory(self, src_path, dst_path):
+        file_list = self.list_directory(src_path)
+        if not file_list:
+            return
+        for file_info in file_list['data']['content']:
+            if file_info['is_dir']:
+                new_src_path = src_path + "/" + file_info['name']
+                new_dst_path = dst_path + "/" + file_info['name']
+                # new_src_path = os.path.join(src_path, file_info['name'])
+                # new_dst_path = os.path.join(dst_path, file_info['name'])
+                print(f"Copying directory: {new_src_path} to {new_dst_path}")
+                self.copy_directory(new_src_path, new_dst_path)
+            else:
+                file_name = file_info['name']
+                print(f"Copying file: {src_path}/{file_name} to {dst_path}/{file_name}")
+                # 这里原本是调用 self.copy_file,现在改为仅打印信息
+                self.copy_file(src_path, dst_path, [file_name])
+
+    def list_directory(self, path, password="", page=1, per_page=0, refresh=False):
+        payload = {
+            "path": path,
+            "password": password,
+            "page": page,
+            "per_page": per_page,
+            "refresh": refresh
+        }
+        response = requests.post(f'{self.url}/fs/list', data=json.dumps(payload), headers=self.headers)
+        if response.status_code == 200:
+            return response.json()
+        else:
+            logging.error(f"Failed to list directory: {response.text}")
+            return None  # 或者 return {} 以保持返回类型的一致性
+
+    def get_file_or_directory_info(self, path, password="", page=1, per_page=0, refresh=False):
+        payload = {
+            "path": path,
+            "password": password,
+            "page": page,
+            "per_page": per_page,
+            "refresh": refresh
+        }
+        response = requests.post(f"{self.url}/fs/get", data=json.dumps(payload), headers=self.headers)
+        if response.status_code == 200:
+            return response.json()
+        return None
+
+    def move_file(self, src_dir, dst_dir, names):
+        payload = json.dumps({
+            "src_dir": src_dir,
+            "dst_dir": dst_dir,
+            "names": names
+        })
+        response = requests.post(f"{self.url}/fs/move", data=payload, headers=self.headers)
+        return response.json()
+
+    def remove_files_or_folders(self, dir_path, names):
+        """删除指定目录下的文件或文件夹"""
+        payload = {
+            "dir": dir_path,
+            "names": names
+        }
+        response = requests.post(
+            f'{self.url}/fs/remove',
+            headers=self.headers,
+            json=payload
+        )
+        return response.json()
+
+    def remove_empty_directory(self, src_dir):
+        """删除空文件夹"""
+        payload = {
+            "src_dir": src_dir
+        }
+        response = requests.post(
+            f'{self.url}/fs/remove_empty_director',
+            headers=self.headers,
+            json=payload
+        )
+        return response.json()
+
+    def recursive_collect_contents(self,
+                                   remote_download_path,
+                                   home_download_path,
+                                   src_dir=None,
+                                   dest_path=None,
+                                   current_sub_path='',
+                                   parent_folder_name=''):
+        contents = []
+        file_list = self.list_directory(remote_download_path)
+        # file_info_json = json.dumps(file_list, indent=4)
+        # logging.info(f'file_info_json: {file_info_json}')
+        if file_list is None or 'data' not in file_list or 'total' not in file_list['data']:
+            logging.error("Directory listing failed or returned unexpected data")
+            return []
+        if file_list is None:
+            return []
+
+        if file_list['data']['total'] == 0:
+            return []
+
+        for file_info in file_list['data']['content']:
+            # 拼接完整的远程路径
+            full_path = os.path.join(remote_download_path, file_info['name']).replace('\\', '/')
+
+            # 初始化本地下载路径和复制/移动目的地路径
+            local_download_path = ''
+            new_dest_path = ''
+            new_src_dir = ''
+            # 根据条件构建本地下载路径和复制/移动目的地路径
+            if home_download_path is not None:
+                local_download_path = os.path.join(home_download_path, current_sub_path, file_info['name']).replace(
+                    '\\',
+                    '/')
+            if dest_path is not None:
+                new_dest_path = os.path.join(dest_path, current_sub_path).replace('\\', '/')
+
+            if src_dir is not None:
+                new_src_dir = os.path.join(src_dir, current_sub_path).replace('\\', '/')
+            parent_path = os.path.dirname(full_path)
+
+            folder_name = os.path.basename(parent_path)
+            item = {
+                'name': file_info['name'],
+                'folder_name': folder_name,
+                'parent_folder_name': parent_folder_name,
+                'is_dir': file_info['is_dir'],
+                'path': full_path,  # 存储完整的远程路径
+                'downloads_path': local_download_path,
+                'src_dir': new_src_dir,
+                'dst_dir': new_dest_path
+            }
+            contents.append(item)
+
+            if file_info['is_dir']:
+                # 更新子路径为当前文件夹的路径
+                new_sub_path = os.path.join(current_sub_path, file_info['name'])
+                sub_contents = self.recursive_collect_contents(full_path,
+                                                               home_download_path,
+                                                               src_dir,
+                                                               dest_path,
+                                                               new_sub_path,
+                                                               parent_folder_name=folder_name)
+                contents.extend(sub_contents)
+
+        return contents
+
+    def copy_files(self, local_json_path, is_debug=False):
+        """执行拷贝文件"""
+        # 读取本地 JSON 文件
+        with open(local_json_path, 'r', encoding='utf-8') as f:
+            directory_contents = json.load(f)
+
+        for item in directory_contents:
+
+            if not item['is_dir']:
+                file_name = item['name']
+                original_path = item['src_dir']  # 获取原始文件路径
+                des_path = item['dst_dir']  # 获取原始文件路径
+
+                if is_debug:
+                    logging.info(f"Debug mode: Copy {file_name}")
+                else:
+                    # 复制文件
+                    self.copy_file(original_path, des_path, [file_name])
+                    logging.info(
+                        f"Copied: {file_name} from {original_path} to {des_path}")
+
+    def save_directory_contents(self,
+                                remote_download_path,
+                                local_download_path,
+                                scy_copy_path,
+                                des_copy_path,
+                                parent_dir):
+        """获取远程和本地对应的目录结构,并保存为 JSON 文件"""
+        # 获取远程目录结构
+        remote_data = self.recursive_collect_contents(
+            remote_download_path,
+            local_download_path
+        )
+        remote_json = os.path.join(parent_dir, 'data', 'remote_data.json')
+        with open(remote_json, 'w', encoding='utf-8') as f:
+            json.dump(remote_data, f, indent=4)
+
+        # # 获取本地目录结构
+        # home_data = self.home_alist_api.recursive_collect_contents(
+        #     scy_copy_path, des_copy_path, scy_copy_path, des_copy_path
+        # )
+        # home_json_path = os.path.join(parent_dir, 'data', self.home_data)
+        # with open(home_json_path, 'w', encoding='utf-8') as f:
+        #     json.dump(home_data, f, indent=4)
+
+        return remote_data

+ 26 - 0
api/api_clients.py

@@ -0,0 +1,26 @@
+# api_clients.py
+from api.alist import AlistAPI
+from api.aria2 import Aria2API
+from api.nas_tools import NasToolsClient
+
+
+class APIManager:
+    def __init__(self, config):
+        self.config = config
+        self.alist_api = AlistAPI(
+            self.config.get_value('REMOTE_ALIST', 'API_URL'),
+            self.config.get_value('REMOTE_ALIST', 'USERNAME'),
+            self.config.get_value('REMOTE_ALIST', 'PASSWORD')
+        )
+        self.aria2_api = Aria2API(
+            self.config.get_value('ARIA2', 'RPC_URL'),
+            self.config.get_value('ARIA2', 'RPC_SECRET')
+        )
+        self.nas_tools_api = NasToolsClient(
+            self.config.get_value('NSTOOLS', 'URL'),
+            self.config.get_value('NSTOOLS', 'API_KEY')
+        )
+
+    def login_apis(self):
+        self.nas_tools_api.login("admin","password")  # Assuming the login method exists
+        self.alist_api.login( self.config.get_value('REMOTE_ALIST', 'USERNAME'), self.config.get_value('REMOTE_ALIST', 'PASSWORD'))  # Assuming the login method exists

+ 69 - 0
api/aria2.py

@@ -0,0 +1,69 @@
+import logging
+import threading
+import time
+from pyarr import RadarrAPI
+from aria2p import API, Client
+
+
+class Aria2API:
+    def __init__(self, url, secret):
+        self.aria2_client = Client(url, secret=secret)
+        self.aria2_api = API(self.aria2_client)
+        self.is_running = True
+        self.check_interval = 10
+        self.monitor_thread = None
+
+    def remove(self, download):
+        return self.aria2_api.remove(download)
+
+    def get_downloads(self):
+        # 返回所有下载信息的列表
+        return self.aria2_api.get_downloads()
+
+    def add_url(self, url, options=None):
+        """
+        将下载链接添加到 Aria2。
+        :param url: 下载链接
+        :param options: Aria2下载选项,例如下载目录
+        """
+        try:
+            # 添加下载链接到 Aria2
+            download = self.aria2_api.add_uris([url], options=options if options else {})
+            return download
+        except Exception as e:
+            print(f"Error adding URL to Aria2: {e}")
+            return None
+
+    def monitor_aria2_and_delete(self):
+        logging.info('Start remote Aria2 download is_complete monitoring')
+        while self.is_running:
+            # 获取 Aria2 当前的下载列表
+            downloads = self.aria2_api.get_downloads()
+            for download in downloads:
+                if download.is_complete:
+                    pass
+                    # movie_name = self.radar_client.get_all_movie_names()
+                    # for file_name in movie_name:
+                    #     # logging.info(f"Download completed: {file_name}")
+                    #     # 处理每个下载完成的文件
+                    #     # 调用方法并获取返回的电影 ID
+                    #     movie_id = self.radar_client.find_movie_id_by_filename(file_name)
+                    #
+                    #     # 打印结果
+                    #     if movie_id is not None:
+                    #         print(f"Found movie ID for '{file_name}': {movie_id}")
+                    #     else:
+                    #         print(f"No movie found for '{file_name}'")
+
+            time.sleep(self.check_interval)
+
+    def start_monitoring(self):
+        if not self.is_running:
+            self.is_running = True
+            self.monitor_thread = threading.Thread(target=self.monitor_aria2_and_delete)
+            self.monitor_thread.start()
+
+    def stop_monitoring(self):
+        self.is_running = False
+        if self.monitor_thread:
+            self.monitor_thread.join()

+ 122 - 0
api/nas_tools.py

@@ -0,0 +1,122 @@
+import requests
+import logging
+
+
+class NasToolsClient:
+    def __init__(self, base_url, api_key=None):
+        self.base_url = base_url
+        self.api_key = api_key
+        self.token = None
+
+    # def login(self, username, password):
+    #     """
+    #     登录以获取Token
+    #     """
+    #     url = f'{self.base_url}/api/v1/user/login'
+    #     data = {'username': username, 'password': password}
+    #     headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+    #     response = requests.post(url, data=data, headers=headers)
+    #     if response.status_code == 200:
+    #         self.token = response.json().get('token')
+    #         return self.token
+    #     else:
+    #         raise Exception("Login failed with status code: " + str(response.status_code))
+
+    def login(self, username, password):
+        """登录并获取令牌"""
+        url = f'{self.base_url}/api/v1/user/login'
+        data = {
+            "username": username,
+            "password": password
+        }
+        headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+        response = requests.post(url, data=data, headers=headers)
+        if response.status_code == 200:
+            self.token = response.json().get("data", {}).get("token")
+            logging.info(self.token)
+            logging.info(self.api_key)
+            return self.token
+        else:
+            raise Exception("Failed to login:", response.text)
+
+    def get(self, endpoint):
+        """执行带有令牌的 GET 请求"""
+        url = f"{self.base_url}{endpoint}"
+        headers = {
+            "Authorization": f"Bearer {self.token}" if self.token else f"ApiKey {self.api_key}"
+        }
+        response = requests.get(url, headers=headers)
+        if response.status_code == 200:
+            return response.json()
+        else:
+            raise Exception("Failed to get data:", response.text)
+
+    def sync_list(self):
+
+        """
+               同步目录
+               """
+        url = f'{self.base_url}/api/v1/sync/directory/list'
+        # {"Bearer Auth": {"type": "apiKey", "name": "Authorization", "in": "header"}}
+        headers = {
+            'accept': 'application/json',
+            'Content-Type': 'application/x-www-form-urlencoded',
+            "Authorization": f"{self.token}",
+        }
+        logging.info(f"headers: {headers}")
+        params = {
+            'apikey': self.api_key
+        }
+        response = requests.post(url, params=params, headers=headers)
+        logging.info(response.json())
+        return response.json()
+
+    def sync(self, sid):
+        """
+        同步目录 {cmd: "run_directory_sync", data: {sid: []}}
+        """
+        url = f'{self.base_url}/api/v1/sync/directory/run'
+        # {"Bearer Auth": {"type": "apiKey", "name": "Authorization", "in": "header"}}
+        headers = {
+            'accept': 'application/json',
+            'Content-Type': 'application/x-www-form-urlencoded',
+            "Authorization": f"{self.token}",
+        }
+        logging.info(f"headers: {headers}")
+        params = {
+            'apikey': self.api_key,
+            'sid': sid
+
+        }
+        response = requests.get(url, params=params, headers=headers)
+        logging.info(response.json())
+        return response.json()
+
+    def run_service(self, service_name):
+        """
+        运行指定的服务
+        """
+        url = f'{self.base_url}/api/v1/service/run'
+        # {"Bearer Auth": {"type": "apiKey", "name": "Authorization", "in": "header"}}
+        headers = {
+            'accept': 'application/json',
+            'Content-Type': 'application/x-www-form-urlencoded',
+            'security': self.api_key,
+            "Authorization": f"{self.token}",
+        }
+        logging.info(f"headers: {headers}")
+        payload = {'item': service_name}
+        response = requests.post(url, data=payload, headers=headers)
+        logging.info(response.json())
+        return response.json()
+# # 使用示例
+# base_url = "http://your-nas-tools-address/api/v1"
+# username = "your_username"
+# password = "your_password"
+# api_key = "your_api_key"
+#
+# client = NasToolsClient(base_url, api_key)
+# token = client.login(username, password)
+# print("Token:", token)
+#
+# # 现在可以使用 client.get 方法来调用其他 API

+ 58 - 0
app.py

@@ -0,0 +1,58 @@
+# app.py
+import logging
+import os
+
+from flask import Flask, render_template, request, redirect, url_for
+from config.config_manager import ConfigManager
+from api.api_clients import APIManager
+from services.download_manager import DownloadManager
+from services.download_service import DownloadService
+from services.nas_sync_service import NasSyncService
+
+app = Flask(__name__)
+# 获取当前脚本的绝对路径
+current_directory = os.path.dirname(os.path.abspath(__file__))
+# 构建 config.ini 文件的路径
+config_path = os.path.join(current_directory, 'config', 'config.ini')
+config = ConfigManager(config_path)
+api_manager = APIManager(config)
+
+download_manager = DownloadManager(config.get_value('ARIA2', 'DOCKER_DOWNLOAD_PATH'))
+download_service = DownloadService(api_manager, download_manager)
+nas_sync_service = NasSyncService(api_manager.nas_tools_api)
+
+
+@app.before_request
+def ensure_api_login():
+    api_manager.login_apis()
+
+
+@app.route('/files')
+def files():
+    file_list = download_service.fetch_directory_contents()
+    return render_template('files.html', file_list=file_list)
+
+
+@app.route('/download/<path:filepath>')
+def download(filepath):
+    # 这里添加触发下载的逻辑
+    return redirect(url_for('files'))
+
+
+@app.route('/', methods=['GET', 'POST'])
+def index():
+    message = ""
+    if request.method == 'POST':
+        if request.form.get('action') == 'sync':
+            nas_sync_service.sync_directories()
+            message = "Directories are being synchronized."
+        elif request.form.get('action') == 'download':
+            download_urls = download_service.fetch_and_download()
+            message = f"Downloading: {len(download_urls)} files."
+        return render_template('index.html', message=message)
+    return render_template('index.html')
+
+
+if __name__ == '__main__':
+    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+    app.run(debug=True, port=5000)

+ 0 - 0
config/__init__.py


+ 47 - 0
config/config.ini

@@ -0,0 +1,47 @@
+[COMMON]
+LOG_LEVEL = INFO
+TIMEOUT = 3600
+CHECK_INTERVAL = 10
+REMOTE_DATA=remote.json
+HOME_DATA=home.json
+
+[NSTOOLS]
+URL = http://192.168.88.249:3000
+API_KEY = vBgKbiM1N7at0TtacAcbVCFf82pydcxC
+
+
+[ARIA2]
+RPC_URL = http://192.168.88.31
+RPC_SECRET = 123456
+DOCKER_DOWNLOAD_PATH = /downloads
+DESTINATION_PATH = /home/downloads/aria2
+DES_COPY_PATH = /home/video/sync
+
+[HOME_ALIST]
+API_URL = http://192.168.88.29:5244/api
+WEB_URL = http://192.168.88.29
+CLIENT_ID = 4e34854d5d7390ef7801
+USERNAME = admin
+PASSWORD = nokidc123@#
+DOWNLOAD_PATH = /downloads/movie
+SCY_COPY_PATH = /downloads/movie
+DES_COPY_PATH = /media/sync/movie
+
+[REMOTE_ALIST]
+API_URL = http://209.141.34.199:5244/api
+WEB_URL = http://209.141.34.199:5244/
+CLIENT_ID = 4e34854d5d7390ef7801
+USERNAME = admin
+PASSWORD = nokidc123@#
+DOWNLOAD_PATH = /meida/moive
+
+[RADAR]
+URL = https://box.szfa.xyz/radarr
+API_KEY = df43851ad8c14c9aab980c5d03622e75
+
+
+[qbittorrent]
+URL = http://box.szfa.xyz:15173/
+USERNAME = alroyso
+PASSWORD = nokidc123@#
+

+ 12 - 0
config/config_manager.py

@@ -0,0 +1,12 @@
+# config_manager.py
+import configparser
+import os
+
+
+class ConfigManager:
+    def __init__(self, config_path):
+        self.config = configparser.ConfigParser()
+        self.config.read(config_path)
+
+    def get_value(self, section, option):
+        return self.config.get(section, option)

+ 114 - 0
main.py

@@ -0,0 +1,114 @@
+# This is a sample Python script.
+import configparser
+import json
+import logging
+import os
+import time
+
+from api.alist import AlistAPI
+from api.aria2 import Aria2API
+from api.nas_tools import NasToolsClient
+
+
+def is_downloaded(fileName, downloaded_files_path=""):
+    if not os.path.exists(downloaded_files_path):
+        return False
+    with open(downloaded_files_path, 'r', encoding='utf-8') as file:
+        downloaded_files = file.readlines()
+    downloaded_files = [line.strip() for line in downloaded_files]
+    return fileName in downloaded_files
+
+
+def record_download(filename, downloaded_files_path=""):
+    with open(downloaded_files_path, 'a', encoding='utf-8') as file:
+        file.write(filename + "\n")
+
+
+if __name__ == '__main__':
+    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+    # 获取当前脚本的绝对路径
+    current_directory = os.path.dirname(os.path.abspath(__file__))
+    # 构建 config.ini 文件的路径
+    config_path = os.path.join(current_directory, 'config', 'config.ini')
+
+    # 读取配置文件
+    config = configparser.ConfigParser()
+    config.read(config_path)
+    download_data_json = os.path.join(current_directory, 'data', 'download.txt')
+    # 获取当前脚本所在目录的上级目录
+    # parent_directory = os.path.dirname(current_directory)
+    base_url = config['NSTOOLS']['URL']
+    api_key = config['NSTOOLS']['API_KEY']
+    nas_tools_api = NasToolsClient(base_url, api_key)
+
+    alist_username = config['REMOTE_ALIST']['USERNAME']
+    alist_password = config['REMOTE_ALIST']['PASSWORD']
+    remote_alist_download_path = config['REMOTE_ALIST']['DOWNLOAD_PATH']
+    remote_alist_api = AlistAPI(config['REMOTE_ALIST']['API_URL'],
+                                config['REMOTE_ALIST']['USERNAME'],
+                                config['REMOTE_ALIST']['PASSWORD'])
+
+    home_alist_download_path = config['HOME_ALIST']['DOWNLOAD_PATH']
+    home_scy_alist_copy_path = config['HOME_ALIST']['SCY_COPY_PATH']
+    home_des_alist_copy_path = config['HOME_ALIST']['DES_COPY_PATH']
+    docker_download_path = config['ARIA2']['DOCKER_DOWNLOAD_PATH']
+
+    aria2_api = Aria2API(config['ARIA2']['RPC_URL'], config['ARIA2']['RPC_SECRET'])
+    destination_path = config['ARIA2']['DES_COPY_PATH']
+
+    download_url = []
+    filename = None
+    # while is_running:
+    row_download_path = None
+
+    remote_alist_api.login(alist_username, alist_password)
+    # 获取远程目录结构
+    remote_json_path = remote_alist_api.save_directory_contents(
+        remote_alist_download_path,
+        home_alist_download_path,
+        home_scy_alist_copy_path,
+        home_des_alist_copy_path,
+        current_directory)
+    for home_item in remote_json_path:
+        if not home_item['is_dir']:
+            if not is_downloaded(home_item['name'], download_data_json):
+                name = home_item['name']
+                parent_folder_name = home_item['parent_folder_name']  # /movie or /tv
+                download_path = docker_download_path + "/" + parent_folder_name
+                cent_data = remote_alist_api.get_file_or_directory_info(home_item['path'])
+                if cent_data['code'] != 200:
+                    break
+                dow_time = {
+                    'raw_url': cent_data['data']['raw_url'],
+                    'download_path': download_path,
+                    'name': name,
+                }
+                download_url.append(dow_time)
+                record_download(name, download_data_json)
+                #
+    aria2_downloads = []
+    for download_item in download_url:
+        if download_item['raw_url'] is not None:
+            tmp_download = aria2_api.add_url(download_item['raw_url'], options={"dir": download_item['download_path']})
+
+            if tmp_download is not None:
+                # 检查下载是否完成
+                while not tmp_download.is_complete:
+                    tmp_download.update()  # 更新下载状态
+                    print("下载中...")
+                    time.sleep(10)  # 等待10秒再次检查
+                if tmp_download.is_complete:
+                    print(f"{download_item['name']} ---- 下载完成")
+
+    # 初始化日志和配置
+    URL = "http://192.168.88.249:3000"
+    API_KEY = "vBgKbiM1N7at0TtacAcbVCFf82pydcxC"
+    nas_tools_api = NasToolsClient(URL, API_KEY)
+    nas_tools_api.login('admin', 'password')
+    # self.nas_tools_api.run_service('sync')
+    sjson = nas_tools_api.sync_list()
+    print(sjson)
+    directory_ids = [sjson['data']['result']['13']['id'], sjson['data']['result']['14']['id']]
+    for directory_id in directory_ids:
+        nas_tools_api.sync(directory_id)
+

+ 57 - 0
pikpak.py

@@ -0,0 +1,57 @@
+import json
+import time
+
+from api.alist import AlistAPI
+from api.aria2 import Aria2API
+
+if __name__ == '__main__':
+    API_URL = 'http://203.184.131.60:5244/api'
+    API_USERNAME = 'admin'
+    API_PASSWORD = 'nokidc123@#'
+    bash_path = '/pikpak/aaaaa'
+    local_download_path = "/aaa"
+    arai2_download_path = "/Volumes/data/xxx"
+    remote_alist_api = AlistAPI(API_URL,
+                                API_USERNAME,
+                                API_PASSWORD)
+    aria2_api = Aria2API("http://127.0.0.1:6800", "")
+    cur_dir = input('输入目录 4.15')
+    remote_alist_api.login(API_USERNAME, API_PASSWORD)
+    remote_download_path = bash_path + "/" + cur_dir
+    print(f"当前目录 {remote_download_path}")
+    # remote_data = remote_alist_api.list_directory(remote_download_path)
+    remote_data = remote_alist_api.recursive_collect_contents(
+        remote_download_path,
+        local_download_path
+    )
+    # js = json.dumps(remote_data, sort_keys=True, indent=4, separators=(',', ':'))
+    # print(js)
+    download_url = []
+    for home_item in remote_data:
+        if not home_item['is_dir']:
+            name = home_item['name']
+            # parent_folder_name = home_item['parent_folder_name']  # /movie or /tv
+            download_path = arai2_download_path
+            cent_data = remote_alist_api.get_file_or_directory_info(home_item['path'])
+            if cent_data['code'] != 200:
+                break
+            dow_time = {
+                'raw_url': cent_data['data']['raw_url'],
+                'download_path': download_path,
+                'name': name,
+            }
+            download_url.append(dow_time)
+
+    aria2_downloads = []
+    for download_item in download_url:
+        if download_item['raw_url'] is not None:
+            tmp_download = aria2_api.add_url(download_item['raw_url'], options={"dir": download_item['download_path']})
+
+            if tmp_download is not None:
+                # 检查下载是否完成
+                while not tmp_download.is_complete:
+                    tmp_download.update()  # 更新下载状态
+                    print("下载中...")
+                    time.sleep(10)  # 等待10秒再次检查
+                if tmp_download.is_complete:
+                    print(f"{download_item['name']} ---- 下载完成")

+ 0 - 0
services/__init__.py


+ 19 - 0
services/download_manager.py

@@ -0,0 +1,19 @@
+# download_manager.py
+import os
+
+
+class DownloadManager:
+    def __init__(self, download_data_path):
+        self.download_data_path = download_data_path
+
+    def is_downloaded(self, fileName):
+        if not os.path.exists(self.download_data_path):
+            return False
+        with open(self.download_data_path, 'r', encoding='utf-8') as file:
+            downloaded_files = file.readlines()
+        downloaded_files = [line.strip() for line in downloaded_files]
+        return fileName in downloaded_files
+
+    def record_download(self, filename):
+        with open(self.download_data_path, 'a', encoding='utf-8') as file:
+            file.write(filename + "\n")

+ 49 - 0
services/download_service.py

@@ -0,0 +1,49 @@
+# download_service.py
+import os
+import time
+
+
+class DownloadService:
+    def __init__(self, api_manager, download_manager):
+        self.api_manager = api_manager
+        self.download_manager = download_manager
+
+    def fetch_directory_contents(self):
+
+        remote_json_path = self.api_manager.alist_api.save_directory_contents(
+            self.api_manager.config.get_value('REMOTE_ALIST', 'DOWNLOAD_PATH'),
+            self.api_manager.config.get_value('HOME_ALIST', 'DOWNLOAD_PATH'),
+            self.api_manager.config.get_value('HOME_ALIST', 'SCY_COPY_PATH'),
+            self.api_manager.config.get_value('HOME_ALIST', 'DES_COPY_PATH'),
+            os.path.dirname(os.path.abspath(__file__))
+        )
+        return remote_json_path  # 假设这返回的是文件和文件夹列表
+
+    def fetch_and_download(self):
+
+        remote_json_path = self.api_manager.alist_api.save_directory_contents(
+            self.api_manager.config.get_value('REMOTE_ALIST', 'DOWNLOAD_PATH'),
+            self.api_manager.config.get_value('HOME_ALIST', 'DOWNLOAD_PATH'),
+            self.api_manager.config.get_value('HOME_ALIST', 'SCY_COPY_PATH'),
+            self.api_manager.config.get_value('HOME_ALIST', 'DES_COPY_PATH'),
+            os.path.dirname(os.path.abspath(__file__))
+        )
+        download_url = []
+        for home_item in remote_json_path:
+            if not home_item['is_dir']:
+                if not self.download_manager.is_downloaded(home_item['name']):
+                    name = home_item['name']
+                    parent_folder_name = home_item['parent_folder_name']
+                    download_path = self.api_manager.config.get_value('ARIA2',
+                                                                      'DOCKER_DOWNLOAD_PATH') + "/" + parent_folder_name
+                    cent_data = self.api_manager.alist_api.get_file_or_directory_info(home_item['path'])
+                    if cent_data['code'] != 200:
+                        break
+                    dow_time = {
+                        'raw_url': cent_data['data']['raw_url'],
+                        'download_path': download_path,
+                        'name': name,
+                    }
+                    download_url.append(dow_time)
+                    self.download_manager.record_download(name)
+        return download_url

+ 15 - 0
services/nas_sync_service.py

@@ -0,0 +1,15 @@
+# nas_sync_service.py
+
+class NasSyncService:
+    def __init__(self, nas_tools_api):
+        self.nas_tools_api = nas_tools_api
+
+    def sync_directories(self):
+
+        # self.nas_tools_api.run_service('sync')
+        sjson = self.nas_tools_api.sync_list()
+        results = sjson['data']['result']
+        # 遍历result字典
+        for key, value in results.items():
+            self.nas_tools_api.sync(value['id'])
+

+ 28 - 0
templates/files.html

@@ -0,0 +1,28 @@
+<!-- templates/files.html -->
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <title>文件列表</title>
+</head>
+<body>
+    <h1>文件列表</h1>
+    <table>
+        <tr>
+            <th>文件名</th>
+            <th>操作</th>
+        </tr>
+        {% for file in file_list %}
+        <tr>
+            <td>{{ file.name }}</td>
+            <td>
+                <!-- 假设每个文件有唯一的路径或标识符 -->
+                <form action="{{ url_for('download', filepath=file.path) }}" method="post">
+                    <button type="submit">下载</button>
+                </form>
+            </td>
+        </tr>
+        {% endfor %}
+    </table>
+</body>
+</html>

+ 28 - 0
templates/index.html

@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title><!-- templates/index.html -->
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <title>下载与同步管理</title>
+<!-- templates/index.html -->
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <title>下载与同步管理</title>
+</head>
+<body>
+    <h1>下载与同步控制面板</h1>
+    <form method="post">
+        <button name="action" value="download">启动下载任务</button>
+        <button name="action" value="sync">同步目录</button>
+    </form>
+    {% if message %}
+        <p>{{ message }}</p>
+    {% endif %}
+</body>
+</html>