From 3219517555391967ec5fdde684459345f54e7744 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Fri, 4 Jul 2025 19:50:12 +0300 Subject: [PATCH 01/12] do not show merge commits when updating github release. --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c4fd07cb..1ba0c5be 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -210,15 +210,17 @@ jobs: return; } - const { data: commits } = await github.rest.repos.compareCommits({ + const { data: comparison } = await github.rest.repos.compareCommits({ owner: context.repo.owner, repo: context.repo.repo, base: latestRelease.data[1]?.tag_name || '', // fallback to second latest tag head: tag, }); + const commits = comparison.commits.filter(c => 1 === c.parents.length); + const changelog = commits.commits.map( - c => `- ${c.sha.substring(0, 7)} ${c.commit.message.split('\n')[0]} by ${c.commit.author.name}` + c => `- ${c.sha.substring(0, 7)} ${c.commit.message.split('\n')[0]} by @${c.commit.author.name}` ).join('\n'); if (!changelog) { From 4849301c5ac216a3d3aec81d6296120b83dd93d7 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sat, 5 Jul 2025 17:54:01 +0300 Subject: [PATCH 02/12] fix incorrect test display. --- ui/components/History.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/components/History.vue b/ui/components/History.vue index dccb8a9b..855b06f1 100644 --- a/ui/components/History.vue +++ b/ui/components/History.vue @@ -173,7 +173,7 @@
@@ -340,7 +340,7 @@ - Remove + {{ config.app.remove_files ? 'Remove' : 'Clear' }} From a448fc43ae07e1518b0a8b1d929b3a71e5bd820e Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sat, 5 Jul 2025 18:15:37 +0300 Subject: [PATCH 03/12] cast file to string for mimetype --- app/routes/api/_static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/api/_static.py b/app/routes/api/_static.py index e1d70068..2f976420 100644 --- a/app/routes/api/_static.py +++ b/app/routes/api/_static.py @@ -100,7 +100,7 @@ def preload_static(root_path: Path, config: Config) -> None: # uri_path: str = f"/{str(file.as_posix()).replace(f'{static_dir.as_posix()!s}/', '')}" contentType = EXT_TO_MIME.get(file.suffix) if not contentType: - contentType = MIME.from_file(file) + contentType = MIME.from_file(str(file)) STATIC_FILES[uri_path] = { "uri": uri_path, From eb440dff36c90c9112fe1ce03dc522e801ff256b Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sat, 5 Jul 2025 18:32:14 +0300 Subject: [PATCH 04/12] minor fixes for running on windows. --- .vscode/settings.json | 3 +++ app/library/config.py | 4 ++-- app/native.py | 10 +++++++++- pyproject.toml | 1 + 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ef14a3c0..de19fdfc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,7 +25,9 @@ "copyts", "cronsim", "datas", + "dateparser", "daterange", + "defusedxml", "dotenv", "edgechromium", "engineio", @@ -56,6 +58,7 @@ "movflags", "mpegts", "msvideo", + "multidict", "muxdelay", "nodesc", "noprogress", diff --git a/app/library/config.py b/app/library/config.py index 1c3d98a5..271feb07 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -539,7 +539,7 @@ class Config: try: import subprocess - branch_result = subprocess.run( # noqa: S603 + branch_result = subprocess.run( ["git", "rev-parse", "--abbrev-ref", "HEAD"], # noqa: S607 cwd=os.path.dirname(git_path), capture_output=True, @@ -556,7 +556,7 @@ class Config: logging.warning("Git branch name is empty.") return - commit_result = subprocess.run( # noqa: S603 + commit_result = subprocess.run( ["git", "log", "-1", "--format=%ct_%H"], # noqa: S607 cwd=os.path.dirname(git_path), capture_output=True, diff --git a/app/native.py b/app/native.py index 9aa302af..581a8fe2 100644 --- a/app/native.py +++ b/app/native.py @@ -1,5 +1,14 @@ #!/usr/bin/env python3 +import io +import os import sys + +os.environ["PYTHONUTF8"] = "1" + +if "utf-8" != sys.stdout.encoding.lower(): + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8") + from pathlib import Path APP_ROOT = str((Path(__file__).parent / "..").resolve()) @@ -8,7 +17,6 @@ if APP_ROOT not in sys.path: import json -import os import queue import socket import threading diff --git a/pyproject.toml b/pyproject.toml index 0d827e3b..5c77e09c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,6 +159,7 @@ ignore = [ "ERA001", "S101", "LOG015", + "PLC0415", ] # Allow fix for all enabled rules (when `--fix`) is provided. From f8bbcc418165f9ff50b9d4b910364a80c131b232 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sat, 5 Jul 2025 18:56:53 +0300 Subject: [PATCH 05/12] removed no longer used file for native build --- app.spec | 1 - 1 file changed, 1 deletion(-) diff --git a/app.spec b/app.spec index cc503913..631a6691 100644 --- a/app.spec +++ b/app.spec @@ -42,7 +42,6 @@ a = Analysis( # noqa: F821 # type: ignore datas=[ ("ui/exported", "ui/exported"), ("app/migrations", "migrations"), - ("app/library/presets.json", "library"), ], hiddenimports=hidden, hookspath=[], From 24de1fc6d50a98817306714121a5b5ae44284559 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sat, 5 Jul 2025 19:06:24 +0300 Subject: [PATCH 06/12] more changes for native builds --- app/native.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/native.py b/app/native.py index 581a8fe2..f62cd79f 100644 --- a/app/native.py +++ b/app/native.py @@ -1,14 +1,9 @@ #!/usr/bin/env python3 -import io import os -import sys os.environ["PYTHONUTF8"] = "1" -if "utf-8" != sys.stdout.encoding.lower(): - sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") - sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8") - +import sys from pathlib import Path APP_ROOT = str((Path(__file__).parent / "..").resolve()) From 4d194fec00f7a35dbecad55b54299d6724fc7c64 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sat, 5 Jul 2025 20:14:44 +0300 Subject: [PATCH 07/12] update app.spec to include all uv packages. --- app.spec | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.spec b/app.spec index 631a6691..2ef14c1f 100644 --- a/app.spec +++ b/app.spec @@ -25,7 +25,7 @@ with open("./uv.lock", "rb") as f: lock = tomllib.load(f) hidden = [ - *lock.get("dependencies", {}).keys(), + *{pkg["name"] for pkg in lock.get("package", [])}, "aiohttp", "socketio", "engineio", @@ -63,5 +63,6 @@ exe = EXE( # type: ignore # noqa: F821 strip=False, upx=True, console=False, + icon="ui/public/favicon.ico", onefile=True, ) From e36552e3369f4b1ce8bf8d975ca8826dd8a1c4e5 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sat, 5 Jul 2025 21:03:48 +0300 Subject: [PATCH 08/12] wip --- app/native.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/native.py b/app/native.py index f62cd79f..dc60dd5f 100644 --- a/app/native.py +++ b/app/native.py @@ -16,6 +16,8 @@ import queue import socket import threading +import platformdirs + ready = threading.Event() exception_holder = queue.Queue() @@ -24,13 +26,14 @@ APP_NAME = "YTPTube" try: import webview # type: ignore except ImportError as e: - msg = "Please run 'pipenv install pywebview[qt]' package to run YTPTube in native mode." + if "nt" == os.name: + msg = "Please run 'uv pip install pywebview[edgechromium]' package to run YTPTube in native mode." + else: + msg = "Please run 'uv pip install pywebview[qt]' package to run YTPTube in native mode." raise ImportError(msg) from e def set_env(): - import platformdirs - dct = {} if not os.getenv("YTP_CONFIG_PATH"): From ef2094f140adb96a60ec04ddc608b569e356bf5d Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sun, 6 Jul 2025 00:08:53 +0300 Subject: [PATCH 09/12] finalize changes for native apps. --- .vscode/settings.json | 1 + app.spec | 5 +- app/library/Utils.py | 6 +- app/native.py | 115 ++++++++++++++++++++-------------- ui/components/NewDownload.vue | 3 +- 5 files changed, 77 insertions(+), 53 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index de19fdfc..e5d1c852 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -52,6 +52,7 @@ "matroska", "mbed", "mccabe", + "MEIPASS", "Microformat", "microformats", "mkvtoolsnix", diff --git a/app.spec b/app.spec index 2ef14c1f..9857a34d 100644 --- a/app.spec +++ b/app.spec @@ -31,6 +31,7 @@ hidden = [ "engineio", "engineio.async_drivers.aiohttp", "socketio.async_drivers.aiohttp", + "app" ] hidden = [f.replace("-", "_") for f in hidden] @@ -41,7 +42,7 @@ a = Analysis( # noqa: F821 # type: ignore binaries=binaries, datas=[ ("ui/exported", "ui/exported"), - ("app/migrations", "migrations"), + ("app/", "app/"), ], hiddenimports=hidden, hookspath=[], @@ -62,7 +63,7 @@ exe = EXE( # type: ignore # noqa: F821 debug=False, strip=False, upx=True, - console=False, + console=True, # Turn on to True if you want a console window for debugging. icon="ui/public/favicon.ico", onefile=True, ) diff --git a/app/library/Utils.py b/app/library/Utils.py index 92505b47..f2be102f 100644 --- a/app/library/Utils.py +++ b/app/library/Utils.py @@ -94,8 +94,7 @@ def calc_download_path(base_path: str | Path, folder: str | None = None, create_ if not folder: return str(base_path) - if folder.startswith("/"): - folder = folder[1:] + folder = folder.removeprefix("/") realBasePath = base_path.resolve() download_path = Path(realBasePath).joinpath(folder).resolve(strict=False) @@ -179,8 +178,7 @@ def extract_info( else: log_wrapper.add_target(target=params["callback"], level=logging.ERROR, name="callback") - if "callback" in params: - del params["callback"] + params.pop("callback", None) if log_wrapper.has_targets(): if "logger" in params: diff --git a/app/native.py b/app/native.py index dc60dd5f..716b4e93 100644 --- a/app/native.py +++ b/app/native.py @@ -1,38 +1,50 @@ #!/usr/bin/env python3 +import asyncio +import json +import logging import os - -os.environ["PYTHONUTF8"] = "1" - +import queue +import socket import sys +import threading +import traceback from pathlib import Path +import platformdirs + +sys.path.insert(0, os.path.join(getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))), "app")) + +APP_NAME = "YTPTube" APP_ROOT = str((Path(__file__).parent / "..").resolve()) if APP_ROOT not in sys.path: sys.path.insert(0, APP_ROOT) - -import json -import queue -import socket -import threading - -import platformdirs - -ready = threading.Event() -exception_holder = queue.Queue() - -APP_NAME = "YTPTube" - try: import webview # type: ignore except ImportError as e: - if "nt" == os.name: - msg = "Please run 'uv pip install pywebview[edgechromium]' package to run YTPTube in native mode." - else: - msg = "Please run 'uv pip install pywebview[qt]' package to run YTPTube in native mode." + pkgs = "pywebview[edgechromium]" if os.name == "nt" else "pywebview[qt]" + msg: str = f"Please run 'uv pip install {pkgs}' to run YTPTube in native mode." raise ImportError(msg) from e +def error_window(exc: Exception | str) -> None: + trace: str = "\n".join(traceback.format_exception(exc)) if isinstance(exc, Exception) else exc + webview.create_window( + f"{APP_NAME} - Error", + html=f"

An error occurred

{trace}
", + width=600, + height=400, + resizable=True, + ) + webview.start( + gui="edgechromium" if os.name == "nt" else "qt", + debug=False, + storage_path=str(Path(os.getenv("YTP_TEMP_PATH", os.getcwd())) / "webview"), + private_mode=False, + ) + sys.exit(1) + + def set_env(): dct = {} @@ -52,32 +64,31 @@ def set_env(): os.environ.update(dct) -def app_start(host: str, port: int) -> None: - import asyncio - - from main import Main - +def run_backend(host: str, port: int, ready_event: threading.Event, error_queue: queue.Queue): try: - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) - try: - Main(is_native=True).start(host, port, cb=lambda: ready.set()) + from app.main import Main + + Main(is_native=True).start(host, port, cb=lambda: ready_event.set()) except Exception as e: - exception_holder.put(e) - ready.set() + logging.exception(e) + error_queue.put(e) + ready_event.set() -if __name__ == "__main__": +def main(): host = "127.0.0.1" set_env() cfg_path: Path = Path(os.getenv("YTP_CONFIG_PATH")) / "webview.json" - port = None win_conf: dict[str, int] = {} + if cfg_path.exists(): data = json.loads(cfg_path.read_text()) port = data.get("port") @@ -91,18 +102,21 @@ if __name__ == "__main__": port = s.getsockname()[1] cfg_path.write_text(json.dumps({"port": port})) - threading.Thread(target=app_start, args=(host, port), daemon=True).start() + ready = threading.Event() + errors: queue.Queue = queue.Queue() + threading.Thread(target=run_backend, args=(host, port, ready, errors), daemon=False).start() - ready.wait() + ready.wait(timeout=5) - if not exception_holder.empty(): - raise exception_holder.get() + if not errors.empty(): + error_window(errors.get()) + return + from app.library.version import APP_VERSION + + gui = "edgechromium" if os.name == "nt" else "qt" create_kwargs = {**win_conf, "resizable": True} - - webview.settings["ALLOW_DOWNLOADS"] = True - webview.settings["OPEN_DEVTOOLS_IN_DEBUG"] = False - window = webview.create_window(APP_NAME, f"http://{host}:{port}", **create_kwargs) + window = webview.create_window(f"{APP_NAME} - {APP_VERSION}", f"http://{host}:{port}", **create_kwargs) def save_geometry(): cfg = { @@ -116,13 +130,22 @@ if __name__ == "__main__": window.events.resized += lambda *_: save_geometry() window.events.moved += lambda *_: save_geometry() + window.events.closing += lambda *_: os._exit(0) - gui = os.getenv("YTP_WV_GUI", None) - gui = "edgechromium" if os.name == "nt" else "qt" - + webview.settings["ALLOW_DOWNLOADS"] = True + webview.settings["OPEN_DEVTOOLS_IN_DEBUG"] = False webview.start( gui=gui, debug=True, storage_path=str(Path(os.getenv("YTP_TEMP_PATH", os.getcwd())) / "webview"), private_mode=False, ) + + +if __name__ == "__main__": + try: + main() + except Exception as e: + logging.exception(e) + error_window(e) + os._exit(1) diff --git a/ui/components/NewDownload.vue b/ui/components/NewDownload.vue index 581d714e..1bb75a0b 100644 --- a/ui/components/NewDownload.vue +++ b/ui/components/NewDownload.vue @@ -76,7 +76,8 @@ Opts -
+
{{ get_preset(form.preset)?.description }} From 8674b7caafe4588d8b93fd0e43ea337f94523b11 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sun, 6 Jul 2025 00:09:18 +0300 Subject: [PATCH 10/12] turn off console for prod builds --- app.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.spec b/app.spec index 9857a34d..149a3c00 100644 --- a/app.spec +++ b/app.spec @@ -63,7 +63,7 @@ exe = EXE( # type: ignore # noqa: F821 debug=False, strip=False, upx=True, - console=True, # Turn on to True if you want a console window for debugging. + console=False, # Turn on to True if you want a console window for debugging. icon="ui/public/favicon.ico", onefile=True, ) From 860fb8181e6211741f9f77b70d490e2d194a29b0 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sun, 6 Jul 2025 17:34:10 +0300 Subject: [PATCH 11/12] support running in linux qt webview --- app/native.py | 54 +++++++++++++++++++++++++++++++++++++------------- pyproject.toml | 1 + 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/app/native.py b/app/native.py index 716b4e93..eacb67da 100644 --- a/app/native.py +++ b/app/native.py @@ -21,6 +21,34 @@ if APP_ROOT not in sys.path: try: import webview # type: ignore + + if "linux" in sys.platform: + os.environ.setdefault("LC_ALL", "C.UTF-8") + os.environ.setdefault("LANG", "C.UTF-8") + import webview.platforms.qt # type: ignore + + # monkey patch the download handler for pywebview to support qt6. + def on_download_requested(self, download): + from qtpy import QtCore # type: ignore + from qtpy.QtWidgets import QFileDialog # type: ignore + + old_path = download.url().path() + suffix = QtCore.QFileInfo(old_path).suffix() + filename, _ = QFileDialog.getSaveFileName( + self, self.localization["global.saveFile"], old_path, "*." + suffix + ) + if filename: + if hasattr(download, "setPath"): + download.setPath(filename) + else: + download.setDownloadDirectory(os.path.dirname(filename)) + download.setDownloadFileName(os.path.basename(filename)) + download.accept() + else: + download.cancel() + + webview.platforms.qt.BrowserView.on_download_requested = on_download_requested + except ImportError as e: pkgs = "pywebview[edgechromium]" if os.name == "nt" else "pywebview[qt]" msg: str = f"Please run 'uv pip install {pkgs}' to run YTPTube in native mode." @@ -46,22 +74,20 @@ def error_window(exc: Exception | str) -> None: def set_env(): - dct = {} + defaults = { + "YTP_CONFIG_PATH": lambda: platformdirs.user_config_dir(APP_NAME.lower(), "arabcoders", ensure_exists=True), + "YTP_TEMP_PATH": lambda: platformdirs.user_cache_dir(APP_NAME.lower(), "arabcoders", ensure_exists=True), + "YTP_DOWNLOAD_PATH": lambda: platformdirs.user_downloads_dir(), + "YTP_ACCESS_LOG": "false", + "YTP_BROWSER_ENABLED": "true", + "YTP_BROWSER_CONTROL_ENABLED": "true", + } - if not os.getenv("YTP_CONFIG_PATH"): - dct["YTP_CONFIG_PATH"] = platformdirs.user_config_dir(APP_NAME.lower(), "arabcoders", ensure_exists=True) + for key, value in defaults.items(): + if os.getenv(key) is not None: + continue - if not os.getenv("YTP_TEMP_PATH"): - dct["YTP_TEMP_PATH"] = platformdirs.user_cache_dir(APP_NAME.lower(), "arabcoders", ensure_exists=True) - - if not os.getenv("YTP_DOWNLOAD_PATH"): - dct["YTP_DOWNLOAD_PATH"] = platformdirs.user_downloads_dir() - - if os.getenv("YTP_ACCESS_LOG", None) is None: - dct["YTP_ACCESS_LOG"] = "false" - - if dct: - os.environ.update(dct) + os.environ[key] = value() if callable(value) else value def run_backend(host: str, port: int, ready_event: threading.Event, error_queue: queue.Queue): diff --git a/pyproject.toml b/pyproject.toml index 5c77e09c..52421121 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,6 +160,7 @@ ignore = [ "S101", "LOG015", "PLC0415", + "S603", ] # Allow fix for all enabled rules (when `--fix`) is provided. From 9ef8f4707110027b9d19a643f3d033d7668b76fb Mon Sep 17 00:00:00 2001 From: arabcoders Date: Sun, 6 Jul 2025 19:23:43 +0300 Subject: [PATCH 12/12] add notice about webview wrappers --- .github/workflows/create-release.yml | 81 -------------------- .github/workflows/native-build.yml | 110 +++++++++++++++++++++++++++ README.md | 7 ++ 3 files changed, 117 insertions(+), 81 deletions(-) delete mode 100644 .github/workflows/create-release.yml create mode 100644 .github/workflows/native-build.yml diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml deleted file mode 100644 index 05d31c5a..00000000 --- a/.github/workflows/create-release.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Create New Release - -on: - workflow_dispatch: - inputs: - new_tag: - description: 'Tag name for the new release' - required: true - -jobs: - create_release: - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get latest release tag from GitHub - id: latest_release - uses: actions/github-script@v6 - with: - script: | - try { - const latestRelease = await github.rest.repos.getLatestRelease({ - owner: context.repo.owner, - repo: context.repo.repo - }); - core.info(`Latest release tag: ${latestRelease.data.tag_name}`); - core.setOutput('last_release', latestRelease.data.tag_name); - } catch (error) { - core.info("No previous release found."); - // If no release exists, output an empty string. - core.setOutput('last_release', ''); - } - - - name: Set new release tag from input - id: new_tag - run: | - echo "NEW_TAG=${{ github.event.inputs.new_tag }}" >> $GITHUB_OUTPUT - - - name: Generate commit log for new release - id: commits - run: | - LAST_RELEASE="${{ steps.latest_release.outputs.last_release }}" - NEW_TAG="${{ steps.new_tag.outputs.NEW_TAG }}" - - if [ -z "$NEW_TAG" ]; then - echo "No new tag provided. Exiting." - exit 1 - fi - - if [ -z "${LAST_RELEASE}" ]; then - echo "No previous release found, using the repository’s initial commit as the starting point." - FIRST_COMMIT=$(git rev-list --max-parents=0 HEAD) - RANGE="${FIRST_COMMIT}..${NEW_TAG}" - else - RANGE="${LAST_RELEASE}..${NEW_TAG}" - fi - - echo "Comparing commits between: ${RANGE}" - LOG=$(git log "${RANGE}" --no-merges --pretty=format:"- %h %s by %an") - - echo "LOG<> "$GITHUB_ENV" - echo "$LOG" >> "$GITHUB_ENV" - echo "EOF" >> "$GITHUB_ENV" - - # Create or update the GitHub release for the new tag. - - name: Create / Update GitHub Release for new tag - uses: softprops/action-gh-release@master - with: - tag_name: ${{ steps.new_tag.outputs.NEW_TAG }} - name: "${{ steps.new_tag.outputs.NEW_TAG }}" - body: ${{ env.LOG }} - append_body: false - generate_release_notes: false - make_latest: true - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/native-build.yml b/.github/workflows/native-build.yml new file mode 100644 index 00000000..75fd02c8 --- /dev/null +++ b/.github/workflows/native-build.yml @@ -0,0 +1,110 @@ +name: Build WebView wrappers + +on: + workflow_dispatch: + inputs: + tag: + required: true + description: "Ref to build from (e.g. v1.0.0)" + +env: + PYTHON_VERSION: 3.11 + PNPM_VERSION: 10 + NODE_VERSION: 20 + +jobs: + build: + permissions: + packages: write + contents: write + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout source repo + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.tag }} + + - name: Cache Chocolatey packages + if: matrix.os == 'windows-latest' + uses: actions/cache@v4 + with: + path: C:\ProgramData\chocolatey\lib + key: choco-${{ runner.os }}-qt6 + restore-keys: | + choco-${{ runner.os }}- + + - name: Install Qt (Windows) + if: matrix.os == 'windows-latest' + run: choco install -y qt6-base-dev + + - name: Cache Python venv + id: cache-python + uses: actions/cache@v4 + with: + path: .venv + key: uv-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('uv.lock') }} + restore-keys: | + uv-${{ runner.os }}-${{ env.PYTHON_VERSION }}- + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + architecture: "x64" + + - name: Install Python dependencies + run: | + pip install uv + uv venv --system-site-packages --relocatable + uv sync --link-mode=copy --active + + - name: Install PyInstaller + Qt backend (Windows) + if: matrix.os == 'windows-latest' + run: | + uv pip install pyinstaller pywebview QtPy PySide6 + + - name: Install PyInstaller + Qt backend (Other OSs) + if: matrix.os != 'windows-latest' + run: | + uv pip install pyinstaller pywebview[qt] + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: "ui/pnpm-lock.yaml" + + - name: Build frontend + working-directory: ui + run: | + pnpm install --production --prefer-offline --frozen-lockfile + pnpm run generate + + - name: Build native binary + run: | + uv run pyinstaller ./app.spec + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: app-${{ matrix.os }}-${{ github.event.inputs.tag }} + path: dist/* + + - name: Upload to GitHub Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.inputs.tag }} + files: dist/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index ac6b82f6..15753c9c 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,13 @@ live streams, and includes features like scheduling downloads, sending notificat * Apply `yt-dlp` options per custom defined conditions. * Custom browser extensions, bookmarklets and iOS shortcuts to send links to YTPTube instance. +### Side project + +We have a side project related to YTPTube, which takes YTPTube source code and build an executable for Windows, macOS and Linux. +The project is in early stages, and might not even work yet. However keep an eye at [This build action](https://github.com/arabcoders/ytptube/actions/workflows/native-build.yml) for builds, simply select last successful build and download the executable for your platform. Help us test out the platforms +and report any issues you might find. We only have windows/linux machines to test on, so we need your help to test it out on macOS. + + # Installation ## Run using docker command