Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
**/docs
**/tests
**/*.md
!streamget/README.md

# Build Artifacts and Cache
**/__pycache__
Expand Down
50 changes: 41 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand All @@ -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"]
2 changes: 1 addition & 1 deletion app/core/media/ffmpeg_builders/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion app/core/media/ffmpeg_builders/audio/aac.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ def build_command(self) -> list[str]:
]

command.extend(additional_commands)
return command
return self._inject_metadata(command)
2 changes: 1 addition & 1 deletion app/core/media/ffmpeg_builders/audio/m4a.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ def build_command(self) -> list[str]:
]

command.extend(additional_commands)
return command
return self._inject_metadata(command)
2 changes: 1 addition & 1 deletion app/core/media/ffmpeg_builders/audio/mp3.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ def build_command(self) -> list[str]:
]

command.extend(additional_commands)
return command
return self._inject_metadata(command)
2 changes: 1 addition & 1 deletion app/core/media/ffmpeg_builders/audio/wav.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ def build_command(self) -> list[str]:
]

command.extend(additional_commands)
return command
return self._inject_metadata(command)
2 changes: 1 addition & 1 deletion app/core/media/ffmpeg_builders/audio/wma.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ def build_command(self) -> list[str]:
]

command.extend(additional_commands)
return command
return self._inject_metadata(command)
17 changes: 17 additions & 0 deletions app/core/media/ffmpeg_builders/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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]:
Expand Down
2 changes: 1 addition & 1 deletion app/core/media/ffmpeg_builders/video/flv.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ def build_command(self) -> list[str]:
self.full_path
]
command.extend(additional_commands)
return command
return self._inject_metadata(command)
2 changes: 1 addition & 1 deletion app/core/media/ffmpeg_builders/video/mkv.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ def build_command(self) -> list[str]:
]

command.extend(additional_commands)
return command
return self._inject_metadata(command)
2 changes: 1 addition & 1 deletion app/core/media/ffmpeg_builders/video/mov.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ def build_command(self) -> list[str]:
]

command.extend(additional_commands)
return command
return self._inject_metadata(command)
2 changes: 1 addition & 1 deletion app/core/media/ffmpeg_builders/video/mp4.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ def build_command(self) -> list[str]:
]

command.extend(additional_commands)
return command
return self._inject_metadata(command)
2 changes: 1 addition & 1 deletion app/core/media/ffmpeg_builders/video/ts.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ def build_command(self) -> list[str]:
]

command.extend(additional_commands)
return command
return self._inject_metadata(command)
25 changes: 24 additions & 1 deletion app/core/recording/stream_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -269,14 +271,35 @@ 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,
proxy=self.proxy,
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(
Expand Down
1 change: 1 addition & 0 deletions app/models/media/video_quality_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class VideoQuality:
HD = "HD"
SD = "SD"
LD = "LD"
AD = "AD" # Audio-only stream

@classmethod
def get_qualities(cls):
Expand Down
32 changes: 31 additions & 1 deletion app/ui/components/business/recording_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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()
Expand All @@ -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(
Expand Down
51 changes: 34 additions & 17 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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}"
Expand All @@ -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"]
Expand All @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading