Compare commits

...

4 Commits

12 changed files with 192 additions and 145 deletions

View File

@@ -1,152 +1,152 @@
name: Build Release Binaries name: Build Release Binaries
on: on:
release: release:
types: [published] types: [published]
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: tag:
description: "Tag/version to build (leave empty for latest)" description: "Tag/version to build (leave empty for latest)"
required: false required: false
type: string type: string
permissions: permissions:
contents: write contents: write
jobs: jobs:
build: build:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- os: ubuntu-latest - os: ubuntu-22.04
target: linux target: linux
asset_name: viu-linux-x86_64 asset_name: viu-linux-x86_64
executable: viu executable: viu
- os: windows-latest - os: windows-latest
target: windows target: windows
asset_name: viu-windows-x86_64.exe asset_name: viu-windows-x86_64.exe
executable: viu.exe executable: viu.exe
- os: macos-latest - os: macos-latest
target: macos target: macos
asset_name: viu-macos-x86_64 asset_name: viu-macos-x86_64
executable: viu executable: viu
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ github.event.inputs.tag || github.ref }} ref: ${{ github.event.inputs.tag || github.ref }}
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v3 uses: astral-sh/setup-uv@v3
with: with:
enable-cache: true enable-cache: true
- name: Install system dependencies (Linux) - name: Install system dependencies (Linux)
if: runner.os == 'Linux' if: runner.os == 'Linux'
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libdbus-1-dev libglib2.0-dev sudo apt-get install -y libdbus-1-dev libglib2.0-dev
- name: Install dependencies - name: Install dependencies
run: uv sync --all-extras --all-groups run: uv sync --all-extras --all-groups
- name: Build executable with PyInstaller - name: Build executable with PyInstaller
run: uv run pyinstaller bundle/pyinstaller.spec --distpath dist --workpath build/pyinstaller --clean run: uv run pyinstaller bundle/pyinstaller.spec --distpath dist --workpath build/pyinstaller --clean
- name: Rename executable - name: Rename executable
shell: bash shell: bash
run: mv dist/${{ matrix.executable }} dist/${{ matrix.asset_name }} run: mv dist/${{ matrix.executable }} dist/${{ matrix.asset_name }}
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.asset_name }} name: ${{ matrix.asset_name }}
path: dist/${{ matrix.asset_name }} path: dist/${{ matrix.asset_name }}
if-no-files-found: error if-no-files-found: error
- name: Upload to Release - name: Upload to Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: dist/${{ matrix.asset_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Build for macOS ARM (Apple Silicon)
build-macos-arm:
runs-on: macos-14
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Install dependencies
run: uv sync --all-extras --all-groups
- name: Build executable with PyInstaller
run: uv run pyinstaller bundle/pyinstaller.spec --distpath dist --workpath build/pyinstaller --clean
- name: Rename executable
run: mv dist/viu dist/viu-macos-arm64
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: viu-macos-arm64
path: dist/viu-macos-arm64
if-no-files-found: error
- name: Upload to Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: dist/viu-macos-arm64
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Create checksums after all builds complete
checksums:
needs: [build, build-macos-arm]
runs-on: ubuntu-latest
if: github.event_name == 'release' if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: dist/${{ matrix.asset_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps: # Build for macOS ARM (Apple Silicon)
- name: Download all artifacts build-macos-arm:
uses: actions/download-artifact@v4 runs-on: macos-14
with:
path: artifacts
merge-multiple: true
- name: Generate checksums steps:
run: | - name: Checkout repository
cd artifacts uses: actions/checkout@v4
sha256sum * > SHA256SUMS.txt with:
cat SHA256SUMS.txt ref: ${{ github.event.inputs.tag || github.ref }}
- name: Upload checksums to Release - name: Set up Python
uses: softprops/action-gh-release@v2 uses: actions/setup-python@v5
with: with:
files: artifacts/SHA256SUMS.txt python-version: "3.11"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Install dependencies
run: uv sync --all-extras --all-groups
- name: Build executable with PyInstaller
run: uv run pyinstaller bundle/pyinstaller.spec --distpath dist --workpath build/pyinstaller --clean
- name: Rename executable
run: mv dist/viu dist/viu-macos-arm64
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: viu-macos-arm64
path: dist/viu-macos-arm64
if-no-files-found: error
- name: Upload to Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: dist/viu-macos-arm64
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Create checksums after all builds complete
checksums:
needs: [build, build-macos-arm]
runs-on: ubuntu-latest
if: github.event_name == 'release'
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Generate checksums
run: |
cd artifacts
sha256sum * > SHA256SUMS.txt
cat SHA256SUMS.txt
- name: Upload checksums to Release
uses: softprops/action-gh-release@v2
with:
files: artifacts/SHA256SUMS.txt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -39,10 +39,18 @@ hiddenimports = [
'viu_media.cli.interactive.menu.media.servers', 'viu_media.cli.interactive.menu.media.servers',
] + collect_submodules('viu_media') ] + collect_submodules('viu_media')
# Exclude OpenSSL libraries on Linux to avoid version conflicts
import sys
binaries = []
if sys.platform == 'linux':
# Remove any bundled libssl or libcrypto
binaries = [b for b in binaries if not any(lib in b[0] for lib in ['libssl', 'libcrypto'])]
a = Analysis( a = Analysis(
['../viu_media/viu.py'], ['../viu_media/viu.py'],
pathex=[], pathex=[],
binaries=[], binaries=binaries,
datas=datas, datas=datas,
hiddenimports=hiddenimports, hiddenimports=hiddenimports,
hookspath=[], hookspath=[],

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "viu-media" name = "viu-media"
version = "3.3.6" version = "3.3.7"
description = "A browser anime site experience from the terminal" description = "A browser anime site experience from the terminal"
license = "UNLICENSE" license = "UNLICENSE"
readme = "README.md" readme = "README.md"

2
uv.lock generated
View File

@@ -3743,7 +3743,7 @@ wheels = [
[[package]] [[package]]
name = "viu-media" name = "viu-media"
version = "3.3.6" version = "3.3.7"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },

View File

@@ -9,6 +9,8 @@ import importlib.util
import click import click
import httpx import httpx
from viu_media.core.utils import detect
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -138,6 +140,7 @@ def render(url: str, capture: bool = False, size: str = "30x30") -> Optional[str
[icat_executable, "--align", "left", url], [icat_executable, "--align", "left", url],
capture_output=capture, capture_output=capture,
text=capture, text=capture,
env=detect.get_clean_env(),
) )
if process.returncode == 0: if process.returncode == 0:
return process.stdout if capture else None return process.stdout if capture else None

View File

@@ -21,7 +21,7 @@ from rich.progress import (
) )
from rich.prompt import Confirm from rich.prompt import Confirm
from ..utils.file import sanitize_filename from ..utils.file import sanitize_filename
from ..utils.detect import get_clean_env
from ..exceptions import ViuError from ..exceptions import ViuError
from ..patterns import TORRENT_REGEX from ..patterns import TORRENT_REGEX
from ..utils.networking import get_remote_filename from ..utils.networking import get_remote_filename
@@ -372,6 +372,7 @@ class DefaultDownloader(BaseDownloader):
capture_output=params.silent, # Only suppress ffmpeg output if silent capture_output=params.silent, # Only suppress ffmpeg output if silent
text=True, text=True,
check=True, check=True,
env=get_clean_env(),
) )
final_output_path = video_path.parent / merged_filename final_output_path = video_path.parent / merged_filename

View File

@@ -11,7 +11,7 @@ from rich.prompt import Confirm
import yt_dlp import yt_dlp
from yt_dlp.utils import sanitize_filename from yt_dlp.utils import sanitize_filename
from ..utils.detect import get_clean_env
from ..exceptions import ViuError from ..exceptions import ViuError
from ..patterns import TORRENT_REGEX from ..patterns import TORRENT_REGEX
from ..utils.networking import get_remote_filename from ..utils.networking import get_remote_filename
@@ -224,7 +224,7 @@ class YtDLPDownloader(BaseDownloader):
# Run the ffmpeg command # Run the ffmpeg command
try: try:
subprocess.run(args) subprocess.run(args, env=get_clean_env())
final_output_path = video_path.parent / merged_filename final_output_path = video_path.parent / merged_filename
if final_output_path.exists(): if final_output_path.exists():

View File

@@ -83,3 +83,21 @@ def get_python_executable() -> str:
return "python" return "python"
else: else:
return sys.executable return sys.executable
def get_clean_env() -> dict[str, str]:
"""
Returns a copy of the environment with LD_LIBRARY_PATH fixed for system subprocesses
when running as a PyInstaller frozen application.
This prevents system binaries (like mpv, ffmpeg) from loading incompatible
libraries from the PyInstaller bundle.
"""
env = os.environ.copy()
if is_frozen():
# PyInstaller saves the original LD_LIBRARY_PATH in LD_LIBRARY_PATH_ORIG
if "LD_LIBRARY_PATH_ORIG" in env:
env["LD_LIBRARY_PATH"] = env["LD_LIBRARY_PATH_ORIG"]
else:
# If orig didn't exist, LD_LIBRARY_PATH shouldn't exist for the subprocess
env.pop("LD_LIBRARY_PATH", None)
return env

View File

@@ -97,7 +97,7 @@ class MpvPlayer(BasePlayer):
"is.xyz.mpv/.MPVActivity", "is.xyz.mpv/.MPVActivity",
] ]
subprocess.run(args) subprocess.run(args,env=detect.get_clean_env())
return PlayerResult(params.episode) return PlayerResult(params.episode)
@@ -146,6 +146,7 @@ class MpvPlayer(BasePlayer):
text=True, text=True,
encoding="utf-8", encoding="utf-8",
check=False, check=False,
env=detect.get_clean_env(),
) )
if proc.stdout: if proc.stdout:
for line in reversed(proc.stdout.split("\n")): for line in reversed(proc.stdout.split("\n")):
@@ -185,7 +186,7 @@ class MpvPlayer(BasePlayer):
logger.info(f"Starting MPV with IPC socket: {socket_path}") logger.info(f"Starting MPV with IPC socket: {socket_path}")
process = subprocess.Popen(pre_args + mpv_args) process = subprocess.Popen(pre_args + mpv_args,env=detect.get_clean_env())
return process return process
@@ -210,7 +211,7 @@ class MpvPlayer(BasePlayer):
args.append("--player-args") args.append("--player-args")
args.extend(mpv_args) args.extend(mpv_args)
subprocess.run(args) subprocess.run(args,env=detect.get_clean_env())
return PlayerResult(params.episode) return PlayerResult(params.episode)
def _stream_on_desktop_with_syncplay(self, params: PlayerParams) -> PlayerResult: def _stream_on_desktop_with_syncplay(self, params: PlayerParams) -> PlayerResult:
@@ -232,7 +233,7 @@ class MpvPlayer(BasePlayer):
if mpv_args := self._create_mpv_cli_options(params): if mpv_args := self._create_mpv_cli_options(params):
args.append("--") args.append("--")
args.extend(mpv_args) args.extend(mpv_args)
subprocess.run(args) subprocess.run(args,env=detect.get_clean_env())
return PlayerResult(params.episode) return PlayerResult(params.episode)

