11import os
22import zipfile
3+ import subprocess
4+ import tempfile
35
46import aiofiles
57import httpx
@@ -48,42 +50,102 @@ async def fetch_data_stream(url: str, request:Request , headers: dict = None, fi
4850 await out_file .write (chunk )
4951 return True
5052
51- @router .get ("/download" , summary = "在线下载抖音|TikTok视频/图片/Online download Douyin|TikTok video/image" )
53+ async def merge_bilibili_video_audio (video_url : str , audio_url : str , request : Request , output_path : str , headers : dict ) -> bool :
54+ """
55+ 下载并合并 Bilibili 的视频流和音频流
56+ """
57+ try :
58+ # 创建临时文件
59+ with tempfile .NamedTemporaryFile (suffix = '.m4v' , delete = False ) as video_temp :
60+ video_temp_path = video_temp .name
61+ with tempfile .NamedTemporaryFile (suffix = '.m4a' , delete = False ) as audio_temp :
62+ audio_temp_path = audio_temp .name
63+
64+ # 下载视频流
65+ video_success = await fetch_data_stream (video_url , request , headers = headers , file_path = video_temp_path )
66+ # 下载音频流
67+ audio_success = await fetch_data_stream (audio_url , request , headers = headers , file_path = audio_temp_path )
68+
69+ if not video_success or not audio_success :
70+ print ("Failed to download video or audio stream" )
71+ return False
72+
73+ # 使用 FFmpeg 合并视频和音频
74+ ffmpeg_cmd = [
75+ 'ffmpeg' , '-y' , # -y 覆盖输出文件
76+ '-i' , video_temp_path , # 视频输入
77+ '-i' , audio_temp_path , # 音频输入
78+ '-c:v' , 'copy' , # 复制视频编码,不重新编码
79+ '-c:a' , 'copy' , # 复制音频编码,不重新编码(保持原始质量)
80+ '-f' , 'mp4' , # 确保输出格式为MP4
81+ output_path
82+ ]
83+
84+ print (f"FFmpeg command: { ' ' .join (ffmpeg_cmd )} " )
85+ result = subprocess .run (ffmpeg_cmd , capture_output = True , text = True )
86+ print (f"FFmpeg return code: { result .returncode } " )
87+ if result .stderr :
88+ print (f"FFmpeg stderr: { result .stderr } " )
89+ if result .stdout :
90+ print (f"FFmpeg stdout: { result .stdout } " )
91+
92+ # 清理临时文件
93+ try :
94+ os .unlink (video_temp_path )
95+ os .unlink (audio_temp_path )
96+ except :
97+ pass
98+
99+ return result .returncode == 0
100+
101+ except Exception as e :
102+ # 清理临时文件
103+ try :
104+ os .unlink (video_temp_path )
105+ os .unlink (audio_temp_path )
106+ except :
107+ pass
108+ print (f"Error merging video and audio: { e } " )
109+ return False
110+
111+ @router .get ("/download" , summary = "在线下载抖音|TikTok|Bilibili视频/图片/Online download Douyin|TikTok|Bilibili video/image" )
52112async def download_file_hybrid (request : Request ,
53113 url : str = Query (
54114 example = "https://www.douyin.com/video/7372484719365098803" ,
55- description = "视频或图片的URL地址,也支持抖音|TikTok的分享链接 ,例如:https://v.douyin.com/e4J8Q7A/" ),
115+ description = "视频或图片的URL地址,支持抖音|TikTok|Bilibili的分享链接 ,例如:https://v.douyin.com/e4J8Q7A/ 或 https://www.bilibili.com/video/BV1xxxxxxxxx " ),
56116 prefix : bool = True ,
57117 with_watermark : bool = False ):
58118 """
59119 # [中文]
60120 ### 用途:
61- - 在线下载抖音|TikTok 无水印或有水印的视频/图片
121+ - 在线下载抖音|TikTok|Bilibili 无水印或有水印的视频/图片
62122 - 通过传入的视频URL参数,获取对应的视频或图片数据,然后下载到本地。
63123 - 如果你在尝试直接访问TikTok单一视频接口的JSON数据中的视频播放地址时遇到HTTP403错误,那么你可以使用此接口来下载视频。
124+ - Bilibili视频会自动合并视频流和音频流,确保下载的视频有声音。
64125 - 这个接口会占用一定的服务器资源,所以在Demo站点是默认关闭的,你可以在本地部署后调用此接口。
65126 ### 参数:
66- - url: 视频或图片的URL地址,也支持抖音|TikTok的分享链接 ,例如:https://v.douyin.com/e4J8Q7A/。
127+ - url: 视频或图片的URL地址,支持抖音|TikTok|Bilibili的分享链接 ,例如:https://v.douyin.com/e4J8Q7A/ 或 https://www.bilibili.com/video/BV1xxxxxxxxx
67128 - prefix: 下载文件的前缀,默认为True,可以在配置文件中修改。
68- - with_watermark: 是否下载带水印的视频或图片,默认为False。
129+ - with_watermark: 是否下载带水印的视频或图片,默认为False。(注意:Bilibili没有水印概念)
69130 ### 返回:
70131 - 返回下载的视频或图片文件响应。
71132
72133 # [English]
73134 ### Purpose:
74- - Download Douyin|TikTok video/image with or without watermark online.
135+ - Download Douyin|TikTok|Bilibili video/image with or without watermark online.
75136 - By passing the video URL parameter, get the corresponding video or image data, and then download it to the local.
76137 - If you encounter an HTTP403 error when trying to access the video playback address in the JSON data of the TikTok single video interface directly, you can use this interface to download the video.
138+ - Bilibili videos will automatically merge video and audio streams to ensure downloaded videos have sound.
77139 - This interface will occupy a certain amount of server resources, so it is disabled by default on the Demo site, you can call this interface after deploying it locally.
78140 ### Parameters:
79- - url: The URL address of the video or image, also supports Douyin|TikTok sharing links, for example: https://v.douyin.com/e4J8Q7A/.
141+ - url: The URL address of the video or image, supports Douyin|TikTok|Bilibili sharing links, for example: https://v.douyin.com/e4J8Q7A/ or https://www.bilibili.com/video/BV1xxxxxxxxx
80142 - prefix: The prefix of the downloaded file, the default is True, and can be modified in the configuration file.
81- - with_watermark: Whether to download videos or images with watermarks, the default is False.
143+ - with_watermark: Whether to download videos or images with watermarks, the default is False. (Note: Bilibili has no watermark concept)
82144 ### Returns:
83145 - Return the response of the downloaded video or image file.
84146
85147 # [示例/Example]
86- url: https://www.douyin .com/video/7372484719365098803
148+ url: https://www.bilibili .com/video/BV1U5efz2Egn
87149 """
88150 # 是否开启此端点/Whether to enable this endpoint
89151 if not config ["API" ]["Download_Switch" ]:
@@ -103,7 +165,7 @@ async def download_file_hybrid(request: Request,
103165 try :
104166 data_type = data .get ('type' )
105167 platform = data .get ('platform' )
106- aweme_id = data .get ('aweme_id' )
168+ video_id = data .get ('video_id' ) # 改为使用video_id
107169 file_prefix = config .get ("API" ).get ("Download_File_Prefix" ) if prefix else ''
108170 download_path = os .path .join (config .get ("API" ).get ("Download_Path" ), f"{ platform } _{ data_type } " )
109171
@@ -112,25 +174,48 @@ async def download_file_hybrid(request: Request,
112174
113175 # 下载视频文件/Download video file
114176 if data_type == 'video' :
115- file_name = f"{ file_prefix } { platform } _{ aweme_id } .mp4" if not with_watermark else f"{ file_prefix } { platform } _{ aweme_id } _watermark.mp4"
116- url = data .get ('video_data' ).get ('nwm_video_url_HQ' ) if not with_watermark else data .get ('video_data' ).get (
117- 'wm_video_url_HQ' )
177+ file_name = f"{ file_prefix } { platform } _{ video_id } .mp4" if not with_watermark else f"{ file_prefix } { platform } _{ video_id } _watermark.mp4"
118178 file_path = os .path .join (download_path , file_name )
119179
120180 # 判断文件是否存在,存在就直接返回
121181 if os .path .exists (file_path ):
122182 return FileResponse (path = file_path , media_type = 'video/mp4' , filename = file_name )
123183
124- # 获取视频文件
125- __headers = await HybridCrawler .TikTokWebCrawler .get_tiktok_headers () if platform == 'tiktok' else await HybridCrawler .DouyinWebCrawler .get_douyin_headers ()
126- # response = await fetch_data(url, headers=__headers)
184+ # 获取对应平台的headers
185+ if platform == 'tiktok' :
186+ __headers = await HybridCrawler .TikTokWebCrawler .get_tiktok_headers ()
187+ elif platform == 'bilibili' :
188+ __headers = await HybridCrawler .BilibiliWebCrawler .get_bilibili_headers ()
189+ else : # douyin
190+ __headers = await HybridCrawler .DouyinWebCrawler .get_douyin_headers ()
127191
128- success = await fetch_data_stream (url , request , headers = __headers , file_path = file_path )
129- if not success :
130- raise HTTPException (
131- status_code = 500 ,
132- detail = "An error occurred while fetching data"
133- )
192+ # Bilibili 特殊处理:音视频分离
193+ if platform == 'bilibili' :
194+ video_data = data .get ('video_data' , {})
195+ video_url = video_data .get ('nwm_video_url_HQ' ) if not with_watermark else video_data .get ('wm_video_url_HQ' )
196+ audio_url = video_data .get ('audio_url' )
197+ if not video_url or not audio_url :
198+ raise HTTPException (
199+ status_code = 500 ,
200+ detail = "Failed to get video or audio URL from Bilibili"
201+ )
202+
203+ # 使用专门的函数合并音视频
204+ success = await merge_bilibili_video_audio (video_url , audio_url , request , file_path , __headers .get ('headers' ))
205+ if not success :
206+ raise HTTPException (
207+ status_code = 500 ,
208+ detail = "Failed to merge Bilibili video and audio streams"
209+ )
210+ else :
211+ # 其他平台的常规处理
212+ url = data .get ('video_data' ).get ('nwm_video_url_HQ' ) if not with_watermark else data .get ('video_data' ).get ('wm_video_url_HQ' )
213+ success = await fetch_data_stream (url , request , headers = __headers , file_path = file_path )
214+ if not success :
215+ raise HTTPException (
216+ status_code = 500 ,
217+ detail = "An error occurred while fetching data"
218+ )
134219
135220 # # 保存文件
136221 # async with aiofiles.open(file_path, 'wb') as out_file:
@@ -142,7 +227,7 @@ async def download_file_hybrid(request: Request,
142227 # 下载图片文件/Download image file
143228 elif data_type == 'image' :
144229 # 压缩文件属性/Compress file properties
145- zip_file_name = f"{ file_prefix } { platform } _{ aweme_id } _images.zip" if not with_watermark else f"{ file_prefix } { platform } _{ aweme_id } _images_watermark.zip"
230+ zip_file_name = f"{ file_prefix } { platform } _{ video_id } _images.zip" if not with_watermark else f"{ file_prefix } { platform } _{ video_id } _images_watermark.zip"
146231 zip_file_path = os .path .join (download_path , zip_file_name )
147232
148233 # 判断文件是否存在,存在就直接返回、
@@ -159,7 +244,7 @@ async def download_file_hybrid(request: Request,
159244 index = int (urls .index (url ))
160245 content_type = response .headers .get ('content-type' )
161246 file_format = content_type .split ('/' )[1 ]
162- file_name = f"{ file_prefix } { platform } _{ aweme_id } _{ index + 1 } .{ file_format } " if not with_watermark else f"{ file_prefix } { platform } _{ aweme_id } _{ index + 1 } _watermark.{ file_format } "
247+ file_name = f"{ file_prefix } { platform } _{ video_id } _{ index + 1 } .{ file_format } " if not with_watermark else f"{ file_prefix } { platform } _{ video_id } _{ index + 1 } _watermark.{ file_format } "
163248 file_path = os .path .join (download_path , file_name )
164249 image_file_list .append (file_path )
165250
0 commit comments