alroyso 1 year ago
parent
commit
9980693407
15 changed files with 654 additions and 432 deletions
  1. 0 0
      .gitignore
  2. 0 141
      Aria2Download.py
  3. 0 201
      alist.py
  4. 26 0
      config/config.ini
  5. 0 0
      data/data.txt
  6. 0 34
      main.py
  7. 0 56
      monitor_aria2.py
  8. 390 0
      src/api/alist.py
  9. 51 0
      src/api/monitor_aria2.py
  10. 52 0
      src/api/radarr.py
  11. 119 0
      src/main.py
  12. 16 0
      src/models/task_type.py
  13. 0 0
      src/services/__init__.py
  14. 0 0
      src/utils/__init__.py
  15. 0 0
      tests/__init__.py

+ 0 - 0
.gitignore


+ 0 - 141
Aria2Download.py

@@ -1,141 +0,0 @@
-import requests
-import json
-
-
-class Aria2Download:
-    def __init__(self, api_url, rpc_secret=None):
-        self.api_url = api_url
-        self.rpc_secret = rpc_secret
-        self.id = "QXJpYU5nXzE2NzUxMzUwMDFfMC42Mzc0MDA5MTc2NjAzNDM="
-
-    def addUri(self, url, path, file=None, proxy=None):
-        """
-        添加任务
-        :param url: 文件下载地址
-        :param path: 文件保存路径
-        :param file: 文件保存名称
-        :param proxy: 代{过}{滤}理地址
-        :return:
-        """
-        data = {
-            "id": self.id,
-            "jsonrpc": "2.0",
-            "method": "aria2.addUri",
-            "params": [[url], {"dir": path, "out": file, "all-proxy": proxy}]
-        }
-        req = requests.post(url=self.api_url, data=json.dumps(data))
-        return_json = req.json()
-        req.close()
-        # print("addUri", return_json)
-        return return_json
-
-    def getGlobalStat(self):
-        """
-        获取全部下载信息
-        :return:
-        """
-        data = {
-            "jsonrpc": "2.0",
-            "method": "aria2.getGlobalStat",
-            "id": self.id
-        }
-        req = requests.post(url=self.api_url, data=json.dumps(data))
-        return_json = req.json()
-        req.close()
-        # print("getGlobalStat", return_json)
-        return return_json
-
-    def tellActive(self):
-        """
-        正在下载
-        :return:
-        """
-        data = {
-            "jsonrpc": "2.0",
-            "method": "aria2.tellActive",
-            "id": self.id, "params": [
-                ["gid", "totalLength", "completedLength", "uploadSpeed", "downloadSpeed", "connections", "numSeeders",
-                 "seeder", "status", "errorCode", "verifiedLength", "verifyIntegrityPending", "files", "bittorrent",
-                 "infoHash"]]
-        }
-
-        req = requests.post(url=self.api_url, data=json.dumps(data))
-        return_json = req.json()
-        req.close()
-        # print("getGlobalStat", return_json)
-        return return_json
-
-    def tellWaiting(self):
-        """
-        正在等待
-        :return:
-        """
-        data = {"jsonrpc": "2.0", "method": "aria2.tellWaiting",
-                "id": self.id,
-                "params": [0, 1000, ["gid", "totalLength",
-                                     "completedLength",
-                                     "uploadSpeed",
-                                     "downloadSpeed",
-                                     "connections",
-                                     "numSeeders",
-                                     "seeder", "status",
-                                     "errorCode",
-                                     "verifiedLength",
-                                     "verifyIntegrityPending"]
-                           ]
-                }
-        req = requests.post(url=self.api_url, data=json.dumps(data))
-        return_json = req.json()
-        req.close()
-        print("tellWaiting", return_json)
-        return return_json
-
-    def tellStopped(self):
-        """
-        已完成/已停止
-        :return:
-        """
-        data = {"jsonrpc": "2.0",
-                "method": "aria2.tellStopped",
-                "id": self.id,
-                "params": [-1, 1000, ["gid", "totalLength",
-                                      "completedLength",
-                                      "uploadSpeed",
-                                      "downloadSpeed",
-                                      "connections",
-                                      "numSeeders", "seeder",
-                                      "status", "errorCode",
-                                      "verifiedLength",
-                                      "verifyIntegrityPending"]]
-                }
-        req = requests.post(url=self.api, data=json.dumps(data))
-        return_json = req.json()
-        req.close()
-        # print("tellStopped", return_json)
-        return return_json
-
-    def tellStatus(self, gid):
-        """
-        任务状态
-        :param gid: 任务ID
-        :return:
-        """
-        data = {"jsonrpc": "2.0", "method": "aria2.tellStatus", "id": self.id, "params": [gid]}
-        req = requests.post(url=self.api_url, data=json.dumps(data))
-        return_json = req.json()
-        req.close()
-        # print("tellWaiting", return_json)
-        return return_json
-
-    def removeDownloadResult(self, gid):
-        """
-        删除下载结束的任务
-        :param gid: 任务ID
-        :return:
-        """
-        data = {"jsonrpc": "2.0", "method": "aria2.removeDownloadResult", "id": self.id, "params": [gid]}
-        req = requests.post(url=self.api_url, data=json.dumps(data))
-        return_json = req.json()
-        req.close()
-        # print("removeDownloadResult", return_json)
-        return return_json