View File

@@ -103,7 +103,7 @@ class VlcPlayer(BasePlayer):
params.title, params.title,
] ]
subprocess.run(args) subprocess.run(args,env=detect.get_clean_env())
return PlayerResult(episode=params.episode) return PlayerResult(episode=params.episode)
@@ -134,7 +134,7 @@ class VlcPlayer(BasePlayer):
if self.config.args: if self.config.args:
args.extend(self.config.args.split(",")) args.extend(self.config.args.split(","))
subprocess.run(args, encoding="utf-8") subprocess.run(args, encoding="utf-8",env=detect.get_clean_env())
return PlayerResult(episode=params.episode) return PlayerResult(episode=params.episode)
def _stream_on_desktop_with_webtorrent_cli( def _stream_on_desktop_with_webtorrent_cli(
@@ -159,7 +159,7 @@ class VlcPlayer(BasePlayer):
args.append("--player-args") args.append("--player-args")
args.extend(self.config.args.split(",")) args.extend(self.config.args.split(","))
subprocess.run(args) subprocess.run(args,env=detect.get_clean_env())
return PlayerResult(episode=params.episode) return PlayerResult(episode=params.episode)

View File

@@ -5,6 +5,8 @@ import subprocess
from rich.prompt import Prompt from rich.prompt import Prompt
from viu_media.core.utils import detect
from ....core.config import FzfConfig from ....core.config import FzfConfig
from ....core.exceptions import ViuError from ....core.exceptions import ViuError
from ..base import BaseSelector from ..base import BaseSelector
@@ -49,6 +51,7 @@ class FzfSelector(BaseSelector):
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, text=True,
encoding="utf-8", encoding="utf-8",
env=detect.get_clean_env(),
) )
if result.returncode != 0: if result.returncode != 0:
return None return None
@@ -76,6 +79,7 @@ class FzfSelector(BaseSelector):
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, text=True,
encoding="utf-8", encoding="utf-8",
env=detect.get_clean_env(),
) )
if result.returncode != 0: if result.returncode != 0:
return [] return []
@@ -117,7 +121,16 @@ class FzfSelector(BaseSelector):
lines = result.stdout.strip().splitlines() lines = result.stdout.strip().splitlines()
return lines[-1] if lines else (default or "") return lines[-1] if lines else (default or "")
def search(self, prompt, search_command, *, preview=None, header=None, initial_query=None, initial_results=None): def search(
self,
prompt,
search_command,
*,
preview=None,
header=None,
initial_query=None,
initial_results=None,
):
"""Enhanced search using fzf's --reload flag for dynamic search.""" """Enhanced search using fzf's --reload flag for dynamic search."""
# Build the header with optional custom header line # Build the header with optional custom header line
display_header = self.header display_header = self.header
@@ -156,6 +169,7 @@ class FzfSelector(BaseSelector):
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, text=True,
encoding="utf-8", encoding="utf-8",
env=detect.get_clean_env(),
) )
if result.returncode != 0: if result.returncode != 0:
return None return None

View File

@@ -43,6 +43,7 @@ class RofiSelector(BaseSelector):
input=rofi_input, input=rofi_input,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, text=True,
env=detect.get_clean_env()
) )
if result.returncode == 0: if result.returncode == 0:
@@ -106,6 +107,7 @@ class RofiSelector(BaseSelector):
input=rofi_input, input=rofi_input,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, text=True,
env=detect.get_clean_env()
) )
if result.returncode == 0: if result.returncode == 0: