mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-05 07:28:04 +08:00
feat: 尝试支持视频标签
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ debug.py
|
|||||||
videos
|
videos
|
||||||
config.test.json
|
config.test.json
|
||||||
database.test.db*
|
database.test.db*
|
||||||
example.json
|
example*.json
|
||||||
thumbs.test
|
thumbs.test
|
||||||
config
|
config
|
||||||
data
|
data
|
||||||
|
|||||||
+30
-2
@@ -4,8 +4,8 @@ from loguru import logger
|
|||||||
|
|
||||||
from constants import MediaStatus, MediaType
|
from constants import MediaStatus, MediaType
|
||||||
from models import FavoriteItem, Upper
|
from models import FavoriteItem, Upper
|
||||||
from processor import download_content
|
from processor import download_content, process_video
|
||||||
from utils import aexists, amakedirs
|
from utils import aexists, amakedirs, aremove
|
||||||
|
|
||||||
|
|
||||||
async def recheck():
|
async def recheck():
|
||||||
@@ -38,6 +38,7 @@ async def recheck():
|
|||||||
|
|
||||||
|
|
||||||
async def upper_thumb():
|
async def upper_thumb():
|
||||||
|
"""将up主的头像批量写入数据库,从不支持up主头像的版本升级上来后需要手动调用一次"""
|
||||||
makedir_tasks = []
|
makedir_tasks = []
|
||||||
other_tasks = []
|
other_tasks = []
|
||||||
for upper in await Upper.all():
|
for upper in await Upper.all():
|
||||||
@@ -60,3 +61,30 @@ async def upper_thumb():
|
|||||||
await asyncio.gather(*makedir_tasks)
|
await asyncio.gather(*makedir_tasks)
|
||||||
await asyncio.gather(*other_tasks)
|
await asyncio.gather(*other_tasks)
|
||||||
logger.info("All done.")
|
logger.info("All done.")
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh_tags():
|
||||||
|
"""刷新已存在的视频的标签,从不支持标签的版本升级上来后需要手动调用一次"""
|
||||||
|
items = await FavoriteItem.filter(
|
||||||
|
type=MediaType.VIDEO,
|
||||||
|
status=MediaStatus.NORMAL,
|
||||||
|
downloaded=True,
|
||||||
|
tags=None,
|
||||||
|
)
|
||||||
|
await asyncio.gather(
|
||||||
|
*[await aremove(item.nfo_path) for item in items],
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
process_video(
|
||||||
|
item,
|
||||||
|
process_poster=False,
|
||||||
|
process_video=False,
|
||||||
|
process_nfo=True,
|
||||||
|
process_upper=False,
|
||||||
|
)
|
||||||
|
for item in items
|
||||||
|
],
|
||||||
|
return_exceptions=True,
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "favoriteitem" ADD "tags" JSON;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "favoriteitem" DROP COLUMN "tags";"""
|
||||||
@@ -79,6 +79,7 @@ class FavoriteItem(Model):
|
|||||||
bvid = fields.CharField(max_length=255)
|
bvid = fields.CharField(max_length=255)
|
||||||
desc = fields.TextField()
|
desc = fields.TextField()
|
||||||
cover = fields.TextField()
|
cover = fields.TextField()
|
||||||
|
tags = fields.JSONField(null=True)
|
||||||
favorite_list = fields.ForeignKeyField(
|
favorite_list = fields.ForeignKeyField(
|
||||||
"models.FavoriteList", related_name="items"
|
"models.FavoriteList", related_name="items"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class Actor:
|
|||||||
class EpisodeInfo:
|
class EpisodeInfo:
|
||||||
title: str
|
title: str
|
||||||
plot: str
|
plot: str
|
||||||
|
tags: list[str]
|
||||||
actor: list[Actor]
|
actor: list[Actor]
|
||||||
bvid: str
|
bvid: str
|
||||||
aired: datetime.datetime
|
aired: datetime.datetime
|
||||||
@@ -35,6 +36,11 @@ class EpisodeInfo:
|
|||||||
|
|
||||||
def to_xml(self) -> str:
|
def to_xml(self) -> str:
|
||||||
actor = "\n".join(_.to_xml() for _ in self.actor)
|
actor = "\n".join(_.to_xml() for _ in self.actor)
|
||||||
|
tags = (
|
||||||
|
"\n".join(f" <genre>{_}</genre>" for _ in self.tags)
|
||||||
|
if isinstance(self.tags, list)
|
||||||
|
else ""
|
||||||
|
)
|
||||||
return f"""
|
return f"""
|
||||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||||
<episodedetails>
|
<episodedetails>
|
||||||
@@ -43,6 +49,7 @@ class EpisodeInfo:
|
|||||||
<title>{self.title}</title>
|
<title>{self.title}</title>
|
||||||
{actor}
|
{actor}
|
||||||
<year>{self.aired.year}</year>
|
<year>{self.aired.year}</year>
|
||||||
|
{tags}
|
||||||
<uniqueid type="bilibili">{self.bvid}</uniqueid>
|
<uniqueid type="bilibili">{self.bvid}</uniqueid>
|
||||||
<aired>{self.aired.strftime("%Y-%m-%d")}</aired>
|
<aired>{self.aired.strftime("%Y-%m-%d")}</aired>
|
||||||
</episodedetails>
|
</episodedetails>
|
||||||
|
|||||||
+123
-78
@@ -156,89 +156,133 @@ async def process_favorite(favorite_id: int) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@concurrent_decorator(4)
|
@concurrent_decorator(4)
|
||||||
async def process_video(fav_item: FavoriteItem) -> None:
|
async def process_video(
|
||||||
|
fav_item: FavoriteItem,
|
||||||
|
process_poster=True,
|
||||||
|
process_video=True,
|
||||||
|
process_nfo=True,
|
||||||
|
process_upper=True,
|
||||||
|
) -> None:
|
||||||
logger.info("Start to process video {} {}", fav_item.bvid, fav_item.name)
|
logger.info("Start to process video {} {}", fav_item.bvid, fav_item.name)
|
||||||
if fav_item.type != MediaType.VIDEO:
|
if fav_item.type != MediaType.VIDEO:
|
||||||
logger.warning("Media {} is not a video, skipped.", fav_item.name)
|
logger.warning("Media {} is not a video, skipped.", fav_item.name)
|
||||||
return
|
return
|
||||||
|
v = video.Video(fav_item.bvid, credential=credential)
|
||||||
try:
|
try:
|
||||||
if await aexists(fav_item.video_path):
|
if process_upper:
|
||||||
fav_item.downloaded = True
|
# 写入 up 主头像
|
||||||
await fav_item.save()
|
if not all(
|
||||||
logger.info(
|
await asyncio.gather(
|
||||||
"{} {} already exists, skipped.", fav_item.bvid, fav_item.name
|
aexists(fav_item.upper.thumb_path),
|
||||||
)
|
aexists(fav_item.upper.meta_path),
|
||||||
return
|
)
|
||||||
# 写入 up 主头像
|
):
|
||||||
if not all(
|
await amakedirs(fav_item.upper.thumb_path.parent, exist_ok=True)
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
aexists(fav_item.upper.thumb_path),
|
fav_item.upper.save_metadata(),
|
||||||
aexists(fav_item.upper.meta_path),
|
download_content(
|
||||||
)
|
fav_item.upper.thumb, fav_item.upper.thumb_path
|
||||||
):
|
),
|
||||||
await amakedirs(fav_item.upper.thumb_path.parent, exist_ok=True)
|
return_exceptions=True,
|
||||||
await fav_item.upper.save_metadata()
|
)
|
||||||
await download_content(
|
else:
|
||||||
fav_item.upper.thumb, fav_item.upper.thumb_path
|
logger.info(
|
||||||
)
|
"Upper {} {} already exists, skipped.",
|
||||||
# 写入 nfo
|
fav_item.upper.mid,
|
||||||
await EpisodeInfo(
|
fav_item.upper.name,
|
||||||
title=fav_item.name,
|
)
|
||||||
plot=fav_item.desc,
|
if process_nfo:
|
||||||
actor=[
|
if not await aexists(fav_item.nfo_path):
|
||||||
Actor(
|
if fav_item.tags is None:
|
||||||
name=fav_item.upper.mid,
|
fav_item.tags = [_["tag_name"] for _ in await v.get_tags()]
|
||||||
role=fav_item.upper.name,
|
# 写入 nfo
|
||||||
|
await EpisodeInfo(
|
||||||
|
title=fav_item.name,
|
||||||
|
plot=fav_item.desc,
|
||||||
|
actor=[
|
||||||
|
Actor(
|
||||||
|
name=fav_item.upper.mid,
|
||||||
|
role=fav_item.upper.name,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
tags=fav_item.tags,
|
||||||
|
bvid=fav_item.bvid,
|
||||||
|
aired=fav_item.ctime,
|
||||||
|
).write_nfo(fav_item.nfo_path)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"NFO of {} {} already exists, skipped.",
|
||||||
|
fav_item.bvid,
|
||||||
|
fav_item.name,
|
||||||
|
)
|
||||||
|
if process_poster:
|
||||||
|
# 写入 poster
|
||||||
|
if not await aexists(fav_item.poster_path):
|
||||||
|
await download_content(fav_item.cover, fav_item.poster_path)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"Poster of {} {} already exists, skipped.",
|
||||||
|
fav_item.bvid,
|
||||||
|
fav_item.name,
|
||||||
|
)
|
||||||
|
if process_video:
|
||||||
|
if await aexists(fav_item.video_path):
|
||||||
|
fav_item.downloaded = True
|
||||||
|
logger.info(
|
||||||
|
"Video {} {} already exists, skipped.",
|
||||||
|
fav_item.bvid,
|
||||||
|
fav_item.name,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 开始处理视频内容
|
||||||
|
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
|
||||||
|
logger.info(
|
||||||
|
"{} {} processed successfully.",
|
||||||
|
fav_item.bvid,
|
||||||
|
fav_item.name,
|
||||||
)
|
)
|
||||||
],
|
|
||||||
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 ResponseCodeException as e:
|
except ResponseCodeException as e:
|
||||||
match e.code:
|
match e.code:
|
||||||
case 62002:
|
case 62002:
|
||||||
@@ -253,7 +297,6 @@ async def process_video(fav_item: FavoriteItem) -> None:
|
|||||||
e.code,
|
e.code,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
await fav_item.save()
|
|
||||||
logger.error(
|
logger.error(
|
||||||
"Video {} {} is not available, marked as {}",
|
"Video {} {} is not available, marked as {}",
|
||||||
fav_item.bvid,
|
fav_item.bvid,
|
||||||
@@ -264,3 +307,5 @@ async def process_video(fav_item: FavoriteItem) -> None:
|
|||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to process video {} {}", fav_item.bvid, fav_item.name
|
"Failed to process video {} {}", fav_item.bvid, fav_item.name
|
||||||
)
|
)
|
||||||
|
finally:
|
||||||
|
await fav_item.save()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
|||||||
import aiofiles
|
import aiofiles
|
||||||
import httpx
|
import httpx
|
||||||
from aiofiles.base import AiofilesContextManager
|
from aiofiles.base import AiofilesContextManager
|
||||||
from aiofiles.os import makedirs
|
from aiofiles.os import makedirs, remove
|
||||||
from aiofiles.ospath import exists
|
from aiofiles.ospath import exists
|
||||||
from aiofiles.threadpool.text import AsyncTextIOWrapper
|
from aiofiles.threadpool.text import AsyncTextIOWrapper
|
||||||
from bilibili_api import HEADERS
|
from bilibili_api import HEADERS
|
||||||
@@ -31,3 +31,7 @@ def aopen(
|
|||||||
path: Path, mode: str = "r", **kwargs
|
path: Path, mode: str = "r", **kwargs
|
||||||
) -> AiofilesContextManager[None, None, AsyncTextIOWrapper]:
|
) -> AiofilesContextManager[None, None, AsyncTextIOWrapper]:
|
||||||
return aiofiles.open(path, mode, **kwargs)
|
return aiofiles.open(path, mode, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def aremove(path: Path) -> None:
|
||||||
|
await remove(path)
|
||||||
|
|||||||
Reference in New Issue
Block a user