diff --git a/app/lib/api-streamer.ts b/app/lib/api-streamer.ts index 76dd58e..5ed3cda 100644 --- a/app/lib/api-streamer.ts +++ b/app/lib/api-streamer.ts @@ -101,7 +101,7 @@ export interface LiveStreamerEntity { upload_id?: number; status?: string | React.ReactNode; format?: string; - time_range?: string; + time_range?: string | Date[]; excluded_keywords?: string[]; preprocessor?: Record<'run', string>[]; segment_processor?: Record<'run', string>[]; diff --git a/app/ui/TemplateModal.tsx b/app/ui/TemplateModal.tsx index 21ebaf0..b6924c9 100644 --- a/app/ui/TemplateModal.tsx +++ b/app/ui/TemplateModal.tsx @@ -78,6 +78,7 @@ const TemplateModal: React.FC = ({ children, entity, onOk }) remark: values?.remark?.trim(), url: values?.url?.trim(), format: values?.format?.trim(), + time_range: JSON.stringify(values?.time_range?.map((date: Date) => date.toISOString())), } await onOk(values) setVisible(false) @@ -104,6 +105,15 @@ const TemplateModal: React.FC = ({ children, entity, onOk }) } }) + try { + if (entity && entity.time_range && typeof entity.time_range === "string") { + const tr: string[] = JSON.parse(entity.time_range) + entity.time_range = tr.map(t => new Date(t)) + } + } catch (e) { + console.error(e) + } + return ( <> {childrenWithProps} @@ -205,18 +215,29 @@ const TemplateModal: React.FC = ({ children, entity, onOk }) style={{ width: 176 }} helpText="视频保存格式。不支持stream-gears下载器和Youtube平台。" /> - + - - 格式:'01:00:00-02:00:00'(时:分:秒-时:分:秒) + 如果设置了录制时间范围,不在时间范围内,将不进行录制
+ 下载器需使用ffmpeg或streamlink } - label="录制时间范围" - placeholder="01:00:00-02:00:00" + label={{ + text: '录制时间范围', + optional: true, + style: { + fontSize: '18px', + marginBottom: '4px', + paddingBottom: '8px', + borderBottom: '1px solid var(--semi-color-border)', + } + }} style={{ width: 176 }} /> diff --git a/biliup/common/util.py b/biliup/common/util.py index fd7b6d1..95166cb 100644 --- a/biliup/common/util.py +++ b/biliup/common/util.py @@ -1,7 +1,8 @@ import asyncio +import json import httpx -from datetime import datetime, time, timezone, timedelta +from datetime import datetime, timezone from biliup.config import config import logging @@ -32,26 +33,25 @@ logger = logging.getLogger('biliup') def check_timerange(name): - time_range = config['streamers'].get(name, {}).get('time_range') - now = datetime.now(tz=timezone(timedelta(hours=8))).time() - logger.debug(f"{name}: 校验时间范围 {time_range} 当前时间 {now.strftime('%H:%M:%S')}") - - if not time_range or '-' not in time_range: - return True - try: - start_time, end_time = map(time_string_to_time, time_range.split('-')) - except (ValueError, IndexError) as e: - logger.exception(f"Invalid time range format: {e}") + time_range_str = config['streamers'].get(name, {}).get('time_range') + if not time_range_str: + return True + time_range = json.loads(time_range_str) + if not isinstance(time_range, (list, tuple)) or len(time_range) != 2: + return True + + start = datetime.fromisoformat(time_range[0].replace('Z', '+00:00')).time() + end = datetime.fromisoformat(time_range[1].replace('Z', '+00:00')).time() + except Exception as e: + logger.error(f'parsing time range {e}') return True - if start_time > end_time: - is_in_range = now >= start_time or now <= end_time - else: - is_in_range = start_time <= now <= end_time - return is_in_range + now = datetime.now(timezone.utc).time() + # Normal interval (e.g. 16:00 → 20:00) + if start <= end: + return start <= now <= end -def time_string_to_time(time_string): - h, m, s = map(int, time_string.split(':')) - return time(h, m, s) + # Cross‑midnight (e.g. 23:00 → 04:00) + return now >= start or now <= end diff --git a/biliup/engine/download.py b/biliup/engine/download.py index 4523572..2b46ebf 100644 --- a/biliup/engine/download.py +++ b/biliup/engine/download.py @@ -1,5 +1,6 @@ import asyncio import inspect +import json import logging import os import queue @@ -11,7 +12,7 @@ import shutil from abc import ABC, abstractmethod from typing import AsyncGenerator, List, Callable, Optional from urllib.parse import urlparse -from datetime import datetime, timedelta +from datetime import datetime, timezone import requests from requests.utils import DEFAULT_ACCEPT_ENCODING @@ -599,41 +600,40 @@ def get_valid_filename(name): return s -def get_duration(segment_time_str, time_range): +def get_duration(segment_time_str, time_range_str): """ 计算当前时间到给定结束时间的时差 如果计算的时差大于segment_time,则返回segment_time。 """ - if not time_range or '-' not in time_range: - return segment_time_str - end_time_str = time_range.split('-')[1] - - now = datetime.now() - end_time_today_str = now.strftime("%Y-%m-%d") + " " + end_time_str - end_time_today = datetime.strptime(end_time_today_str, "%Y-%m-%d %H:%M:%S") - # 判断结束时间是否是第二天的时间 - if now > end_time_today: - end_time_today += timedelta(days=1) - - time_diff = end_time_today - now - if segment_time_str: - segment_time_parts = list(map(int, segment_time_str.split(":"))) - segment_time = timedelta(hours=segment_time_parts[0], - minutes=segment_time_parts[1], seconds=segment_time_parts[2]) - - if time_diff > segment_time: + try: + time_range = json.loads(time_range_str) + if not isinstance(time_range, (list, tuple)) or len(time_range) != 2: return segment_time_str + end_time = datetime.fromisoformat(time_range[1].replace('Z', '+00:00')).time() + except Exception as e: + return segment_time_str - # 增加10s,防止time_diff过小多次执行下载 - if time_diff.total_seconds() <= 60: - time_diff = time_diff + timedelta(seconds=10) + now = datetime.now(timezone.utc).time() + now_sec = now.hour * 3600 + now.minute * 60 + now.second + end_sec = end_time.hour * 3600 + end_time.minute * 60 + end_time.second - hours, remainder = divmod(time_diff.total_seconds(), 3600) - minutes, seconds = divmod(remainder, 60) + # 计算到结束时间的秒数 + diff = end_sec - now_sec if end_sec >= now_sec else (24 * 3600 - now_sec + end_sec) - to_parameter = f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}" + try: + h, m, s = map(int, segment_time_str.split(':')) + segment_sec = h * 3600 + m * 60 + s + except Exception: + return segment_time_str - return to_parameter + if diff > segment_sec: + return segment_time_str + + hours = diff // 3600 + minutes = (diff % 3600) // 60 + seconds = diff % 60 + + return f"{hours:02}:{minutes:02}:{seconds:02}" class BatchCheck(ABC): diff --git a/public/config.toml b/public/config.toml index 24d4edc..ebd6e9f 100644 --- a/public/config.toml +++ b/public/config.toml @@ -229,7 +229,7 @@ user_cookie = "cookies.json" ### 使用指定的账号上传 #use_live_cover = true # 获取BILIBILI直播间封面并作为投稿封面。此封面优先级低于单个主播指定的自定义封面。 tags = ["biliup", "视频标签"] #format = "mp4" ### 视频保存格式。如需使用mp4格式,必须切换downloader为ffmpeg或者streamlink,youtube不支持。 -#time_range = "01:00:00-23:00:00" ### 录制时间范围 +#time_range: "[\"2025-03-26T16:00:00.000Z\",\"2025-03-27T15:59:59.000Z\"]" ### 录制时间范围 ISO 8601 格式的时间数组字符串 #excluded_keywords: ### 如果房间名包含关键词,则停止或不录制该场直播,每个关键词需单独一行 ### 开始下载直播时触发,将按自定义顺序执行自定义操作 注:preprocessor仅支持shell指令 #preprocessor = [ diff --git a/public/config.yaml b/public/config.yaml index e85533e..1b05ac7 100644 --- a/public/config.yaml +++ b/public/config.yaml @@ -223,7 +223,7 @@ streamers: tags: - biliup - 视频标签 - #time_range: "01:00:00-23:00:00" ### 录制时间范围 + #time_range: "[\"2025-03-26T16:00:00.000Z\",\"2025-03-27T15:59:59.000Z\"]" ### 录制时间范围 ISO 8601 格式的时间数组字符串 #excluded_keywords: ### 如果房间名包含关键词,则停止或不录制该场直播,每个关键词需单独一行 #preprocessor: ### 开始下载直播时触发,将按自定义顺序执行自定义操作 注:preprocessor仅支持shell指令 #- run: sh ./run.sh, ### 执行任意命令,等同于在shell中运行,preprocessor输出的数据为JSON格式,包含主播名字(name)、开播地址(url)和开播时间(start_time)(时间戳)