diff --git a/src/main.py b/src/main.py index 7a4d0559..a6b803e5 100644 --- a/src/main.py +++ b/src/main.py @@ -1,85 +1,16 @@ import os -import signal import logging import uvicorn -from fastapi import Request -from fastapi.responses import HTMLResponse, RedirectResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates from module.api import router -from module.sub_thread import start_thread, start_program, stop_thread, stop_event -from module.conf import VERSION, settings, setup_logger +from module.conf import settings from module.conf.uvicorn_logging import logging_config logger = logging.getLogger(__name__) -@router.on_event("startup") -async def startup(): - log_level = logging.DEBUG if settings.log.debug_enable else logging.INFO - setup_logger(log_level) - start_program() - - -@router.on_event("shutdown") -async def shutdown(): - stop_event.set() - logger.info("Stopping program...") - - -@router.get("/api/v1/restart", tags=["program"]) -async def restart(): - stop_thread() - start_thread() - return {"status": "ok"} - - -@router.get("/api/v1/start", tags=["program"]) -async def start(): - start_thread() - return {"status": "ok"} - - -@router.get("/api/v1/stop", tags=["program"]) -async def stop(): - stop_thread() - return {"status": "ok"} - - -@router.get("/api/v1/status", tags=["program"]) -async def status(): - if stop_event.is_set(): - return {"status": "stop"} - else: - return {"status": "running"} - - -@router.get("/api/v1/shutdown", tags=["program"]) -async def shutdown_program(): - stop_thread() - logger.info("Shutting down program...") - os.kill(os.getpid(), signal.SIGINT) - return {"status": "ok"} - - -if VERSION != "DEV_VERSION": - router.mount("/assets", StaticFiles(directory="templates/assets"), name="assets") - templates = Jinja2Templates(directory="templates") - - # HTML Response - @router.get("/{full_path:path}", response_class=HTMLResponse, tags=["html"]) - def index(request: Request): - context = {"request": request} - return templates.TemplateResponse("index.html", context) -else: - @router.get("/", status_code=302, tags=["html"]) - def index(): - return RedirectResponse("/docs") - - if __name__ == "__main__": if not os.path.isdir("data"): os.mkdir("data") diff --git a/src/module/api.py b/src/module/api.py deleted file mode 100644 index 8e3fa709..00000000 --- a/src/module/api.py +++ /dev/null @@ -1,114 +0,0 @@ -import logging -import os - -from fastapi import FastAPI -from fastapi.responses import FileResponse, Response - -from module.core import APIProcess -from module.conf import DATA_PATH, LOG_PATH, settings -from module.utils import json_config -from module.models.api import * -from module.models import Config - -logger = logging.getLogger(__name__) - -router = FastAPI() -api_func = APIProcess() - - -@router.get("/api/v1/data", tags=["info"]) -async def get_data(): - try: - data = json_config.load(DATA_PATH) - return data - except FileNotFoundError: - return { - "rss_link": "", - "data_version": settings.data_version, - "bangumi_info": [], - } - - -@router.get("/api/v1/log", tags=["info"]) -async def get_log(): - if os.path.isfile(LOG_PATH): - return FileResponse(LOG_PATH) - else: - return Response("Log file not found", status_code=404) - - -@router.get("/api/v1/resetRule") -def reset_rule(): - return api_func.reset_rule() - - -@router.get("api/v1/removeRule/{bangumi_id}") -def remove_rule(bangumi_id: str): - bangumi_id = int(bangumi_id) - return api_func.remove_rule(bangumi_id) - - -@router.post("/api/v1/collection", tags=["download"]) -async def collection(link: RssLink): - response = api_func.download_collection(link.rss_link) - if response: - return response.dict() - else: - return {"status": "Failed to parse link"} - - -@router.post("/api/v1/subscribe", tags=["download"]) -async def subscribe(link: RssLink): - response = api_func.add_subscribe(link.rss_link) - if response: - return response.dict() - else: - return {"status": "Failed to parse link"} - - -@router.post("/api/v1/addRule", tags=["download"]) -async def add_rule(info: AddRule): - return api_func.add_rule(info.title, info.season) - - -@router.get("/api/v1/getConfig", tags=["config"]) -async def get_config(): - return api_func.get_config() - - -@router.post("/api/v1/updateConfig", tags=["config"]) -async def update_config(config: Config): - return api_func.update_config(config) - - -@router.get("/RSS/MyBangumi", tags=["proxy"]) -async def get_my_bangumi(token: str): - full_path = "MyBangumi?token=" + token - content = api_func.get_rss(full_path) - return Response(content, media_type="application/xml") - - -@router.get("/RSS/Search", tags=["proxy"]) -async def get_search_result(searchstr: str): - full_path = "Search?searchstr=" + searchstr - content = api_func.get_rss(full_path) - return Response(content, media_type="application/xml") - - -@router.get("/RSS/Bangumi", tags=["proxy"]) -async def get_bangumi(bangumiId: str, groupid: str): - full_path = "Bangumi?bangumiId=" + bangumiId + "&groupid=" + groupid - content = api_func.get_rss(full_path) - return Response(content, media_type="application/xml") - - -@router.get("/RSS/{full_path:path}", tags=["proxy"]) -async def get_rss(full_path: str): - content = api_func.get_rss(full_path) - return Response(content, media_type="application/xml") - - -@router.get("/Download/{full_path:path}", tags=["proxy"]) -async def download(full_path: str): - torrent = api_func.get_torrent(full_path) - return Response(torrent, media_type="application/x-bittorrent") diff --git a/src/module/api/__init__.py b/src/module/api/__init__.py new file mode 100644 index 00000000..546d37eb --- /dev/null +++ b/src/module/api/__init__.py @@ -0,0 +1 @@ +from .program import router diff --git a/src/module/api/api.py b/src/module/api/api.py new file mode 100644 index 00000000..d48cace2 --- /dev/null +++ b/src/module/api/api.py @@ -0,0 +1,29 @@ +import logging +import os + +from fastapi import FastAPI +from fastapi.responses import FileResponse, Response + +from module.conf import LOG_PATH + +logger = logging.getLogger(__name__) + +router = FastAPI() + + +@router.get("/api/v1/log", tags=["log"]) +async def get_log(): + if os.path.isfile(LOG_PATH): + return FileResponse(LOG_PATH) + else: + return Response("Log file not found", status_code=404) + + +@router.get("/api/v1/resetRule") +def reset_rule(): + pass + + + + + diff --git a/src/module/api/bangumi.py b/src/module/api/bangumi.py new file mode 100644 index 00000000..64c462ea --- /dev/null +++ b/src/module/api/bangumi.py @@ -0,0 +1,28 @@ +from .api import router + +from module.models import BangumiData +from module.database import BangumiDatabase +from module.manager import set_new_path + + +@router.get("/api/v1/bangumi/getData", tags=["bangumi"]) +async def get_all_data(): + with BangumiDatabase() as database: + return database.search_all() + + +@router.get("/api/v1/bangumi/getData/{bangumi_id}", tags=["bangumi"]) +async def get_data(bangumi_id: str): + with BangumiDatabase() as database: + data = database.search_id(int(bangumi_id)) + if data: + return data + else: + return {"": "data not exist"} + + +@router.post("/api/v1/bangumi/UpdateData", tags=["bangumi"]) +async def update_data(data: BangumiData): + with BangumiDatabase() as database: + database.update(data) + set_new_path(data) diff --git a/src/module/api/config.py b/src/module/api/config.py new file mode 100644 index 00000000..c58a98a6 --- /dev/null +++ b/src/module/api/config.py @@ -0,0 +1,16 @@ +from .bangumi import router + +from module.conf import settings +from module.models import Config + + +@router.get("/api/v1/getConfig", tags=["config"], response_model=Config) +async def get_config(): + return settings + + +# Reverse proxy +@router.post("/api/v1/updateConfig", tags=["config"], response_model=Config) +async def update_config(config: Config): + settings.save(config_dict=config.dict()) + return settings diff --git a/src/module/api/download.py b/src/module/api/download.py new file mode 100644 index 00000000..50849fa5 --- /dev/null +++ b/src/module/api/download.py @@ -0,0 +1,31 @@ +from .config import router + +from module.models.api import * +from module.manager import FullSeasonGet +from module.rss import RSSAnalyser + + +def link_process(link): + return RSSAnalyser().rss_to_data(link, full_parse=False) + + +@router.post("/api/v1/collection", tags=["download"]) +async def collection(link: RssLink): + data = link_process(link) + if data: + with FullSeasonGet() as season: + season.download_collection(data[0], link) + return data[0] + else: + return {"status": "Failed to parse link"} + + +@router.post("/api/v1/subscribe", tags=["download"]) +async def subscribe(link: RssLink): + data = link_process(link) + if data: + with FullSeasonGet() as season: + season.add_subscribe(data[0], link) + return data[0] + else: + return {"status": "Failed to parse link"} diff --git a/src/module/api/program.py b/src/module/api/program.py new file mode 100644 index 00000000..cf4b751c --- /dev/null +++ b/src/module/api/program.py @@ -0,0 +1,79 @@ +import os +import signal +import logging + +from fastapi import Request +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +from .proxy import router + +from module.core import start_thread, start_program, stop_thread, stop_event +from module.conf import VERSION, settings, setup_logger + + +logger = logging.getLogger(__name__) + + +@router.on_event("startup") +async def startup(): + log_level = logging.DEBUG if settings.log.debug_enable else logging.INFO + setup_logger(log_level) + start_program() + + +@router.on_event("shutdown") +async def shutdown(): + stop_event.set() + logger.info("Stopping program...") + + +@router.get("/api/v1/restart", tags=["program"]) +async def restart(): + stop_thread() + start_thread() + return {"status": "ok"} + + +@router.get("/api/v1/start", tags=["program"]) +async def start(): + start_thread() + return {"status": "ok"} + + +@router.get("/api/v1/stop", tags=["program"]) +async def stop(): + stop_thread() + return {"status": "ok"} + + +@router.get("/api/v1/status", tags=["program"]) +async def status(): + if stop_event.is_set(): + return {"status": "stop"} + else: + return {"status": "running"} + + +@router.get("/api/v1/shutdown", tags=["program"]) +async def shutdown_program(): + stop_thread() + logger.info("Shutting down program...") + os.kill(os.getpid(), signal.SIGINT) + return {"status": "ok"} + + +if VERSION != "DEV_VERSION": + router.mount("/assets", StaticFiles(directory="templates/assets"), name="assets") + templates = Jinja2Templates(directory="templates") + + # HTML Response + @router.get("/{full_path:path}", response_class=HTMLResponse, tags=["html"]) + def index(request: Request): + context = {"request": request} + return templates.TemplateResponse("index.html", context) +else: + @router.get("/", status_code=302, tags=["html"]) + def index(): + return RedirectResponse("/docs") \ No newline at end of file diff --git a/src/module/api/proxy.py b/src/module/api/proxy.py new file mode 100644 index 00000000..314b54f8 --- /dev/null +++ b/src/module/api/proxy.py @@ -0,0 +1,61 @@ +import re +import logging + +from fastapi.responses import Response + +from .download import router + +from module.conf import settings +from module.network import RequestContent + + +logger = logging.getLogger(__name__) + + +def get_rss(full_path): + url = f"https://mikanani.me/RSS/{full_path}" + custom_url = settings.rss_parser.custom_url + if "://" not in custom_url: + custom_url = f"https://{custom_url}" + with RequestContent() as request: + content = request.get_html(url) + return re.sub(r"https://mikanani.me", custom_url, content) + + +def get_torrent(full_path): + url = f"https://mikanani.me/Download/{full_path}" + with RequestContent() as request: + return request.get_content(url) + + +@router.get("/RSS/MyBangumi", tags=["proxy"]) +async def get_my_bangumi(token: str): + full_path = "MyBangumi?token=" + token + content = get_rss(full_path) + return Response(content, media_type="application/xml") + + +@router.get("/RSS/Search", tags=["proxy"]) +async def get_search_result(searchstr: str): + full_path = "Search?searchstr=" + searchstr + content = get_rss(full_path) + return Response(content, media_type="application/xml") + + +@router.get("/RSS/Bangumi", tags=["proxy"]) +async def get_bangumi(bangumiId: str, groupid: str): + full_path = "Bangumi?bangumiId=" + bangumiId + "&groupid=" + groupid + content = get_rss(full_path) + return Response(content, media_type="application/xml") + + +@router.get("/RSS/{full_path:path}", tags=["proxy"]) +async def get_rss(full_path: str): + content = get_rss(full_path) + return Response(content, media_type="application/xml") + + +@router.get("/Download/{full_path:path}", tags=["proxy"]) +async def download(full_path: str): + torrent = get_torrent(full_path) + return Response(torrent, media_type="application/x-bittorrent") \ No newline at end of file diff --git a/src/module/conf/config.py b/src/module/conf/config.py index f1239fe0..8a19d8df 100644 --- a/src/module/conf/config.py +++ b/src/module/conf/config.py @@ -41,8 +41,9 @@ class Settings(Config): self.__dict__.update(config_obj.__dict__) logger.info(f"Config loaded") - def save(self): - config_dict = self.dict() + def save(self, config_dict: dict | None = None): + if not config_dict: + config_dict = self.dict() with open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(config_dict, f, indent=4) logger.info(f"Config saved") diff --git a/src/module/core/__init__.py b/src/module/core/__init__.py index 9f94c2f1..14c45acb 100644 --- a/src/module/core/__init__.py +++ b/src/module/core/__init__.py @@ -1 +1,2 @@ from .api_func import APIProcess +from .sub_thread import * diff --git a/src/module/sub_thread.py b/src/module/core/sub_thread.py similarity index 91% rename from src/module/sub_thread.py rename to src/module/core/sub_thread.py index 9a2546d0..f8e5436a 100644 --- a/src/module/sub_thread.py +++ b/src/module/core/sub_thread.py @@ -7,8 +7,7 @@ from module.rss import RSSAnalyser, add_rules from module.manager import Renamer, FullSeasonGet from module.database import BangumiDatabase from module.downloader import DownloadClient -from module.conf import settings -from module.conf import VERSION +from module.conf import settings, VERSION, DATA_PATH logger = logging.getLogger(__name__) @@ -80,11 +79,12 @@ def start_thread(): def start_program(): global rss_thread, rename_thread start_info() + if not os.path.exists(DATA_PATH): + with DownloadClient() as client: + client.init_downloader() + client.add_rss_feed(settings.rss_link()) with BangumiDatabase() as database: database.update_table() - with DownloadClient() as client: - client.init_downloader() - client.add_rss_feed(settings.rss_link()) rss_thread = threading.Thread(target=rss_loop, args=(stop_event,)) rename_thread = threading.Thread(target=rename_loop, args=(stop_event,)) rss_thread.start() diff --git a/src/module/database/bangumi.py b/src/module/database/bangumi.py index 287f2c7d..7dc33e83 100644 --- a/src/module/database/bangumi.py +++ b/src/module/database/bangumi.py @@ -62,10 +62,10 @@ class BangumiDatabase(DataConnector): db_data[key] = item.split(",") return BangumiData(**db_data) - def __fetch_data(self) -> list[BangumiData] | None: + def __fetch_data(self) -> list[BangumiData]: values = self._cursor.fetchall() if values is None: - return None + return [] keys = [x[0] for x in self._cursor.description] dict_data = [dict(zip(keys, value)) for value in values] return [self.__db_to_data(x) for x in dict_data] @@ -112,6 +112,14 @@ class BangumiDatabase(DataConnector): self._conn.commit() logger.debug(f"Update {title_raw} rss_link to {rss_set}.") + def search_all(self) -> list[BangumiData]: + self._cursor.execute( + """ + SELECT * FROM bangumi + """ + ) + return self.__fetch_data() + def search_id(self, _id: int) -> BangumiData | None: self._cursor.execute( """ @@ -203,7 +211,7 @@ class BangumiDatabase(DataConnector): self.update_rss(title_raw, rss_set) return titles - def not_complete(self) -> list[BangumiData] | None: + def not_complete(self) -> list[BangumiData]: # Find eps_complete = False self._cursor.execute( """ @@ -212,7 +220,7 @@ class BangumiDatabase(DataConnector): ) return self.__fetch_data() - def not_added(self) -> list[BangumiData] | None: + def not_added(self) -> list[BangumiData]: self._cursor.execute( """ SELECT * FROM bangumi WHERE added = 0 diff --git a/src/module/downloader/download_client.py b/src/module/downloader/download_client.py index 20add8bd..3ac4223d 100644 --- a/src/module/downloader/download_client.py +++ b/src/module/downloader/download_client.py @@ -1,6 +1,5 @@ import re import logging -import os from module.models import BangumiData from module.conf import settings @@ -8,6 +7,11 @@ from module.conf import settings logger = logging.getLogger(__name__) +if ":\\" in settings.downloader.path: + import ntpath as path +else: + import os.path as path + class DownloadClient: def __init__(self): @@ -58,7 +62,7 @@ class DownloadClient: logger.debug(e) if self.download_path == "": prefs = self.client.get_app_prefs() - self.download_path = os.path.join(prefs["save_path"], "Bangumi") + self.download_path = path.join(prefs["save_path"], "Bangumi") def set_rule(self, info: BangumiData): official_name = f"{info.official_title}({info.year})" if info.year else info.official_title @@ -81,7 +85,7 @@ class DownloadClient: "addPaused": False, "assignedCategory": "Bangumi", "savePath": str( - os.path.join( + path.join( self.download_path, re.sub(r"[:/.]", " ", official_name).strip(), f"Season {season}", diff --git a/src/module/manager/__init__.py b/src/module/manager/__init__.py index 0dc4ebee..73979ee7 100644 --- a/src/module/manager/__init__.py +++ b/src/module/manager/__init__.py @@ -1,2 +1,3 @@ from .eps_complete import FullSeasonGet from .renamer import Renamer +from .repath import set_new_path diff --git a/src/module/manager/eps_complete.py b/src/module/manager/eps_complete.py index dcad48d6..888892fd 100644 --- a/src/module/manager/eps_complete.py +++ b/src/module/manager/eps_complete.py @@ -93,6 +93,10 @@ class FullSeasonGet(DownloadClient): self.add_torrent(download) logger.info("Completed!") + def add_subscribe(self, data: BangumiData, link): + self.add_rss_feed(link, item_path=data.official_title) + self.set_rule(data) + if __name__ == '__main__': from module.conf import setup_logger diff --git a/src/module/manager/repath.py b/src/module/manager/repath.py index 32ac1a9e..ce6b245e 100644 --- a/src/module/manager/repath.py +++ b/src/module/manager/repath.py @@ -1,91 +1,35 @@ import logging -import re -from dataclasses import dataclass -from pathlib import PurePath, PureWindowsPath -from module.core import DownloadClient +from module.downloader import DownloadClient from module.conf import settings -from module.utils import json_config +from module.models import BangumiData logger = logging.getLogger(__name__) -@dataclass -class RuleInfo: - rule_name: str - contain: str - season: int - folder_name: str - new_path: str +def __gen_path(data: BangumiData): + download_path = settings.downloader.path + if ":\\" in download_path: + import ntpath as path + else: + import os.path as path + folder = f"{data.official_title}({data.year})" if data.year else data.official_title + path = path.join(download_path, folder, f"Season {data.season}") + return path -@dataclass -class RepathInfo: - path: str - hashes: list +def match_torrents_list(title_raw: str) -> list: + with DownloadClient() as client: + torrents = client.get_torrent_info() + return [torrent.hash for torrent in torrents if title_raw in torrent.name] -class RepathTorrents: - def __init__(self, download_client: DownloadClient): - self._client = download_client - self.re_season = re.compile(r"S\d{1,2}") +def set_new_path(data: BangumiData): + with DownloadClient() as client: + # set download rule + client.set_rule(data) + # set torrent path + match_list = match_torrents_list(data.title_raw) + path = __gen_path(data) + client.move_torrent(match_list, path) - @staticmethod - def analyse_path(path: str): - path_parts = PurePath(path).parts - folder_name = path_parts[-2] - season_folder = path_parts[-1] - season = int(re.search(r"\d{1,2}", season_folder).group()) - return season, folder_name - - def get_rule(self) -> list[RuleInfo]: - rules = self._client.get_download_rules() - all_rule = [] - for rule in rules: - path = rules.get(rule).savePath - must_contain = rules.get(rule).mustContain - season, folder_name = self.analyse_path(path) - new_path = PurePath( - settings.downloader.path, folder_name, f"Season {season}" - ).__str__() - all_rule.append(RuleInfo(rule, must_contain, season, folder_name, new_path)) - return all_rule - - @staticmethod - def get_difference(bangumi_data: list, rules: list[RuleInfo]) -> list[RuleInfo]: - different_data = [] - for data in bangumi_data: - for rule in rules: - rule_name = re.sub(r"S\d", "", rule.rule_name).strip() - if data.get("official_title") == rule_name: - if data.get("season") != rule.season: - different_data.append(rule) - data["season"] = rule.season - break - return different_data - - def get_matched_torrents_list( - self, repath_rules: list[RuleInfo] - ) -> list[RepathInfo]: - infos = self._client.get_torrent_info() - repath_list = [] - for rule in repath_rules: - hashes = [] - for info in infos: - if re.search(rule.contain, info.name): - if rule.new_path != info.save_path: - hashes.append(info.hash) - infos.remove(info) - if hashes: - repath_list.append(RepathInfo(rule.new_path, hashes)) - return repath_list - - def re_path(self, repath_info: RepathInfo): - self._client.move_torrent(repath_info.hashes, repath_info.path) - - def run(self): - rules = self.get_rule() - match_list = self.get_matched_torrents_list(rules) - logging.info(f"Starting repath process.") - for list in match_list: - self.re_path(list)