diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e4883950..e0c92cb4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,6 +90,7 @@ jobs: libwebkit2gtk-4.1-dev \ librsvg2-dev \ patchelf \ + squashfs-tools \ libssl-dev \ libgtk-3-dev \ libayatana-appindicator3-dev @@ -129,6 +130,19 @@ jobs: bash scripts/fix-deb-deep-link-desktop-entry.sh \ "src-tauri/target/${{ matrix.target }}/release/bundle/deb" + - name: Repair AppImage desktop runtime compatibility + if: matrix.os_name == 'linux' + run: | + APPIMAGE_DIR="src-tauri/target/${{ matrix.target }}/release/bundle/appimage" + APPIMAGE=$(find "$APPIMAGE_DIR" -maxdepth 1 -type f -name "*.AppImage" | head -n 1) + test -n "$APPIMAGE" + bash scripts/repair-appimage-runtime.sh "$APPIMAGE" "youwee" + + # The AppImage payload changed after Tauri produced updater artifacts. + # Regenerate its signature so latest.json always matches the release file. + rm -f "$APPIMAGE.sig" + bun run tauri signer sign "$APPIMAGE" + - name: Rename artifacts with friendly names (macOS) if: matrix.os_name == 'macos' run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9ef0a25..aad89dbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,7 @@ jobs: libwebkit2gtk-4.1-dev \ librsvg2-dev \ patchelf \ + squashfs-tools \ libssl-dev \ libgtk-3-dev \ libayatana-appindicator3-dev @@ -138,14 +139,14 @@ jobs: tauriScript: bun run tauri args: --target ${{ matrix.target }} --no-bundle - - name: Build Linux deb bundle (unsigned) + - name: Build Linux deb and AppImage bundles (unsigned) if: matrix.platform == 'ubuntu-22.04' uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tauriScript: bun run tauri - args: --target ${{ matrix.target }} --bundles deb --no-sign --ci + args: --target ${{ matrix.target }} --bundles deb,appimage --no-sign --ci - name: Validate Linux deep-link desktop entry if: matrix.platform == 'ubuntu-22.04' @@ -164,5 +165,33 @@ jobs: grep -Fx 'Exec=youwee %U' "$DESKTOP_FILE" grep -Fqx 'MimeType=x-scheme-handler/youwee;' "$DESKTOP_FILE" + - name: Validate repaired Linux AppImage runtime + if: matrix.platform == 'ubuntu-22.04' + run: | + set -eu + APPIMAGE_DIR="src-tauri/target/${{ matrix.target }}/release/bundle/appimage" + APPIMAGE="$(find "$APPIMAGE_DIR" -maxdepth 1 -type f -name '*.AppImage' | sort | head -n 1)" + test -n "$APPIMAGE" + APPIMAGE="$(cd "$(dirname "$APPIMAGE")" && pwd)/$(basename "$APPIMAGE")" + bash scripts/repair-appimage-runtime.sh "$APPIMAGE" "youwee" + + INSPECT_DIR="$(mktemp -d)" + trap 'rm -rf "$INSPECT_DIR"' EXIT + ( + cd "$INSPECT_DIR" + APPIMAGE_EXTRACT_AND_RUN=1 "$APPIMAGE" --appimage-extract >/dev/null + ) + + test -x "$INSPECT_DIR/squashfs-root/AppRun" + grep -Fqx 'unset GST_PLUGIN_SYSTEM_PATH GST_PLUGIN_SYSTEM_PATH_1_0 GST_PLUGIN_PATH_1_0' \ + "$INSPECT_DIR/squashfs-root/AppRun" + ! find "$INSPECT_DIR/squashfs-root/usr/lib" -maxdepth 1 \( -type f -o -type l \) \( \ + -name 'libwayland-*.so*' -o \ + -name 'libglib-2.0.so*' -o \ + -name 'libgio-2.0.so*' -o \ + -name 'libgobject-2.0.so*' -o \ + -name 'libgmodule-2.0.so*' \ + \) -print -quit | grep -q . + - name: Skip artifact upload run: echo "build-test validates build artifacts but does not upload them." diff --git a/CHANGELOG.md b/CHANGELOG.md index d8fd1e46..b610bd00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - **Browser extension download settings** - Preserve global Youwee download settings such as container, output folder, playlists, and subtitles when a browser extension starts a download +- **AppImage runtime compatibility** - Rebuild Linux AppImages without bundled Wayland, GLib, and GStreamer runtime libraries that can conflict with newer Mesa and WebKitGTK, preventing blank startup windows on modern Linux distributions +- **Auto subtitle language selection** - Respect selected languages for automatic subtitles instead of requesting every available or translated subtitle language + +### Changed +- **AppImage release validation** - Validate the rebuilt AppImage runtime layout in Linux CI and re-sign the finalized artifact for updater integrity ## [0.20.3] - 2026-08-23 diff --git a/docs/CHANGELOG.vi.md b/docs/CHANGELOG.vi.md index fe27d74e..75901ba2 100644 --- a/docs/CHANGELOG.vi.md +++ b/docs/CHANGELOG.vi.md @@ -9,6 +9,11 @@ và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Sửa lỗi - **Cài đặt tải từ browser extension** - Giữ lại các cài đặt Youwee global như container, thư mục đầu ra, playlist và subtitle khi browser extension bắt đầu tải xuống +- **Tương thích runtime AppImage** - Đóng gói lại AppImage Linux không kèm các thư viện runtime Wayland, GLib và GStreamer có thể xung đột với Mesa và WebKitGTK mới hơn, tránh cửa sổ trắng khi khởi động trên các bản Linux hiện đại +- **Chọn ngôn ngữ subtitle tự động** - Tôn trọng các ngôn ngữ đã chọn cho subtitle tự động thay vì yêu cầu mọi ngôn ngữ subtitle có sẵn hoặc được dịch + +### Thay đổi +- **Kiểm tra release AppImage** - Kiểm tra layout runtime AppImage sau khi đóng gói lại trong CI Linux và ký lại artifact hoàn chỉnh để đảm bảo tính toàn vẹn khi cập nhật ## [0.20.3] - 2026-08-23 diff --git a/docs/CHANGELOG.zh-CN.md b/docs/CHANGELOG.zh-CN.md index 813f9db8..f54fbdce 100644 --- a/docs/CHANGELOG.zh-CN.md +++ b/docs/CHANGELOG.zh-CN.md @@ -9,6 +9,11 @@ Youwee 的所有重要更改都将记录在此文件中。 ### 修复 - **浏览器扩展下载设置** - 浏览器扩展启动下载时保留 Youwee 的全局设置,例如容器、输出目录、播放列表和字幕 +- **AppImage 运行时兼容性** - 重新打包 Linux AppImage,不再捆绑可能与新版 Mesa 和 WebKitGTK 冲突的 Wayland、GLib 与 GStreamer 运行时库,避免现代 Linux 发行版启动时出现白色窗口 +- **自动字幕语言选择** - 自动字幕现在会遵循已选择的语言,而非请求所有可用或翻译后的字幕语言 + +### 变更 +- **AppImage 发布验证** - 在 Linux CI 中验证重新打包后的 AppImage 运行时布局,并重新签名最终产物以确保更新完整性 ## [0.20.3] - 2026-08-23 diff --git a/scripts/repair-appimage-runtime.sh b/scripts/repair-appimage-runtime.sh new file mode 100755 index 00000000..b66c66de --- /dev/null +++ b/scripts/repair-appimage-runtime.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Make Tauri's default AppImage safe on hosts newer than the build image. +# +# linuxdeploy copies core Wayland/GLib/GStreamer libraries into the AppImage. +# Those libraries must stay in sync with the host Mesa and desktop stack; mixing +# the Ubuntu 22.04 copies with a newer host causes WebKitGTK to abort while +# creating its EGL display. This script keeps application libraries (including +# WebKitGTK) bundled, but defers the desktop-stack libraries to the host. +set -euo pipefail + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " >&2 + exit 64 +fi + +appimage=$1 +main_binary=$2 + +if [ ! -f "$appimage" ]; then + echo "AppImage does not exist: $appimage" >&2 + exit 1 +fi + +for required_command in dd mksquashfs mktemp; do + command -v "$required_command" >/dev/null || { + echo "Missing required command: $required_command" >&2 + exit 1 + } +done + +workspace=$(mktemp -d) +trap 'rm -rf "$workspace"' EXIT + +appimage_abs=$(cd "$(dirname "$appimage")" && pwd)/$(basename "$appimage") +cd "$workspace" + +# --appimage-extract works without FUSE. APPIMAGE_EXTRACT_AND_RUN also makes +# extraction work on GitHub runners where FUSE is unavailable. +APPIMAGE_EXTRACT_AND_RUN=1 "$appimage_abs" --appimage-extract >/dev/null +appdir="$workspace/squashfs-root" + +if [ ! -x "$appdir/usr/bin/$main_binary" ]; then + echo "Expected executable is missing from AppImage: usr/bin/$main_binary" >&2 + exit 1 +fi + +# Keep these ABI-coupled libraries on the host. Do not broaden this list to +# application dependencies: the goal is to preserve AppImage portability while +# avoiding the Mesa/Wayland and GLib ABI mismatch that causes EGL_BAD_PARAMETER. +for pattern in \ + 'libwayland-*.so*' \ + 'libxkbcommon*.so*' \ + 'libglib-2.0.so*' \ + 'libgio-2.0.so*' \ + 'libgobject-2.0.so*' \ + 'libgmodule-2.0.so*' \ + 'libgst*.so*' \ + 'libmount.so*' \ + 'libblkid.so*' \ + 'libselinux.so*' \ + 'libpcre2-8.so*' \ + 'libzstd.so*' \ + 'libelf.so*' \ + 'libffi.so*'; do + find "$appdir/usr/lib" -maxdepth 1 \( -type f -o -type l \) -name "$pattern" -delete +done + +# Bypass linuxdeploy's AppRun.wrapped, which exports paths for bundled +# GStreamer/Python/Qt components that Youwee does not ship. In particular, an +# empty GST_PLUGIN_SYSTEM_PATH prevents WebKitGTK from finding host plugins. +cat > "$appdir/AppRun" </dev/null +cat "$runtime" "$payload" > "$rebuilt" +chmod +x "$rebuilt" +mv "$rebuilt" "$appimage_abs" + +echo "Rebuilt AppImage with host Wayland/GLib/GStreamer runtime libraries: $appimage_abs" diff --git a/src-tauri/src/commands/download.rs b/src-tauri/src/commands/download.rs index 74477b19..c20d1a61 100644 --- a/src-tauri/src/commands/download.rs +++ b/src-tauri/src/commands/download.rs @@ -53,6 +53,89 @@ fn extract_time_range(download_sections: &Option) -> Option { }) } +fn append_subtitle_args( + args: &mut Vec, + subtitle_mode: &str, + subtitle_langs: &str, + subtitle_embed: bool, + subtitle_format: &str, +) { + if subtitle_mode == "off" { + return; + } + + args.push("--write-subs".to_string()); + if subtitle_mode == "auto" { + args.push("--write-auto-subs".to_string()); + } + + args.push("--sub-langs".to_string()); + if subtitle_mode == "auto" && subtitle_langs.trim().is_empty() { + // An empty Auto selection deliberately means every available language. + args.push("all".to_string()); + } else { + args.push(subtitle_langs.to_string()); + } + + args.push("--sub-format".to_string()); + args.push(subtitle_format.to_string()); + if subtitle_embed { + args.push("--embed-subs".to_string()); + } +} + +#[cfg(test)] +mod subtitle_args_tests { + use super::*; + + #[test] + fn auto_subtitles_respect_selected_languages() { + let mut args = Vec::new(); + + append_subtitle_args(&mut args, "auto", "en,ru", false, "srt"); + + assert_eq!( + args, + vec![ + "--write-subs", + "--write-auto-subs", + "--sub-langs", + "en,ru", + "--sub-format", + "srt", + ] + ); + } + + #[test] + fn auto_subtitles_use_all_only_without_a_language_selection() { + let mut args = Vec::new(); + + append_subtitle_args(&mut args, "auto", " ", false, "vtt"); + + assert_eq!(args[3], "all"); + } + + #[test] + fn manual_subtitles_do_not_request_auto_generated_captions() { + let mut args = Vec::new(); + + append_subtitle_args(&mut args, "manual", "vi", true, "ass"); + + assert_eq!( + args, + vec![ + "--write-subs", + "--sub-langs", + "vi", + "--sub-format", + "ass", + "--embed-subs", + ] + ); + } +} + fn number_width(total: Option) -> usize { total .filter(|value| *value >= 100) @@ -1329,22 +1412,13 @@ pub async fn download_video( } // Subtitle settings - if subtitle_mode != "off" { - args.push("--write-subs".to_string()); - if subtitle_mode == "auto" { - args.push("--write-auto-subs".to_string()); - args.push("--sub-langs".to_string()); - args.push("all".to_string()); - } else { - args.push("--sub-langs".to_string()); - args.push(subtitle_langs.clone()); - } - args.push("--sub-format".to_string()); - args.push(subtitle_format.clone()); - if subtitle_embed { - args.push("--embed-subs".to_string()); - } - } + append_subtitle_args( + &mut args, + &subtitle_mode, + &subtitle_langs, + subtitle_embed, + &subtitle_format, + ); args.extend(build_site_header_args(&url));