From 51854aa01a962348e8e592c9438605e37150a2ce Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 20 Dec 2024 13:49:36 +0100 Subject: [PATCH] pyinstaller builds for macOS and Linux in the CI (#1348) * Build for macOS * Add sdl2 * add sdl2_image * Add assets back * Try pysdl2-dll * Pack theme * Add templates * add librsvg * List all files in .app * Try build with --wheel * Attempt to bundle phazor * Try adding pysdl2-dll and Rsvg as hidden imports * Attempt to throw DYLD_LIBRARY_PATH back into spec * Try adding librsvg-2.2.dylib * Try fixing localization * rsvg hidden import try #2 * Stop duping CustomLoggingFormatter * compile_translations.py: Throw a proper exception on error * Fix up command order * Install gettext * Use partial path for gettext * Move logging classes to a separate module * Cleanup and brew upgrade * Add locale dir * Fix brew commands * Which genius decided to litter the FS with this garbage * Remove extra space * cleanup glib and gettext * Attempt to bundle SDL2 prebuilt frameworks * Try importing hidden chromecast * try lower case pychromecast * Fix error message for setproctitle * Add support for PEP508 deps definitions * Remove duped dep * Add initial optdepends * Fix up deps * Move out import and add test exception * pyinstaller debug build * Try reverting deps changes * Kill optdepends in pyproject * Fix ext-modules * Fix optional * install pyinstaller in unified deps * Fix opus include * Init linux.spec * Add Linux CI * Swap macOS CI to unified reqs * Try fixing up hiddenimports * Linux CI: Add gobject-introspection * Linux CI: Add python3-gi-cairo * try adding libgirepository1.0-dev * Add libcairo2-dev * Linux CI: moar deps * Log error for SDL renderer errors too * Fix up CI * Editor is rendering pipes curved here... * Linux CI: Fix syntax * Jpeg XL dep * Try 24.10 * Make requirements.txt be the cross-platform full one * yeet JXLPY to temp pass CI * Fix backslash * test if sdl framework still gets copied * Linux CI: Clone submodules too * Linux CI: Add devel packages for audio * Linux CI: Add libsamplerate0-dev * Linux CI: Fix backslash * Try copying the entire sdl2dll dir * Rename compile translations job * Try adding zeroconf to reqs * Fix pychromecast on pyinstaller * Fix pyproject.toml indents * Clean up requirements a bit * Stop always printing pychromecast exception, now just to debug * Move Chrome() creation back to try block * pysdl fix attempt * try to switch over to dev pyinstaller better * Fix up reqs * Use pysdl2-dll on all OSs * Fix up Linux spec file * Try bumping ffmpeg from v5 to v7 * Ffmpeg v2 attempt numero duo * Attempt to add a Rsvg hook * Indent user files location * Add toggle console button to Misc * Log PATH when looking for ffmpeg * Add the second part necessary for Rsvg * Fix up DConsole self reference * Fix up optdeps * try hacking libjxl on Linux * Cleanup and document workarounds * Fix dpkg -i * Add libgif dep for jxl * Cleanup TODO notes * Try packaging ffmpeg better * Cleanup docs * Fix up action names --- .github/workflows/build_Linux.yaml | 94 +++++++++++ .github/workflows/build_macOS.yaml | 95 +++++++++++ .github/workflows/compile_translations.yaml | 2 +- MANIFEST.in | 2 + compile_translations.py | 40 +---- .../hook-gi.repository.Rsvg.py | 5 + .../hook-gi.repository.Rsvg.py | 16 ++ linux.spec | 58 +++++++ mac.spec | 157 +++++++++++------- pyproject.toml | 107 ++++++++---- requirements.txt | 22 ++- requirements_linux.txt | 12 ++ requirements_macos.txt | 12 ++ requirements_optional.txt | 2 +- requirements_windows.txt | 2 - src/tauon/__main__.py | 47 +----- src/tauon/t_modules/logging.py | 49 ++++++ src/tauon/t_modules/t_main.py | 24 ++- 18 files changed, 563 insertions(+), 183 deletions(-) create mode 100644 .github/workflows/build_Linux.yaml create mode 100644 .github/workflows/build_macOS.yaml create mode 100644 MANIFEST.in create mode 100644 extra/pyinstaller-hooks/hook-gi.repository.Rsvg.py create mode 100644 extra/pyinstaller-hooks/pre_safe_import_module/hook-gi.repository.Rsvg.py create mode 100644 linux.spec create mode 100644 requirements_linux.txt create mode 100644 requirements_macos.txt create mode 100644 src/tauon/t_modules/logging.py diff --git a/.github/workflows/build_Linux.yaml b/.github/workflows/build_Linux.yaml new file mode 100644 index 000000000..56bcc68ab --- /dev/null +++ b/.github/workflows/build_Linux.yaml @@ -0,0 +1,94 @@ +name: Build Linux app + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Install deps + run: | + sudo apt-get update + sudo apt-get install -y \ + gettext \ + gobject-introspection \ + libgirepository1.0-dev \ + python3-gi-cairo \ + libcairo2-dev \ + libpipewire-0.3-dev \ + libdbus-1-dev \ + libjxl-dev \ + libflac-dev \ + libgme-dev \ + libmpg123-dev \ + libopenmpt-dev \ + libopusfile-dev \ + libsamplerate0-dev \ + libvorbis-dev \ + libwavpack-dev + # JPEG-XL hack since 24.04 is too old + sudo apt-get install -y \ + libgif7 \ + wget + wget http://mirrors.kernel.org/ubuntu/pool/universe/j/jpeg-xl/libjxl-dev_0.10.3-4ubuntu1_amd64.deb + wget http://mirrors.kernel.org/ubuntu/pool/universe/j/jpeg-xl/libjxl0.10_0.10.3-4ubuntu1_amd64.deb + wget http://mirrors.kernel.org/ubuntu/pool/universe/h/highway/libhwy-dev_1.2.0-3ubuntu2_amd64.deb + wget http://mirrors.kernel.org/ubuntu/pool/universe/h/highway/libhwy1t64_1.2.0-3ubuntu2_amd64.deb + wget http://mirrors.kernel.org/ubuntu/pool/main/l/lcms2/liblcms2-dev_2.14-2build1_amd64.deb + sudo dpkg -i *.deb + + - name: Install Python dependencies and setup venv + run: | + python -m pip install --upgrade pip + python -m venv .venv + source .venv/bin/activate + pip install \ + -r requirements.txt \ + build \ + pyinstaller + + - name: Build the project using python-build + run: | + source .venv/bin/activate + python -m compile_translations + python -m build --wheel + + - name: Install the project into a venv + run: | + source .venv/bin/activate + pip install --prefix ".venv" dist/*.whl + + - name: "[DEBUG] List all files" + run: find . + + - name: Build Linux App with PyInstaller + run: | + source .venv/bin/activate + pyinstaller --log-level=DEBUG linux.spec + + - name: Create ZIP + run: | + mkdir -p dist/zip + APP_NAME="TauonMusicBox" + APP_PATH="dist/${APP_NAME}" + ZIP_PATH="dist/zip/${APP_NAME}.zip" + + zip -r "${ZIP_PATH}" "${APP_PATH}" + + - name: Upload ZIP artifact + uses: actions/upload-artifact@v4 + with: + name: TauonMusicBox-linux + path: dist/zip/TauonMusicBox.zip diff --git a/.github/workflows/build_macOS.yaml b/.github/workflows/build_macOS.yaml new file mode 100644 index 000000000..7560ab313 --- /dev/null +++ b/.github/workflows/build_macOS.yaml @@ -0,0 +1,95 @@ +name: Build macOS app + +on: + push: + pull_request: + +jobs: + build: + runs-on: macos-latest + + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: brew update and upgrade + run: brew update && brew upgrade + + - name: Install brew dependencies + run: | + brew install \ + gobject-introspection \ + gtk+3 \ + pango \ + sdl2 \ + sdl2_image \ + jpeg-xl \ + ffmpeg \ + librsvg \ + opusfile \ + libopenmpt \ + wavpack \ + game-music-emu + + - name: Install Python dependencies and setup venv + run: | + python -m pip install --upgrade pip + python -m venv .venv + source .venv/bin/activate + pip install \ + -r requirements.txt \ + build + # Hack until https://github.com/pyinstaller/pyinstaller/issues/8936 is resolved + pip install https://github.com/rokm/pyinstaller/archive/refs/heads/macos-nested-framework-bundles.zip +# \ +# pyinstaller +# pip uninstall pyinstaller +# CFLAGS: "-I/opt/homebrew/include" +# LDFLAGS: "-L/opt/homebrew/lib" + + - name: Build the project using python-build + run: | + source .venv/bin/activate + python -m compile_translations + python -m build --wheel + + - name: Install the project into a venv + run: | + source .venv/bin/activate + pip install --prefix ".venv" dist/*.whl + + - name: "[DEBUG] List all files" + run: find . + + - name: Build macOS app with PyInstaller + run: | + source .venv/bin/activate + pyinstaller --log-level=DEBUG mac.spec + env: + DYLD_LIBRARY_PATH: "/opt/homebrew/lib" + + - name: "[DEBUG] List all files in .app" + run: find "dist/TauonMusicBox.app" + + - name: Create DMG + run: | + mkdir -p dist/dmg + APP_NAME="TauonMusicBox" + APP_PATH="dist/${APP_NAME}.app" + DMG_PATH="dist/dmg/${APP_NAME}.dmg" + + # Create a .dmg package + hdiutil create -volname "$APP_NAME" -srcfolder "$APP_PATH" -ov -format UDZO "$DMG_PATH" + + - name: Upload DMG artifact + uses: actions/upload-artifact@v4 + with: + name: TauonMusicBox-dmg + path: dist/dmg/TauonMusicBox.dmg diff --git a/.github/workflows/compile_translations.yaml b/.github/workflows/compile_translations.yaml index 823a651c5..786ab20e3 100644 --- a/.github/workflows/compile_translations.yaml +++ b/.github/workflows/compile_translations.yaml @@ -1,4 +1,4 @@ -name: Tauon Linux CI +name: Compile translations on Linux on: push: diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..5a499f658 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include src/phazor/kissfft *.h +recursive-include src/phazor/miniaudio *.h diff --git a/compile_translations.py b/compile_translations.py index 102d6f2bf..a0834765a 100644 --- a/compile_translations.py +++ b/compile_translations.py @@ -3,38 +3,7 @@ import subprocess from pathlib import Path - -# TODO(Martin): import this class from tauon.py instead -class CustomLoggingFormatter(logging.Formatter): - """Nicely format logging.loglevel logs""" - - grey = "\x1b[38;20m" - grey_bold = "\x1b[38;1m" - yellow = "\x1b[33;20m" - yellow_bold = "\x1b[33;1m" - red = "\x1b[31;20m" - bold_red = "\x1b[31;1m" - reset = "\x1b[0m" - format = "%(asctime)s [%(levelname)s] [%(module)s] %(message)s" - format_verbose = "%(asctime)s [%(levelname)s] [%(module)s] %(message)s (%(filename)s:%(lineno)d)" - - FORMATS = { - logging.DEBUG: grey_bold + format_verbose + reset, - logging.INFO: yellow + format + reset, - logging.WARNING: yellow_bold + format + reset, - logging.ERROR: red + format + reset, - logging.CRITICAL: bold_red + format_verbose + reset, - } - - def format(self, record: dict) -> str: - log_fmt = self.FORMATS.get(record.levelno) - # Remove the miliseconds(%f) from the default string - date_fmt = "%Y-%m-%d %H:%M:%S" - formatter = logging.Formatter(log_fmt, date_fmt) - # Center align + min length things to prevent logs jumping around when switching between different values - record.levelname = f"{record.levelname:^7}" - record.module = f"{record.module:^10}" - return formatter.format(record) +from src.tauon.t_modules.logging import CustomLoggingFormatter # DEBUG+ to file and std_err logging.basicConfig( @@ -56,7 +25,7 @@ def main() -> None: for lang_file in languages: - if lang_file.name == "messages.pot": + if lang_file.name in ("messages.pot", ".DS_Store"): continue po_path = locale_folder / lang_file.name / "LC_MESSAGES" / "tauon.po" @@ -69,10 +38,9 @@ def main() -> None: if po_path.exists(): try: - subprocess.run(["/usr/bin/msgfmt", "-o", mo_path, po_path], check=True) + subprocess.run(["msgfmt", "-o", mo_path, po_path], check=True) except Exception: - # Don't log the exception to make the build log clear - logging.error(f"Failed to compile translations for {lang_file.name}") + logging.exception(f"Failed to compile translations for {lang_file.name}") compile_failure = True else: logging.info(f"Compiled: {lang_file.name}") diff --git a/extra/pyinstaller-hooks/hook-gi.repository.Rsvg.py b/extra/pyinstaller-hooks/hook-gi.repository.Rsvg.py new file mode 100644 index 000000000..7ac85cd35 --- /dev/null +++ b/extra/pyinstaller-hooks/hook-gi.repository.Rsvg.py @@ -0,0 +1,5 @@ +from PyInstaller.utils.hooks.gi import GiModuleInfo + +module_info = GiModuleInfo("Rsvg", "2.0") +if module_info.available: + binaries, datas, hiddenimports = module_info.collect_typelib_data() diff --git a/extra/pyinstaller-hooks/pre_safe_import_module/hook-gi.repository.Rsvg.py b/extra/pyinstaller-hooks/pre_safe_import_module/hook-gi.repository.Rsvg.py new file mode 100644 index 000000000..1eb1d6e33 --- /dev/null +++ b/extra/pyinstaller-hooks/pre_safe_import_module/hook-gi.repository.Rsvg.py @@ -0,0 +1,16 @@ +#----------------------------------------------------------------------------- +# Copyright (c) 2005-2023, PyInstaller Development Team. +# +# Distributed under the terms of the GNU General Public License (version 2 +# or later) with exception for distributing the bootloader. +# +# The full license is in the file COPYING.txt, distributed with this software. +# +# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception) +#----------------------------------------------------------------------------- + + +def pre_safe_import_module(api): + # PyGObject modules loaded through the gi repository are marked as MissingModules by modulegraph, so we convert them + # to RuntimeModules in order for their hooks to be loaded and executed. + api.add_runtime_module(api.module_name) diff --git a/linux.spec b/linux.spec new file mode 100644 index 000000000..06dce2d31 --- /dev/null +++ b/linux.spec @@ -0,0 +1,58 @@ + + +a = Analysis( + ["src/tauon/__main__.py"], + pathex=[], + binaries=[], + datas=[ + ("src/tauon/assets", "assets"), + ("src/tauon/locale", "locale"), + ("src/tauon/theme", "theme"), + ("src/tauon/templates", "templates"), + # This could only have SDL2.framework and SDL2_image.framework to save space... + (".venv/lib/python3.13/site-packages/sdl2dll/dll", "sdl2dll/dll"), +# (".venv/lib/python3.13/site-packages/sdl2dll/dll/SDL2.framework", "sdl2dll/dll/SDL2.framework"), +# (".venv/lib/python3.13/site-packages/sdl2dll/dll/SDL2_image.framework", "sdl2dll/dll/SDL2_image.framework"), + ], + hiddenimports=[ + "pylast", + "phazor", + # Zeroconf is hacked until this issue is resolved: https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/840 + "zeroconf._utils.ipaddress", + "zeroconf._handlers.answers", + ], + hookspath=["extra/pyinstaller-hooks"], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name="Tauon Music Box", + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name="TauonMusicBox", +) diff --git a/mac.spec b/mac.spec index cf5df1a79..fe590d251 100644 --- a/mac.spec +++ b/mac.spec @@ -1,60 +1,103 @@ -# -*- mode: python ; coding: utf-8 -*- - +import os +import subprocess block_cipher = None -import subprocess -prefix = subprocess.run(['brew', '--prefix'], capture_output=True, text=True).stdout.strip() - - -a = Analysis(['tauon.py'], - binaries=[('lib/libphazor.so', 'lib/'), - (prefix + '/bin/ffmpeg', '.'), - (prefix + '/lib/*.dylib', '.'), - ], - datas=[('assets', 'assets'), ('theme', 'theme'), ('input.txt', '.')], - hiddenimports=['sdl2', 'pylast'], - hookspath=['extra/pyinstaller-hooks'], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) - -exe = EXE(pyz, - a.scripts, - [], - exclude_binaries=True, - name='Tauon Music Box', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=False, - disable_windowed_traceback=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None , icon='assets/tau-mac.icns') -coll = COLLECT(exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='TauonMusicBox') -app = BUNDLE(coll, - name='TauonMusicBox.app', - icon='assets/tau-mac.icns', - bundle_identifier=None, - info_plist={ - 'LSEnvironment': { - 'LANG': 'en_US.UTF-8', - 'LC_CTYPE': 'en_US.UTF-8' - } - } - ) +# default PATH=/usr/bin:/bin:/usr/sbin:/sbin:/Applications/TauonMusicBox.app/Contents/Frameworks + +# Should resolve as /opt/homebrew +prefix = subprocess.run(["brew", "--prefix"], capture_output=True, text=True).stdout.strip() + +libs = [ + "libpangocairo-1.0.0.dylib", + "libharfbuzz.0.dylib", + "libgobject-2.0.0.dylib", + "libgio-2.0.0.dylib", + "librsvg-2.2.dylib" +] + +lib_paths = [(f"{prefix}/lib/{lib}", ".") for lib in libs] +phazor_path = f"build/lib.macosx-10.13-universal2-cpython-313/phazor.cpython-313-darwin.so" + +a = Analysis( + ["src/tauon/__main__.py"], + binaries=[ + *lib_paths, + (phazor_path, "."), + (f"{prefix}/bin/ffmpeg", "."), + ], + datas=[ + ("src/tauon/assets", "assets"), + ("src/tauon/locale", "locale"), + ("src/tauon/theme", "theme"), + ("src/tauon/templates", "templates"), + # This could only have SDL2.framework and SDL2_image.framework to save space... + (".venv/lib/python3.13/site-packages/sdl2dll/dll", "sdl2dll/dll"), +# (".venv/lib/python3.13/site-packages/sdl2dll/dll/SDL2.framework", "sdl2dll/dll/SDL2.framework"), +# (".venv/lib/python3.13/site-packages/sdl2dll/dll/SDL2_image.framework", "sdl2dll/dll/SDL2_image.framework"), + ], + hiddenimports=[ + "sdl2", + "phazor", + "pylast", + # Zeroconf is hacked until this issue is resolved: https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/840 + "zeroconf._utils.ipaddress", + "zeroconf._handlers.answers", + ], + # TODO(Martin): Rsvg hooks are a hack until pyinstaller releases something newer than 6.11.1 - https://github.com/pyinstaller/pyinstaller/releases + hookspath=["extra/pyinstaller-hooks"], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) + +pyz = PYZ( + a.pure, + a.zipped_data, + cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name="Tauon Music Box", + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon="src/tauon/assets/tau-mac.icns") + +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name="TauonMusicBox") + +app = BUNDLE( + coll, + name="TauonMusicBox.app", + icon="src/tauon/assets/tau-mac.icns", + bundle_identifier=None, + info_plist={ + "LSEnvironment": { + "LANG": "en_US.UTF-8", + "LC_CTYPE": "en_US.UTF-8", + }}) + +for lib in lib_paths: + lib_name, _ = lib + os.system(f'install_name_tool -add_rpath "@executable_path/." "{lib_name}"') + os.system(f'install_name_tool -add_rpath "@executable_path/." "{phazor_path}"') diff --git a/pyproject.toml b/pyproject.toml index a73db2307..dba58dd22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,45 @@ "Typing :: Typed", ] dynamic = ["dependencies"] +# dependencies = [ +# "beautifulsoup4", +# "musicbrainzngs", +# "mutagen", +# "PlexAPI", # OPTDEP +# "PyGObject", +# "pylast>=3.1.0", +# "PySDL2", +# "requests", +# "Send2Trash", +# "unidecode", +# "dbus-python; sys_platform == 'linux'", +# "pysdl2-dll; sys_platform == 'darwin'", # Don't rely on system https://github.com/py-sdl/py-sdl2#requirements +# "comtypes; sys_platform == 'win32'", +# "infi.systray; sys_platform == 'win32'", +# "keyboard; sys_platform == 'win32'", +# "Pillow; sys_platform != 'win32'", +# "opencc; sys_platform != 'win32'", # OPTDEP +# "opencc-python-reimplemented; sys_platform == 'win32'", # OPTDEP +# "pyinstaller; sys_platform != 'linux'", # for macOS at least +# "pypresence", # optdep +# "tekore", # optdep, +# "natsort", # optdep +# "jxlpy; sys_platform != 'darwin'", # macOS hates it - fails to find jxl/types.h - https://github.com/olokelo/jxlpy/issues/25#issuecomment-2547928563 +# #librespot - https://github.com/kokarare1212/librespot-python/pull/286 +# #picard - picard 2.12.3 requires charset-normalizer~=3.3.2, but you have charset-normalizer 3.4.0 which is incompatible. +# "PyChromecast", # OPTDEP +# "setproctitle", # OPTDEP +# "tidalapi", # OPTDEP +# "colored_traceback", # very opt +# ] + +#[project.optional-dependencies] +# dev = [ +# "pygobject-stubs", # needs PYGOBJECT_STUB_CONFIG=Gtk3,Gdk3 env var when being installed +# ] +# all = [ +# "opencc; sys_platform != 'win32'", # OPTDEP +# ] [project.gui-scripts] tauonmb = "tauon.__main__:main" @@ -46,10 +85,17 @@ # ext-modules = [ # {name = "phazor", sources = ["src/phazor/kissfft/kiss_fftr.c", "src/phazor/kissfft/kiss_fft.c", "src/phazor/phazor.c"], include-dirs = ["src/phazor/miniaudio", "C:/Users/Yeet/Tauon/src/phazor/miniaudio", "C:/Users/Yeet/Tauon/vcpkg/installed/x64-windows/include", "C:/Users/Yeet/Tauon/src/phazor/kissfft", "C:/Users/Yeet/Tauon/src/phazor/miniaudio"], libraries=["samplerate", "wavpackdll", "opusfile", "ogg", "opus", "vorbisfile", "mpg123", "FLAC", "openmpt", "pthreadVC3", "gme"], library-dirs = ["vcpkg/installed/x64-windows/lib"] },] - # Linux + # Linux + macOS ext-modules = [ - {name = "phazor", sources = ["src/phazor/kissfft/kiss_fftr.c", "src/phazor/kissfft/kiss_fft.c", "src/phazor/phazor.c"], include-dirs = ["/usr/include/opus"], libraries=["samplerate", "wavpack", "opusfile", "vorbisfile", "mpg123", "FLAC", "openmpt", "gme"] }, - {name = "phazor-pw", sources = ["src/phazor/kissfft/kiss_fftr.c", "src/phazor/kissfft/kiss_fft.c", "src/phazor/phazor.c"], include-dirs = ["/usr/include/opus"], libraries=["samplerate", "wavpack", "opusfile", "vorbisfile", "mpg123", "FLAC", "openmpt", "gme", "pipewire-0.3"] },] + {name = "phazor", sources = ["src/phazor/kissfft/kiss_fftr.c", "src/phazor/kissfft/kiss_fft.c", "src/phazor/phazor.c"], include-dirs = ["/usr/include/opus", "/opt/homebrew/include/opus", "/opt/homebrew/include"], libraries = ["samplerate", "wavpack", "opusfile", "vorbisfile", "mpg123", "FLAC", "openmpt", "gme"], library-dirs = ["/opt/homebrew/lib"] }, + # Set as optional to allow soft-failure, it's expected not to build on Windows/macOS, but we don't want to fail the entire build on it + # I have not found a better way to solve this, ideally we would somehow tag this as Linux-exclusive + {name = "phazor-pw", sources = ["src/phazor/kissfft/kiss_fftr.c", "src/phazor/kissfft/kiss_fft.c", "src/phazor/phazor.c"], include-dirs = ["/usr/include/opus"], libraries = ["samplerate", "wavpack", "opusfile", "vorbisfile", "mpg123", "FLAC", "openmpt", "gme", "pipewire-0.3"], optional = true }, + ] + + # macOS +# ext-modules = [ +# {name = "phazor", sources = ["src/phazor/kissfft/kiss_fftr.c", "src/phazor/kissfft/kiss_fft.c", "src/phazor/phazor.c"], include-dirs = ["/opt/homebrew/include/opus", "src/phazor/kissfft", "/opt/homebrew/include"], libraries=["samplerate", "wavpack", "opusfile", "vorbisfile", "mpg123", "FLAC", "openmpt", "gme"], library-dirs = ["/opt/homebrew/lib"] },] package-dir = {"" = "src"} @@ -63,13 +109,10 @@ ] [tool.setuptools.dynamic] - # Windows -# dependencies = {file = "requirements_windows.txt"} - # Linux dependencies = {file = "requirements.txt"} [tool.setuptools.package-data] -"tauon" = ["assets/*", "assets/svg/*", "locale/*/*/*.mo", "templates/*", "theme/*"] + "tauon" = ["assets/*", "assets/svg/*", "locale/*/*/*.mo", "templates/*", "theme/*"] # https://github.com/microsoft/pyright/blob/main/docs/configuration.md#pyright-configuration [tool.pyright] @@ -146,28 +189,28 @@ # https://docs.astral.sh/ruff/rules/ [tool.ruff.lint] -select = ['ALL'] - ignore = [ - 'Q003', # avoidable-escaped-quote - It's not that important, we just use escapes, keeping the quotes consistent - 'W191', # tab-indentation - We use tabs for indents, disabling this PEP 8 recommendation - 'D206', # indent-with-spaces - ^ - 'D100', # undocumented-public-module - TODO(Martin): We currently don't even have typing fully implemented, let's maybe care about undocumented functions/classes later - 'D101', # undocumented-public-class - ^ - 'D102', # public-method - ^ - 'D103', # undocumented-public-function - ^ - 'D104', # undocumented-public-package - ^ -# 'D105', # undocumented-magic-method - ^ < This one is a bit more severe though - 'D106', # undocumented-public-nested-class - ^ - 'D107', # undocumented-public-init - ^ - 'D400', # ends-in-period - We do not care if docstrings end with a period - 'D415', # ends-in-punctuation - ^ - 'D401', # non-imperative-mood - Wants docstrings in imperative language but it's really not foolproof, disable - 'EM101', # raw-string-in-exception - We do not care about .format in exceptions, readability is not *our* problem, traceback should be colorized to avoid this instead - 'EM102', # f-string-in-exception - ^ - 'EM103', # dot-format-in-exception - ^ - 'ERA001', # commented-out-code - Tests for commented out code, but it has way too many false positives, so disable - 'FBT001', # boolean-type-hint-positional-argument - Allow positional booleans in functions, it's not really that much of an issue - 'FBT002', # boolean-default-value-positional-argument - ^ - 'FBT003', # boolean-positional-value-in-call - ^ - 'TD003', # missing-todo-link - We're fine not linking existing issues in TODOs, it's fine to have them noted in code only - ] + select = ['ALL'] + ignore = [ + 'Q003', # avoidable-escaped-quote - It's not that important, we just use escapes, keeping the quotes consistent + 'W191', # tab-indentation - We use tabs for indents, disabling this PEP 8 recommendation + 'D206', # indent-with-spaces - ^ + 'D100', # undocumented-public-module - TODO(Martin): We currently don't even have typing fully implemented, let's maybe care about undocumented functions/classes later + 'D101', # undocumented-public-class - ^ + 'D102', # public-method - ^ + 'D103', # undocumented-public-function - ^ + 'D104', # undocumented-public-package - ^ +# 'D105', # undocumented-magic-method - ^ < This one is a bit more severe though + 'D106', # undocumented-public-nested-class - ^ + 'D107', # undocumented-public-init - ^ + 'D400', # ends-in-period - We do not care if docstrings end with a period + 'D415', # ends-in-punctuation - ^ + 'D401', # non-imperative-mood - Wants docstrings in imperative language but it's really not foolproof, disable + 'EM101', # raw-string-in-exception - We do not care about .format in exceptions, readability is not *our* problem, traceback should be colorized to avoid this instead + 'EM102', # f-string-in-exception - ^ + 'EM103', # dot-format-in-exception - ^ + 'ERA001', # commented-out-code - Tests for commented out code, but it has way too many false positives, so disable + 'FBT001', # boolean-type-hint-positional-argument - Allow positional booleans in functions, it's not really that much of an issue + 'FBT002', # boolean-default-value-positional-argument - ^ + 'FBT003', # boolean-positional-value-in-call - ^ + 'TD003', # missing-todo-link - We're fine not linking existing issues in TODOs, it's fine to have them noted in code only + ] diff --git a/requirements.txt b/requirements.txt index c7e87b862..333c79370 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,6 @@ beautifulsoup4 -dbus-python musicbrainzngs mutagen -Pillow PlexAPI PyGObject pylast>=3.1.0 @@ -10,3 +8,23 @@ PySDL2 requests Send2Trash unidecode +dbus-python; sys_platform == 'linux' +pysdl2-dll # Don't rely on system SDL2 https://github.com/py-sdl/py-sdl2#requirements +comtypes; sys_platform == 'win32' +infi.systray; sys_platform == 'win32' +keyboard; sys_platform == 'win32' +Pillow; sys_platform != 'win32' +opencc; sys_platform != 'win32' # OPTDEP +opencc-python-reimplemented; sys_platform == 'win32' # OPTDEP +#pyinstaller # ; sys_platform != 'linux' # for macOS at least +pypresence # optdep +tekore # optdep, +natsort # optdep +jxlpy; sys_platform != 'darwin' # macOS hates it - fails to find jxl/types.h - https://github.com/olokelo/jxlpy/issues/25#issuecomment-2547928563 +#librespot - https://github.com/kokarare1212/librespot-python/pull/286 +#picard - picard 2.12.3 requires charset-normalizer~=3.3.2, but you have charset-normalizer 3.4.0 which is incompatible. +PyChromecast # OPTDEP +setproctitle # OPTDEP +tidalapi # OPTDEP +colored_traceback # very opt +zeroconf # pychromecast dependency, TODO(Martin): This is a test, remove me diff --git a/requirements_linux.txt b/requirements_linux.txt new file mode 100644 index 000000000..c7e87b862 --- /dev/null +++ b/requirements_linux.txt @@ -0,0 +1,12 @@ +beautifulsoup4 +dbus-python +musicbrainzngs +mutagen +Pillow +PlexAPI +PyGObject +pylast>=3.1.0 +PySDL2 +requests +Send2Trash +unidecode diff --git a/requirements_macos.txt b/requirements_macos.txt new file mode 100644 index 000000000..bd174a6f9 --- /dev/null +++ b/requirements_macos.txt @@ -0,0 +1,12 @@ +beautifulsoup4 +musicbrainzngs +mutagen +Pillow +PlexAPI +PyGObject +pylast>=3.1.0 +pysdl2-dll +PySDL2 +requests +Send2Trash +unidecode diff --git a/requirements_optional.txt b/requirements_optional.txt index c8233eb16..222ae36fe 100644 --- a/requirements_optional.txt +++ b/requirements_optional.txt @@ -1,5 +1,5 @@ colored_traceback -jxlpy +jxlpy; sys_platform != 'darwin' # macOS hates it - fails to find jxl/types.h - https://github.com/olokelo/jxlpy/issues/25#issuecomment-2547928563 #librespot - https://github.com/kokarare1212/librespot-python/pull/286 natsort opencc diff --git a/requirements_windows.txt b/requirements_windows.txt index 3b8d17270..caac82813 100644 --- a/requirements_windows.txt +++ b/requirements_windows.txt @@ -1,13 +1,11 @@ beautifulsoup4 comtypes # Windows dep -# dbus-python # Linux dep infi.systray keyboard # Windows dep musicbrainzngs mutagen natsort # optdep opencc-python-reimplemented # Windows version of openCC optdep -# Pillow # Linux dep PlexAPI PyGObject pyinstaller diff --git a/src/tauon/__main__.py b/src/tauon/__main__.py index 77119c0b3..32491aa8e 100755 --- a/src/tauon/__main__.py +++ b/src/tauon/__main__.py @@ -62,55 +62,13 @@ ) from sdl2.sdlimage import IMG_Load +from tauon.t_modules.logging import CustomLoggingFormatter, LogHistoryHandler + install_directory: Path = Path(__file__).resolve().parent sys.path.append(str(install_directory.parent)) from tauon.t_modules import t_bootstrap - -class CustomLoggingFormatter(logging.Formatter): - """Nicely format logging.loglevel logs""" - - grey = "\x1b[0;20m" - grey_bold = "\x1b[0;1m" - yellow = "\x1b[33;20m" - yellow_bold = "\x1b[33;1m" - red = "\x1b[31;20m" - bold_red = "\x1b[31;1m" - purple = "\x1b[0;35m" - reset = "\x1b[0m" - format = "%(asctime)s [%(levelname)s] [%(module)s] %(message)s" - format_verbose = "%(asctime)s [%(levelname)s] [%(module)s] %(message)s (%(filename)s:%(lineno)d)" - - # TODO(Martin): Add some way in which devel mode uses everything verbose - FORMATS = { - logging.DEBUG: grey_bold + format_verbose + reset, - logging.INFO: grey + format + reset, - logging.WARNING: purple + format_verbose + reset, - logging.ERROR: red + format_verbose + reset, - logging.CRITICAL: bold_red + format_verbose + reset, - } - - def format(self, record: dict) -> str: - log_fmt = self.FORMATS.get(record.levelno) - # Remove the miliseconds(%f) from the default string - date_fmt = "%Y-%m-%d %H:%M:%S" - formatter = logging.Formatter(log_fmt, date_fmt) - # Center align + min length things to prevent logs jumping around when switching between different values - record.levelname = f"{record.levelname:^7}" - record.module = f"{record.module:^10}" - return formatter.format(record) - -class LogHistoryHandler(logging.Handler): - def __init__(self): - super().__init__() - self.log_history = [] # List to store log messages - - def emit(self, record: dict): - self.log_history.append(record) # Append to the log history - if len(self.log_history) > 50: - del self.log_history[0] - log = LogHistoryHandler() formatter = logging.Formatter('[%(levelname)s] %(message)s') log.setFormatter(formatter) @@ -405,6 +363,7 @@ def transfer_args_and_exit() -> None: if not renderer: logging.error("ERROR CREATING RENDERER!") + logging.error(f"SDL Error: {SDL_GetError()}") sys.exit(1) SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND) diff --git a/src/tauon/t_modules/logging.py b/src/tauon/t_modules/logging.py new file mode 100644 index 000000000..3523b0301 --- /dev/null +++ b/src/tauon/t_modules/logging.py @@ -0,0 +1,49 @@ +import logging +from logging import LogRecord +from typing import override + + +class CustomLoggingFormatter(logging.Formatter): + """Nicely format logging.loglevel logs""" + + grey = "\x1b[0;20m" + grey_bold = "\x1b[0;1m" + yellow = "\x1b[33;20m" + yellow_bold = "\x1b[33;1m" + red = "\x1b[31;20m" + bold_red = "\x1b[31;1m" + purple = "\x1b[0;35m" + reset = "\x1b[0m" + format_simple = "%(asctime)s [%(levelname)s] [%(module)s] %(message)s" + format_verbose = "%(asctime)s [%(levelname)s] [%(module)s] %(message)s (%(filename)s:%(lineno)d)" + + # TODO(Martin): Add some way in which devel mode uses everything verbose + FORMATS = { + logging.DEBUG: grey_bold + format_verbose + reset, + logging.INFO: grey + format_simple + reset, + logging.WARNING: purple + format_verbose + reset, + logging.ERROR: red + format_verbose + reset, + logging.CRITICAL: bold_red + format_verbose + reset, + } + + @override + def format(self, record: LogRecord) -> str: + log_fmt = self.FORMATS.get(record.levelno) + # Remove the miliseconds(%f) from the default string + date_fmt = "%Y-%m-%d %H:%M:%S" + formatter = logging.Formatter(log_fmt, date_fmt) + # Center align + min length things to prevent logs jumping around when switching between different values + record.levelname = f"{record.levelname:^7}" + record.module = f"{record.module:^10}" + return formatter.format(record) + +class LogHistoryHandler(logging.Handler): + def __init__(self): + super().__init__() + self.log_history = [] # List to store log messages + + @override + def emit(self, record: LogRecord): + self.log_history.append(record) # Append to the log history + if len(self.log_history) > 50: + del self.log_history[0] diff --git a/src/tauon/t_modules/t_main.py b/src/tauon/t_modules/t_main.py index ed13d1835..c8d5fe62b 100644 --- a/src/tauon/t_modules/t_main.py +++ b/src/tauon/t_modules/t_main.py @@ -364,7 +364,7 @@ except ModuleNotFoundError: logging.warning("Unable to import setproctitle, won't be setting process title.") except Exception: - logging.exception("Unknown error trying to import setproctitle, JPEG XL support will be disabled.") + logging.exception("Unknown error trying to import setproctitle, won't be setting process title.") else: setproctitle.setproctitle("tauonmb") @@ -608,7 +608,7 @@ else: logging.info("Running from installed location") - logging.info("User files location: " + user_directory) + logging.info(f" User files location: {user_directory}") if not Path(Path(user_directory) / "encoder").is_dir(): os.makedirs(Path(user_directory) / "encoder") @@ -1008,9 +1008,14 @@ def no_padding() -> int: # Variables now go in the gui, pctl, input and prefs class instances. The following just haven't been moved yet class DConsole: + """GUI console with logs""" def __init__(self) -> None: self.show: bool = False + def toggle(self) -> None: + """Toggle the GUI console with logs on and off""" + self.show ^= True + console = DConsole() spot_cache_saved_albums = [] @@ -8937,6 +8942,7 @@ def test_ffmpeg(self) -> bool: return False def get_ffmpeg(self) -> str | None: + logging.debug(f"Looking for ffmpeg in PATH: {os.environ.get('PATH')}") p = shutil.which("ffmpeg") if p: return p @@ -8998,11 +9004,13 @@ def wake(self) -> None: try: from tauon.t_modules.t_chrome import Chrome chrome = Chrome(tauon) - logging.debug("Found import Chrome(pychromecast) for chromecast support") -except ModuleNotFoundError: +except ModuleNotFoundError as e: + logging.debug("pychromecast import error: {e}") logging.warning("Unable to import Chrome(pychromecast), chromecast support will be disabled.") except Exception: logging.exception("Unknown error trying to import Chrome(pychromecast), chromecast support will be disabled.") +finally: + logging.debug("Found import Chrome(pychromecast) for chromecast support") tauon.chrome = chrome @@ -16938,7 +16946,7 @@ def parse_template(string, track_object: TrackClass, up_ext: bool = False, stric radio_tab_menu = Menu(160, show_icons=True) -def rename_playlist(index, generator=False): +def rename_playlist(index, generator: bool = False) -> None: gui.rename_playlist_box = True rename_playlist_box.edit_generator = False rename_playlist_box.playlist_index = index @@ -16965,7 +16973,7 @@ def rename_playlist(index, generator=False): rename_playlist_box.toggle_edit_gen() -def edit_generator_box(index: int): +def edit_generator_box(index: int) -> None: rename_playlist(index, generator=True) @@ -19143,6 +19151,7 @@ def export_playlist_albums(pl: int) -> None: tab_menu.add_to_sub(2, MenuItem(_("Set as Downloads Playlist"), set_download_playlist, set_download_deco, pass_ref_deco=True, pass_ref=True)) tab_menu.add_to_sub(2, MenuItem(_("Set podcast mode"), set_podcast_playlist, set_podcast_deco, pass_ref_deco=True, pass_ref=True)) tab_menu.add_to_sub(2, MenuItem(_("Remove Duplicates"), remove_duplicates, pass_ref=True)) +tab_menu.add_to_sub(2, MenuItem(_("Toggle Console"), console.toggle)) # tab_menu.add_to_sub("Empty Playlist", 0, new_playlist) @@ -39154,7 +39163,6 @@ def right_remove_item(self) -> None: show_message(_("Looks like it's gone now anyway")) def toggle_pause(self) -> None: - pctl.pause_queue ^= True def draw_card( @@ -43972,7 +43980,7 @@ def drop_file(target): cycle_playlist_pinned(-1) if keymaps.test("toggle-console"): - console.show ^= True + console.toggle() if keymaps.test("toggle-fullscreen"): if not gui.fullscreen and gui.mode != 3: