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 contents: write
env: env:
PYTHON_VERSION: 3.11 PYTHON_VERSION: "3.13"
BUN_VERSION: latest BUN_VERSION: latest
NODE_VERSION: 20 NODE_VERSION: 20
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }} TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
@ -63,6 +63,35 @@ jobs:
shell: bash shell: bash
run: cp temp-spec/app.spec ./app.spec 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 - name: Cache Python venv
id: cache-python id: cache-python
uses: actions/cache@v4 uses: actions/cache@v4
@ -161,7 +190,7 @@ jobs:
rm -rf "dist/${PKG_DIR}" rm -rf "dist/${PKG_DIR}"
mv "dist/YTPTube" "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) - name: Package artifact (Windows)
if: startsWith(env.TAG_NAME, 'v') && runner.os == 'Windows' if: startsWith(env.TAG_NAME, 'v') && runner.os == 'Windows'

View file

@ -1,7 +1,9 @@
import asyncio import asyncio
import functools import functools
import logging import logging
import multiprocessing
import pickle import pickle
import sys
from concurrent.futures import ProcessPoolExecutor from concurrent.futures import ProcessPoolExecutor
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -16,6 +18,14 @@ from app.library.Singleton import Singleton
LOG: logging.Logger = logging.getLogger("downloads.extractor") 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: class ExtractorConfig:
"""Configuration for the extractor.""" """Configuration for the extractor."""
@ -83,7 +93,7 @@ class ExtractorPool(metaclass=Singleton):
self._semaphore = asyncio.Semaphore(config.concurrency) self._semaphore = asyncio.Semaphore(config.concurrency)
if self._pool is None: 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) LOG.info("Initialized extractor process pool with %s workers", config.concurrency)
def get_pool(self, config: ExtractorConfig) -> ProcessPoolExecutor: def get_pool(self, config: ExtractorConfig) -> ProcessPoolExecutor:
@ -366,6 +376,7 @@ async def fetch_info(
) )
except Exception as exc: 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) LOG.warning("extract_info process pool failed, falling back to thread pool url=%s error=%s", url, exc)
return await asyncio.wait_for( return await asyncio.wait_for(
fut=loop.run_in_executor( fut=loop.run_in_executor(

View file

@ -1,6 +1,48 @@
from unittest.mock import MagicMock, patch 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: class TestExtractInfo:

View file

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

View file

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

View file

@ -1,93 +1,64 @@
<template> <template>
<div id="main_container" class="container"> <div
<div class="columns"> class="relative flex min-h-screen flex-1 items-center justify-center overflow-hidden px-4 py-6 sm:px-6"
<div class="column"> >
<section <div class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
class="hero has-text-centered is-flex is-align-items-center is-justify-content-center" <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="goodbye-box"> />
<div class="goodbye-title">Goodbye!</div> <div
<p class="goodbye-subtitle">YTPTube has shut down.</p> class="absolute top-1/2 left-1/2 size-64 translate-x-[8%] translate-y-[4%] rounded-full bg-secondary/12 blur-3xl"
<p class="goodbye-hint">You may now close this window.</p> />
<div class="goodbye-spinner" />
</div>
</section>
</div>
</div> </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> </div>
</template> </template>
<style scoped> <script setup lang="ts">
.goodbye-box { const pageCardUi = {
animation: fadeInUp 0.6s ease-out; root: 'w-full bg-transparent shadow-2xl shadow-primary/5',
background: radial-gradient(circle at top left, #6f42c1, #f66d9b); container: 'w-full p-0',
color: white; wrapper: 'w-full items-stretch',
padding: 3rem 4rem; body: 'w-full p-0',
border-radius: 1rem; };
box-shadow: 0 0 50px rgba(255, 255, 255, 0.15); </script>
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>