diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d72a9de --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.11.6-alpine3.18 AS base + +WORKDIR /app + +ENV BILI_IN_DOCKER=true + +COPY poetry.lock pyproject.toml ./ + +RUN apk add ffmpeg \ + && apk add --no-cache --virtual .build-deps \ + gcc \ + musl-dev \ + libffi-dev \ + openssl-dev \ + && pip install poetry \ + && poetry config virtualenvs.create false \ + && poetry install --no-dev --no-interaction --no-ansi \ + && apk del .build-deps + +COPY . . + +ENTRYPOINT [ "python", "entry.py" ] \ No newline at end of file diff --git a/constants.py b/constants.py index 8c260ee..1d09ca2 100644 --- a/constants.py +++ b/constants.py @@ -2,23 +2,22 @@ import os from enum import IntEnum from pathlib import Path -DEFAULT_CONFIG_PATH = ( - Path(__file__).parent / "config.json" - if not os.getenv("TESTING") - else Path(__file__).parent / "config.test.json" -) -DEFAULT_DATABASE_PATH = ( - Path(__file__).parent / "database.db" - if not os.getenv("TESTING") - else Path(__file__).parent / "database.test.db" -) +def get_base(dir_name: str) -> Path: + path = ( + Path(base) + if (base := os.getenv(f"{dir_name.upper()}_PATH")) + else Path(__file__).parent / dir_name + ) + path.mkdir(parents=True, exist_ok=True) + return path -DEFAULT_THUMB_PATH = ( - Path(__file__).parent / "thumbs" - if not os.getenv("TESTING") - else Path(__file__).parent / "thumbs.test" -) + +DEFAULT_CONFIG_PATH = get_base("config") / "config.json" + +DEFAULT_DATABASE_PATH = get_base("data") / "data.db" + +DEFAULT_THUMB_PATH = get_base("thumb") FFMPEG_COMMAND = "ffmpeg" diff --git a/migrations/models/0_20231125010440_init.py b/migrations/models/0_20231125111222_init.py similarity index 92% rename from migrations/models/0_20231125010440_init.py rename to migrations/models/0_20231125111222_init.py index 5a28fe4..b357812 100644 --- a/migrations/models/0_20231125010440_init.py +++ b/migrations/models/0_20231125111222_init.py @@ -27,6 +27,8 @@ CREATE TABLE IF NOT EXISTS "favoriteitem" ( "pubtime" TIMESTAMP NOT NULL, "fav_time" TIMESTAMP NOT NULL, "downloaded" INT NOT NULL DEFAULT 0, + "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "favorite_list_id" INT NOT NULL REFERENCES "favoritelist" ("id") ON DELETE CASCADE, "upper_id" INT NOT NULL REFERENCES "upper" ("mid") ON DELETE CASCADE, CONSTRAINT "uid_favoriteite_bvid_d7b8ea" UNIQUE ("bvid", "favorite_list_id") diff --git a/models.py b/models.py index bf4c643..3842921 100644 --- a/models.py +++ b/models.py @@ -1,3 +1,4 @@ +import os from asyncio import create_subprocess_exec from pathlib import Path @@ -57,6 +58,8 @@ class FavoriteItem(Model): pubtime = fields.DatetimeField() fav_time = fields.DatetimeField() downloaded = fields.BooleanField(default=False) + created_at = fields.DatetimeField(auto_now_add=True) + updated_at = fields.DatetimeField(auto_now=True) class Meta: unique_together = (("bvid", "favorite_list_id"),) @@ -103,10 +106,10 @@ class FavoriteItem(Model): async def init_model() -> None: await Tortoise.init(config=TORTOISE_ORM) - process = await create_subprocess_exec( - "poetry", - "run", - MIGRATE_COMMAND, - "upgrade", + migrate_commands = ( + [MIGRATE_COMMAND, "upgrade"] + if os.getenv("BILI_IN_DOCKER") + else ["poetry", "run", MIGRATE_COMMAND, "upgrade"] ) + process = await create_subprocess_exec(*migrate_commands) await process.communicate() diff --git a/processor.py b/processor.py index cbef43b..2c969b1 100644 --- a/processor.py +++ b/processor.py @@ -10,7 +10,7 @@ from bilibili_api import HEADERS, favorite_list, video from loguru import logger from tortoise import Tortoise -from constants import DEFAULT_THUMB_PATH, FFMPEG_COMMAND, MediaType +from constants import FFMPEG_COMMAND, MediaType from credential import credential from models import FavoriteItem, FavoriteList, Upper from nfo import Actor, EpisodeInfo @@ -126,7 +126,6 @@ async def process_favorite(favorite_id: int) -> None: id=favorite_id, defaults={"name": favorite_video_list["info"]["title"]} ) fav_list.video_list_path.mkdir(parents=True, exist_ok=True) - DEFAULT_THUMB_PATH.mkdir(parents=True, exist_ok=True) page = 0 while True: page += 1 @@ -169,70 +168,77 @@ async def process_video(fav_item: FavoriteItem) -> None: if fav_item.type != MediaType.VIDEO: logger.warning("Media {} is not a video, skipped.", fav_item.name) return - if fav_item.video_path.exists(): + try: + if fav_item.video_path.exists(): + fav_item.downloaded = True + await fav_item.save() + logger.info( + "{} {} already exists, skipped.", fav_item.bvid, fav_item.name + ) + return + # 写入 up 主头像 + if not fav_item.upper.thumb_path.exists(): + await download_content( + fav_item.upper.thumb, fav_item.upper.thumb_path + ) + # 写入 nfo + 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, + aired=fav_item.ctime, + ).write_nfo(fav_item.nfo_path) + # 写入 poster + await download_content(fav_item.cover, fav_item.poster_path) + # 开始处理视频内容 + v = video.Video(fav_item.bvid, credential=credential) + detector = video.VideoDownloadURLDataDetecter( + await v.get_download_url(page_index=0) + ) + streams = detector.detect_best_streams() + if detector.check_flv_stream(): + await download_content(streams[0].url, fav_item.tmp_video_path) + process = await create_subprocess_exec( + FFMPEG_COMMAND, + "-i", + str(fav_item.tmp_video_path), + str(fav_item.video_path), + stdout=DEVNULL, + stderr=DEVNULL, + ) + await process.communicate() + fav_item.tmp_video_path.unlink() + else: + await asyncio.gather( + download_content(streams[0].url, fav_item.tmp_video_path), + download_content(streams[1].url, fav_item.tmp_audio_path), + ) + process = await create_subprocess_exec( + FFMPEG_COMMAND, + "-i", + str(fav_item.tmp_video_path), + "-i", + str(fav_item.tmp_audio_path), + "-c", + "copy", + str(fav_item.video_path), + stdout=DEVNULL, + stderr=DEVNULL, + ) + await process.communicate() + fav_item.tmp_video_path.unlink() + fav_item.tmp_audio_path.unlink() fav_item.downloaded = True await fav_item.save() logger.info( - "{} {} already exists, skipped.", fav_item.bvid, fav_item.name + "{} {} processed successfully.", fav_item.bvid, fav_item.name ) - return - # 写入 up 主头像 - if not fav_item.upper.thumb_path.exists(): - await download_content(fav_item.upper.thumb, fav_item.upper.thumb_path) - # 写入 nfo - 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, - aired=fav_item.ctime, - ).write_nfo(fav_item.nfo_path) - # 写入 poster - await download_content(fav_item.cover, fav_item.poster_path) - # 开始处理视频内容 - v = video.Video(fav_item.bvid, credential=credential) - detector = video.VideoDownloadURLDataDetecter( - await v.get_download_url(page_index=0) - ) - streams = detector.detect_best_streams() - if detector.check_flv_stream(): - await download_content(streams[0].url, fav_item.tmp_video_path) - process = await create_subprocess_exec( - FFMPEG_COMMAND, - "-i", - str(fav_item.tmp_video_path), - str(fav_item.video_path), - stdout=DEVNULL, - stderr=DEVNULL, - ) - await process.communicate() - fav_item.tmp_video_path.unlink() - else: - await asyncio.gather( - download_content(streams[0].url, fav_item.tmp_video_path), - download_content(streams[1].url, fav_item.tmp_audio_path), - ) - process = await create_subprocess_exec( - FFMPEG_COMMAND, - "-i", - str(fav_item.tmp_video_path), - "-i", - str(fav_item.tmp_audio_path), - "-c", - "copy", - str(fav_item.video_path), - stdout=DEVNULL, - stderr=DEVNULL, - ) - await process.communicate() - fav_item.tmp_video_path.unlink() - fav_item.tmp_audio_path.unlink() - fav_item.downloaded = True - await fav_item.save() - logger.info("{} {} processed successfully.", fav_item.bvid, fav_item.name) + except Exception: + logger.exception("Failed to process video {}", fav_item.name)