Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions base/base_crawler.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ async def store_video(self, video_content_item: Dict):
pass


class AbstractStoreAudio(ABC):
# TODO: support all platform
async def store_audio(self, audio_content_item: Dict):
pass


class AbstractApiClient(ABC):

@abstractmethod
Expand Down
11 changes: 11 additions & 0 deletions config/base_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@
# Whether to enable crawling media mode (including image or video resources), crawling media is not enabled by default
ENABLE_GET_MEIDAS = False

# Whether to enable audio download/extraction mode.
# When True, the crawler will automatically extract audio from downloaded videos and save it as a separate file.
ENABLE_GET_AUDIO = False

# Audio output format, supports: mp3, aac
AUDIO_FORMAT = "mp3"

# Whether to keep the original video file after extracting audio.
# Set to False to delete the video and keep only the audio, saving disk space.
KEEP_ORIGINAL_VIDEO = True

# Whether to enable comment crawling mode. Comment crawling is enabled by default.
ENABLE_GET_COMMENTS = True

Expand Down
5 changes: 5 additions & 0 deletions media_platform/bilibili/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,11 @@ async def get_bilibili_video(self, video_item: Dict, semaphore: asyncio.Semaphor
extension_file_name = f"video.mp4"
await bilibili_store.store_video(aid, content, extension_file_name)

if config.ENABLE_GET_AUDIO:
video_path = f"{bilibili_store.BilibiliVideo().video_store_path}/{aid}/{extension_file_name}"
audio_extension_file_name = f"{extension_file_name.rsplit('.', 1)[0]}.{config.AUDIO_FORMAT}"
await bilibili_store.store_audio(aid, video_path, audio_extension_file_name)

async def get_all_creator_details(self, creator_url_list: List[str]):
"""
creator_url_list: get details for creator from creator URL list
Expand Down
5 changes: 5 additions & 0 deletions media_platform/douyin/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,3 +468,8 @@ async def get_aweme_video(self, aweme_item: Dict):
return
extension_file_name = f"video.mp4"
await douyin_store.update_dy_aweme_video(aweme_id, content, extension_file_name)

if config.ENABLE_GET_AUDIO:
video_path = f"{douyin_store.DouYinVideo().video_store_path}/{aweme_id}/{extension_file_name}"
audio_extension_file_name = f"{extension_file_name.rsplit('.', 1)[0]}.{config.AUDIO_FORMAT}"
await douyin_store.update_dy_aweme_audio(aweme_id, video_path, audio_extension_file_name)
5 changes: 5 additions & 0 deletions media_platform/xhs/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,3 +520,8 @@ async def get_notice_video(self, note_item: Dict):
extension_file_name = f"{videoNum}.mp4"
videoNum += 1
await xhs_store.update_xhs_note_video(note_id, content, extension_file_name)

if config.ENABLE_GET_AUDIO:
video_path = f"{xhs_store.XiaoHongShuVideo().video_store_path}/{note_id}/{extension_file_name}"
audio_extension_file_name = f"{extension_file_name.rsplit('.', 1)[0]}.{config.AUDIO_FORMAT}"
await xhs_store.update_xhs_note_audio(note_id, video_path, audio_extension_file_name)
19 changes: 19 additions & 0 deletions store/bilibili/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

from ._store_impl import *
from .bilibilli_store_media import *
from .bilibili_store_audio import *


class BiliStoreFactory:
Expand Down Expand Up @@ -131,6 +132,24 @@ async def store_video(aid, video_content, extension_file_name):
})


async def store_audio(aid, video_path, extension_file_name):
"""
Extract and save Bilibili video audio from a downloaded video.
Args:
aid:
video_path:
extension_file_name:

Returns:
Path to the extracted audio file, or None if failed.
"""
return await BilibiliAudio().store_audio({
"aid": aid,
"video_path": video_path,
"extension_file_name": extension_file_name,
})


async def batch_update_bilibili_creator_fans(creator_info: Dict, fans_list: List[Dict]):
# 教学版:不再采集/存储粉丝列表(其他用户的个人信息),防骚扰。
return
Expand Down
76 changes: 76 additions & 0 deletions store/bilibili/bilibili_store_audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2025 relakkes@gmail.com
#
# This file is part of MediaCrawler project.
# Repository: https://github.com/NanmiCoder/MediaCrawler/blob/main/store/bilibili/bilibili_store_audio.py
# GitHub: https://github.com/NanmiCoder
# Licensed under NON-COMMERCIAL LEARNING LICENSE 1.1
#
# 声明:本代码仅供学习和研究目的使用。使用者应遵守以下原则:
# 1. 不得用于任何商业用途。
# 2. 使用时应遵守目标平台的使用条款和robots.txt规则。
# 3. 不得进行大规模爬取或对目标平台造成运营干扰。
# 4. 应合理控制请求频率,避免给目标平台带来不必要的负担。
# 5. 不得用于任何非法或不当的用途。
#
# 详细许可条款请参阅项目根目录下的LICENSE文件。
# 使用本代码即表示您同意遵守上述原则和LICENSE中的所有条款。

from pathlib import Path
from typing import Dict, Optional

from base.base_crawler import AbstractStoreAudio
from tools import utils
from tools.media_util import extract_audio_from_video
import config


class BilibiliAudio(AbstractStoreAudio):
def __init__(self):
if config.SAVE_DATA_PATH:
self.audio_store_path = f"{config.SAVE_DATA_PATH}/bili/audios"
else:
self.audio_store_path = "data/bili/audios"

def make_save_file_name(self, aid: str, extension_file_name: str) -> str:
"""
make save file name by store type

Args:
aid: aid
extension_file_name: audio filename with extension

Returns:

"""
return f"{self.audio_store_path}/{aid}/{extension_file_name}"

async def store_audio(self, audio_content_item: Dict) -> Optional[str]:
"""
Extract audio from a downloaded video and save it to the audio store path.

Args:
audio_content_item: dict with keys:
- aid: aid
- video_path: path to the downloaded video file
- extension_file_name: audio filename with extension

Returns:
Path to the extracted audio file, or None if failed.
"""
aid: str = str(audio_content_item.get("aid"))
video_path: str = audio_content_item.get("video_path")
extension_file_name: str = audio_content_item.get("extension_file_name")

if not video_path or not Path(video_path).exists():
utils.logger.warning(f"[BilibiliAudio.store_audio] Video not found: {video_path}")
return None

Path(self.audio_store_path + "/" + aid).mkdir(parents=True, exist_ok=True)
audio_path = self.make_save_file_name(aid, extension_file_name)

return await extract_audio_from_video(
video_path=video_path,
audio_path=audio_path,
audio_format=config.AUDIO_FORMAT,
)
16 changes: 16 additions & 0 deletions store/douyin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

from ._store_impl import *
from .douyin_store_media import *
from .douyin_store_audio import *


class DouyinStoreFactory:
Expand Down Expand Up @@ -249,3 +250,18 @@ async def update_dy_aweme_video(aweme_id, video_content, extension_file_name):
"""

await DouYinVideo().store_video({"aweme_id": aweme_id, "video_content": video_content, "extension_file_name": extension_file_name})


async def update_dy_aweme_audio(aweme_id, video_path, extension_file_name):
"""
Extract and save Douyin aweme audio from a downloaded video.
Args:
aweme_id:
video_path:
extension_file_name:

Returns:
Path to the extracted audio file, or None if failed.
"""

return await DouYinAudio().store_audio({"aweme_id": aweme_id, "video_path": video_path, "extension_file_name": extension_file_name})
76 changes: 76 additions & 0 deletions store/douyin/douyin_store_audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2025 relakkes@gmail.com
#
# This file is part of MediaCrawler project.
# Repository: https://github.com/NanmiCoder/MediaCrawler/blob/main/store/douyin/douyin_store_audio.py
# GitHub: https://github.com/NanmiCoder
# Licensed under NON-COMMERCIAL LEARNING LICENSE 1.1
#
# 声明:本代码仅供学习和研究目的使用。使用者应遵守以下原则:
# 1. 不得用于任何商业用途。
# 2. 使用时应遵守目标平台的使用条款和robots.txt规则。
# 3. 不得进行大规模爬取或对目标平台造成运营干扰。
# 4. 应合理控制请求频率,避免给目标平台带来不必要的负担。
# 5. 不得用于任何非法或不当的用途。
#
# 详细许可条款请参阅项目根目录下的LICENSE文件。
# 使用本代码即表示您同意遵守上述原则和LICENSE中的所有条款。

from pathlib import Path
from typing import Dict, Optional

from base.base_crawler import AbstractStoreAudio
from tools import utils
from tools.media_util import extract_audio_from_video
import config


class DouYinAudio(AbstractStoreAudio):
def __init__(self):
if config.SAVE_DATA_PATH:
self.audio_store_path = f"{config.SAVE_DATA_PATH}/douyin/audios"
else:
self.audio_store_path = "data/douyin/audios"

def make_save_file_name(self, aweme_id: str, extension_file_name: str) -> str:
"""
make save file name by store type

Args:
aweme_id: aweme id
extension_file_name: audio filename with extension

Returns:

"""
return f"{self.audio_store_path}/{aweme_id}/{extension_file_name}"

async def store_audio(self, audio_content_item: Dict) -> Optional[str]:
"""
Extract audio from a downloaded video and save it to the audio store path.

Args:
audio_content_item: dict with keys:
- aweme_id: aweme id
- video_path: path to the downloaded video file
- extension_file_name: audio filename with extension

Returns:
Path to the extracted audio file, or None if failed.
"""
aweme_id: str = audio_content_item.get("aweme_id")
video_path: str = audio_content_item.get("video_path")
extension_file_name: str = audio_content_item.get("extension_file_name")

if not video_path or not Path(video_path).exists():
utils.logger.warning(f"[DouYinAudio.store_audio] Video not found: {video_path}")
return None

Path(self.audio_store_path + "/" + aweme_id).mkdir(parents=True, exist_ok=True)
audio_path = self.make_save_file_name(aweme_id, extension_file_name)

return await extract_audio_from_video(
video_path=video_path,
audio_path=audio_path,
audio_format=config.AUDIO_FORMAT,
)
16 changes: 16 additions & 0 deletions store/xhs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from tools.user_hash import anonymize_user_id, mask_nickname

from .xhs_store_media import *
from .xhs_store_audio import *
from ._store_impl import *


Expand Down Expand Up @@ -220,3 +221,18 @@ async def update_xhs_note_video(note_id, video_content, extension_file_name):
"""

await XiaoHongShuVideo().store_video({"notice_id": note_id, "video_content": video_content, "extension_file_name": extension_file_name})


async def update_xhs_note_audio(note_id, video_path, extension_file_name):
"""
Extract and save Xiaohongshu note audio from a downloaded video.
Args:
note_id:
video_path:
extension_file_name:

Returns:
Path to the extracted audio file, or None if failed.
"""

return await XiaoHongShuAudio().store_audio({"notice_id": note_id, "video_path": video_path, "extension_file_name": extension_file_name})
76 changes: 76 additions & 0 deletions store/xhs/xhs_store_audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2025 relakkes@gmail.com
#
# This file is part of MediaCrawler project.
# Repository: https://github.com/NanmiCoder/MediaCrawler/blob/main/store/xhs/xhs_store_audio.py
# GitHub: https://github.com/NanmiCoder
# Licensed under NON-COMMERCIAL LEARNING LICENSE 1.1
#
# 声明:本代码仅供学习和研究目的使用。使用者应遵守以下原则:
# 1. 不得用于任何商业用途。
# 2. 使用时应遵守目标平台的使用条款和robots.txt规则。
# 3. 不得进行大规模爬取或对目标平台造成运营干扰。
# 4. 应合理控制请求频率,避免给目标平台带来不必要的负担。
# 5. 不得用于任何非法或不当的用途。
#
# 详细许可条款请参阅项目根目录下的LICENSE文件。
# 使用本代码即表示您同意遵守上述原则和LICENSE中的所有条款。

from pathlib import Path
from typing import Dict, Optional

from base.base_crawler import AbstractStoreAudio
from tools import utils
from tools.media_util import extract_audio_from_video
import config


class XiaoHongShuAudio(AbstractStoreAudio):
def __init__(self):
if config.SAVE_DATA_PATH:
self.audio_store_path = f"{config.SAVE_DATA_PATH}/xhs/audios"
else:
self.audio_store_path = "data/xhs/audios"

def make_save_file_name(self, notice_id: str, extension_file_name: str) -> str:
"""
make save file name by store type

Args:
notice_id: notice id
extension_file_name: audio filename with extension

Returns:

"""
return f"{self.audio_store_path}/{notice_id}/{extension_file_name}"

async def store_audio(self, audio_content_item: Dict) -> Optional[str]:
"""
Extract audio from a downloaded video and save it to the audio store path.

Args:
audio_content_item: dict with keys:
- notice_id: note id
- video_path: path to the downloaded video file
- extension_file_name: audio filename with extension

Returns:
Path to the extracted audio file, or None if failed.
"""
notice_id: str = audio_content_item.get("notice_id")
video_path: str = audio_content_item.get("video_path")
extension_file_name: str = audio_content_item.get("extension_file_name")

if not video_path or not Path(video_path).exists():
utils.logger.warning(f"[XiaoHongShuAudio.store_audio] Video not found: {video_path}")
return None

Path(self.audio_store_path + "/" + notice_id).mkdir(parents=True, exist_ok=True)
audio_path = self.make_save_file_name(notice_id, extension_file_name)

return await extract_audio_from_video(
video_path=video_path,
audio_path=audio_path,
audio_format=config.AUDIO_FORMAT,
)
Loading