From 0859aeeb797483f1d8ceeb026bf3fd83be36650d Mon Sep 17 00:00:00 2001 From: solitude Date: Sun, 4 Jan 2026 20:11:56 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=BB=E8=B4=A8?= =?UTF-8?q?=E9=80=89=E9=A1=B9=E4=B8=AD=E4=BB=85=E9=9F=B3=E9=A2=91=E5=BD=95?= =?UTF-8?q?=E5=88=B6=E9=80=89=E9=A1=B9AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/media/video_quality_model.py | 1 + locales/en.json | 3 ++- locales/zh_CN.json | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/models/media/video_quality_model.py b/app/models/media/video_quality_model.py index c7191603..ad6cc909 100644 --- a/app/models/media/video_quality_model.py +++ b/app/models/media/video_quality_model.py @@ -4,6 +4,7 @@ class VideoQuality: HD = "HD" SD = "SD" LD = "LD" + AD = "AD" # Audio-only stream @classmethod def get_qualities(cls): diff --git a/locales/en.json b/locales/en.json index c17f237c..67cf14b3 100644 --- a/locales/en.json +++ b/locales/en.json @@ -406,7 +406,8 @@ "UHD": "Ultra HD", "HD": "High Definition", "SD": "Standard Definition", - "LD": "Low Definition" + "LD": "Low Definition", + "AD": "Audio Only" }, "help_dialog": { "shortcut_key_help": "Shortcut Key Help", diff --git a/locales/zh_CN.json b/locales/zh_CN.json index 42b8ca5e..af57c609 100644 --- a/locales/zh_CN.json +++ b/locales/zh_CN.json @@ -406,7 +406,8 @@ "UHD": "超清", "HD": "高清", "SD": "标清", - "LD": "流畅" + "LD": "流畅", + "AD": "音频" }, "help_dialog": { "shortcut_key_help": "快捷键帮助", From 322662304c710ae0c0907ef1811725f88bd75448 Mon Sep 17 00:00:00 2001 From: iso_SER Date: Sun, 4 Jan 2026 22:17:35 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BB=A5=E9=9D=9Esudo?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=BF=90=E8=A1=8C=E5=AE=B9=E5=99=A8=E7=9A=84?= =?UTF-8?q?=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加仅音频格式录制选项的功能 --- Dockerfile | 50 ++++++++++++++---- .../components/business/recording_dialog.py | 32 +++++++++++- docker-compose.yml | 51 ++++++++++++------- streamget | 1 + 4 files changed, 107 insertions(+), 27 deletions(-) create mode 160000 streamget diff --git a/Dockerfile b/Dockerfile index 6e86f084..e85e8b1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,40 @@ +# --- 第一阶段: 编译/构建阶段 --- +ARG http_proxy +ARG https_proxy + FROM python:3.12-slim AS builder WORKDIR /app -# Install system dependencies for build RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -# Install Python dependencies +# 先复制 streamget 目录和 requirements 文件 +COPY streamget/ ./streamget/ COPY requirements-web.txt . + +# 优先从本地安装 streamget +RUN pip install --no-cache-dir ./streamget + +# 然后安装其他依赖(streamget 已安装,会被跳过) RUN pip install --no-cache-dir -r requirements-web.txt +# 复制其他项目文件 COPY . . -RUN mkdir -p /app/logs /app/downloads +# 提示:在这里创建目录其实没用,因为第二阶段是干净的镜像,建议在第二阶段创建 -# Final stage: production image +# --- 第二阶段: 最终生产镜像 --- FROM python:3.12-slim +# 1. 定义构建参数,允许在 docker-compose 中传入宿主机的 UID/GID +ARG PUID=1000 +ARG PGID=1000 + WORKDIR /app +# 2. 安装运行时依赖 RUN apt-get update && apt-get install -y --no-install-recommends \ ffmpeg \ tzdata \ @@ -30,13 +45,30 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -# Set timezone +# 3. 设置时区 ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo "$TZ" > /etc/timezone -# Copy Python dependencies and project files from the builder stage -COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages -COPY --from=builder /usr/local/bin /usr/local/bin -COPY --from=builder /app/ ./ +# 4. 创建非 root 用户 +# 创建一个名为 appuser 的用户,并赋予指定的 UID 和 GID +RUN groupadd -g ${PGID} appgroup && \ + useradd -u ${PUID} -g appgroup -s /bin/sh -m appuser + +# 5. 从 builder 阶段复制文件,并利用 --chown 直接修改所有权 +# 关键点:将 site-packages 和可执行文件的权限也交给 appuser +COPY --from=builder --chown=appuser:appgroup /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages +COPY --from=builder --chown=appuser:appgroup /usr/local/bin /usr/local/bin +COPY --from=builder --chown=appuser:appgroup /app/ ./ + +# 6. 预创建必要的持久化目录并授权 +# 确保在容器启动前,这些目录的所有者就是 appuser +RUN mkdir -p /app/logs /app/downloads /app/config && \ + chown -R appuser:appgroup /app + +# 7. 切换到非 root 用户运行 +USER appuser + +# 暴露端口(仅作为文档声明,不影响实际映射) +EXPOSE 6006 CMD ["sh", "-c", "python main.py --web --host 0.0.0.0"] \ No newline at end of file diff --git a/app/ui/components/business/recording_dialog.py b/app/ui/components/business/recording_dialog.py index 8916023f..9bd6cf4e 100644 --- a/app/ui/components/business/recording_dialog.py +++ b/app/ui/components/business/recording_dialog.py @@ -50,6 +50,30 @@ async def update_format_options(e): record_format_field.value = record_format_field.options[0].key record_format_field.update() + async def on_quality_change(e): + """当画质选择改变时,如果选择了AD(仅音频),自动切换到音频格式""" + selected_quality = e.control.value + if selected_quality == VideoQuality.AD: + # 切换到音频模式 + media_type_dropdown.value = "audio" + media_type_dropdown.disabled = True # 禁用媒体类型选择 + # 更新格式选项为音频格式 + record_format_field.options = [ft.dropdown.Option(i) for i in AudioFormat.get_formats()] + if record_format_field.value not in AudioFormat.get_formats(): + record_format_field.value = AudioFormat.M4A + else: + # 恢复媒体类型选择的可用性 + media_type_dropdown.disabled = False + # 如果当前是音频模式,切换回视频模式 + if media_type_dropdown.value == "audio": + media_type_dropdown.value = "video" + record_format_field.options = [ft.dropdown.Option(i) for i in VideoFormat.get_formats()] + if record_format_field.value not in VideoFormat.get_formats(): + record_format_field.value = VideoFormat.TS + + media_type_dropdown.update() + record_format_field.update() + url_field = ft.TextField( label=self._["input_live_link"], hint_text=self._["example"] + ":https://www.example.com/xxxxxx", @@ -77,7 +101,12 @@ async def update_format_options(e): on_change=update_format_options ) - if default_record_type == "video": + # 如果初始画质是AD,强制设置为音频模式并禁用媒体类型选择 + if default_record_quality == VideoQuality.AD: + media_type_dropdown.value = "audio" + media_type_dropdown.disabled = True + record_formats = AudioFormat.get_formats() + elif default_record_type == "video": record_formats = VideoFormat.get_formats() else: record_formats = AudioFormat.get_formats() @@ -98,6 +127,7 @@ async def update_format_options(e): filled=False, value=default_record_quality, width=245, + on_change=on_quality_change, ) flv_use_direct_download_dropdown = ft.Dropdown( diff --git a/docker-compose.yml b/docker-compose.yml index e9607889..db00514c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,15 @@ - services: streamcap: - image: ihmily/streamcap + # 1. 使用本地构建,不再直接拉取镜像 + build: + context: . + dockerfile: Dockerfile + args: + - PUID=${CURRENT_UID:-1000} + - PGID=${CURRENT_GID:-1000} + - http_proxy=http://192.168.50.66:7890 + - https_proxy=http://192.168.50.66:7890 + - no_proxy=localhost,127.0.0.1 tty: true ports: - "${PORT:-6006}:${PORT:-6006}" @@ -11,7 +19,7 @@ services: volumes: - ./logs:/app/logs - ./config:/app/config - - ./downloads:/app/downloads + - /media/iso/hdd3/steamcap/:/app/downloads - ./.env:/app/.env healthcheck: test: ["CMD", "sh", "-c", "curl -f http://localhost:${PORT:-6006}/about"] @@ -22,20 +30,29 @@ services: - streamcap-network video_api: - image: ihmily/streamcap - command: python -m app.api.video_stream_service - ports: - - "${VIDEO_API_PORT:-6007}:${VIDEO_API_PORT:-6007}" - environment: - - TZ=${TZ:-Asia/Shanghai} - - CUSTOM_VIDEO_ROOT_DIR=${CUSTOM_VIDEO_ROOT_DIR:-./downloads} - volumes: - - ./downloads:/app/downloads - - ./.env:/app/.env - depends_on: - - streamcap - networks: - - streamcap-network + # 同样使用构建模式,共享同一套构建代码,但运行不同的命令 + build: + context: . + dockerfile: Dockerfile + args: + - PUID=${CURRENT_UID:-1000} + - PGID=${CURRENT_GID:-1000} + - http_proxy=http://192.168.50.66:7890 + - https_proxy=http://192.168.50.66:7890 + - no_proxy=localhost,127.0.0.1 + command: python -m app.api.video_stream_service + ports: + - "${VIDEO_API_PORT:-6007}:${VIDEO_API_PORT:-6007}" + environment: + - TZ=${TZ:-Asia/Shanghai} + - CUSTOM_VIDEO_ROOT_DIR=${CUSTOM_VIDEO_ROOT_DIR:-./downloads} + volumes: + - /media/iso/hdd3/steamcap/:/app/downloads + - ./.env:/app/.env + depends_on: + - streamcap + networks: + - streamcap-network networks: streamcap-network: diff --git a/streamget b/streamget new file mode 160000 index 00000000..41423040 --- /dev/null +++ b/streamget @@ -0,0 +1 @@ +Subproject commit 414230402ece3cae8cdebc0d6853673b637ffc28 From 36dc6687b3f745be9e92e72e7a2190b1e92af9d9 Mon Sep 17 00:00:00 2001 From: iso_SER Date: Mon, 5 Jan 2026 00:59:15 +0800 Subject: [PATCH 3/4] fix dockerignore --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index c452a854..47af65ce 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,6 +10,7 @@ **/docs **/tests **/*.md +!streamget/README.md # Build Artifacts and Cache **/__pycache__ From ff220a36490848530cc8fcd7b3c51fd06d7a52b6 Mon Sep 17 00:00:00 2001 From: iso_SER Date: Sun, 22 Mar 2026 10:31:27 +0800 Subject: [PATCH 4/4] add metadate in ffmpeg command --- app/core/media/ffmpeg_builders/__init__.py | 2 +- app/core/media/ffmpeg_builders/audio/aac.py | 2 +- app/core/media/ffmpeg_builders/audio/m4a.py | 2 +- app/core/media/ffmpeg_builders/audio/mp3.py | 2 +- app/core/media/ffmpeg_builders/audio/wav.py | 2 +- app/core/media/ffmpeg_builders/audio/wma.py | 2 +- app/core/media/ffmpeg_builders/base.py | 17 ++++++++++++++ app/core/media/ffmpeg_builders/video/flv.py | 2 +- app/core/media/ffmpeg_builders/video/mkv.py | 2 +- app/core/media/ffmpeg_builders/video/mov.py | 2 +- app/core/media/ffmpeg_builders/video/mp4.py | 2 +- app/core/media/ffmpeg_builders/video/ts.py | 2 +- app/core/recording/stream_manager.py | 25 ++++++++++++++++++++- streamget | 2 +- 14 files changed, 53 insertions(+), 13 deletions(-) diff --git a/app/core/media/ffmpeg_builders/__init__.py b/app/core/media/ffmpeg_builders/__init__.py index b043ea40..5247e9cf 100644 --- a/app/core/media/ffmpeg_builders/__init__.py +++ b/app/core/media/ffmpeg_builders/__init__.py @@ -17,7 +17,7 @@ def create_builder(format_type: str, *args: Any, **kwargs: Any) -> Any: format_to_class = { "mkv": MKVCommandBuilder, "mp4": MP4CommandBuilder, - "ts": TSCommandBuilder, + "ts" : TSCommandBuilder, "flv": FLVCommandBuilder, "mov": MOVCommandBuilder, "mp3": MP3CommandBuilder, diff --git a/app/core/media/ffmpeg_builders/audio/aac.py b/app/core/media/ffmpeg_builders/audio/aac.py index 3f4690b5..ecd705bc 100644 --- a/app/core/media/ffmpeg_builders/audio/aac.py +++ b/app/core/media/ffmpeg_builders/audio/aac.py @@ -27,4 +27,4 @@ def build_command(self) -> list[str]: ] command.extend(additional_commands) - return command + return self._inject_metadata(command) diff --git a/app/core/media/ffmpeg_builders/audio/m4a.py b/app/core/media/ffmpeg_builders/audio/m4a.py index c0d5a03d..ac475db1 100644 --- a/app/core/media/ffmpeg_builders/audio/m4a.py +++ b/app/core/media/ffmpeg_builders/audio/m4a.py @@ -25,4 +25,4 @@ def build_command(self) -> list[str]: ] command.extend(additional_commands) - return command + return self._inject_metadata(command) diff --git a/app/core/media/ffmpeg_builders/audio/mp3.py b/app/core/media/ffmpeg_builders/audio/mp3.py index 2e2f075e..8ad9038d 100644 --- a/app/core/media/ffmpeg_builders/audio/mp3.py +++ b/app/core/media/ffmpeg_builders/audio/mp3.py @@ -25,4 +25,4 @@ def build_command(self) -> list[str]: ] command.extend(additional_commands) - return command + return self._inject_metadata(command) diff --git a/app/core/media/ffmpeg_builders/audio/wav.py b/app/core/media/ffmpeg_builders/audio/wav.py index b113a302..e17da224 100644 --- a/app/core/media/ffmpeg_builders/audio/wav.py +++ b/app/core/media/ffmpeg_builders/audio/wav.py @@ -27,4 +27,4 @@ def build_command(self) -> list[str]: ] command.extend(additional_commands) - return command + return self._inject_metadata(command) diff --git a/app/core/media/ffmpeg_builders/audio/wma.py b/app/core/media/ffmpeg_builders/audio/wma.py index c17c4fa8..ba4543fc 100644 --- a/app/core/media/ffmpeg_builders/audio/wma.py +++ b/app/core/media/ffmpeg_builders/audio/wma.py @@ -27,4 +27,4 @@ def build_command(self) -> list[str]: ] command.extend(additional_commands) - return command \ No newline at end of file + return self._inject_metadata(command) \ No newline at end of file diff --git a/app/core/media/ffmpeg_builders/base.py b/app/core/media/ffmpeg_builders/base.py index cf008ab7..0e1206f4 100644 --- a/app/core/media/ffmpeg_builders/base.py +++ b/app/core/media/ffmpeg_builders/base.py @@ -36,6 +36,7 @@ def __init__( full_path: str | None = None, headers: str | None = None, proxy: str | None = None, + metadata: dict | None = None, ): """ Initializes the FFmpegCommandBuilder. @@ -47,6 +48,7 @@ def __init__( :param full_path: Full path where the output file will be saved. :param headers: Additional headers to include in the request. :param proxy: Proxy server URL to use for the connection. + :param metadata: Optional dict of metadata tags to embed (e.g. title, artist, album). """ self.record_url = record_url self.is_overseas = is_overseas @@ -55,6 +57,21 @@ def __init__( self.full_path = full_path or "" self.proxy = proxy or "" self.headers = headers or "" + self.metadata = metadata or {} + + def _get_metadata_args(self) -> list[str]: + args = [] + for key, value in self.metadata.items(): + if value: + args += ["-metadata", f"{key}={value}"] + return args + + def _inject_metadata(self, command: list[str]) -> list[str]: + """Insert metadata args before the output file path (last element).""" + metadata_args = self._get_metadata_args() + if metadata_args and command: + return command[:-1] + metadata_args + [command[-1]] + return command @abc.abstractmethod def build_command(self) -> list[str]: diff --git a/app/core/media/ffmpeg_builders/video/flv.py b/app/core/media/ffmpeg_builders/video/flv.py index 9b7d2b07..4937a06d 100644 --- a/app/core/media/ffmpeg_builders/video/flv.py +++ b/app/core/media/ffmpeg_builders/video/flv.py @@ -26,4 +26,4 @@ def build_command(self) -> list[str]: self.full_path ] command.extend(additional_commands) - return command + return self._inject_metadata(command) diff --git a/app/core/media/ffmpeg_builders/video/mkv.py b/app/core/media/ffmpeg_builders/video/mkv.py index 4b527e8f..e8f8e94f 100644 --- a/app/core/media/ffmpeg_builders/video/mkv.py +++ b/app/core/media/ffmpeg_builders/video/mkv.py @@ -27,4 +27,4 @@ def build_command(self) -> list[str]: ] command.extend(additional_commands) - return command + return self._inject_metadata(command) diff --git a/app/core/media/ffmpeg_builders/video/mov.py b/app/core/media/ffmpeg_builders/video/mov.py index 5e7b23af..04ba93a1 100644 --- a/app/core/media/ffmpeg_builders/video/mov.py +++ b/app/core/media/ffmpeg_builders/video/mov.py @@ -29,4 +29,4 @@ def build_command(self) -> list[str]: ] command.extend(additional_commands) - return command + return self._inject_metadata(command) diff --git a/app/core/media/ffmpeg_builders/video/mp4.py b/app/core/media/ffmpeg_builders/video/mp4.py index 4675abb9..643666ed 100644 --- a/app/core/media/ffmpeg_builders/video/mp4.py +++ b/app/core/media/ffmpeg_builders/video/mp4.py @@ -28,4 +28,4 @@ def build_command(self) -> list[str]: ] command.extend(additional_commands) - return command + return self._inject_metadata(command) diff --git a/app/core/media/ffmpeg_builders/video/ts.py b/app/core/media/ffmpeg_builders/video/ts.py index e3f036a9..d3c032cf 100644 --- a/app/core/media/ffmpeg_builders/video/ts.py +++ b/app/core/media/ffmpeg_builders/video/ts.py @@ -31,4 +31,4 @@ def build_command(self) -> list[str]: ] command.extend(additional_commands) - return command + return self._inject_metadata(command) diff --git a/app/core/recording/stream_manager.py b/app/core/recording/stream_manager.py index 6e1dd176..70b8b56d 100644 --- a/app/core/recording/stream_manager.py +++ b/app/core/recording/stream_manager.py @@ -3,11 +3,13 @@ import shutil import subprocess import time +from datetime import date from datetime import datetime from typing import TypeVar from ...messages import desktop_notify, message_pusher from ...models.media.video_quality_model import VideoQuality +from ...models.media.audio_format_model import AudioFormat from ...models.recording.recording_status_model import RecordingStatus from ...utils import utils from ...utils.logger import logger @@ -269,6 +271,26 @@ async def start_recording(self, stream_info: StreamData): self.user_config.get("custom_script_command") ) else: + audio_formats = {fmt.lower() for fmt in AudioFormat.get_formats()} + if self.save_format.lower() in audio_formats: + metadata = { + "title": stream_info.title, + "artist": stream_info.anchor_name, + "album": f"{stream_info.platform} {stream_info.anchor_name}", + "album_artist": stream_info.anchor_name, + "date": date.today().isoformat(), + "genre": "Live Stream", + "comment": self.live_url, + } + else: + metadata = { + "title": stream_info.title, + "artist": stream_info.anchor_name, + "date": date.today().isoformat(), + "comment": self.live_url, + "network": stream_info.platform, + "service_name": stream_info.platform, + } ffmpeg_builder = ffmpeg_builders.create_builder( self.save_format, record_url=record_url, @@ -276,7 +298,8 @@ async def start_recording(self, stream_info: StreamData): segment_record=self.segment_record, segment_time=self.segment_time, full_path=save_path, - headers=self.get_headers_params(record_url, self.platform_key) + headers=self.get_headers_params(record_url, self.platform_key), + metadata=metadata, ) ffmpeg_command = ffmpeg_builder.build_command() self.app.page.run_task( diff --git a/streamget b/streamget index 41423040..3fd1a6c2 160000 --- a/streamget +++ b/streamget @@ -1 +1 @@ -Subproject commit 414230402ece3cae8cdebc0d6853673b637ffc28 +Subproject commit 3fd1a6c2850ef0eec93200c9df99f1b37ad4bc55