Compare commits

...

23 Commits
1.1.0 ... 1.1.4

Author SHA1 Message Date
amtoaer
668c67da53 chore: bump version from 1.1.3 to 1.1.4 2024-01-20 15:50:12 +08:00
ᴀᴍᴛᴏᴀᴇʀ
9204bbb4ad fix: 修复新的配置项没有写入配置文件的问题,扩充单行字符限制 (#33) 2024-01-20 15:37:43 +08:00
ᴀᴍᴛᴏᴀᴇʀ
d467750d4f feat: 支持指定编码优先级 (#32) 2024-01-20 15:16:48 +08:00
amtoaer
641cc3f48b chore: 优化 dockerfile,缩小镜像体积 2024-01-06 02:13:00 +08:00
amtoaer
345c764463 fix: 修复 docker 退出时不会释放资源的问题 2024-01-06 00:41:28 +08:00
amtoaer
85b7d3dc9b chore: 恢复 dockerfile 写法,难以复用缓存但减小容器体积 2024-01-05 23:34:32 +08:00
amtoaer
f1ada17f30 chore: bump version from 1.1.2 to 1.1.3 2024-01-05 01:15:12 +08:00
amtoaer
cb0ac7eb67 chore: 开启自动提交和自动标签 2024-01-05 01:13:36 +08:00
amtoaer
31efedbde9 chore: 修复依赖异常,优化 dockerfile 流程 2024-01-05 01:11:10 +08:00
amtoaer
3defb07325 chore: 存版本号并添加入口,方便触发版本间的迁移逻辑 2024-01-04 22:13:03 +08:00
amtoaer
e36f829e70 chore: 引入 bump-version 并正确设置版本号 2024-01-04 22:04:10 +08:00
amtoaer
c20b579523 chore: 排序一下依赖 2024-01-04 21:54:27 +08:00
amtoaer
ceec222604 chore: 更新上游依赖,修复刷新 cookie 失败的错误 2024-01-04 21:50:28 +08:00
amtoaer
60ea7795ae chore: 修改基础镜像标签 2024-01-04 21:07:08 +08:00
DDSDerek
6cbacbd127 chore: Optimization docker (#17)
* feat: docker build adds cache

* fix: dockerfile optimization

* doc: dockerhub pictures are not displayed properly

---------

Co-authored-by: DDSRem <1448139087@qq.com>
2024-01-04 20:51:03 +08:00
DDSDerek
8ea2fbe0f9 fix: docker meta username error (#16)
Co-authored-by: DDSRem <1448139087@qq.com>
2023-12-30 14:31:48 +08:00
DDSDerek
e3fded16ac feat: support arm64 architecture (#15)
Co-authored-by: DDSRem <1448139087@qq.com>
2023-12-30 14:22:26 +08:00
amtoaer
961913c4fb doc: 加入字幕相关文档 2023-12-07 22:11:37 +08:00
amtoaer
fa20e5efee feat: 开放弹幕的各项设置 2023-12-07 21:45:18 +08:00
amtoaer
38fb0a4560 fix: 安全地移除配置项 2023-12-07 21:29:57 +08:00
amtoaer
9e94e3b73e chore: try except 按块分割,移除无用的设置项 2023-12-07 21:15:40 +08:00
amtoaer
b955a9fe45 chore: 替换掉被标记 deprecated 的方法 2023-12-06 18:17:17 +08:00
amtoaer
9d151b4731 feat: 命令默认不覆盖现有内容,更新文档 2023-12-06 01:19:08 +08:00
13 changed files with 1025 additions and 689 deletions

View File

@@ -12,18 +12,37 @@ jobs:
-
name: Checkout
uses: actions/checkout@v3
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/bili-sync
tags: |
type=raw,value=debug
-
name: Set Up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set Up Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
-
name: Build and push images
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
file: Dockerfile
platforms: |
linux/amd64
linux/arm64/v8
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/bili-sync:debug
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha, scope=${{ github.workflow }}
cache-to: type=gha, scope=${{ github.workflow }}

View File

@@ -12,22 +12,41 @@ jobs:
-
name: Checkout
uses: actions/checkout@v3
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/bili-sync
tags: |
type=raw,value=${{ github.ref_name }}
type=raw,value=latest
-
name: Set Up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set Up Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
-
name: Build and push images
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
file: Dockerfile
platforms: |
linux/amd64
linux/arm64/v8
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/bili-sync:${{ github.ref_name }}
${{ secrets.DOCKERHUB_USERNAME }}/bili-sync:latest
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha, scope=${{ github.workflow }}
cache-to: type=gha, scope=${{ github.workflow }}
-
name: Update DockerHub description
uses: peter-evans/dockerhub-description@v3

View File

@@ -1,22 +1,41 @@
FROM python:3.11.6-alpine3.18 AS base
FROM python:3.11.7-alpine3.19 as base
WORKDIR /app
ENV BILI_IN_DOCKER=true
ENV LANG=zh_CN.UTF-8 \
TZ=Asia/Shanghai \
BILI_IN_DOCKER=true
RUN apk add --no-cache ffmpeg tini \
&& apk add --no-cache --virtual .build-deps \
gcc \
musl-dev \
libffi-dev \
openssl-dev \
&& pip install poetry==1.7.1 pip3-autoremove==1.2.0
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
RUN poetry config virtualenvs.create false \
&& poetry install --only main --no-root \
&& pip3-autoremove -y poetry pip3-autoremove \
&& apk del .build-deps \
&& rm -rf \
/root/.cache \
/tmp/*
COPY . .
ENTRYPOINT [ "python", "entry.py" ]
FROM scratch
WORKDIR /app
ENV LANG=zh_CN.UTF-8 \
TZ=Asia/Shanghai \
BILI_IN_DOCKER=true
COPY --from=base / /
ENTRYPOINT [ "tini", "python", "entry.py" ]
VOLUME [ "/app/config", "/app/data", "/app/thumb", "/Videos/Bilibilis" ]

View File

@@ -13,15 +13,23 @@
## 工作截图
![下载视频](asset/run.png)
![下载视频](https://raw.githubusercontent.com/amtoaer/bili-sync/main/asset/run.png)
![EMBY 识别](asset/emby.png)
![EMBY 识别](https://raw.githubusercontent.com/amtoaer/bili-sync/main/asset/emby.png)
## 配置文件
对于配置文件的前五项,请参考[凭据获取流程](https://nemo2011.github.io/bilibili-api/#/get-credential)。
```python
@dataclass
class SubtitleConfig(DataClassJsonMixin):
font_name: str = "微软雅黑,黑体" # 字体
font_size: float = 40 # 字号
alpha: float = 0.8 # 透明度
fly_time: float = 5 # 滚动弹幕持续时间
static_time: float = 10 # 静态弹幕持续时间
class Config(DataClassJsonMixin):
sessdata: str = ""
bili_jct: str = ""
@@ -29,8 +37,8 @@ class Config(DataClassJsonMixin):
dedeuserid: str = ""
ac_time_value: str = ""
interval: int = 20 # 任务执行的间隔时间
favorite_ids: list[int] = field(default_factory=list) # 收藏夹的 id
path_mapper: dict[int, str] = field(default_factory=dict) # 收藏夹的 id 到存储目录的映射
subtitle: SubtitleConfig = field(default_factory=SubtitleConfig) # 字幕相关设置
```
程序默认会将配置文件存储于 `${程序路径}/config/config.json`,数据库文件存储于 `${程序路径}/data/data.db`,如果发现不存在则新建并写入初始配置。
@@ -75,11 +83,15 @@ services:
"dedeuserid": "xxxxxxxxxxxxxxxxxx",
"ac_time_value": "xxxxxxxxxxxxxxxxxx",
"interval": 20,
"favorite_ids": [
711322958
],
"path_mapper": {
"711322958": "/Videos/Bilibilis/Bilibili-711322958/"
},
"subtitle": {
"font_name": "微软雅黑,黑体",
"font_size": 40.0,
"alpha": 0.8,
"fly_time": 5.0,
"static_time": 10.0
}
}
```
@@ -88,15 +100,29 @@ services:
为满足需要,该应用包含几个单独的命令,可在程序目录下使用 `python entry.py ${command name}` 运行。
1. `once`
1. `once`
处理收藏夹,和一般定时任务触发时执行的操作完全相同,但仅运行一次。
2. `recheck`
将本地不存在的视频文件标记成未下载,下次定时任务触发时将一并下载。
3. `upper_thumb`
3. `refresh_refresh_poster`
手动触发全量下载 up 主头像,为使用老版本时下载的没有 up 头像的视频添加头像
更新本地视频的封面
3. `refresh_upper`
更新本地up的头像和元数据。
3. `refresh_nfo`
更新本地视频的元数据。(如标签、标题等信息)
3. `refresh_video`
更新本地的视频源文件。
3. `refresh_subtitle`
更新本地的弹幕文件。
**对于以 refresh 开头的命令,均支持 --force 参数,如果有 --force 参数,将全量覆盖对应内容,否则默认仅更新缺失的部分。**
## 路线图

View File

@@ -47,12 +47,15 @@ async def _refresh_favorite_item_info(
process_nfo: bool = False,
process_upper: bool = False,
process_subtitle: bool = False,
force: bool = False,
):
items = await FavoriteItem.filter(downloaded=True).prefetch_related("upper")
await asyncio.gather(
*[aremove(path) for item in items for path in path_getter(item)],
return_exceptions=True,
)
if force:
# 如果强制刷新,那么就先把现存的所有内容删除
await asyncio.gather(
*[aremove(path) for item in items for path in path_getter(item)],
return_exceptions=True,
)
await asyncio.gather(
*[
process_favorite_item(

View File

@@ -1,4 +1,6 @@
import asyncio
import os
import signal
import sys
import uvloop
@@ -21,6 +23,7 @@ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
async def entry() -> None:
await init_model()
force = any("force" in _ for _ in sys.argv)
for command, func in (
("once", process),
("recheck", recheck),
@@ -32,7 +35,10 @@ async def entry() -> None:
):
if any(command in _ for _ in sys.argv):
logger.info("Running {}...", command)
await func()
if command.startswith("refresh"):
await func(force=force)
else:
await func()
return
logger.info("Running daemon...")
while True:
@@ -41,8 +47,16 @@ async def entry() -> None:
if __name__ == "__main__":
# 确保 docker 退出时正确触发资源释放
signal.signal(signal.SIGTERM, lambda *_: os.kill(os.getpid(), signal.SIGINT))
with asyncio.Runner() as runner:
try:
runner.run(entry())
except Exception:
logger.exception("Unexpected error occurred, exiting...")
except KeyboardInterrupt:
logger.error("Exit Signal Received, exiting...")
finally:
logger.info("Cleaning up resources...")
runner.run(cleanup())
logger.info("Done, exited.")

View File

@@ -0,0 +1,14 @@
from tortoise import BaseDBAsyncClient
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
CREATE TABLE IF NOT EXISTS "program" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"version" VARCHAR(20) NOT NULL
);"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
DROP TABLE IF EXISTS "program";"""

View File

@@ -14,6 +14,7 @@ from constants import (
)
from settings import settings
from utils import aopen
from version import VERSION
class FavoriteList(Model):
@@ -40,15 +41,11 @@ class Upper(Model):
@property
def thumb_path(self) -> Path:
return (
DEFAULT_THUMB_PATH / str(self.mid)[0] / f"{self.mid}" / "folder.jpg"
)
return DEFAULT_THUMB_PATH / str(self.mid)[0] / f"{self.mid}" / "folder.jpg"
@property
def meta_path(self) -> Path:
return (
DEFAULT_THUMB_PATH / str(self.mid)[0] / f"{self.mid}" / "person.nfo"
)
return DEFAULT_THUMB_PATH / str(self.mid)[0] / f"{self.mid}" / "person.nfo"
async def save_metadata(self):
async with aopen(self.meta_path, "w") as f:
@@ -73,16 +70,12 @@ class FavoriteItem(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=255)
type = fields.IntEnumField(enum_type=MediaType)
status = fields.IntEnumField(
enum_type=MediaStatus, default=MediaStatus.NORMAL
)
status = fields.IntEnumField(enum_type=MediaStatus, default=MediaStatus.NORMAL)
bvid = fields.CharField(max_length=255)
desc = fields.TextField()
cover = fields.TextField()
tags = fields.JSONField(null=True)
favorite_list = fields.ForeignKeyField(
"models.FavoriteList", related_name="items"
)
favorite_list = fields.ForeignKeyField("models.FavoriteList", related_name="items")
upper = fields.ForeignKeyField("models.Upper", related_name="uploads")
ctime = fields.DatetimeField()
pubtime = fields.DatetimeField()
@@ -100,38 +93,23 @@ class FavoriteItem(Model):
@property
def tmp_video_path(self) -> Path:
return (
Path(settings.path_mapper[self.favorite_list_id])
/ f"tmp_{self.bvid}_video"
)
return Path(settings.path_mapper[self.favorite_list_id]) / f"tmp_{self.bvid}_video"
@property
def tmp_audio_path(self) -> Path:
return (
Path(settings.path_mapper[self.favorite_list_id])
/ f"tmp_{self.bvid}_audio"
)
return Path(settings.path_mapper[self.favorite_list_id]) / f"tmp_{self.bvid}_audio"
@property
def video_path(self) -> Path:
return (
Path(settings.path_mapper[self.favorite_list_id])
/ f"{self.bvid}.mp4"
)
return Path(settings.path_mapper[self.favorite_list_id]) / f"{self.bvid}.mp4"
@property
def nfo_path(self) -> Path:
return (
Path(settings.path_mapper[self.favorite_list_id])
/ f"{self.bvid}.nfo"
)
return Path(settings.path_mapper[self.favorite_list_id]) / f"{self.bvid}.nfo"
@property
def poster_path(self) -> Path:
return (
Path(settings.path_mapper[self.favorite_list_id])
/ f"{self.bvid}-poster.jpg"
)
return Path(settings.path_mapper[self.favorite_list_id]) / f"{self.bvid}-poster.jpg"
@property
def upper_path(self) -> list[Path]:
@@ -142,10 +120,12 @@ class FavoriteItem(Model):
@property
def subtitle_path(self) -> Path:
return (
Path(settings.path_mapper[self.favorite_list_id])
/ f"{self.bvid}.zh-CN.default.ass"
)
return Path(settings.path_mapper[self.favorite_list_id]) / f"{self.bvid}.zh-CN.default.ass"
class Program(Model):
id = fields.IntField(pk=True)
version = fields.CharField(max_length=20)
async def init_model() -> None:
@@ -157,3 +137,13 @@ async def init_model() -> None:
)
process = await create_subprocess_exec(*migrate_commands)
await process.communicate()
program, created = await Program.get_or_create(
defaults={
"version": VERSION,
}
)
if created or program.version != VERSION:
# 把新版本的迁移逻辑写在这里
pass
program.version = VERSION
await program.save()

1165
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ from asyncio.subprocess import DEVNULL
from bilibili_api import ass, favorite_list, video
from bilibili_api.exceptions import ResponseCodeException
from loguru import logger
from tortoise import Tortoise
from tortoise.connection import connections
from constants import FFMPEG_COMMAND, MediaStatus, MediaType
from credential import credential
@@ -20,7 +20,7 @@ anchor = datetime.date.today()
async def cleanup() -> None:
await client.aclose()
await Tortoise.close_connections()
await connections.close_all()
def concurrent_decorator(concurrency: int) -> callable:
@@ -45,9 +45,7 @@ async def manage_model(medias: list[dict], fav_list: FavoriteList) -> None:
)
for media in medias
]
await Upper.bulk_create(
uppers, on_conflict=["mid"], update_fields=["name", "thumb"]
)
await Upper.bulk_create(uppers, on_conflict=["mid"], update_fields=["name", "thumb"])
items = [
FavoriteItem(
name=media["title"],
@@ -91,12 +89,7 @@ async def process() -> None:
except Exception:
logger.exception("Failed to refresh credential.")
return
for favorite_id in settings.favorite_ids:
if favorite_id not in settings.path_mapper:
logger.warning(
f"Favorite {favorite_id} not in path mapper, ignored."
)
continue
for favorite_id in settings.path_mapper:
await process_favorite(favorite_id)
@@ -119,10 +112,8 @@ async def process_favorite(favorite_id: int) -> None:
while True:
page += 1
if page > 1:
favorite_video_list = (
await favorite_list.get_video_favorite_list_content(
favorite_id, page=page, credential=credential
)
favorite_video_list = await favorite_list.get_video_favorite_list_content(
favorite_id, page=page, credential=credential
)
# 先看看对应 bvid 的记录是否存在
existed_items = await FavoriteItem.filter(
@@ -130,14 +121,10 @@ async def process_favorite(favorite_id: int) -> None:
bvid__in=[media["bvid"] for media in favorite_video_list["medias"]],
)
# 记录一下获得的列表中的 bvid 和 fav_time
media_info = {
(media["bvid"], media["fav_time"])
for media in favorite_video_list["medias"]
}
media_info = {(media["bvid"], media["fav_time"]) for media in favorite_video_list["medias"]}
# 如果有 bvid 和 fav_time 都相同的记录,说明已经到达了上次处理到的位置
continue_flag = not media_info & {
(item.bvid, int(item.fav_time.timestamp()))
for item in existed_items
(item.bvid, int(item.fav_time.timestamp())) for item in existed_items
}
await manage_model(favorite_video_list["medias"], fav_list)
if not (continue_flag and favorite_video_list["has_more"]):
@@ -169,9 +156,19 @@ async def process_favorite_item(
logger.warning("Media {} is not a video, skipped.", fav_item.name)
return
v = video.Video(fav_item.bvid, credential=credential)
# 如果没有获取过 tags那么尝试获取一下
try:
if process_upper:
# 写入 up 主头像
if fav_item.tags is None:
fav_item.tags = [_["tag_name"] for _ in await v.get_tags()]
except Exception:
logger.exception(
"Failed to get tags of video {} {}",
fav_item.bvid,
fav_item.name,
)
if process_upper:
try:
if not all(
await asyncio.gather(
aexists(fav_item.upper.thumb_path),
@@ -181,9 +178,7 @@ async def process_favorite_item(
await amakedirs(fav_item.upper.thumb_path.parent, exist_ok=True)
await asyncio.gather(
fav_item.upper.save_metadata(),
download_content(
fav_item.upper.thumb, fav_item.upper.thumb_path
),
download_content(fav_item.upper.thumb, fav_item.upper.thumb_path),
return_exceptions=True,
)
else:
@@ -192,20 +187,16 @@ async def process_favorite_item(
fav_item.upper.mid,
fav_item.upper.name,
)
if process_nfo:
except Exception:
logger.exception(
"Failed to process upper {} {}",
fav_item.upper.mid,
fav_item.upper.name,
)
if process_nfo:
try:
if not await aexists(fav_item.nfo_path):
if fav_item.tags is None:
try:
fav_item.tags = [
_["tag_name"] for _ in await v.get_tags()
]
except Exception:
logger.exception(
"Failed to get tags of video {} {}",
fav_item.bvid,
fav_item.name,
)
# 写入 nfo
await EpisodeInfo(
title=fav_item.name,
plot=fav_item.desc,
@@ -225,20 +216,50 @@ async def process_favorite_item(
fav_item.bvid,
fav_item.name,
)
if process_poster:
# 写入 poster
except Exception:
logger.exception(
"Failed to process nfo of video {} {}",
fav_item.bvid,
fav_item.name,
)
if process_poster:
try:
if not await aexists(fav_item.poster_path):
await download_content(fav_item.cover, fav_item.poster_path)
try:
await download_content(fav_item.cover, fav_item.poster_path)
except Exception:
logger.exception(
"Failed to download poster of video {} {}",
fav_item.bvid,
fav_item.name,
)
else:
logger.info(
"Poster of {} {} already exists, skipped.",
fav_item.bvid,
fav_item.name,
)
if process_subtitle:
except Exception:
logger.exception(
"Failed to process poster of video {} {}",
fav_item.bvid,
fav_item.name,
)
if process_subtitle:
try:
if not await aexists(fav_item.subtitle_path):
await ass.make_ass_file_danmakus_protobuf(
v, 0, str(fav_item.subtitle_path.resolve())
v,
0,
str(fav_item.subtitle_path.resolve()),
credential=credential,
font_name=settings.subtitle.font_name,
font_size=settings.subtitle.font_size,
alpha=settings.subtitle.alpha,
fly_time=settings.subtitle.fly_time,
static_time=settings.subtitle.static_time,
)
else:
logger.info(
@@ -246,7 +267,14 @@ async def process_favorite_item(
fav_item.bvid,
fav_item.name,
)
if process_video:
except Exception:
logger.exception(
"Failed to process subtitle of video {} {}",
fav_item.bvid,
fav_item.name,
)
if process_video:
try:
if await aexists(fav_item.video_path):
fav_item.downloaded = True
logger.info(
@@ -259,11 +287,9 @@ async def process_favorite_item(
detector = video.VideoDownloadURLDataDetecter(
await v.get_download_url(page_index=0)
)
streams = detector.detect_best_streams()
streams = detector.detect_best_streams(codecs=settings.codec)
if detector.check_flv_stream():
await download_content(
streams[0].url, fav_item.tmp_video_path
)
await download_content(streams[0].url, fav_item.tmp_video_path)
process = await create_subprocess_exec(
FFMPEG_COMMAND,
"-i",
@@ -276,12 +302,8 @@ async def process_favorite_item(
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
),
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,
@@ -299,34 +321,31 @@ async def process_favorite_item(
fav_item.tmp_video_path.unlink()
fav_item.tmp_audio_path.unlink()
fav_item.downloaded = True
logger.info(
"{} {} processed successfully.",
fav_item.bvid,
fav_item.name,
)
except ResponseCodeException as e:
match e.code:
case 62002:
fav_item.status = MediaStatus.INVISIBLE
case -404:
fav_item.status = MediaStatus.DELETED
case _:
logger.exception(
"Failed to process video {} {}, error_code: {}",
except ResponseCodeException as e:
match e.code:
case 62002:
fav_item.status = MediaStatus.INVISIBLE
case -404:
fav_item.status = MediaStatus.DELETED
case _:
logger.exception(
"Failed to process video {} {}, error_code: {}",
fav_item.bvid,
fav_item.name,
e.code,
)
if fav_item.status != MediaStatus.NORMAL:
logger.error(
"Video {} {} is not available, marked as {}",
fav_item.bvid,
fav_item.name,
e.code,
fav_item.status.text,
)
return
logger.error(
"Video {} {} is not available, marked as {}",
fav_item.bvid,
fav_item.name,
fav_item.status.text,
)
except Exception:
logger.exception(
"Failed to process video {} {}", fav_item.bvid, fav_item.name
)
finally:
await fav_item.save()
except Exception:
logger.exception("Failed to process video {} {}", fav_item.bvid, fav_item.name)
await fav_item.save()
logger.info(
"{} {} is processed successfully.",
fav_item.bvid,
fav_item.name,
)

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "bili-sync"
version = "1.0.1"
version = "1.1.4"
description = ""
authors = ["amtoaer <amtoaer@gmail.com>"]
license = "GPL-3.0"
@@ -8,24 +8,26 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
bilibili-api-python = { git = "https://github.com/amtoaer/bilibili-api.git", rev = "dev" }
dataclasses-json = "0.6.2"
tortoise-orm = "0.20.0"
loguru = "0.7.2"
uvloop = "0.19.0"
aiofiles = "23.2.1"
aerich = "0.7.2"
aiofiles = "23.2.1"
bilibili-api-python = {git = "https://github.com/amtoaer/bilibili-api", rev = "dev"}
dataclasses-json = "0.6.2"
loguru = "0.7.2"
pydantic = "2.5.3"
tortoise-orm = "0.20.0"
uvloop = "0.19.0"
[tool.poetry.group.dev.dependencies]
black = "23.11.0"
ruff = "0.1.6"
bump-my-version = "0.15.4"
ipython = "8.17.2"
ruff = "0.1.6"
[tool.black]
line-length = 80
line-length = 100
[tool.ruff]
line-length = 80
line-length = 100
select = [
"F", # https://beta.ruff.rs/docs/rules/#pyflakes-f
"E",
@@ -60,6 +62,23 @@ tortoise_orm = "constants.TORTOISE_ORM"
location = "./migrations"
src_folder = "./."
[tool.bumpversion]
commit = true
message = "chore: bump version from {current_version} to {new_version}"
tag = true
tag_name = "{new_version}"
tag_message = ""
current_version = "1.1.4"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
[[tool.bumpversion.files]]
filename = "version.py"
[[tool.bumpversion.files]]
filename = "pyproject.toml"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -1,63 +1,73 @@
from dataclasses import dataclass, field, fields
from pathlib import Path
from typing import Self
from dataclasses_json import DataClassJsonMixin
from bilibili_api.video import VideoCodecs
from pydantic import BaseModel, Field, field_validator
from pydantic_core import PydanticCustomError
from typing_extensions import Annotated
from constants import DEFAULT_CONFIG_PATH
@dataclass
class Config(DataClassJsonMixin):
sessdata: str = ""
bili_jct: str = ""
buvid3: str = ""
dedeuserid: str = ""
ac_time_value: str = ""
interval: int = 20
favorite_ids: list[int] = field(default_factory=list)
path_mapper: dict[int, str] = field(default_factory=dict)
class SubtitleConfig(BaseModel):
font_name: str = "微软雅黑,黑体" # 字体
font_size: float = 40 # 字号
alpha: float = 0.8 # 透明度
fly_time: float = 5 # 滚动弹幕持续时间
static_time: float = 10 # 静态弹幕持续时间
def validate(self) -> Self:
"""所有值必须被设置"""
if not all(getattr(self, f.name) for f in fields(self)):
raise ValueError("Some config values are not set.")
return self
class Config(BaseModel):
sessdata: Annotated[str, Field(min_length=1)] = ""
bili_jct: Annotated[str, Field(min_length=1)] = ""
buvid3: Annotated[str, Field(min_length=1)] = ""
dedeuserid: Annotated[str, Field(min_length=1)] = ""
ac_time_value: Annotated[str, Field(min_length=1)] = ""
interval: int = 20
path_mapper: dict[int, str] = Field(default_factory=dict)
subtitle: SubtitleConfig = Field(default_factory=SubtitleConfig)
codec: list[VideoCodecs] = Field(
default_factory=lambda: [
VideoCodecs.AV1,
VideoCodecs.AVC,
VideoCodecs.HEV,
],
min_length=1,
)
@field_validator("codec", mode="after")
def codec_validator(cls, codecs: list[VideoCodecs]) -> list[VideoCodecs]:
if len(codecs) != len(set(codecs)):
raise PydanticCustomError("unique_list", "List must be unique")
return codecs
@staticmethod
def load(path: Path | None = None) -> Self:
def load(path: Path | None = None) -> "Config":
if not path:
path = DEFAULT_CONFIG_PATH
try:
with path.open("r") as f:
return Config.schema().loads(f.read())
return Config.model_validate_json(f.read())
except Exception as e:
raise RuntimeError(f"Failed to load config file: {path}") from e
def save(self, path: Path | None = None) -> Self:
def save(self, path: Path | None = None) -> "Config":
if not path:
path = DEFAULT_CONFIG_PATH
try:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w") as f:
f.write(
Config.schema().dumps(self, indent=4, ensure_ascii=False)
)
f.write(Config.model_dump_json(self, indent=4))
return self
except Exception as e:
raise RuntimeError(f"Failed to save config file: {path}") from e
def init_settings() -> Config:
return (
(
Config.load(DEFAULT_CONFIG_PATH)
if DEFAULT_CONFIG_PATH.exists()
else Config()
)
.save(DEFAULT_CONFIG_PATH)
.validate()
)
if not DEFAULT_CONFIG_PATH.exists():
# 配置文件不存在的情况下,写入空的默认值
Config().save(DEFAULT_CONFIG_PATH)
# 读取配置文件,校验出错会抛出异常,校验通过则重新保存一下配置文件(写入新配置项的默认值)
return Config.load(DEFAULT_CONFIG_PATH).save()
settings = init_settings()

1
version.py Normal file
View File

@@ -0,0 +1 @@
VERSION = "1.1.4"