Merge pull request #587 from arabcoders/dev
Some checks failed
native-build / build (amd64, ubuntu-latest) (push) Has been cancelled
native-build / build (amd64, windows-latest) (push) Has been cancelled
native-build / build (arm64, macos-latest) (push) Has been cancelled
native-build / build (arm64, ubuntu-latest) (push) Has been cancelled
native-build / build (arm64, windows-latest) (push) Has been cancelled

fix: improve the native build
This commit is contained in:
Abdulmohsen 2026-04-10 20:11:51 +03:00 committed by GitHub
commit 28ec496430
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 155 additions and 98 deletions

View file

@ -37,7 +37,7 @@ jobs:
contents: write
env:
PYTHON_VERSION: 3.11
PYTHON_VERSION: "3.13"
BUN_VERSION: latest
NODE_VERSION: 20
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
@ -63,6 +63,35 @@ jobs:
shell: bash
run: cp temp-spec/app.spec ./app.spec
- name: Update app/library/version.py
shell: bash
run: |
VERSION="${TAG_NAME}"
SHA=$(git rev-parse HEAD)
DATE=$(date -u +"%Y%m%d")
BRANCH=$(git branch --show-current)
if [ -z "${BRANCH}" ]; then
BRANCH="${TAG_NAME}"
fi
BRANCH=$(echo "${BRANCH}" | sed 's#/#-#g')
echo "APP_VERSION=${VERSION}" >> "$GITHUB_ENV"
echo "APP_SHA=${SHA}" >> "$GITHUB_ENV"
echo "APP_DATE=${DATE}" >> "$GITHUB_ENV"
echo "APP_BRANCH=${BRANCH}" >> "$GITHUB_ENV"
sed -i \
-e "s/^APP_VERSION = \".*\"/APP_VERSION = \"${VERSION}\"/" \
-e "s/^APP_COMMIT_SHA = \".*\"/APP_COMMIT_SHA = \"${SHA}\"/" \
-e "s/^APP_BUILD_DATE = \".*\"/APP_BUILD_DATE = \"${DATE}\"/" \
-e "s/^APP_BRANCH = \".*\"/APP_BRANCH = \"${BRANCH}\"/" \
app/library/version.py
echo "Updated version info:"
cat app/library/version.py
- name: Cache Python venv
id: cache-python
uses: actions/cache@v4
@ -161,7 +190,7 @@ jobs:
rm -rf "dist/${PKG_DIR}"
mv "dist/YTPTube" "dist/${PKG_DIR}"
zip -r "release/${ZIP_NAME}" "dist/${PKG_DIR}"
zip -yr "release/${ZIP_NAME}" "dist/${PKG_DIR}"
- name: Package artifact (Windows)
if: startsWith(env.TAG_NAME, 'v') && runner.os == 'Windows'

View file

