mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-05-08 01:02:49 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
961913c4fb | ||
|
|
fa20e5efee | ||
|
|
38fb0a4560 | ||
|
|
9e94e3b73e | ||
|
|
b955a9fe45 | ||
|
|
9d151b4731 |
40
README.md
40
README.md
@@ -22,6 +22,14 @@
|
||||
对于配置文件的前五项,请参考[凭据获取流程](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 参数,将全量覆盖对应内容,否则默认仅更新缺失的部分。**
|
||||
|
||||
## 路线图
|
||||
|
||||
|
||||
11
commands.py
11
commands.py
@@ -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(
|
||||
|
||||
6
entry.py
6
entry.py
@@ -21,6 +21,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 +33,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:
|
||||
|
||||
151
processor.py
151
processor.py
@@ -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:
|
||||
@@ -91,12 +91,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)
|
||||
|
||||
|
||||
@@ -169,9 +164,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),
|
||||
@@ -192,20 +197,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 +226,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 +277,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(
|
||||
@@ -299,34 +337,33 @@ 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,
|
||||
)
|
||||
|
||||
17
settings.py
17
settings.py
@@ -2,21 +2,34 @@ from dataclasses import dataclass, field, fields
|
||||
from pathlib import Path
|
||||
from typing import Self
|
||||
|
||||
from dataclasses_json import DataClassJsonMixin
|
||||
from dataclasses_json import DataClassJsonMixin, Undefined
|
||||
|
||||
from constants import DEFAULT_CONFIG_PATH
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubtitleConfig(DataClassJsonMixin):
|
||||
dataclass_json_config = {"undefined": Undefined.EXCLUDE}
|
||||
|
||||
font_name: str = "微软雅黑,黑体" # 字体
|
||||
font_size: float = 40 # 字号
|
||||
alpha: float = 0.8 # 透明度
|
||||
fly_time: float = 5 # 滚动弹幕持续时间
|
||||
static_time: float = 10 # 静态弹幕持续时间
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config(DataClassJsonMixin):
|
||||
dataclass_json_config = {"undefined": Undefined.EXCLUDE}
|
||||
|
||||
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)
|
||||
subtitle: SubtitleConfig = field(default_factory=SubtitleConfig)
|
||||
|
||||
def validate(self) -> Self:
|
||||
"""所有值必须被设置"""
|
||||
|
||||
Reference in New Issue
Block a user