Add custom video bitrate support

This commit is contained in:
ihmily
2026-08-08 16:33:04 +08:00
parent be8c281ddf
commit 2de671cb81
15 changed files with 1552 additions and 1548 deletions

View File

@@ -41,6 +41,7 @@ class FFmpegCommandBuilder(abc.ABC):
headers: str | None = None,
proxy: str | None = None,
platform_key: str | None = None,
video_bitrate: int | None = None,
):
"""
Initializes the FFmpegCommandBuilder.
@@ -53,6 +54,7 @@ class FFmpegCommandBuilder(abc.ABC):
:param headers: Additional headers to include in the request.
:param proxy: Proxy server URL to use for the connection.
:param platform_key: Platform identifier used for platform-specific FFmpeg compatibility options.
:param video_bitrate: Custom output video bitrate in kbps. Enables H.264 transcoding when set.
"""
self.record_url = record_url
self.is_overseas = is_overseas
@@ -62,6 +64,7 @@ class FFmpegCommandBuilder(abc.ABC):
self.proxy = proxy or ""
self.headers = headers or ""
self.platform_key = platform_key
self.video_bitrate = video_bitrate
@abc.abstractmethod
def build_command(self) -> list[str]:
@@ -118,3 +121,8 @@ class FFmpegCommandBuilder(abc.ABC):
command.insert(2, self.proxy)
return command
def _get_video_codec_options(self) -> list[str]:
if self.video_bitrate:
return ["-c:v", "libx264", "-preset", "veryfast", "-b:v", f"{self.video_bitrate}k"]
return ["-c:v", "copy"]

View File

@@ -1,31 +1,31 @@
from ..base import FFmpegCommandBuilder
class FLVCommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
"-map", "0",
"-c:v", "copy",
"-c:a", "copy",
"-bsf:a", "aac_adtstoasc",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "flv",
"-reset_timestamps", "1",
self.full_path
]
else:
additional_commands = [
"-map", "0",
"-c:v", "copy",
"-c:a", "copy",
"-bsf:a", "aac_adtstoasc",
"-f", "flv",
self.full_path
]
# fmt: on
command.extend(additional_commands)
return command
from ..base import FFmpegCommandBuilder
class FLVCommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
"-map", "0",
*self._get_video_codec_options(),
"-c:a", "copy",
"-bsf:a", "aac_adtstoasc",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "flv",
"-reset_timestamps", "1",
self.full_path
]
else:
additional_commands = [
"-map", "0",
*self._get_video_codec_options(),
"-c:a", "copy",
"-bsf:a", "aac_adtstoasc",
"-f", "flv",
self.full_path
]
# fmt: on
command.extend(additional_commands)
return command

View File

@@ -1,32 +1,32 @@
from ..base import FFmpegCommandBuilder
class MKVCommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
"-flags", "global_header",
"-c:v", "copy",
"-c:a", "aac",
"-map", "0",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "matroska",
"-reset_timestamps", "1",
self.full_path,
]
else:
additional_commands = [
"-flags", "global_header",
"-map", "0",
"-c:v", "copy",
"-c:a", "copy",
"-f", "matroska",
self.full_path,
]
# fmt: on
command.extend(additional_commands)
return command
from ..base import FFmpegCommandBuilder
class MKVCommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
"-flags", "global_header",
*self._get_video_codec_options(),
"-c:a", "aac",
"-map", "0",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "matroska",
"-reset_timestamps", "1",
self.full_path,
]
else:
additional_commands = [
"-flags", "global_header",
"-map", "0",
*self._get_video_codec_options(),
"-c:a", "copy",
"-f", "matroska",
self.full_path,
]
# fmt: on
command.extend(additional_commands)
return command

View File