+ 0 - 201
alist.py

@@ -1,201 +0,0 @@
-import json
-import os
-import shutil
-import time
-
-import requests
-from aria2p import API, Client
-
-
-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, aria2_rpc_url=None, aria2_rpc_secret=None, username=None, password=None):
-        self.url = url
-        self.UserAgent = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
-                          'Chrome/87.0.4280.88 Safari/537.36')
-        self.headers = {
-            'UserAgent': self.UserAgent,
-            'Content-Type': 'application/json'
-        }
-        self.aria2_client = Client(aria2_rpc_url, secret=aria2_rpc_secret)
-        self.aria2_api = API(self.aria2_client)
-        if username and password:
-            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 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)
-        return response.json()
-
-    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 download_directory(self, remote_path, local_base_path, current_sub_path=''):
-        file_list = self.list_directory(remote_path)
-        if not file_list:
-            return
-        for file_info in file_list['data']['content']:
-            # 检查是否是目录
-            if file_info['is_dir']:
-                # 构建新的子路径
-                new_sub_path = os.path.join(current_sub_path, file_info['name'])
-                # 递归下载该目录
-                self.download_directory(remote_path + "/" + file_info['name'], local_base_path, new_sub_path)
-            else:
-                # 获取文件的详细信息
-                file_detail = self.get_file_or_directory_info(remote_path + "/" + file_info['name'])
-                if file_detail and file_detail['code'] == 200:
-                    raw_url = file_detail['data']['raw_url']
-                    # 下载完整路径
-                    full_download_path = local_base_path + "/" + current_sub_path
-                    print(f"aria2  File: {file_info['name']}")
-                    print(f"aria2  Download URL: {raw_url}")
-                    print(f"aria2  Local Path: {full_download_path}")
-                    # 将真实 URL 添加到 Aria2 下载队列
-                    self.aria2_api.add_uris([raw_url], options={"dir": full_download_path})
-                else:
-                    print(f"Failed to get detailed information for {file_info['name']}")
-
-    def debug_directory(self, remote_path, local_base_path, current_sub_path=''):
-        file_list = self.list_directory(remote_path)
-        if not file_list:
-            return
-        for file_info in file_list['data']['content']:
-            if file_info['is_dir']:
-                new_sub_path = os.path.join(current_sub_path, file_info['name'])
-                print(f"Directory: {remote_path}/{file_info['name']}")
-                print(f"  Local Path: {local_base_path}/{new_sub_path}")
-                self.debug_directory(remote_path + "/" + file_info['name'], local_base_path, new_sub_path)
-            else:
-                # print(f"remote_path: {remote_path} + / + {file_info['name']}")
-                file_detail = self.get_file_or_directory_info(remote_path + "/" + file_info['name'])
-                if file_detail and file_detail['code'] == 200:
-                    raw_url = file_detail['data']['raw_url']
-                    full_local_path = local_base_path + "/" + current_sub_path
-                    print(f"File: {file_info['name']}")
-                    print(f"  Download URL: {raw_url}")
-                    print(f"  Local Path: {full_local_path}")
-                else:
-                    print(f"Failed to get detailed information for {file_info['name']}")
-
-    # alist 下载完成复制
-    def monitor_and_copy(self, download_path, destination_path, check_interval=10):
-        while True:
-            downloads = self.aria2_api.get_downloads()
-            for download in downloads:
-                if download.is_complete:
-                    for file in download.files:
-                        if file.selected:
-                            file_name = os.path.basename(file.path)
-                            names = [file_name]
-
-                            # 复制文件
-                            self.copy_file(download_path, destination_path, names)
-                            print(f"Copied: {file_name} from {download_path} to {destination_path}")
-
-                            # 从 Aria2 中移除已完成的下载记录
-                            download.remove()
-
-            time.sleep(check_interval)
-
-    # alist 下载完成移动
-    def monitor_and_move(self, download_path, destination_path, check_interval=10):
-        while True:
-            downloads = self.aria2_api.get_downloads()
-            for download in downloads:
-                if download.is_complete:
-                    for file in download.files:
-                        if file.selected:
-                            file_name = os.path.basename(file.path)
-                            names = [file_name]
-
-                            # 移动文件
-                            self.move_file(download_path, destination_path, names)
-                            print(f"Moved: {file_name} from {download_path} to {destination_path}")
-
-                            # 从 Aria2 中移除已完成的下载记录
-                            # download.remove()
-
-            time.sleep(check_interval)