@ -1,7 +1,9 @@
import asyncio
import functools
import logging
import multiprocessing
import pickle
import sys
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path
from typing import Any
@ -16,6 +18,14 @@ from app.library.Singleton import Singleton
LOG: logging.Logger = logging.getLogger("downloads.extractor")
def _get_process_pool_kwargs() -> dict[str, Any]:
"""Use a fork-based pool for frozen Linux builds."""
if sys.platform == "linux" and getattr(sys, "frozen", False):
return {"mp_context": multiprocessing.get_context("fork")}
return {}
class ExtractorConfig:
"""Configuration for the extractor."""
@ -83,7 +93,7 @@ class ExtractorPool(metaclass=Singleton):
self._semaphore = asyncio.Semaphore(config.concurrency)
if self._pool is None:
self._pool = ProcessPoolExecutor(max_workers=config.concurrency)
self._pool = ProcessPoolExecutor(max_workers=config.concurrency, **_get_process_pool_kwargs())
LOG.info("Initialized extractor process pool with %s workers", config.concurrency)
def get_pool(self, config: ExtractorConfig) -> ProcessPoolExecutor:
@ -366,6 +376,7 @@ async def fetch_info(
)
except Exception as exc:
LOG.exception(exc)
LOG.warning("extract_info process pool failed, falling back to thread pool url=%s error=%s", url, exc)
return await asyncio.wait_for(
fut=loop.run_in_executor(

View file

@ -1,6 +1,48 @@
from unittest.mock import MagicMock, patch
from app.features.ytdlp.extractor import extract_info_sync
from app.features.ytdlp.extractor import ExtractorConfig, ExtractorPool, _get_process_pool_kwargs, extract_info_sync
class TestProcessPoolConfiguration:
def setup_method(self):
ExtractorPool._reset_singleton()
def test_uses_fork_context_for_frozen_linux(self, monkeypatch):
monkeypatch.setattr("app.features.ytdlp.extractor.sys.platform", "linux")
monkeypatch.setattr("app.features.ytdlp.extractor.sys.frozen", True, raising=False)
context = object()
get_context = MagicMock(return_value=context)
monkeypatch.setattr("app.features.ytdlp.extractor.multiprocessing.get_context", get_context)
kwargs = _get_process_pool_kwargs()
assert kwargs == {"mp_context": context}
get_context.assert_called_once_with("fork")
def test_uses_default_context_when_not_frozen_linux(self, monkeypatch):
monkeypatch.setattr("app.features.ytdlp.extractor.sys.platform", "linux")
monkeypatch.delattr("app.features.ytdlp.extractor.sys.frozen", raising=False)
get_context = MagicMock()
monkeypatch.setattr("app.features.ytdlp.extractor.multiprocessing.get_context", get_context)
assert _get_process_pool_kwargs() == {}
get_context.assert_not_called()
def test_initializes_process_pool_with_context_kwargs(self, monkeypatch):
context = object()
monkeypatch.setattr("app.features.ytdlp.extractor._get_process_pool_kwargs", lambda: {"mp_context": context})
executor = MagicMock()
executor_cls = MagicMock(return_value=executor)
monkeypatch.setattr("app.features.ytdlp.extractor.ProcessPoolExecutor", executor_cls)
pool = ExtractorPool.get_instance()
pool._ensure_initialized(ExtractorConfig(concurrency=3))
executor_cls.assert_called_once_with(max_workers=3, mp_context=context)
assert pool.get_pool(ExtractorConfig(concurrency=3)) is executor
class TestExtractInfo:

View file

@ -10,6 +10,11 @@ import time
import urllib.request
import webbrowser
if __name__ == "__main__":
from multiprocessing import freeze_support
freeze_support()
import dotenv
os.environ["PYTHONUTF8"] = "1"
@ -135,7 +140,4 @@ def main():
if __name__ == "__main__":
from multiprocessing import freeze_support
freeze_support()
main()

View file

@ -2,6 +2,11 @@
import sys
from pathlib import Path
if __name__ == "__main__":
from multiprocessing import freeze_support
freeze_support()
APP_ROOT = str((Path(__file__).parent / "..").resolve())
if APP_ROOT not in sys.path:
sys.path.insert(0, APP_ROOT)
@ -179,7 +184,4 @@ class Main:
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
from multiprocessing import freeze_support
freeze_support()
Main().start()

View file

@ -1,93 +1,64 @@
<template>
<div id="main_container" class="container">
<div class="columns">
<div class="column">
<section
class="hero has-text-centered is-flex is-align-items-center is-justify-content-center"
>
<div class="goodbye-box">
<div class="goodbye-title">Goodbye!</div>
<p class="goodbye-subtitle">YTPTube has shut down.</p>
<p class="goodbye-hint">You may now close this window.</p>
<div class="goodbye-spinner" />
</div>
</section>
</div>
<div
class="relative flex min-h-screen flex-1 items-center justify-center overflow-hidden px-4 py-6 sm:px-6"
>
<div class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
<div
class="absolute top-1/2 left-1/2 size-72 -translate-x-[68%] -translate-y-[70%] rounded-full bg-primary/12 blur-3xl"
/>
<div
class="absolute top-1/2 left-1/2 size-64 translate-x-[8%] translate-y-[4%] rounded-full bg-secondary/12 blur-3xl"
/>
</div>
<UPageCard
variant="outline"
:ui="pageCardUi"
class="relative w-full max-w-xl overflow-hidden bg-default/95"
>
<template #body>
<div class="space-y-6 px-5 py-6 sm:px-7 sm:py-8" role="status" aria-live="polite">
<div
class="inline-flex items-center gap-2 rounded-full border border-default bg-elevated/60 px-3 py-1.5 text-xs font-semibold tracking-[0.22em] text-toned uppercase"
>
<UIcon
name="i-lucide-loader-circle"
class="size-4 animate-spin text-info"
aria-hidden="true"
/>
<span>Shutdown in progress</span>
</div>
<div class="space-y-3">
<h1 class="text-3xl font-semibold tracking-tight text-highlighted sm:text-4xl">
Goodbye!
</h1>
<p class="text-base leading-7 text-default sm:text-lg">YTPTube is shutting down.</p>
<p class="max-w-lg text-sm leading-6 text-toned sm:text-base">
You may now close this window. Thanks for using YTPTube.
</p>
</div>
<UAlert
color="info"
variant="soft"
icon="i-lucide-power"
title="Wrapping things up"
description="The native app is closing background services and should exit shortly."
/>
</div>
</template>
</UPageCard>
</div>
</template>
<style scoped>
.goodbye-box {
animation: fadeInUp 0.6s ease-out;
background: radial-gradient(circle at top left, #6f42c1, #f66d9b);
color: white;
padding: 3rem 4rem;
border-radius: 1rem;
box-shadow: 0 0 50px rgba(255, 255, 255, 0.15);
max-width: 480px;
margin: 2rem auto;
position: relative;
overflow: hidden;
transform-style: preserve-3d;
}
.goodbye-title {
font-size: 3rem;
font-weight: bold;
margin-bottom: 1rem;
animation: floatTitle 3s ease-in-out infinite;
}
.goodbye-subtitle {
font-size: 1.5rem;
margin-bottom: 0.75rem;
opacity: 0.95;
}
.goodbye-hint {
opacity: 0.75;
font-size: 1rem;
margin-bottom: 1rem;
}
.goodbye-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top: 3px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 1rem auto 0;
}
@keyframes fadeInUp {
from {
transform: translateY(40px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes floatTitle {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-8px);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<script setup lang="ts">
const pageCardUi = {
root: 'w-full bg-transparent shadow-2xl shadow-primary/5',
container: 'w-full p-0',
wrapper: 'w-full items-stretch',
body: 'w-full p-0',
};
</script>