@@ -1,34 +1,34 @@
from ..base import FFmpegCommandBuilder
class MOVCommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
"-c:v", "copy",
"-c:a", "aac",
"-map", "0",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "mov",
"-reset_timestamps", "1",
"-movflags", "+frag_keyframe+empty_moov+faststart",
"-flags", "global_header",
self.full_path,
]
else:
additional_commands = [
"-map", "0",
"-c:v", "copy",
"-c:a", "aac",
"-f", "mov",
"-movflags", "+faststart",
self.full_path,
]
# fmt: on
command.extend(additional_commands)
return command
from ..base import FFmpegCommandBuilder
class MOVCommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
*self._get_video_codec_options(),
"-c:a", "aac",
"-map", "0",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "mov",
"-reset_timestamps", "1",
"-movflags", "+frag_keyframe+empty_moov+faststart",
"-flags", "global_header",
self.full_path,
]
else:
additional_commands = [
"-map", "0",
*self._get_video_codec_options(),
"-c:a", "aac",
"-f", "mov",
"-movflags", "+faststart",
self.full_path,
]
# fmt: on
command.extend(additional_commands)
return command

View File

@@ -1,33 +1,33 @@
from ..base import FFmpegCommandBuilder
class MP4CommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
"-c:v", "copy",
"-c:a", "aac",
"-map", "0",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "mp4",
"-reset_timestamps", "1",
"-movflags", "+frag_keyframe+empty_moov+faststart+delay_moov",
"-flags", "global_header",
self.full_path,
]
else:
additional_commands = [
"-map", "0",
"-c:v", "copy",
"-c:a", "copy",
"-f", "mp4",
"-movflags", "+faststart+frag_keyframe+empty_moov+delay_moov",
self.full_path,
]
# fmt: on
command.extend(additional_commands)
return command
from ..base import FFmpegCommandBuilder
class MP4CommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
*self._get_video_codec_options(),
"-c:a", "aac",
"-map", "0",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "mp4",
"-reset_timestamps", "1",
"-movflags", "+frag_keyframe+empty_moov+faststart+delay_moov",
"-flags", "global_header",
self.full_path,
]
else:
additional_commands = [
"-map", "0",
*self._get_video_codec_options(),
"-c:a", "copy",
"-f", "mp4",
"-movflags", "+faststart+frag_keyframe+empty_moov+delay_moov",
self.full_path,
]
# fmt: on
command.extend(additional_commands)
return command

View File

@@ -1,34 +1,34 @@
from ..base import FFmpegCommandBuilder
class NUTCommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
"-c:v", "copy",
"-c:a", "copy",
"-map", "0",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "nut",
"-reset_timestamps", "1",
"-muxdelay", "0",
"-muxpreload", "0",
self.full_path,
]
else:
additional_commands = [
"-c:v", "copy",
"-c:a", "copy",
"-map", "0",
"-f", "nut",
"-muxdelay", "0",
"-muxpreload", "0",
self.full_path,
]
# fmt: on
command.extend(additional_commands)
return command
from ..base import FFmpegCommandBuilder
class NUTCommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
*self._get_video_codec_options(),
"-c:a", "copy",
"-map", "0",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "nut",
"-reset_timestamps", "1",
"-muxdelay", "0",
"-muxpreload", "0",
self.full_path,
]
else:
additional_commands = [
*self._get_video_codec_options(),
"-c:a", "copy",
"-map", "0",
"-f", "nut",
"-muxdelay", "0",
"-muxpreload", "0",
self.full_path,
]
# fmt: on
command.extend(additional_commands)
return command

View File

@@ -1,35 +1,35 @@
from ..base import FFmpegCommandBuilder
class TSCommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
"-c:v", "copy",
"-c:a", "copy",
"-map", "0",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "mpegts",
"-reset_timestamps", "1",
"-mpegts_flags", "+resend_headers",
"-muxdelay", "0",
"-muxpreload", "0",
self.full_path,
]
else:
additional_commands = [
"-c:v", "copy",
"-c:a", "copy",
"-map", "0",
"-f", "mpegts",
"-mpegts_flags", "+resend_headers",
"-muxdelay", "0",
"-muxpreload", "0",
self.full_path,
]
# fmt: on
command.extend(additional_commands)
return command
from ..base import FFmpegCommandBuilder
class TSCommandBuilder(FFmpegCommandBuilder):
def build_command(self) -> list[str]:
command = self._get_basic_ffmpeg_command()
# fmt: off
if self.segment_record:
additional_commands = [
*self._get_video_codec_options(),
"-c:a", "copy",
"-map", "0",
"-f", "segment",
"-segment_time", str(self.segment_time),
"-segment_format", "mpegts",
"-reset_timestamps", "1",
"-mpegts_flags", "+resend_headers",
"-muxdelay", "0",
"-muxpreload", "0",
self.full_path,
]
else:
additional_commands = [
*self._get_video_codec_options(),
"-c:a", "copy",
"-map", "0",
"-f", "mpegts",
"-mpegts_flags", "+resend_headers",
"-muxdelay", "0",
"-muxpreload", "0",
self.full_path,
]
# fmt: on
command.extend(additional_commands)
return command

File diff suppressed because it is too large Load Diff

View File

@@ -45,6 +45,7 @@ class LiveStreamRecorder:
self.segment_record = self._get_info("segment_record", default=False)
self.segment_time = self._get_info("segment_time", default=self.DEFAULT_SEGMENT_TIME)
self.quality = self._get_info("quality", default=self.DEFAULT_QUALITY)
self.video_bitrate = self._get_info("video_bitrate")
self.save_format = self._get_info("save_format", default=self.DEFAULT_SAVE_FORMAT).lower()
self.proxy = self.is_use_proxy()
self.direct_downloader = None
@@ -316,6 +317,7 @@ class LiveStreamRecorder:
full_path=save_path,
headers=self.get_headers_params(record_url, self.platform_key),
platform_key=self.platform_key,
video_bitrate=self.video_bitrate,
)
ffmpeg_command = ffmpeg_builder.build_command()
self.services.run_coro(

View File

@@ -1,147 +1,152 @@
from datetime import timedelta
class Recording:
def __init__(
self,
rec_id,
url,
streamer_name,
record_format,
quality,
segment_record,
segment_time,
monitor_status,
scheduled_recording,
scheduled_start_time,
monitor_hours,
recording_dir,
enabled_message_push,
only_notify_no_record,
flv_use_direct_download,
):
"""
Initialize a recording object.
:param rec_id: Unique identifier for the recording task.
:param url: URL address of the live stream.
:param streamer_name: Name of the streamer.
:param record_format: Format of the recorded file, e.g., 'mp4', 'ts', 'mkv'.
:param quality: Quality of the recorded video, e.g., 'OD', 'UHD', 'HD'.
:param segment_record: Whether to enable segmented recording.
:param segment_time: Time interval (in seconds) for segmented recording if enabled.
:param monitor_status: Monitoring status, whether the live room is being monitored.
:param scheduled_recording: Whether to enable scheduled recording.
:param scheduled_start_time: Scheduled start time for recording (string format like '18:30:00').
:param monitor_hours: Number of hours to monitor from the scheduled recording start time, e.g., 3.
:param recording_dir: Directory path where the recorded files will be saved.
:param enabled_message_push: Whether to enable message push.
:param only_notify_no_record: Whether to only notify when no record is made.
:param flv_use_direct_download: Whether to use direct downloader to cache FLV stream.
"""
self.rec_id = rec_id
self.url = url
self.quality = quality
self.record_format = record_format
self.monitor_status = monitor_status
self.segment_record = segment_record
self.segment_time = segment_time
self.streamer_name = streamer_name
self.scheduled_recording = scheduled_recording
self.scheduled_start_time = scheduled_start_time
self.monitor_hours = monitor_hours
self.recording_dir = recording_dir
self.enabled_message_push = enabled_message_push
self.only_notify_no_record = only_notify_no_record
self.flv_use_direct_download = flv_use_direct_download
self.scheduled_time_range = None
self.title = f"{streamer_name} - {self.quality}"
self.speed = "X KB/s"
self.is_live = False
self.is_recording = False
self.start_time = None
self.manually_stopped = False
self.force_stop = False
self.stopping_in_progress = False
self.stop_requested = False
self.platform = None
self.platform_key = None
self.notified_live_start = False
self.notified_live_end = False
self.cumulative_duration = timedelta() # Accumulated recording time
self.last_duration = timedelta() # Save the total time of the last recording
self.display_title = self.title
self.selected = False
self.is_checking = False
self.showed_checking_status = False
self.status_info = None
self.live_title = None
self.detection_time = None
self.loop_time_seconds = None
self.use_proxy = None
self.record_url = None
self.preview_url = None
def to_dict(self):
"""Convert the Recording instance to a dictionary for saving."""
return {
"rec_id": self.rec_id,
"url": self.url,
"streamer_name": self.streamer_name,
"record_format": self.record_format,
"quality": self.quality,
"segment_record": self.segment_record,
"segment_time": self.segment_time,
"monitor_status": self.monitor_status,
"scheduled_recording": self.scheduled_recording,
"scheduled_start_time": self.scheduled_start_time,
"monitor_hours": self.monitor_hours,
"recording_dir": self.recording_dir,
"enabled_message_push": self.enabled_message_push,
"platform": self.platform,
"platform_key": self.platform_key,
"only_notify_no_record": self.only_notify_no_record,
"flv_use_direct_download": self.flv_use_direct_download,
}
@classmethod
def from_dict(cls, data):
"""Create a Recording instance from a dictionary."""
recording = cls(
data.get("rec_id"),
data.get("url"),
data.get("streamer_name"),
data.get("record_format"),
data.get("quality"),
data.get("segment_record"),
data.get("segment_time"),
data.get("monitor_status"),
data.get("scheduled_recording"),
data.get("scheduled_start_time"),
data.get("monitor_hours"),
data.get("recording_dir"),
data.get("enabled_message_push"),
data.get("only_notify_no_record"),
data.get("flv_use_direct_download"),
)
recording.title = data.get("title", recording.title)
recording.display_title = data.get("display_title", recording.title)
recording.last_duration_str = data.get("last_duration")
recording.platform = data.get("platform")
recording.platform_key = data.get("platform_key")
if recording.last_duration_str is not None:
recording.last_duration = timedelta(seconds=float(recording.last_duration_str))
return recording
def update_title(self, quality_info, prefix=None):
"""Helper method to update the title."""
self.title = f"{self.streamer_name} - {quality_info}"
self.display_title = f"{prefix or ''}{self.title}"
def update(self, updated_info: dict):
"""Update the recording object with new information."""
for attr, value in updated_info.items():
if hasattr(self, attr):
setattr(self, attr, value)
from datetime import timedelta
class Recording:
def __init__(
self,
rec_id,
url,
streamer_name,
record_format,
quality,
segment_record,
segment_time,
monitor_status,
scheduled_recording,
scheduled_start_time,
monitor_hours,
recording_dir,
enabled_message_push,
only_notify_no_record,
flv_use_direct_download,
video_bitrate=None,
):
"""
Initialize a recording object.
:param rec_id: Unique identifier for the recording task.
:param url: URL address of the live stream.
:param streamer_name: Name of the streamer.
:param record_format: Format of the recorded file, e.g., 'mp4', 'ts', 'mkv'.
:param quality: Quality of the recorded video, e.g., 'OD', 'UHD', 'HD'.
:param segment_record: Whether to enable segmented recording.
:param segment_time: Time interval (in seconds) for segmented recording if enabled.
:param monitor_status: Monitoring status, whether the live room is being monitored.
:param scheduled_recording: Whether to enable scheduled recording.
:param scheduled_start_time: Scheduled start time for recording (string format like '18:30:00').
:param monitor_hours: Number of hours to monitor from the scheduled recording start time, e.g., 3.
:param recording_dir: Directory path where the recorded files will be saved.
:param enabled_message_push: Whether to enable message push.
:param only_notify_no_record: Whether to only notify when no record is made.
:param flv_use_direct_download: Whether to use direct downloader to cache FLV stream.
:param video_bitrate: Custom output video bitrate in kbps, or None to copy the source video stream.
"""
self.rec_id = rec_id
self.url = url
self.quality = quality
self.record_format = record_format
self.monitor_status = monitor_status
self.segment_record = segment_record
self.segment_time = segment_time
self.streamer_name = streamer_name
self.scheduled_recording = scheduled_recording
self.scheduled_start_time = scheduled_start_time
self.monitor_hours = monitor_hours
self.recording_dir = recording_dir
self.enabled_message_push = enabled_message_push
self.only_notify_no_record = only_notify_no_record
self.flv_use_direct_download = flv_use_direct_download
self.video_bitrate = video_bitrate
self.scheduled_time_range = None
self.title = f"{streamer_name} - {self.quality}"
self.speed = "X KB/s"
self.is_live = False
self.is_recording = False
self.start_time = None
self.manually_stopped = False
self.force_stop = False
self.stopping_in_progress = False
self.stop_requested = False
self.platform = None
self.platform_key = None
self.notified_live_start = False
self.notified_live_end = False
self.cumulative_duration = timedelta() # Accumulated recording time
self.last_duration = timedelta() # Save the total time of the last recording
self.display_title = self.title
self.selected = False
self.is_checking = False
self.showed_checking_status = False
self.status_info = None
self.live_title = None
self.detection_time = None
self.loop_time_seconds = None
self.use_proxy = None
self.record_url = None
self.preview_url = None
def to_dict(self):
"""Convert the Recording instance to a dictionary for saving."""
return {
"rec_id": self.rec_id,
"url": self.url,
"streamer_name": self.streamer_name,
"record_format": self.record_format,
"quality": self.quality,
"segment_record": self.segment_record,
"segment_time": self.segment_time,
"monitor_status": self.monitor_status,
"scheduled_recording": self.scheduled_recording,
"scheduled_start_time": self.scheduled_start_time,
"monitor_hours": self.monitor_hours,
"recording_dir": self.recording_dir,
"enabled_message_push": self.enabled_message_push,
"platform": self.platform,
"platform_key": self.platform_key,
"only_notify_no_record": self.only_notify_no_record,
"flv_use_direct_download": self.flv_use_direct_download,
"video_bitrate": self.video_bitrate,
}
@classmethod
def from_dict(cls, data):
"""Create a Recording instance from a dictionary."""
recording = cls(
data.get("rec_id"),
data.get("url"),
data.get("streamer_name"),
data.get("record_format"),
data.get("quality"),
data.get("segment_record"),
data.get("segment_time"),
data.get("monitor_status"),
data.get("scheduled_recording"),
data.get("scheduled_start_time"),
data.get("monitor_hours"),
data.get("recording_dir"),
data.get("enabled_message_push"),
data.get("only_notify_no_record"),
data.get("flv_use_direct_download"),
data.get("video_bitrate"),
)
recording.title = data.get("title", recording.title)
recording.display_title = data.get("display_title", recording.title)
recording.last_duration_str = data.get("last_duration")
recording.platform = data.get("platform")
recording.platform_key = data.get("platform_key")
if recording.last_duration_str is not None:
recording.last_duration = timedelta(seconds=float(recording.last_duration_str))
return recording
def update_title(self, quality_info, prefix=None):
"""Helper method to update the title."""
self.title = f"{self.streamer_name} - {quality_info}"
self.display_title = f"{prefix or ''}{self.title}"
def update(self, updated_info: dict):
"""Update the recording object with new information."""
for attr, value in updated_info.items():
if hasattr(self, attr):
setattr(self, attr, value)

File diff suppressed because it is too large Load Diff

View File

@@ -1,76 +1,78 @@
import flet as ft
from ....messages.message_pusher import MessagePusher
from ....models.recording.recording_status_model import RecordingStatus
class CardDialog(ft.AlertDialog):
def __init__(self, app, recording):
self.app = app
self._ = {}
self.load()
super().__init__(
title=ft.Text(self._["recording_info"]),
content=self.get_content(recording),
actions=[
ft.TextButton(self._["close"], on_click=self.close_panel),
],
actions_alignment=ft.MainAxisAlignment.END,
modal=False,
)
def load(self):
language = self.app.language_manager.language
for key in ("recording_card", "recording_manager", "base", "video_quality", "recording_dialog"):
self._.update(language.get(key, {}))
def get_content(self, recording):
"""Record card information content"""
anchor_name = recording.streamer_name
platform_name = recording.platform
live_link = recording.url
live_title = recording.live_title or self._["none"]
record_format = recording.record_format
quality_info = self._[recording.quality]
use_proxy = self._["yes"] if recording.use_proxy else self._["no"]
segment_record_status = self._["enabled"] if recording.segment_record else self._["disabled"]
segment_time = f"{recording.segment_time}{self._['seconds']}"
monitor_status = self._["enabled"] if recording.monitor_status else self._["disabled"]
scheduled_recording_status = self._["enabled"] if recording.scheduled_recording else self._["disabled"]
scheduled_time_range = recording.scheduled_time_range or self._["none"]
save_path = recording.recording_dir or self._["no_recording_dir_tip"]
status_info = RecordingStatus.MONITORING if recording.monitor_status else RecordingStatus.STOPPED_MONITORING
recording_status_info = self._[recording.status_info or status_info]
should_push_message = MessagePusher.should_push_message(self.app.settings, recording, message_type="other")
message_push = self._["enabled"] if should_push_message else self._["disabled"]
if not should_push_message and recording.enabled_message_push:
message_push = self._["disabled"] + f" ({self._['not_config_tip']})"
only_notify_no_record = self._["enabled"] if recording.only_notify_no_record else self._["disabled"]
dialog_content = ft.Column(
[
ft.Text(f"{self._['anchor_name']}: {anchor_name}", size=14, selectable=True),
ft.Text(f"{self._['platform_name']}: {platform_name}", size=14, selectable=True),
ft.Text(f"{self._['live_link']}: {live_link}", size=14, selectable=True),
ft.Text(f"{self._['live_title']}: {live_title}", size=14, selectable=True),
ft.Text(f"{self._['record_format']}: {record_format}", size=14),
ft.Text(f"{self._['record_quality']}: {quality_info}", size=14),
ft.Text(f"{self._['use_proxy']}: {use_proxy}", size=14),
ft.Text(f"{self._['segment_record']}: {segment_record_status}", size=14),
ft.Text(f"{self._['segment_time']}: {segment_time}", size=14),
ft.Text(f"{self._['monitor_status']}: {monitor_status}", size=14),
ft.Text(f"{self._['scheduled_recording']}: {scheduled_recording_status}", size=14),
ft.Text(f"{self._['scheduled_time_range']}: {scheduled_time_range}", size=14),
ft.Text(f"{self._['message_push']}: {message_push}", size=14),
ft.Text(f"{self._['only_notify_no_record']}: {only_notify_no_record}", size=14),
ft.Text(f"{self._['save_path']}: {save_path}", size=14, selectable=True),
ft.Text(f"{self._['recording_status']}: {recording_status_info}", size=14),
],
spacing=8,
scroll=ft.ScrollMode.AUTO,
)
return dialog_content
def close_panel(self, _):
self.open = False
self.update()
import flet as ft
from ....messages.message_pusher import MessagePusher
from ....models.recording.recording_status_model import RecordingStatus
class CardDialog(ft.AlertDialog):
def __init__(self, app, recording):
self.app = app
self._ = {}
self.load()
super().__init__(
title=ft.Text(self._["recording_info"]),
content=self.get_content(recording),
actions=[
ft.TextButton(self._["close"], on_click=self.close_panel),
],
actions_alignment=ft.MainAxisAlignment.END,
modal=False,
)
def load(self):
language = self.app.language_manager.language
for key in ("recording_card", "recording_manager", "base", "video_quality", "recording_dialog"):
self._.update(language.get(key, {}))
def get_content(self, recording):
"""Record card information content"""
anchor_name = recording.streamer_name
platform_name = recording.platform
live_link = recording.url
live_title = recording.live_title or self._["none"]
record_format = recording.record_format
quality_info = self._[recording.quality]
video_bitrate = recording.video_bitrate or self._["none"]
use_proxy = self._["yes"] if recording.use_proxy else self._["no"]
segment_record_status = self._["enabled"] if recording.segment_record else self._["disabled"]
segment_time = f"{recording.segment_time}{self._['seconds']}"
monitor_status = self._["enabled"] if recording.monitor_status else self._["disabled"]
scheduled_recording_status = self._["enabled"] if recording.scheduled_recording else self._["disabled"]
scheduled_time_range = recording.scheduled_time_range or self._["none"]
save_path = recording.recording_dir or self._["no_recording_dir_tip"]
status_info = RecordingStatus.MONITORING if recording.monitor_status else RecordingStatus.STOPPED_MONITORING
recording_status_info = self._[recording.status_info or status_info]
should_push_message = MessagePusher.should_push_message(self.app.settings, recording, message_type="other")
message_push = self._["enabled"] if should_push_message else self._["disabled"]
if not should_push_message and recording.enabled_message_push:
message_push = self._["disabled"] + f" ({self._['not_config_tip']})"
only_notify_no_record = self._["enabled"] if recording.only_notify_no_record else self._["disabled"]
dialog_content = ft.Column(
[
ft.Text(f"{self._['anchor_name']}: {anchor_name}", size=14, selectable=True),
ft.Text(f"{self._['platform_name']}: {platform_name}", size=14, selectable=True),
ft.Text(f"{self._['live_link']}: {live_link}", size=14, selectable=True),
ft.Text(f"{self._['live_title']}: {live_title}", size=14, selectable=True),
ft.Text(f"{self._['record_format']}: {record_format}", size=14),
ft.Text(f"{self._['record_quality']}: {quality_info}", size=14),
ft.Text(f"{self._['custom_video_bitrate']}: {video_bitrate}", size=14),
ft.Text(f"{self._['use_proxy']}: {use_proxy}", size=14),
ft.Text(f"{self._['segment_record']}: {segment_record_status}", size=14),
ft.Text(f"{self._['segment_time']}: {segment_time}", size=14),
ft.Text(f"{self._['monitor_status']}: {monitor_status}", size=14),
ft.Text(f"{self._['scheduled_recording']}: {scheduled_recording_status}", size=14),
ft.Text(f"{self._['scheduled_time_range']}: {scheduled_time_range}", size=14),
ft.Text(f"{self._['message_push']}: {message_push}", size=14),
ft.Text(f"{self._['only_notify_no_record']}: {only_notify_no_record}", size=14),
ft.Text(f"{self._['save_path']}: {save_path}", size=14, selectable=True),
ft.Text(f"{self._['recording_status']}: {recording_status_info}", size=14),
],
spacing=8,
scroll=ft.ScrollMode.AUTO,
)
return dialog_content
def close_panel(self, _):
self.open = False
self.update()

View File

@@ -499,6 +499,7 @@ class RecordingsPage(PageBase):
enabled_message_push=recording_info["enabled_message_push"],
only_notify_no_record=recording_info["only_notify_no_record"],
flv_use_direct_download=recording_info["flv_use_direct_download"],
video_bitrate=recording_info["video_bitrate"],
)
else:
recording = Recording(
@@ -517,6 +518,7 @@ class RecordingsPage(PageBase):
enabled_message_push=False,
only_notify_no_record=user_config.get("only_notify_no_record"),
flv_use_direct_download=user_config.get("flv_use_direct_download"),
video_bitrate=None,
)
platform, platform_key = get_platform_info(recording.url)