+ 26 - 0
config/config.ini

@@ -0,0 +1,26 @@
+[ARIA2]
+RPC_URL = http://192.168.88.10
+RPC_SECRET = 4e34854d5d7390ef7801
+DESTINATION_PATH = /mnt/data1/downloads/movie
+
+[ALIST]
+API_URL = http://192.168.88.10:5244/api
+WEB_URL = http://192.168.88.10
+CLIENT_ID = 4e34854d5d7390ef7801
+USERNAME = admin
+PASSWORD = nokidc123@#
+DOWNLOAD_PATH = /downloads/movie
+DESTINATION_PATH = /media/sync/movie
+
+[ALIST2]
+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
+LOCAL_DESTINATION_PATH = /mnt/data1/downloads/movie
+
+[RADAR]
+URL = http://box.szfa.xyz:7878
+API_KEY = bd45569f422a4c159600964b7b85a0bd

+ 0 - 0
data/data.txt


+ 0 - 34
main.py

@@ -1,34 +0,0 @@
-import json
-
-from alist import AlistAPI
-
-if __name__ == '__main__':
-    remote_path = '/data/media/moive'
-    local_base_path = '/mnt/data1/downloads/moive'
-    alist = AlistAPI('http://box.szfa.xyz:5244/api', 'http://192.168.88.10', '4e34854d5d7390ef7801', 'admin',
-                     'nokidc123@#')
-
-    # lis_file_debug = alist.debug_directory(remote_path, local_base_path)
-    # print(lis_file_debug)
-    alist.download_directory(remote_path, local_base_path)
-
-    # print(lis_file)
-    #
-    # destination_folder = "/mnt/video/download/movie"
-    # alist.monitor_and_move(destination_folder)
-
-    # 调用复制函数
-    # alist.monitor_and_copy(download_path, destination_path)
-    # 调用移动函数
-    # alist.monitor_and_move(download_path, destination_path)
-
-    # file_or_dir = alist.list_directory('/data1/download/moive')
-    # print(file_or_dir['data']['content'])
-    # # 遍历content数组
-    # for item in file_or_dir['data']['content']:
-    #     if item['is_dir']:
-    #         # 是目录
-    #         print(f"{item['name']} is a directory")
-    #     else:
-    #         # 是文件
-    #         print(f"{item['name']} is a file")

+ 0 - 56
monitor_aria2.py

