From 012b3f9f3175cd6b0677f58605fe7bd7d8d33076 Mon Sep 17 00:00:00 2001 From: amtoaer Date: Sat, 2 Dec 2023 00:26:19 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B0=9D=E8=AF=95=E4=BF=AE=E5=A4=8D=20e?= =?UTF-8?q?mby=20=E5=A4=B4=E5=83=8F=E8=B7=AF=E5=BE=84=EF=BC=8C=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E5=8C=96=E6=89=80=E6=9C=89=E6=96=87=E4=BB=B6=E6=93=8D?= =?UTF-8?q?=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- commands.py | 31 ++++++++++++++++++++++++++----- entry.py | 21 ++++++++++----------- models.py | 27 ++++++++++++++++++++++++++- nfo.py | 10 +++++----- processor.py | 34 +++++++++++++--------------------- utils.py | 33 +++++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 43 deletions(-) create mode 100644 utils.py diff --git a/commands.py b/commands.py index 7beb617..a5dbddd 100644 --- a/commands.py +++ b/commands.py @@ -1,10 +1,11 @@ import asyncio -from aiofiles.os import path from loguru import logger from constants import MediaStatus, MediaType -from models import FavoriteItem +from models import FavoriteItem, Upper +from processor import download_content +from utils import aexists, amakedirs async def recheck(): @@ -14,9 +15,7 @@ async def recheck(): status=MediaStatus.NORMAL, downloaded=True, ) - exists = await asyncio.gather( - *[path.exists(item.video_path) for item in items] - ) + exists = await asyncio.gather(*[aexists(item.video_path) for item in items]) for item, exist in zip(items, exists): if isinstance(exist, Exception): logger.error( @@ -36,3 +35,25 @@ async def recheck(): logger.info("Updating database...") await FavoriteItem.bulk_update(items, fields=["downloaded"]) logger.info("Database updated.") + + +async def upper_thumb(): + makedir_tasks = [] + other_tasks = [] + for upper in await Upper.all(): + if not all( + await asyncio.gather( + aexists(upper.thumb_path), aexists(upper.meta_path) + ) + ): + makedir_tasks.append( + amakedirs(upper.thumb_path.parent, exist_ok=True) + ) + other_tasks.extend( + [ + upper.save_metadata(), + download_content(upper.thumb_url, upper.thumb_path), + ] + ) + await asyncio.gather(*makedir_tasks) + await asyncio.gather(*other_tasks) diff --git a/entry.py b/entry.py index 4abf665..cdf8527 100644 --- a/entry.py +++ b/entry.py @@ -4,7 +4,7 @@ import uvloop from loguru import logger -from commands import recheck +from commands import recheck, upper_thumb from models import init_model from processor import cleanup, process from settings import settings @@ -14,16 +14,15 @@ async def entry() -> None: await init_model() - if any("once" in _ for _ in sys.argv): - # 单次运行 - logger.info("Running once...") - await process() - return - if any("recheck" in _ for _ in sys.argv): - # 重新检查 - logger.info("Rechecking...") - await recheck() - return + for command, func in [ + ("once", process), + ("recheck", recheck), + ("upper_thumb", upper_thumb), + ]: + if any(command in _ for _ in sys.argv): + logger.info("Running {}...", command) + await func() + return logger.info("Running daemon...") while True: await process() diff --git a/models.py b/models.py index 44ae533..c018f27 100644 --- a/models.py +++ b/models.py @@ -13,6 +13,7 @@ MediaType, ) from settings import settings +from utils import aopen class FavoriteList(Model): @@ -39,7 +40,31 @@ class Upper(Model): @property def thumb_path(self) -> Path: - return DEFAULT_THUMB_PATH / f"{self.mid}.jpg" + return ( + DEFAULT_THUMB_PATH / f"{self.mid[0]}" / f"{self.mid}" / "folder.jpg" + ) + + @property + def meta_path(self) -> Path: + return ( + DEFAULT_THUMB_PATH / f"{self.mid[0]}" / f"{self.mid}" / "person.nfo" + ) + + async def save_metadata(self): + async with aopen(self.meta_path, "w") as f: + await f.write( + f""" + + + + + false + {self.created_at.strftime("%Y-%m-%d %H:%M:%S")} + {self.mid} + {self.mid} + +""".strip() + ) class FavoriteItem(Model): diff --git a/nfo.py b/nfo.py index a515d1d..049a0b1 100644 --- a/nfo.py +++ b/nfo.py @@ -2,19 +2,19 @@ from dataclasses import dataclass from pathlib import Path +from utils import aopen + @dataclass class Actor: name: str role: str - thumb: Path def to_xml(self) -> str: return f""" {self.name} {self.role} - {self.thumb.resolve()} """.strip( "\n" @@ -29,9 +29,9 @@ class EpisodeInfo: bvid: str aired: datetime.datetime - def write_nfo(self, path: Path) -> None: - with path.open("w", encoding="utf-8") as f: - f.write(self.to_xml()) + async def write_nfo(self, path: Path) -> None: + async with aopen(path, "w", encoding="utf-8") as f: + await f.write(self.to_xml()) def to_xml(self) -> str: actor = "\n".join(_.to_xml() for _ in self.actor) diff --git a/processor.py b/processor.py index 14a813d..a5c2df7 100644 --- a/processor.py +++ b/processor.py @@ -2,11 +2,8 @@ import datetime from asyncio import Semaphore, create_subprocess_exec from asyncio.subprocess import DEVNULL -from pathlib import Path -import aiofiles -import httpx -from bilibili_api import HEADERS, favorite_list, video +from bilibili_api import favorite_list, video from bilibili_api.exceptions import ResponseCodeException from loguru import logger from tortoise import Tortoise @@ -16,8 +13,7 @@ from models import FavoriteItem, FavoriteList, Upper from nfo import Actor, EpisodeInfo from settings import settings - -client = httpx.AsyncClient(headers=HEADERS) +from utils import aexists, amakedirs, client, download_content anchor = datetime.date.today() @@ -40,16 +36,6 @@ async def wrapper(*args, **kwargs) -> any: return decorator -async def download_content(url: str, path: Path) -> None: - async with client.stream("GET", url) as resp, aiofiles.open( - path, "wb" - ) as f: - async for chunk in resp.aiter_bytes(40960): - if not chunk: - return - await f.write(chunk) - - async def manage_model(medias: list[dict], fav_list: FavoriteList) -> None: uppers = [ Upper( @@ -174,7 +160,7 @@ async def process_video(fav_item: FavoriteItem) -> None: logger.warning("Media {} is not a video, skipped.", fav_item.name) return try: - if fav_item.video_path.exists(): + if await aexists(fav_item.video_path): fav_item.downloaded = True await fav_item.save() logger.info( @@ -182,19 +168,25 @@ async def process_video(fav_item: FavoriteItem) -> None: ) return # 写入 up 主头像 - if not fav_item.upper.thumb_path.exists(): + if not all( + await asyncio.gather( + aexists(fav_item.upper.thumb_path), + aexists(fav_item.upper.meta_path), + ) + ): + await amakedirs(fav_item.upper.thumb_path.parent, exist_ok=True) + await fav_item.upper.save_metadata() await download_content( - fav_item.upper.thumb, fav_item.upper.thumb_path + fav_item.upper.thumb_url, fav_item.upper.thumb_path ) # 写入 nfo - EpisodeInfo( + await EpisodeInfo( title=fav_item.name, plot=fav_item.desc, actor=[ Actor( name=fav_item.upper.mid, role=fav_item.upper.name, - thumb=fav_item.upper.thumb_path, ) ], bvid=fav_item.bvid, diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..068670c --- /dev/null +++ b/utils.py @@ -0,0 +1,33 @@ +from pathlib import Path + +import aiofiles +import httpx +from aiofiles.base import AiofilesContextManager +from aiofiles.os import makedirs +from aiofiles.ospath import exists +from aiofiles.threadpool.text import AsyncTextIOWrapper +from bilibili_api import HEADERS + +client = httpx.AsyncClient(headers=HEADERS) + + +async def download_content(url: str, path: Path) -> None: + async with client.stream("GET", url) as resp, aopen(path, "wb") as f: + async for chunk in resp.aiter_bytes(40960): + if not chunk: + return + await f.write(chunk) + + +async def aexists(path: Path) -> bool: + return await exists(path) + + +async def amakedirs(path: Path, exist_ok=False) -> None: + await makedirs(path, parents=True, exist_ok=exist_ok) + + +def aopen( + path: Path, mode: str = "r", **kwargs +) -> AiofilesContextManager[None, None, AsyncTextIOWrapper]: + return aiofiles.open(path, mode, **kwargs)