View File

@@ -77,6 +77,9 @@
"input_live_link": "Enter Live Room URL",
"example": "Example",
"select_resolution": "Select Recording Resolution",
"custom_video_bitrate": "Custom Video Bitrate (kbps)",
"custom_video_bitrate_hint": "Leave blank to keep the source bitrate",
"custom_video_bitrate_invalid": "Enter a whole-number bitrate greater than 0",
"flv_use_direct_download": "FLV Source Use Direct Downloader",
"flv_use_direct_download_tip": "Enable lower latency, but does not support segmented recording",
"input_anchor_name": "Enter Broadcaster Name",
@@ -215,33 +218,7 @@
"title": "title",
"proxy_settings": "Proxy Settings",
"remember_window_size": "Remember Window Size",
"scheduled_shutdown": "Scheduled Shutdown",
"scheduled_shutdown_time": "Daily shutdown time",
"scheduled_shutdown_pick_time": "Select daily shutdown time",
"scheduled_shutdown_more": "Open shutdown settings",
"scheduled_shutdown_tip": "Start the shutdown countdown at the selected time every day",
"scheduled_shutdown_system_message": "StreamCap scheduled shutdown started; it can be cancelled within 60 seconds",
"scheduled_shutdown_countdown": "The system will shut down in {seconds} seconds.",
"scheduled_shutdown_cancel": "Cancel this shutdown",
"scheduled_shutdown_cancelled": "This shutdown has been cancelled",
"scheduled_shutdown_failed": "Unable to start scheduled shutdown: {error}",
"scheduled_shutdown_cancel_failed": "Unable to cancel this shutdown: {error}",
"quick_shutdown": "Quick Shutdown",
"quick_shutdown_hours_suffix": "hours",
"quick_shutdown_ready": "Enter 1-{max_hours} hours, then start",
"quick_shutdown_invalid_hours": "Enter a number of hours from 1 to {max_hours}",
"quick_shutdown_start": "Start",
"quick_shutdown_start_tip": "Schedule shutdown after a number of hours",
"quick_shutdown_cancel": "Cancel quick shutdown",
"quick_shutdown_confirm_title": "Confirm Quick Shutdown",
"quick_shutdown_confirm_content": "The computer will shut down in {hours} hour(s), at {time}. Start the timer?",
"quick_shutdown_active": "Scheduled shutdown: {time}",
"quick_shutdown_started": "Shutdown scheduled for {time}",
"quick_shutdown_cancelled": "Quick shutdown cancelled",
"quick_shutdown_failed": "Unable to schedule quick shutdown: {error}",
"quick_shutdown_cancel_failed": "Unable to cancel quick shutdown: {error}",
"quick_shutdown_system_message": "StreamCap quick shutdown: {hours}-hour countdown",
"is_proxy_enabled": "Configuration for using proxy and related settings",
"is_proxy_enabled": "Configuration for using proxy and related settings",
"enable_proxy": "Enable Proxy",
"proxy_address": "Proxy Address",
"skip_proxy_detection": "Skip Proxy Detection",

View File

@@ -77,6 +77,9 @@
"input_live_link": "输入直播间地址",
"example": "例如",
"select_resolution": "选择录制清晰度",
"custom_video_bitrate": "自定义视频码率kbps",
"custom_video_bitrate_hint": "留空则保持原始码率(推荐默认不填)",
"custom_video_bitrate_invalid": "请输入大于 0 的整数码率",
"flv_use_direct_download": "FLV源直接使用下载器缓存",
"flv_use_direct_download_tip": "开启后延迟更低,但不支持分段录制",
"input_anchor_name": "输入主播名称",
@@ -214,33 +217,7 @@
"time": "时间",
"title": "标题",
"remember_window_size": "退出时记住窗口大小",
"scheduled_shutdown": "定时关机",
"scheduled_shutdown_time": "每日关机时间",
"scheduled_shutdown_pick_time": "选择每日关机时间",
"scheduled_shutdown_more": "打开关机设置",
"scheduled_shutdown_tip": "启用后,程序将在每天指定时间启动关机倒计时",
"scheduled_shutdown_system_message": "StreamCap 定时关机任务已触发,可在 60 秒内取消",
"scheduled_shutdown_countdown": "系统将在 {seconds} 秒后关机。",
"scheduled_shutdown_cancel": "取消本次关机",
"scheduled_shutdown_cancelled": "已取消本次关机",
"scheduled_shutdown_failed": "无法启动定时关机:{error}",
"scheduled_shutdown_cancel_failed": "无法取消本次关机:{error}",
"quick_shutdown": "快捷关机",
"quick_shutdown_hours_suffix": "小时后",
"quick_shutdown_ready": "输入 1{max_hours} 小时后开始",
"quick_shutdown_invalid_hours": "请输入 1 到 {max_hours} 之间的小时数",
"quick_shutdown_start": "开始",
"quick_shutdown_start_tip": "设置小时倒计时关机",
"quick_shutdown_cancel": "取消快捷关机",
"quick_shutdown_confirm_title": "确认快捷关机",
"quick_shutdown_confirm_content": "电脑将在 {hours} 小时后({time})关机,是否开始?",
"quick_shutdown_active": "计划关机时间:{time}",
"quick_shutdown_started": "已设置在 {time} 关机",
"quick_shutdown_cancelled": "已取消快捷关机",
"quick_shutdown_failed": "无法设置快捷关机:{error}",
"quick_shutdown_cancel_failed": "无法取消快捷关机:{error}",
"quick_shutdown_system_message": "StreamCap 快捷关机:{hours} 小时倒计时",
"proxy_settings": "代理设置",
"proxy_settings": "代理设置",
"is_proxy_enabled": "设置是否使用代理及相关配置",
"enable_proxy": "开启代理",
"proxy_address": "代理地址",