@@ -1,56 +0,0 @@
-import json
-import os
-import shutil
-import time
-from aria2p import API, Client
-from alist import AlistAPI
-
-
-class Aria2Monitor:
-    def __init__(self, aria2_rpc_url, aria2_rpc_secret=None):
-        self.aria2_client = Client(aria2_rpc_url, secret=aria2_rpc_secret)
-        self.aria2_api = API(self.aria2_client)
-
-    def monitor_and_move_shutil(self, destination_folder, check_interval=10):
-        while True:
-            downloads = self.aria2_api.get_downloads()
-            for download in downloads:
-                if download.is_complete:
-                    all_files_moved = True
-                    for file in download.files:
-                        if file.selected:
-                            file_path = file.path
-                            file_name = os.path.basename(file_path)
-                            target_path = os.path.join(destination_folder, file_name)
-                            print(f"Prepare to move: {file_path} -> {target_path}")
-
-                            try:
-                                if os.path.exists(file_path):
-                                    shutil.move(file_path, target_path)
-                                    print(f"Successfully moved: {file_path} -> {target_path}")
-                                else:
-                                    print(f"File not found: {file_path}")
-                                    all_files_moved = False
-                            except Exception as e:
-                                print(f"Error moving file: {e}")
-                                all_files_moved = False
-
-                    # 仅在所有文件都成功移动后删除记录
-                    if all_files_moved:
-                        download.remove()
-
-            time.sleep(check_interval)
-
-
-if __name__ == '__main__':
-    alist = AlistAPI('http://192.168.88.10:5244/api', 'http://192.168.88.10', '4e34854d5d7390ef7801', 'admin',
-                     'nokidc123@#')
-    # 使用示例
-    # aria2_monitor = Aria2Monitor("http://192.168.88.10", "4e34854d5d7390ef7801")
-    # destination_folder = "/mnt/video/sync/movie"
-    # aria2_monitor.monitor_and_move_shutil(destination_folder)
-    download_path = "/downloads/moive"
-    destination_path = "/media/sync/movie"
-    alist.copy_directory(download_path, destination_path)
-    #m_ret = json.dumps(alist.copy_file(download_path + '/War of the Worlds (2005) 1080p', destination_path, ['poster.jpg']), indent=4)
-    #print(m_ret)

+ 390 - 0
src/api/alist.py

@@ -0,0 +1,390 @@
+import json
+import logging
+import os
+import shutil
+import time
+
+import requests
+from aria2p import API, Client
+
+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, rpc_url, rpc_secret):
+        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)
+        return response.json()
+
+    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 save_directory_contents_to_json(self, remote_path, local_base_path=None, copy_or_move_destination_path=None,
+                                        json_file_path=''):
+        file_list = self.list_directory(remote_path)
+        if not file_list:
+            return
+
+        directory_contents = self._recursive_collect_contents(remote_path, local_base_path,
+                                                              copy_or_move_destination_path)
+
+        with open(json_file_path, 'w', encoding='utf-8') as f:
+            json.dump(directory_contents, f, indent=4, ensure_ascii=False)
+
+    def _recursive_collect_contents(self, path, local_base_path, copy_or_move_destination_path, current_sub_path=''):
+        contents = []
+        file_list = self.list_directory(path)
+        for file_info in file_list['data']['content']:
+            # 拼接完整的远程路径
+            full_path = os.path.join(path, file_info['name']).replace('\\', '/')
+
+            # 初始化本地下载路径和复制/移动目的地路径
+            local_download_path = ''
+            copy_move_dest_path = ''
+
+            # 根据条件构建本地下载路径和复制/移动目的地路径
+            if local_base_path is not None:
+                local_download_path = os.path.join(local_base_path, current_sub_path, file_info['name']).replace('\\',
+                                                                                                                 '/')
+            if copy_or_move_destination_path is not None:
+                copy_move_dest_path = os.path.join(copy_or_move_destination_path, current_sub_path,
+                                                   file_info['name']).replace('\\', '/')
+
+            item = {
+                'name': file_info['name'],
+                'is_dir': file_info['is_dir'],
+                'path': full_path,  # 存储完整的远程路径
+                'downloads_path': local_download_path,
+                'copy_des_path': copy_move_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, local_base_path,
+                                                                copy_or_move_destination_path, new_sub_path)
+                contents.extend(sub_contents)
+
+        return contents
+
+    def download_directory(self, is_debug=False, json_file_path='directory_contents.json'):
+        # 读取 JSON 文件中的目录内容
+        with open(json_file_path, 'r', encoding='utf-8') as f:
+            directory_contents = json.load(f)
+
+        # 获取 Aria2 的所有下载记录
+        downloads = self.aria2_api.get_downloads()
+
+        # 获取已完成下载的文件名列表
+        completed_files = []
+        for download in downloads:
+            if download.is_complete:
+                for file in download.files:
+                    if file.selected:
+                        file_name = os.path.basename(file.path)
+                        completed_files.append(file_name)
+
+        for item in directory_contents:
+            if not item['is_dir']:
+                # 构建完整的本地路径
+                # full_local_path = os.path.join(os.path.relpath(item['path'], start="/")).replace('\\', '/')
+                # local_file_path = os.path.join(local_base_path, item['name']).replace('\\', '/')
+                full_local_path = item['downloads_path']
+                local_file = item['name']
+                if local_file not in completed_files:
+                    file_detail = self.get_file_or_directory_info(item['path'])
+                    if file_detail and file_detail['code'] == 200:
+                        raw_url = file_detail['data']['raw_url']
+                        if not is_debug:
+                            # 添加到 Aria2 下载队列
+                            self.aria2_api.add_uris([raw_url], options={"dir": full_local_path})
+                    else:
+                        logging.error(f"Failed to get detailed information for {local_file}")
+                else:
+                    logging.info(f"File already downloaded, skipping: {local_file}")
+
+    def monitor_and_copy(self, local_json_path, check_interval=10, is_debug=False, is_running=True):
+        """监控 Aria2 下载完成后,执行拷贝操作"""
+        try:
+            while is_running:
+                # 读取本地 JSON 文件
+                with open(local_json_path, 'r', encoding='utf-8') as f:
+                    directory_contents = json.load(f)
+
+                downloads = self.aria2_api.get_downloads()
+                for download in downloads:
+                    if download.is_complete:
+                        for file in download.files:
+                            if file.selected:
+                                file_name = os.path.basename(file.path)
+
+                                for item in directory_contents:
+                                    if item['name'] == file_name and not item['is_dir']:
+                                        original_path = item['path']  # 获取原始文件路径
+                                        des_path = item['copy_des_path']  # 获取原始文件路径
+
+                                        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}")
+
+                time.sleep(check_interval)
+        except Exception as e:
+            logging.error(f"Error occurred: {e}")
+        finally:
+            logging.info("Monitoring and copying completed")
+
+    def monitor_and_move_or_copy(self, local_json_path, check_interval=10,
+                                 is_debug=False, is_running=True, timeout=3600,
+                                 condition=None, action_type=ActionType.COPY):
+        """监控 Aria2 下载完成后,执行移动操作并检查任务完成状态"""
+        start_time = time.time()
+        try:
+            while is_running:
+                current_time = time.time()
+                if current_time - start_time > timeout:
+                    logging.info(f"Timeout: {timeout} seconds")
+                    break
+
+                # 读取本地 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']:
+                        local_file_path = item['downloads_path']
+                        remote_file_name = item['remote_file_name']
+                        scy_des_path = item['path']
+                        des_des_path = item['copy_des_path']
+                        # 检查 Aria2 是否已完成该文件的下载
+                        downloads = self.aria2_api.get_downloads()
+                        for download in downloads:
+                            if download.is_complete:
+                                for file in download.files:
+                                    if file.selected and os.path.basename(file.path) == remote_file_name:
+                                        if is_debug:
+                                            logging.info(f"Debug mode: {action_type} {remote_file_name}")
+                                        else:
+                                            if action_type == ActionType.MOVE:
+                                                self.move_file(scy_des_path, des_des_path)
+                                                logging.info(f"Moved: {scy_des_path} to {des_des_path}")
+                                            elif action_type == ActionType.COPY:
+                                                self.copy_file(scy_des_path, des_des_path)
+                                                logging.info(f"Copied: {scy_des_path} to {des_des_path}")
+                                                # self.remove_files_or_folders([scy_des_path], item['path'])
+                                                # logging.info(
+                                                #     f"Removed original: {scy_des_path} from {item['path']}")
+
+                if condition is not None and condition():
+                    logging.info("Condition met, stopping monitoring")
+                    break
+
+                time.sleep(check_interval)
+        except Exception as e:
+            logging.error(f"Error occurred: {e}")
+        finally:
+            logging.info("Monitoring stopped")
+    # def monitor_and_move_or_copy(self, download_path, destination_path, check_interval=10,
+    #                              is_debug=False, is_running=True, timeout=3600,
+    #                              condition=None, action_type=ActionType.COPY):
+    #     """监控 Aria2 下载完成后,执行移动操作并检查任务完成状态"""
+    #     # 记录开始监控的时间
+    #     start_time = time.time()
+    #     try:
+    #         while is_running:
+    #             # 检查是否超时
+    #             current_time = time.time()
+    #             if current_time - start_time > timeout:
+    #                 logging.info(f"Timeout: {timeout} seconds")
+    #                 break
+    #             downloads = self.aria2_api.get_downloads()
+    #             for download in downloads:
+    #                 if download.is_complete:
+    #                     all_tasks_completed = True
+    #                     for file in download.files:
+    #                         if file.selected:
+    #                             file_name = os.path.basename(file.path)
+    #                             names = [file_name]
+    #
+    #                             if is_debug is False:
+    #                                 if action_type == ActionType.MOVE:
+    #                                     # 移动文件
+    #                                     self.move_file(download_path, destination_path, names)
+    #                                     logging.info(f"Moved: {file_name} from {download_path} to {destination_path}")
+    #                                 if action_type == ActionType.COPY:
+    #                                     # 复制文件
+    #                                     self.copy_file(download_path, destination_path, names)
+    #                                     logging.info(f"Copied: {file_name} from {download_path} to {destination_path}")
+    #
+    #                                     # 检查任务完成状态
+
+    #                             else:
+    #                                 """调试信息不真到进行操作"""
+    #                                 if action_type == ActionType.MOVE:
+    #                                     # 移动文件
+    #                                     logging.info(f"Moved: {file_name} from {download_path} to {destination_path}")
+    #                                 if action_type == ActionType.COPY:
+    #                                     # 复制文件
+    #                                     logging.info(f"Copied: {file_name} from {download_path} to {destination_path}")
+    #
+    #             # 检查是否满足条件
+    #             if condition is not None and condition():
+    #                 logging.info(f"Condition met: {condition}")
+    #                 break
+    #
+    #             time.sleep(check_interval)
+    #     except Exception as e:
+    #         # 捕获异常,并记录日志
+    #         logging.error(f"An error occurred: {e}")
+    #     finally:
+    #         # 做一些清理工作,比如关闭 Aria2 的连接,删除临时文件等
+    #         # self.aria2_api.close()
+    #         logging.info(f"Closed Aria2 connection")
+    #         # ...其他清理工作
+    #         logging.info(f"monitor_and_move_or_copy is run {is_running}")

+ 51 - 0
src/api/monitor_aria2.py

@@ -0,0 +1,51 @@
+import configparser
+import logging
+import os
+import shutil
+import time
+from aria2p import API, Client
+
+
+class Aria2Monitor:
+    def __init__(self, config):
+        self.aria2_client = Client(config['ARIA2']['RPC_URL'], secret=config['ARIA2']['RPC_SECRET'])
+        self.aria2_api = API(self.aria2_client)
+
+    def monitor_and_move_shutil(self, destination_folder, check_interval=10):
+        while True:
+            downloads = self.aria2_api.get_downloads()
+            for download in downloads:
+                if download.is_complete:
+                    all_files_moved = True
+                    for file in download.files:
+                        if file.selected:
+                            file_path = file.path
+                            file_name = os.path.basename(file_path)
+                            target_path = os.path.join(destination_folder, file_name)
+                            logging.info(f"Prepare to move: {file_path} -> {target_path}")
+
+                            try:
+                                if os.path.exists(file_path):
+                                    shutil.move(file_path, target_path)
+                                    logging.info(f"Successfully moved: {file_path} -> {target_path}")
+                                else:
+                                    logging.warning(f"File not found: {file_path}")
+                                    all_files_moved = False
+                            except Exception as e:
+                                logging.error("Error moving file", exc_info=True)
+                                all_files_moved = False
+
+                    if all_files_moved:
+                        download.remove()
+
+            time.sleep(check_interval)
+
+
+# if __name__ == '__main__':
+#     logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+#
+#     config = configparser.ConfigParser()
+#     config.read('config.ini')
+#
+#     destination_folder = "/media/sync/movie"
+#

+ 52 - 0
src/api/radarr.py

