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
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:
commit
28ec496430
6 changed files with 155 additions and 98 deletions
33
.github/workflows/native-build.yml
vendored
33
.github/workflows/native-build.yml
vendored
|
|
@ -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'
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue