瀏覽代碼

first commit

cauto 1 年之前
當前提交
7f7952d190

+ 0 - 0
.gitignore


+ 95 - 0
.idea/workspace.xml

@@ -0,0 +1,95 @@
+<?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="38e8c227-19ee-467f-af3e-6b7d633d6d02" name="Changes" comment="" />
+    <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="Git.Settings">
+    <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
+  </component>
+  <component name="ProjectColorInfo"><![CDATA[{
+  "associatedIndex": 4
+}]]></component>
+  <component name="ProjectId" id="2bhtBmhVPAXg7EviyM1s2zIMAk0" />
+  <component name="ProjectViewState">
+    <option name="hideEmptyMiddlePackages" value="true" />
+    <option name="showLibraryContents" value="true" />
+  </component>
+  <component name="PropertiesComponent"><![CDATA[{
+  "keyToString": {
+    "RunOnceActivity.OpenProjectViewOnStart": "true",
+    "RunOnceActivity.ShowReadmeOnStart": "true",
+    "git-widget-placeholder": "3f423df6",
+    "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="RunManager">
+    <configuration name="main" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
+      <module name="alist-airia2-py" />
+      <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>
+  </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="38e8c227-19ee-467f-af3e-6b7d633d6d02" name="Changes" comment="" />
+      <created>1706679004722</created>
+      <option name="number" value="Default" />
+      <option name="presentableId" value="Default" />
+      <updated>1706679004722</updated>
+      <workItem from="1706679006152" duration="853000" />
+    </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$/main.py</url>
+          <line>19</line>
+          <option name="timeStamp" value="1" />
+        </line-breakpoint>
+      </breakpoints>
+    </breakpoint-manager>
+  </component>
+</project>

+ 0 - 0
README.md


+ 39 - 0
app_arai2.py

@@ -0,0 +1,39 @@
+import argparse
+import configparser
+import logging
+import os
+
+from src.app.application import Application
+
+
+if __name__ == '__main__':
+    # parser = argparse.ArgumentParser(description='Aria2 and Alist Integration Service')
+    # parser.add_argument('--aria2', action='store_true', help='Run Aria2 service only')
+    # args = parser.parse_args()
+
+    # 初始化日志和配置
+    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+    # 获取当前脚本的绝对路径
+    current_directory = os.path.dirname(os.path.abspath(__file__))
+    logging.info(f"current_dir -> {current_directory} ")
+    # 获取当前脚本所在目录的上级目录
+    # parent_directory = os.path.dirname(current_directory)
+
+    # 构建 config.ini 文件的路径
+    config_path = os.path.join(current_directory, 'config', 'config.ini')
+
+    # 读取配置文件
+    config = configparser.ConfigParser()
+    config.read(config_path)
+
+    logging.info(f"config_paht: {config_path}")
+    logging.info(f"config_data: {config['ARIA2']}")
+
+    # 创建服务实例
+    app_service = Application(config, current_directory, remote_download_service=False)
+
+    # task
+    app_service.start_aria2_monitoring()
+
+    # 执行主循环
+    app_service.main_loop()

+ 35 - 0
app_pikpak.py

@@ -0,0 +1,35 @@
+import argparse
+import configparser
+import logging
+import os
+
+from src.app.PikPakApp import PikPakApp
+
+if __name__ == '__main__':
+    # parser = argparse.ArgumentParser(description='Aria2 and Alist Integration Service')
+    # parser.add_argument('--aria2', action='store_true', help='Run Aria2 service only')
+    # args = parser.parse_args()
+
+    # 初始化日志和配置
+    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+    # 获取当前脚本的绝对路径
+    current_directory = os.path.dirname(os.path.abspath(__file__))
+    logging.info(f"current_dir -> {current_directory} ")
+    # 获取当前脚本所在目录的上级目录
+    # parent_directory = os.path.dirname(current_directory)
+
+    # 构建 config.ini 文件的路径
+    config_path = os.path.join(current_directory, 'config', 'config.ini')
+
+    # 读取配置文件
+    config = configparser.ConfigParser()
+    config.read(config_path)
+
+    # 创建服务实例
+    app_service = PikPakApp(config,parent_directory=current_directory)
+
+    # task
+    app_service.start()
+
+    # # 执行主循环
+    # app_service.main_loop()

+ 53 - 0
config/config.ini

@@ -0,0 +1,53 @@
+[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 = sfbwc4GOvP9NGG5hKMFZBzBDdKCzgMwr
+
+
+[ARIA2]
+RPC_URL = http://192.168.88.29
+RPC_SECRET = 123456
+DOCKER_DOWNLOAD_PATH = /downloads
+DESTINATION_PATH = /home/downloads/aria2
+DES_COPY_PATH = /home/video/sync
+
+[MAC_MINI_ARIA2]
+RPC_URL = http://192.168.88.50
+RPC_SECRET = 123456
+DESTINATION_PATH = /Volumes/data/xxx
+
+[HOME_ALIST_PIK_PAK]
+API_URL = https://webssl.pylas.xyz/api
+WEB_URL =  https://webssl.pylas.xyz
+CLIENT_ID = 4e34854d5d7390ef7801
+USERNAME = admin
+PASSWORD = nokidc123@#
+DOWNLOAD_PATH = /pikapk/电影/1.29
+
+[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://box.szfa.xyz:5244/api
+WEB_URL = http://box.szfa.xyz:5244
+CLIENT_ID = 4e34854d5d7390ef7801
+USERNAME = admin
+PASSWORD = nokidc123@#
+DOWNLOAD_PATH = /data/media/moive
+
+[RADAR]
+URL = http://box.szfa.xyz:7878
+API_KEY = bd45569f422a4c159600964b7b85a0bd

+ 0 - 0
data/data.txt


+ 35 - 0
main.py

@@ -0,0 +1,35 @@
+import argparse
+import configparser
+import logging
+import os
+
+from src.app.application import Application
+
+if __name__ == '__main__':
+    # parser = argparse.ArgumentParser(description='Aria2 and Alist Integration Service')
+    # parser.add_argument('--aria2', action='store_true', help='Run Aria2 service only')
+    # args = parser.parse_args()
+
+    # 初始化日志和配置
+    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+    # 获取当前脚本的绝对路径
+    current_directory = os.path.dirname(os.path.abspath(__file__))
+
+    # 获取当前脚本所在目录的上级目录
+    # parent_directory = os.path.dirname(current_directory)
+
+    # 构建 config.ini 文件的路径
+    config_path = os.path.join(current_directory, 'config', 'config.ini')
+
+    # 读取配置文件
+    config = configparser.ConfigParser()
+    config.read(config_path)
+
+    # 创建服务实例
+    app_service = Application(config, current_directory)
+
+    # task
+    app_service.run()
+
+    # 执行主循环
+    app_service.main_loop()

+ 14 - 0
requirements.txt

@@ -0,0 +1,14 @@
+appdirs==1.4.4
+aria2p==0.12.0
+certifi==2023.11.17
+charset-normalizer==3.3.2
+idna==3.6
+loguru==0.7.2
+overrides==7.6.0
+pyarr==5.2.0
+requests==2.31.0
+tomli==2.0.1
+types-requests==2.31.0.20240125
+typing_extensions==4.9.0
+urllib3==2.1.0
+websocket-client==1.7.0

+ 0 - 0
src/__init__.py


+ 27 - 0
src/api/Aria2API.py

@@ -0,0 +1,27 @@
+from aria2p import API, Client
+
+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)
+
+    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

+ 123 - 0
src/api/NasToolsClient.py

@@ -0,0 +1,123 @@
+import logging
+
+import requests
+
+
+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

+ 0 - 0
src/api/__init__.py


+ 234 - 0
src/api/alist.py

@@ -0,0 +1,234 @@
+import json
+import logging
+import os
+import requests
+from src.models.task_type import TaskType, ActionType
+
+
+def construct_path(base_path, sub_path, file_name):
+    # 确保路径部分不包含反斜杠
+    base_path = base_path.replace('\\', '/')
+    sub_path = sub_path.replace('\\', '/')
+    file_name = file_name.replace('\\', '/')
+
+    # 使用 os.path.join 构建整个路径
+    return os.path.join(base_path, sub_path, file_name)
+
+
+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)
+        self.login(username, password)
+
+    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)
+        return response.json()
+
+    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=''):
+        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['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('\\', '/')
+
+            item = {
+                'name': file_info['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)
+                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}")

+ 74 - 0
src/api/radarr.py

@@ -0,0 +1,74 @@
+import time
+
+from pyarr import RadarrAPI
+import json
+
+
+class RadarClient:
+    def __init__(self, url, key):
+        self.url = url
+        self.api_key = key
+        self.client = RadarrAPI(self.url, self.api_key)
+
+    def get_all_movies(self):
+        return self.client.get_movie()
+
+    def delete_movie(self, movie_id):
+        return self.client.del_movie(movie_id, delete_files=True)
+
+    def save_movies_to_json(self, file_name='data/movies.json'):
+        movies = self.get_all_movies()
+        with open(file_name, 'w') as f:
+            json.dump(movies, f, indent=4)
+        print(f"Movies saved to {file_name}")
+
+    def get_already_movies(self):
+        """获取全部已经跟踪已经处理完成的电影"""
+        already_processed = set()  # 用于跟踪已处理的电影
+        movies = self.get_all_movies()
+        for movie in movies:
+            if 'movieFile' in movie and movie['movieFile'] and movie['id'] not in already_processed:
+                already_processed.add(movie['id'])
+        return already_processed
+
+    def continuous_monitoring(self, check_interval=60, custom_action=None):
+        """
+        Continuously monitor movies and perform a custom action if movieFile exists.
+        check_interval: Time in seconds between checks
+        custom_action: Function to be called for each downloaded movie
+        """
+        already_processed = set()  # 用于跟踪已处理的电影
+        while True:
+            movies = self.get_all_movies()
+            for movie in movies:
+                if 'movieFile' in movie and movie['movieFile'] and movie['id'] not in already_processed:
+                    custom_action(movie)
+                    already_processed.add(movie['id'])
+            time.sleep(check_interval)
+
+    def find_movie_id_by_filename(self, filename):
+        """根据文件名搜索并返回已完成下载的电影的 ID"""
+        try:
+
+            movies = self.get_all_movies()
+            for movie in movies:
+                # 检查电影是否已下载
+                if 'movieFile' in movie and movie['movieFile']:
+                    movie_file_name = movie['movieFile']['relativePath']
+                    # 检查文件名是否匹配
+                    if filename in movie_file_name:
+                        return movie['id']
+            return None
+        except Exception as e:
+            return None
+
+    def get_all_movie_names(self):
+        """获取所有电影名称的集合"""
+        movie_names = set()
+        movies = self.get_all_movies()
+        for movie in movies:
+            if 'movieFile' in movie and movie['movieFile']:
+                movie_file_name = movie['movieFile']['relativePath']
+                movie_names.add(movie_file_name)
+        return movie_names
+

+ 29 - 0
src/app/PikPakApp.py

@@ -0,0 +1,29 @@
+import signal
+import time
+
+from src.services.RemotePikPakDownloadSerivce import RemotePikPakDownloadService
+
+
+class PikPakApp:
+    def __init__(self, config, parent_directory, ):
+        self.pik_pak_service = RemotePikPakDownloadService(config, parent_directory)
+
+        self.is_running = True
+
+        # 注册信号处理函数
+        signal.signal(signal.SIGINT, self.handle_signal)
+
+    def handle_signal(self, signum, frame):
+        """处理中断信号,优雅地退出循环"""
+        self.is_running = False
+        print("Exiting...")
+
+    def main_loop(self):
+        # 主循环
+        while self.is_running:
+            # print("Running...")
+            time.sleep(1)  # 模拟任务执行
+        print("Stopped.")
+
+    def start(self):
+        self.pik_pak_service.start_download()

+ 0 - 0
src/app/__init__.py


+ 69 - 0
src/app/application.py

@@ -0,0 +1,69 @@
+import json
+import logging
+import os
+import signal
+import threading
+import time
+
+from src.api.NasToolsClient import NasToolsClient
+from src.services.Aria2Service import Aria2Service
+from src.services.RemoteDownloadSerivce import RemoteDownloadService
+
+
+def save_directory_contents_to_json(directory_contents,
+                                    json_file_path=''):
+    with open(json_file_path, 'w', encoding='utf-8') as f:
+        json.dump(directory_contents, f, indent=4, ensure_ascii=False)
+
+
+class Application:
+    def __init__(self, config, parent_directory, remote_download_service=True):
+        self.config = config
+        self.parent_dir = parent_directory
+        # base_url = config['NSTOOLS']['URL']
+        # api_key = config['NSTOOLS']['API_KEY']
+        #
+        # self.nas_tools_api = NasToolsClient(base_url, api_key)
+
+        self.aria2_service = Aria2Service(config)
+
+        if remote_download_service:
+            self.remote_download_service = RemoteDownloadService(config, parent_directory)
+
+        self.is_running = True
+
+        # 注册信号处理函数
+        signal.signal(signal.SIGINT, self.handle_signal)
+
+    def test_nas_tools_service(self):
+        pass
+
+    def handle_signal(self, signum, frame):
+        """处理中断信号,优雅地退出循环"""
+        self.is_running = False
+        print("Exiting...")
+
+    def start_remote_download(self):
+        self.remote_download_service.start_monitoring()
+
+    def stop_remote_download(self):
+        self.remote_download_service.stop_monitoring()
+
+    def start_aria2_monitoring(self):
+        self.aria2_service.start_monitoring()
+
+    def stop_aria2_monitoring(self):
+        self.aria2_service.stop_monitoring()
+
+    def run(self):
+        self.start_remote_download()
+        self.start_aria2_monitoring()
+
+    def main_loop(self):
+        # 主循环
+        while self.is_running:
+            # print("Running...")
+            time.sleep(1)  # 模拟任务执行
+        print("Stopped.")
+        self.stop_remote_download()
+        self.stop_aria2_monitoring()

+ 16 - 0
src/models/task_type.py

@@ -0,0 +1,16 @@
+from enum import Enum, unique
+
+
+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'

+ 27 - 0
src/services/Aria2Service.py

@@ -0,0 +1,27 @@
+from src.api.Aria2API import Aria2API
+from src.api.NasToolsClient import NasToolsClient
+from src.task.Aria2FileCopyTask import Aria2FileCopyTask
+
+
+class Aria2Service:
+    def __init__(self, config):
+        self.config = config
+        self.check_interval = config['COMMON']['CHECK_INTERVAL']
+        base_url = config['NSTOOLS']['URL']
+        api_key = config['NSTOOLS']['API_KEY']
+        self.nas_tools_api = NasToolsClient(base_url, api_key)
+        self.aria2_api = Aria2API(config['ARIA2']['RPC_URL'], config['ARIA2']['RPC_SECRET'])
+        self.aria2_api = Aria2API(config['ARIA2']['RPC_URL'], config['ARIA2']['RPC_SECRET'])
+        self.file_copy_service = Aria2FileCopyTask(
+            aria2_api=self.aria2_api,
+            nas_tools_api=self.nas_tools_api,
+            download_path=config['ARIA2']['DESTINATION_PATH'],
+            destination_path=config['ARIA2']['DES_COPY_PATH'],
+            check_interval=self.check_interval
+        )
+
+    def start_monitoring(self):
+        self.file_copy_service.start_monitoring()
+
+    def stop_monitoring(self):
+        self.file_copy_service.stop_monitoring()

+ 22 - 0
src/services/RemoteDownloadSerivce.py

@@ -0,0 +1,22 @@
+import json
+import logging
+import os
+
+from src.api.Aria2API import Aria2API
+from src.api.alist import AlistAPI
+from src.api.radarr import RadarClient
+from src.task.RemoteDownloadTask import RemoteDownloadTask
+
+
+class RemoteDownloadService:
+    def __init__(self, config, parent_dir):
+        self.remote_download_task = RemoteDownloadTask(config, parent_dir)
+
+    def download(self):
+        self.remote_download_task.process_downloads()
+
+    def start_monitoring(self):
+        self.remote_download_task.start_monitoring()
+
+    def stop_monitoring(self):
+        self.remote_download_task.stop_monitoring()

+ 13 - 0
src/services/RemotePikPakDownloadSerivce.py

@@ -0,0 +1,13 @@
+import json
+import logging
+import os
+
+from src.task.RemotePikPakDownloadTask import RemotePikPakDownloadTask
+
+
+class RemotePikPakDownloadService:
+    def __init__(self, config, parent_dir):
+        self.remote_download_task = RemotePikPakDownloadTask(config, parent_dir)
+
+    def start_download(self):
+        self.remote_download_task.start_download()

+ 0 - 0
src/services/__init__.py


+ 107 - 0
src/task/Aria2FileCopyTask.py

@@ -0,0 +1,107 @@
+import threading
+import logging
+import os
+import shutil
+import time
+
+
+class Aria2FileCopyTask:
+    def __init__(self, aria2_api, nas_tools_api, download_path, destination_path, check_interval):
+        self.nas_tools_api = nas_tools_api
+        self.aria2_api = aria2_api
+        self.download_path = download_path
+        self.destination_path = destination_path
+        self.is_running = False
+        self.thread = None
+        self.check_interval = check_interval
+
+    def start_monitoring(self):
+        if not self.is_running:
+            self.is_running = True
+            self.thread = threading.Thread(target=self.monitor_aria2_and_move)
+            self.thread.start()
+
+    def stop_monitoring(self):
+        self.is_running = False
+        if self.thread:
+            self.thread.join()
+
+    def monitor_aria2_and_move(self):
+        logging.info("Starting monitoring aria2 is complete and move to " + self.destination_path)
+        while self.is_running:
+            # 获取 Aria2 当前的下载列表
+            downloads = self.aria2_api.get_downloads()
+            for download in downloads:
+                if download.is_complete:
+                    # 检查下载路径是否存在并且里面是否有文件
+                    if os.path.exists(self.download_path) and os.listdir(self.download_path):
+                        # 无论 Aria2 是否有记录,都执行移动操作
+                        self.move_new_files(self.download_path, self.destination_path)
+
+                    else:
+                        logging.info("No new files to move.")
+
+            time.sleep(10)
+
+    def nas_sync(self):
+        logging.info("Notify nas tools synchronization")
+        self.nas_tools_api.login('admin', 'password')
+        # self.nas_tools_api.run_service('sync')
+
+        directory_ids = [29, 30]
+
+        # for directory_id in sync_directories:
+        #     directory_info = sync_directories[directory_id]
+        #     directory_ids.append(directory_info["id"])
+
+        for directory_id in directory_ids:
+            self.nas_tools_api.sync(directory_id)
+
+    def move_new_files(self, src_dir, dest_dir):
+        """移动新文件和目录,如果它们在目标目录中不存在"""
+        if not os.path.exists(dest_dir):
+            os.makedirs(dest_dir)
+        for item in os.listdir(src_dir):
+            src_item = os.path.join(src_dir, item)
+            dest_item = os.path.join(dest_dir, item)
+
+            # 如果是目录
+            if os.path.isdir(src_item):
+                # 检查目标目录是否存在
+                if not os.path.exists(dest_item):
+                    # 直接移动整个目录
+                    shutil.move(src_item, dest_item)
+                    logging.info(f"Moved directory from {src_item} to {dest_item}")
+                else:
+                    # 如果目标目录已存在,递归处理子目录内容
+                    self.move_new_files(src_item, dest_item)
+            else:
+                # 如果是文件,处理逻辑与之前相同
+                if not os.path.exists(dest_item):
+                    shutil.move(src_item, dest_item)
+                    logging.info(f"Moved file from {src_item} to {dest_item}")
+                    # 添加移动完成的提示
+                    logging.info(f"move complete {dest_item} ")
+                    self.nas_sync()
+                    logging.info("Completed checking for new files to move.")
+                else:
+                    logging.info(f"File already exists, skipping: {dest_item}")
+
+    def copy_new_files(self, src_dir, dest_dir):
+        """复制新文件,如果它们在目标目录中不存在"""
+        if not os.path.exists(dest_dir):
+            os.makedirs(dest_dir)
+        for item in os.listdir(src_dir):
+            src_item = os.path.join(src_dir, item)
+            dest_item = os.path.join(dest_dir, item)
+
+            if os.path.isdir(src_item):
+                # 如果是目录,则递归复制
+                self.copy_new_files(src_item, dest_item)
+            else:
+                # 如果是文件,则检查目标文件是否存在
+                if not os.path.exists(dest_item):
+                    shutil.copy(src_item, dest_item)
+                    logging.info(f"Copied new file from {src_item} to {dest_item}")
+                else:
+                    logging.info(f"File already exists, skipping: {dest_item}")

+ 166 - 0
src/task/RemoteDownloadTask.py

@@ -0,0 +1,166 @@
+import json
+import logging
+import os
+import threading
+import time
+
+from src.api.Aria2API import Aria2API
+from src.api.alist import AlistAPI
+from src.api.radarr import RadarClient
+
+
+class RemoteDownloadTask:
+    def __init__(self, config, parent_dir):
+
+        self.current_downloads = None
+        self.completed_downloads = None
+        self.config = config
+        self.is_running = False
+
+        self.monitor_thread = None
+        self.download_thread = None
+        self.check_interval = int(config['COMMON']['CHECK_INTERVAL'])
+        self.remote_alist_api = AlistAPI(self.config['REMOTE_ALIST']['API_URL'],
+                                         self.config['REMOTE_ALIST']['USERNAME'],
+                                         self.config['REMOTE_ALIST']['PASSWORD'])
+
+        self.radar_client = RadarClient(self.config['RADAR']['URL'],
+                                        self.config['RADAR']['API_KEY'])
+
+        self.home_alist_api = AlistAPI(self.config['HOME_ALIST']['API_URL'],
+                                       self.config['HOME_ALIST']['USERNAME'],
+                                       self.config['HOME_ALIST']['PASSWORD'])
+
+        self.aria2_api = Aria2API(config['ARIA2']['RPC_URL'], config['ARIA2']['RPC_SECRET'])
+
+        # 读取路径等配置
+        self.remote_alist_download_path = self.config['REMOTE_ALIST']['DOWNLOAD_PATH']
+        self.home_alist_download_path = self.config['HOME_ALIST']['DOWNLOAD_PATH']
+        self.home_scy_alist_copy_path = self.config['HOME_ALIST']['SCY_COPY_PATH']
+        self.home_des_alist_copy_path = self.config['HOME_ALIST']['DES_COPY_PATH']
+        self.remote_data = self.config['COMMON']['REMOTE_DATA']
+        self.home_data = self.config['COMMON']['HOME_DATA']
+        self.aria2_download_path = self.config['ARIA2']['DESTINATION_PATH']
+        self.aria2_docker_download_path = self.config['ARIA2']['DOCKER_DOWNLOAD_PATH']
+        self.parent_dir = parent_dir
+        self.home_json_path = os.path.join(parent_dir, 'data', self.home_data)
+
+    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.download_thread = threading.Thread(target=self.process_downloads)
+
+            self.monitor_thread.start()
+            self.download_thread.start()
+
+    def stop_monitoring(self):
+        self.is_running = False
+        if self.monitor_thread:
+            self.monitor_thread.join()
+        if self.download_thread:
+            self.download_thread.join()
+
+    def _save_directory_contents(self,
+                                 remote_download_path,
+                                 local_download_path,
+                                 scy_copy_path,
+                                 des_copy_path,
+                                 parent_dir):
+        """获取远程和本地对应的目录结构,并保存为 JSON 文件"""
+        # 获取远程目录结构
+        remote_data = self.remote_alist_api.recursive_collect_contents(
+            remote_download_path,
+            local_download_path
+        )
+        remote_json_path = os.path.join(parent_dir, 'data', 'remote_data.json')
+        with open(remote_json_path, '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
+
+    def init_download_states(self):
+        # 获取已完成下载的电影文件名
+        self.completed_downloads = self.radar_client.get_all_movie_names()
+
+        # 获取当前 Aria2 下载队列中的文件名
+        self.current_downloads = {os.path.basename(file.path) for download in self.aria2_api.get_downloads() for file in
+                                  download.files if file.selected}
+
+    def process_downloads(self):
+        logging.info('Start remote download monitoring')
+        self.init_download_states()
+
+        while self.is_running:
+            # 更新当前下载队列集合
+            self.current_downloads = {os.path.basename(file.path) for download in self.aria2_api.get_downloads() for
+                                      file in download.files if file.selected}
+
+            # 获取已完成电影的文件名集合
+            already_movie_names = self.radar_client.get_all_movie_names()
+
+            for home in already_movie_names:
+                if home not in self.current_downloads and home not in self.current_downloads:
+                    # 获取远程目录结构
+                    remote_json_path = self._save_directory_contents(self.remote_alist_download_path,
+                                                                     self.home_alist_download_path,
+                                                                     self.home_scy_alist_copy_path,
+                                                                     self.home_des_alist_copy_path, self.parent_dir)
+
+                    for home_item in remote_json_path:
+                        if not home_item['is_dir'] and home_item['name'] == home:
+                            cent_data = self.remote_alist_api.get_file_or_directory_info(home_item['path'])
+                            if cent_data:
+                                self.send_to_aria2(cent_data['data']['raw_url'])
+                                logging.info(f"Started to download {cent_data['data']['raw_url']}")
+                                self.current_downloads.add(home_item['name'])
+                            else:
+                                logging.info("File not found")
+
+            time.sleep(self.check_interval)
+
+    def send_to_aria2(self, file_path):
+        # 将文件添加到 Aria2 下载队列
+        self.aria2_api.add_url(file_path, options={"dir": self.aria2_docker_download_path + "/movie"})
+        logging.info(f"Added to Aria2: {file_path}")
+
+    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:
+                    # 获取下载完成的文件名
+                    # completed_files = [os.path.basename(file.path) for file in download.files if file.selected]
+
+                    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}'")
+
+                        # try:
+                        #     delete_info = self.radar_client.delete_movie(movie_id)
+                        #     if delete_info is not None:
+                        #         delete_info = json.dumps(delete_info, indent=4)
+                        #         print(f"delete_info: {delete_info}")
+                        # except Exception as e:
+                        #     print(f'Exception: {e}')
+
+            time.sleep(self.check_interval)

+ 46 - 0
src/task/RemotePikPakDownloadTask.py

@@ -0,0 +1,46 @@
+import json
+import logging
+import os
+
+from src.api.Aria2API import Aria2API
+from src.api.alist import AlistAPI
+
+
+class RemotePikPakDownloadTask:
+    def __init__(self, config, parent_dir):
+        self.parent_dir = parent_dir
+        self.pik_alist_api = AlistAPI(config['HOME_ALIST_PIK_PAK']['API_URL'],
+                                      config['HOME_ALIST_PIK_PAK']['USERNAME'],
+                                      config['HOME_ALIST_PIK_PAK']['PASSWORD'])
+
+        self.aria2_api = Aria2API(config['MAC_MINI_ARIA2']['RPC_URL'],
+                                  config['MAC_MINI_ARIA2']['RPC_SECRET'])
+
+        self.remote_alist_download_path = config['HOME_ALIST_PIK_PAK']['DOWNLOAD_PATH']
+
+        self.aria2_download_path = config['MAC_MINI_ARIA2']['DESTINATION_PATH']
+
+    def start_download(self):
+        # 获取远程目录结构
+        remote_data = self.pik_alist_api.recursive_collect_contents(
+            self.remote_alist_download_path,
+            self.aria2_download_path
+        )
+        # remote_json_path = os.path.join(self.parent_dir, 'data', 'remote_pikpak.json')
+        # with open(remote_json_path, 'w', encoding='utf-8') as f:
+        #     json.dump(remote_data, f, indent=4)
+
+        for home_item in remote_data:
+            if not home_item['is_dir']:
+                cent_data = self.pik_alist_api.get_file_or_directory_info(home_item['path'])
+                if cent_data:
+                    self.send_to_aria2(cent_data['data']['raw_url'])
+                    logging.info(f"Started to download {cent_data['data']['raw_url']}")
+                else:
+                    logging.info("File not found")
+                break
+
+    def send_to_aria2(self, file_path):
+        # 将文件添加到 Aria2 下载队列
+        self.aria2_api.add_url(file_path, options={"dir": self.aria2_download_path})
+        logging.info(f"Added to Aria2: {file_path}")

+ 0 - 0
src/task/__init__.py


+ 0 - 0
src/utils/__init__.py


+ 0 - 0
tests/__init__.py


+ 50 - 0
tests/test.py

@@ -0,0 +1,50 @@
+import configparser
+import json
+import logging
+import os
+
+from src.api.Aria2API import Aria2API
+from src.api.radarr import RadarClient
+
+
+# 运行测试
+
+def test_find_movie_id_by_filename():
+    # 初始化日志和配置
+    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+    # 获取当前脚本的绝对路径
+    current_directory = os.path.dirname(os.path.abspath(__file__))
+
+    # 获取当前脚本所在目录的上级目录
+    parent_directory = os.path.dirname(current_directory)
+
+    # 构建 config.ini 文件的路径
+    config_path = os.path.join(parent_directory, 'config', 'config.ini')
+
+    # 读取配置文件
+    config = configparser.ConfigParser()
+    config.read(config_path)
+
+    # 创建 RadarClient 实例
+    radar_client = RadarClient(config['RADAR']['URL'],
+                               config['RADAR']['API_KEY'])
+
+    movies = radar_client.get_all_movie_names()
+
+    print(f"movies_jons: {movies}")
+
+    aria2_api = Aria2API(config['ARIA2']['RPC_URL'], config['ARIA2']['RPC_SECRET'])
+    downloads = aria2_api.get_downloads()
+    for download in downloads:
+        if download.is_complete:
+            completed_files = [os.path.basename(file.path) for file in download.files if file.selected]
+            print(f"completed_files: {completed_files}")
+            # 查找电影ID
+            for completed_file in completed_files:
+                ids = radar_client.find_movie_id_by_filename(completed_file)
+                if ids is not None:
+                    print(f"ids: {ids}")
+
+
+# 运行测试
+test_find_movie_id_by_filename()