@@ -0,0 +1,52 @@
+import time
+
+from pyarr import RadarrAPI
+import json
+
+
+class RadarrClient:
+    def __init__(self, config):
+        self.url = config['RADAR']['URL']
+        self.api_key = config['RADAR']['API_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)
+
+    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 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 my_custom_action(movie):
+#     print(f"Performing custom action on movie: {movie['title']}")
+#
+#
+# if __name__ == '__main__':
+#     # 使用
+#     radarr_client = RadarrClient("http://box.szfa.xyz:7878", "bd45569f422a4c159600964b7b85a0bd")
+#     # 保存所有电影到JSON文件
+#     radarr_client.save_movies_to_json()
+#     # 监控电影完成
+

+ 119 - 0
src/main.py

@@ -0,0 +1,119 @@
+import configparser
+import json
+import logging
+import signal
+import threading
+import time
+
+from src.api.alist import AlistAPI
+from src.api.radarr import RadarrClient
+
+# 创建一个全局变量,用来表示是否继续监控
+is_running = True
+
+
+# 定义信号处理函数
+def handle_signal(signum, frame):
+    global is_running
+    is_running = False
+    logging.info(f"is run {is_running}")
+
+
+def my_custom_action(movie,
+                     remote_alist_api,
+                     home_alist_api,
+                     remote_download_path,
+                     Local_download_path,
+                     home_download_path,
+                     copy_or_move_download_path,
+                     copy_or_move_destination_path,
+                     ):
+    global is_running
+
+    print(f"Performing custom action on movie: {movie['title']}")
+    # 这里的 movie_path 可能需要根据实际情况调整
+    movie_path = remote_download_path + "/" + movie['title']
+    print(f"movie path -> {movie_path}")
+    # 这里是 home 路径
+    remote_alist_api.save_directory_contents_to_json(remote_download_path, Local_download_path,
+                                                     json_file_path='data/remote.json')
+    logging.info(f" remote save_directory path {remote_download_path} to data/remote.json")
+    # 这里是 home 路径
+    home_alist_api.save_directory_contents_to_json(home_download_path,
+                                                   copy_or_move_destination_path=copy_or_move_destination_path,
+                                                   json_file_path='data/home.json')
+    logging.info(f" remote save_directory path {home_download_path} to data/home.json")
+    # 从服务器上遍历需要下载的数据,推送到本地
+    remote_alist_api.download_directory(is_debug=True,
+                                        json_file_path='data/remote.json')
+
+    logging.info("Task completion")
+
+
+if __name__ == '__main__':
+
+    # 注册信号处理函数
+    signal.signal(signal.SIGINT, handle_signal)
+
+    logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+    config = configparser.ConfigParser()
+    config.read('./config/config.ini')
+    # 创建 AlistAPI 和 RadarrClient 实例
+
+    # 服务器上的alist,
+    remote_alist_api = AlistAPI(
+        config['ALIST2']['API_URL'],
+        config['ALIST2']['USERNAME'],
+        config['ALIST2']['PASSWORD'],
+        config['ARIA2']['RPC_URL'],
+        config['ARIA2']['RPC_SECRET'],
+    )
+
+    # 本地的alist,
+    home_alist_api = AlistAPI(
+        config['ALIST']['API_URL'],
+        config['ALIST']['USERNAME'],
+        config['ALIST']['PASSWORD'],
+        config['ARIA2']['RPC_URL'],
+        config['ARIA2']['RPC_SECRET'],
+    )
+
+    radar_client = RadarrClient(config)
+
+    # 从配置文件中读取路径
+    # 服务器下载路径
+    remote_download_path = config['ALIST2']['DOWNLOAD_PATH']
+    logging.info(f"remote download path {remote_download_path}")
+    # Remote to Local
+    Local_download_path = config['ALIST2']['LOCAL_DESTINATION_PATH']
+    logging.info(f"LOCAL download path {Local_download_path}")
+
+    home_download_path = config['ALIST']['DOWNLOAD_PATH']
+    logging.info(f"home download path {home_download_path}")
+    # 拷贝路径
+    copy_or_move_download_path = config['ALIST']['DOWNLOAD_PATH']
+    copy_or_move_destination_path = config['ALIST']['DESTINATION_PATH']
+
+    radar_client.save_movies_to_json()
+
+    # 开始监控
+    radar_client.continuous_monitoring(
+        custom_action=lambda movie: my_custom_action(movie,
+                                                     remote_alist_api,
+                                                     home_alist_api,
+                                                     remote_download_path,
+                                                     Local_download_path,
+                                                     home_download_path,
+                                                     copy_or_move_download_path,
+                                                     copy_or_move_destination_path
+                                                     )
+    )
+    # 监控本地ar2 下载并复制
+    # home_alist_api.monitor_and_move_or_copy(copy_or_move_download_path, copy_or_move_destination_path, is_debug=False,
+    #                                         is_running=is_running, )
+
+    # 主循环
+    while is_running:
+        print("Running...")
+        time.sleep(1)  # 模拟任务执行
+        print("Stopped.")

+ 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'

+ 0 - 0
src/services/__init__.py


+ 0 - 0
src/utils/__init__.py


+ 0 - 0
tests/__init__.py