import json
import requests
import os
import time
from datetime import datetime, timedelta
import pytz
from mutagen.mp3 import MP3
from mutagen.mp4 import MP4
import subprocess
import platform
def get_broadcast_data():
"""
获取并提取播报数据
"""
# 获取 tenant_access_token
tenant_access_token = get_auth_token()
if not tenant_access_token:
print("获取 tenant_access_token 失败!")
return []
# 获取 Feishu Bitable 数据
url = 'https://open.feishu.cn/open-apis/bitable/v1/apps/E1zybPqiqa0TaesZjKKch5ZcnJd/tables/tblwFY4k3pmrV5WK/records/search'
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {tenant_access_token}' # 使用获取到的 token
}
data = {} # 如果需要传递查询条件,可以在这里添加
try:
response = requests.post(url, headers=headers, json=data)
response.raise_for_status() # 如果响应失败,将抛出异常
response_dict = response.json() # 将返回的 JSON 数据转换为字典
items = response_dict.get("data", {}).get("items", [])
data = []
for item in items:
fields = item.get("fields", {})
data.append({
"播音日期": extract_broadcast_date(fields, '播音日期'),
"时间段": extract_time_segment(fields, '时间段'),
"开播音乐file_token": extract_file_token(fields, '开播音乐'),
"开场白-播报file_token": extract_file_token(fields, '开场白-播报'),
"需更新文案-播报file_token": extract_file_token(fields, '需更新文案-播报'),
"壹首歌file_token": extract_file_token(fields, '壹首歌'),
"需更新文案2-播报file_token": extract_file_token(fields, '需更新文案2-播报'),
"贰首歌file_token": extract_file_token(fields, '贰首歌'),
"结束语-播报file_token": extract_file_token(fields, '结束语-播报'),
"结束音乐file_token": extract_file_token(fields, '结束音乐')
})
return data
except requests.exceptions.HTTPError as http_err:
print(f"HTTP 错误发生: {http_err}")
except Exception as err:
print(f"其他错误发生: {err}")
return []
def extract_file_token(fields, field_name):
"""提取 file_token"""
field_data = fields.get(field_name, [])
if isinstance(field_data, list) and len(field_data) > 0:
value = field_data[0]
if isinstance(value, dict):
return value.get("file_token", "")
return ''
def extract_time_segment(fields, field_name):
"""提取时间段字段"""
field_data = fields.get(field_name, [])
if isinstance(field_data, list) and len(field_data) > 0:
value = field_data[0]
if isinstance(value, dict):
return value.get("text", "")
return None
def extract_broadcast_date(fields, field_name):
"""提取播音日期字段"""
field_data = fields.get(field_name, 0)
if isinstance(field_data, int):
try:
timestamp = field_data / 1000 # 时间戳转化为秒
parsed_date = datetime.fromtimestamp(timestamp, tz=pytz.utc).astimezone(pytz.timezone('Asia/Shanghai'))
return parsed_date.strftime("%Y-%m-%d") # 转换为 "YYYY-MM-DD" 格式
except (ValueError, OverflowError):
pass
return None
def get_auth_token():
"""获取认证 token"""
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
headers = {"Content-Type": "application/json; charset=utf-8"}
payload = {"app_id": "cli_a882683e8779d00c", "app_secret": "3NKkALA7vyMRVnpKJinmrb1LJ7YuK4H0"}
try:
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
if data["code"] == 0:
return data["tenant_access_token"]
else:
print(f"请求失败:{data['msg']}(错误码:{data['code']})")
except requests.exceptions.RequestException as e:
print(f"请求异常:{e}")
return None
def create_folder(folder_name):
"""创建文件夹"""
if not os.path.exists(folder_name):
os.makedirs(folder_name)
def download_file(file_token, save_path, authorization):
"""下载文件"""
url = f"https://open.feishu.cn/open-apis/drive/v1/medias/{file_token}/download"
headers = {"Authorization": "Bearer " + authorization}
try:
response = requests.get(url, headers=headers, stream=True)
if response.status_code == 200:
with open(save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"文件已成功下载到: {save_path}")
return True
else:
print(f"请求失败,状态码: {response.status_code}")
print(f"错误信息: {response.text}")
except Exception as e:
print(f"发生异常: {str(e)}")
return False
def get_audio_duration(file_path):
"""获取音频时长"""
try:
if file_path.endswith(".mp3"):
audio = MP3(file_path)
elif file_path.endswith(".mp4"):
audio = MP4(file_path)
else:
print(f"不支持的文件格式: {file_path}")
return 0
return audio.info.length
except Exception as e:
print(f"获取音频时长失败: {e}")
return 0
def kill_previous_players():
"""清理之前残留的播放器进程"""
system = platform.system() # 获取当前操作系统类型
try:
if system == "Windows":
subprocess.run(['taskkill', '/F', '/IM', 'wmplayer.exe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subprocess.run(['taskkill', '/F', '/IM', 'vlc.exe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
elif system == "Darwin": # macOS
subprocess.run(['killall', 'afplay'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
elif system == "Linux":
subprocess.run(['pkill', 'mpg123'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print("已清理之前残留的播放器进程")
except Exception as e:
print(f"清理播放器进程时发生错误: {e}")
def play_music_in_folder(folder_path):
"""播放文件夹中的音频文件,并在播放完成后关闭播放器"""
audio_files = [f for f in os.listdir(folder_path) if f.endswith((".mp3", ".mp4"))]
processes = [] # 用于存储播放器进程
for file_name in audio_files:
full_file_path = os.path.join(folder_path, file_name)
try:
duration = get_audio_duration(full_file_path)
if duration <= 0:
print(f"无法获取 {file_name} 的时长,跳过播放")
continue
print(f"播放 {file_name},预计播放时长:{duration} 秒")
if os.name == 'nt': # Windows 系统
process = subprocess.Popen(['start', '', full_file_path], shell=True)
elif os.name == 'posix': # MacOS 或 Linux 系统
process = subprocess.Popen(['afplay', full_file_path]) # 对于 MacOS 使用 afplay
else:
print(f"不支持的操作系统类型: {os.name}")
continue
processes.append(process) # 保存进程对象
time.sleep(duration) # 等待音频播放完成
except Exception as e:
print(f"无法播放 {full_file_path}: {e}")
# 关闭所有播放器进程
for process in processes:
try:
if process.poll() is None: # 检查进程是否仍在运行
process.kill() # 强制终止进程
print("播放器已强制关闭")
except Exception as e:
print(f"关闭播放器失败: {e}")
def process_time_segment(segment, download_offset, play_offset, data, authorization, folder_name):
"""处理一个时间段的下载和播放"""
target_data = next((entry for entry in data if entry["时间段"] == segment), None)
if not target_data:
print(f"未找到时间段 {segment} 的文件数据!")
return
current_time = datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%H:%M')
segment_start_time = segment.split("-")[0]
# 如果当前时间已经超过该时间段的开始时间,则跳过
if current_time > segment_start_time:
print(f"当前时间已超过时间段 {segment} 的开始时间,跳过该时间段的处理")
return
# 等待到达下载时间
download_time = (datetime.strptime(segment_start_time, "%H:%M") - timedelta(minutes=download_offset)).strftime("%H:%M")
while current_time != download_time:
current_time = datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%H:%M')
time.sleep(1)
print(f"开始下载 {segment} 的文件")
files = []
file_tokens = [
("开播音乐file_token", "mp3"),
("开场白-播报file_token", "mp4"),
("需更新文案-播报file_token", "mp4"),
("壹首歌file_token", "mp3"),
("需更新文案2-播报file_token", "mp4"),
("贰首歌file_token", "mp3"),
("结束语-播报file_token", "mp4"),
("结束音乐file_token", "mp3")
]
for i, (key, file_format) in enumerate(file_tokens):
token = target_data.get(key)
if token:
save_path = os.path.join(folder_name, f"file_{i+1}.{file_format}")
if download_file(token, save_path, authorization):
files.append(save_path)
# 清理之前残留的播放器进程(调整到这里)
kill_previous_players()
# 等待到达播放时间
play_time = (datetime.strptime(segment_start_time, "%H:%M") - timedelta(minutes=play_offset)).strftime("%H:%M")
while current_time != play_time:
current_time = datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%H:%M')
time.sleep(1)
print(f"开始播放 {segment} 的文件")
play_music_in_folder(folder_name)
# 播放结束后再次清理播放器进程
kill_previous_players()
# 删除下载的文件
for file in files:
os.remove(file)
print(f"已删除文件: {file}")
def main():
"""主函数"""
data = get_broadcast_data()
if not data:
print("未获取到有效的数据!")
return
authorization = get_auth_token()
if not authorization:
return
folder_name = "bobao"
create_folder(folder_name)
segments = [
("15:00-15:10", 10, 0) # 提前10分钟下载文件,0-准点播放
]
for segment, download_offset, play_offset in segments:
process_time_segment(segment, download_offset, play_offset, data, authorization, folder_name)
# 主程序结束后,再清理一次播放器进程
kill_previous_players()
if __name__ == "__main__":
main()
上述代码中,播放开场音乐和结束音乐没有播放完,但是获取的播放时长正确