feat(flatpak): add Flatpak packaging and CI release pipeline

- GNOME Platform 48 runtime (WebKitGTK included)
- GDK_BACKEND=wayland,x11 via finish-args (fixes GTK panic on pure-Wayland)
- libappindicator3 from flathub/shared-modules (not in GNOME 48 runtime)
- npm run tauri -- build --no-bundle (enables custom-protocol, fixes blank window)
- build-flatpak job in reusable-channel-publish.yml
- In-app updater disabled via VITE_PSYSONIC_FLATPAK=1
This commit is contained in:
kilyabin
2026-04-29 01:26:32 +04:00
parent a9573625f4
commit 97d99b8c5e
6 changed files with 307 additions and 0 deletions
@@ -270,6 +270,90 @@ jobs:
\( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \) \
| xargs gh release upload "$RELEASE_TAG" --clobber
build-flatpak:
needs: create-release
runs-on: ubuntu-24.04
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-50
options: --privileged
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.source_ref }}
- name: install source-generation tools
run: |
python3 -m ensurepip --upgrade
python3 -m pip install --quiet tomlkit aiohttp
- name: install flatpak-builder-tools
run: |
git clone --depth=1 \
https://github.com/flatpak/flatpak-builder-tools.git \
/tmp/flatpak-builder-tools
python3 -m pip install --quiet \
/tmp/flatpak-builder-tools/node
- name: clone shared-modules
run: |
git clone --depth=1 \
https://github.com/flathub/shared-modules.git \
packages/flatpak/shared-modules
- name: generate cargo offline sources
run: |
python3 /tmp/flatpak-builder-tools/cargo/flatpak-cargo-generator.py \
src-tauri/Cargo.lock -o packages/flatpak/cargo-sources.json
- name: generate npm offline sources
run: |
flatpak-node-generator npm package-lock.json \
-o packages/flatpak/npm-sources.json
- name: patch manifest for this release
env:
RELEASE_TAG: ${{ needs.create-release.outputs.release_tag }}
run: |
git config --global --add safe.directory /__w/psysonic/psysonic
COMMIT=$(git ls-remote origin "refs/tags/${RELEASE_TAG}^{}" | awk '{print $1}')
if [ -z "$COMMIT" ]; then
COMMIT=$(git ls-remote origin "refs/tags/${RELEASE_TAG}" | awk '{print $1}')
fi
sed -i \
-e "s|tag: app-v.*|tag: ${RELEASE_TAG}|" \
-e "s|commit: PLACEHOLDER_COMMIT|commit: ${COMMIT}|" \
-e "s|VITE_LASTFM_API_KEY=__LASTFM_KEY__|VITE_LASTFM_API_KEY=${{ secrets.VITE_LASTFM_API_KEY }}|" \
-e "s|VITE_LASTFM_API_SECRET=__LASTFM_SECRET__|VITE_LASTFM_API_SECRET=${{ secrets.VITE_LASTFM_API_SECRET }}|" \
packages/flatpak/dev.psysonic.Psysonic.yml
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
bundle: Psysonic_${{ needs.create-release.outputs.package_version }}.flatpak
manifest-path: packages/flatpak/dev.psysonic.Psysonic.yml
cache-key: flatpak-${{ github.sha }}
upload-flatpak:
needs: [create-release, build-flatpak]
permissions:
contents: write
runs-on: ubuntu-24.04
steps:
- name: download flatpak artifact
uses: actions/download-artifact@v4
with:
name: Psysonic_${{ needs.create-release.outputs.package_version }}-x86_64.flatpak
- name: upload to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION=${{ needs.create-release.outputs.package_version }}
RELEASE_TAG=${{ needs.create-release.outputs.release_tag }}
gh release upload "$RELEASE_TAG" \
"Psysonic_${VERSION}.flatpak" \
--clobber \
--repo ${{ github.repository }}
verify-nix:
if: ${{ inputs.verify_nix }}
needs: create-release
+7
View File
@@ -0,0 +1,7 @@
# Generated by flatpak-cargo-generator and flatpak-node-generator.
# Committed only in the Flathub mirror repo, not here.
cargo-sources.json
npm-sources.json
# Cloned at CI build time and manually for local builds (see CLAUDE.md).
shared-modules/
@@ -0,0 +1,10 @@
[Desktop Entry]
Name=Psysonic
Comment=Desktop music player for Subsonic API-compatible servers
Exec=psysonic
Icon=dev.psysonic.Psysonic
Terminal=false
Type=Application
Categories=AudioVideo;Audio;Music;Player;
StartupWMClass=psysonic
Keywords=music;player;subsonic;navidrome;streaming;
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>dev.psysonic.Psysonic</id>
<name>Psysonic</name>
<summary>Music player for Subsonic-compatible servers</summary>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-only</project_license>
<developer id="dev.psysonic">
<name>Psychotoxical</name>
</developer>
<description>
<p>
Psysonic is a desktop music player for Subsonic-compatible servers such as
Navidrome and Gonic. It connects to your self-hosted music library and lets
you stream your collection from anywhere.
</p>
<p>Features:</p>
<ul>
<li>Gapless playback with a native Rust audio engine</li>
<li>Fullscreen ambient player with album art backdrop</li>
<li>Synced and plain lyrics via LRCLIB and YouLy+</li>
<li>Scrobbling to Last.fm</li>
<li>Discord Rich Presence integration</li>
<li>MPRIS media controls and global shortcuts</li>
<li>Multiple server profiles</li>
<li>Catppuccin-themed UI with glassmorphism effects</li>
<li>Play queue management with drag-and-drop</li>
</ul>
</description>
<launchable type="desktop-id">dev.psysonic.Psysonic.desktop</launchable>
<url type="homepage">https://github.com/Psychotoxical/psysonic</url>
<url type="bugtracker">https://github.com/Psychotoxical/psysonic/issues</url>
<url type="donation">https://ko-fi.com/psychotoxic</url>
<url type="vcs-browser">https://github.com/Psychotoxical/psysonic</url>
<categories>
<category>AudioVideo</category>
<category>Audio</category>
<category>Music</category>
<category>Player</category>
</categories>
<keywords>
<keyword>music</keyword>
<keyword>player</keyword>
<keyword>subsonic</keyword>
<keyword>navidrome</keyword>
<keyword>gonic</keyword>
<keyword>streaming</keyword>
<keyword>self-hosted</keyword>
</keywords>
<!-- OARS 1.1 — music streaming player, no objectionable content generated by the app.
social-info:mild covers Last.fm scrobbling and Discord Rich Presence. -->
<content_rating type="oars-1.1">
<content_attribute id="social-info">mild</content_attribute>
</content_rating>
<recommends>
<display_length compare="ge">768</display_length>
<control>keyboard</control>
<control>pointing</control>
</recommends>
<supports>
<internet>always</internet>
</supports>
<!-- Screenshots: replace placeholders with real 16:9 screenshots (no window
decorations) before submitting to Flathub. At least 3 are required. -->
<screenshots>
<screenshot type="default">
<caption>Main player view with album art and play queue</caption>
<image type="source" width="1600" height="900">https://raw.githubusercontent.com/Psychotoxical/psysonic/main/assets/screenshots/main.png</image>
</screenshot>
<screenshot>
<caption>Fullscreen ambient player</caption>
<image type="source" width="1600" height="900">https://raw.githubusercontent.com/Psychotoxical/psysonic/main/assets/screenshots/fullscreen.png</image>
</screenshot>
<screenshot>
<caption>Settings — server profiles and appearance</caption>
<image type="source" width="1600" height="900">https://raw.githubusercontent.com/Psychotoxical/psysonic/main/assets/screenshots/settings.png</image>
</screenshot>
</screenshots>
<releases>
<release version="1.43.0" date="2026-04-20">
<url type="details">https://github.com/Psychotoxical/psysonic/releases/tag/app-v1.43.0</url>
<description>
<p>Added user management for Navidrome admins, seekable HTTP streaming,
mini player improvements, floating player bar, NVIDIA DMA-BUF auto-disable,
and smoother lyrics scroll animation.</p>
</description>
</release>
<release version="1.42.1" date="2026-04-19">
<url type="details">https://github.com/Psychotoxical/psysonic/releases/tag/app-v1.42.1</url>
<description>
<p>Critical fix: mini player no longer hangs the app on Windows. Added
optional "Preload mini player" setting on Linux and macOS.</p>
</description>
</release>
</releases>
</component>
@@ -0,0 +1,96 @@
app-id: dev.psysonic.Psysonic
runtime: org.gnome.Platform
runtime-version: '50'
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
- org.freedesktop.Sdk.Extension.node20
command: psysonic
separate-locales: false
finish-args:
# Network: Subsonic server, Last.fm, Discord RPC, LRCLIB, Bandsintown
- --share=network
- --share=ipc
# Display: prefer Wayland, fall back to X11
- --socket=wayland
- --socket=fallback-x11
# On pure-Wayland sessions the X11 socket is absent; tell GDK to try
# Wayland first so GTK doesn't panic when XDG_SESSION_TYPE=wayland.
- --env=GDK_BACKEND=wayland,x11
# Audio: PulseAudio + ALSA, USB DACs (UMC202HD etc.)
- --socket=pulseaudio
- --device=all
# D-Bus: MPRIS, Discord RPC, tray icon
- --socket=session-bus
- --talk-name=org.mpris.MediaPlayer2.*
- --own-name=org.mpris.MediaPlayer2.psysonic
- --talk-name=com.discordapp.*
# Discord RPC unix socket lives in $XDG_RUNTIME_DIR
- --filesystem=xdg-run/discord-ipc-0:ro
- --filesystem=xdg-run/discord-ipc-1:ro
- --filesystem=xdg-run/discord-ipc-2:ro
- --filesystem=xdg-run/discord-ipc-3:ro
- --filesystem=xdg-run/discord-ipc-4:ro
# Tray icon (GNOME needs AppIndicator extension installed)
- --talk-name=org.kde.StatusNotifierWatcher
- --talk-name=org.freedesktop.Notifications
# Local music library (read-only, for future local-file playback)
- --filesystem=xdg-music:ro
build-options:
append-path: /usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/node20/bin
env:
- CARGO_HOME=/run/build/psysonic/cargo
- CARGO_NET_OFFLINE=true
- RUSTUP_TOOLCHAIN=stable
modules:
# libappindicator3 is not in the GNOME 48 runtime; build the Canonical
# version from shared-modules (cloned by the CI job before flatpak-builder
# runs, and manually for local builds per CLAUDE.md).
- shared-modules/libappindicator/libappindicator-gtk3-12.10.json
- name: psysonic
buildsystem: simple
build-options:
env:
# Disables the in-app updater UI — Flathub owns updates for Flatpak installs
- VITE_PSYSONIC_FLATPAK=1
# Prevents tauri-build from checking whether the Vite dev server is running
- TAURI_SKIP_DEVSERVER_CHECK=true
# Last.fm API keys — replaced with real values by the CI job at build time
- VITE_LASTFM_API_KEY=__LASTFM_KEY__
- VITE_LASTFM_API_SECRET=__LASTFM_SECRET__
# npm offline cache — flatpak-node-generator places sources in flatpak-node/npm-cache
- npm_config_cache=/run/build/psysonic/flatpak-node/npm-cache
build-commands:
# Install npm dependencies from pre-generated offline sources
- npm ci --offline
# `tauri build` runs beforeBuildCommand (vite) then compiles the Rust
# binary with the custom-protocol feature enabled (required for the
# embedded asset server). --no-bundle skips deb/rpm/AppImage generation.
- npm run tauri -- build --no-bundle
# Install binary and assets
- install -Dm755 src-tauri/target/release/psysonic "${FLATPAK_DEST}/bin/psysonic"
- install -Dm644 dev.psysonic.Psysonic.desktop "${FLATPAK_DEST}/share/applications/dev.psysonic.Psysonic.desktop"
- install -Dm644 dev.psysonic.Psysonic.metainfo.xml "${FLATPAK_DEST}/share/metainfo/dev.psysonic.Psysonic.metainfo.xml"
- install -Dm644 src-tauri/icons/32x32.png "${FLATPAK_DEST}/share/icons/hicolor/32x32/apps/dev.psysonic.Psysonic.png"
- install -Dm644 src-tauri/icons/128x128.png "${FLATPAK_DEST}/share/icons/hicolor/128x128/apps/dev.psysonic.Psysonic.png"
- install -Dm644 src-tauri/icons/128x128@2x.png "${FLATPAK_DEST}/share/icons/hicolor/256x256/apps/dev.psysonic.Psysonic.png"
- install -Dm644 src-tauri/icons/icon.png "${FLATPAK_DEST}/share/icons/hicolor/512x512/apps/dev.psysonic.Psysonic.png"
sources:
- type: git
url: https://github.com/Psychotoxical/psysonic.git
# Updated automatically by the CI release job; bump manually for local builds
tag: app-v1.43.0
commit: PLACEHOLDER_COMMIT
# Generated by the CI job via flatpak-cargo-generator and flatpak-node-generator.
# Committed into git only for the Flathub mirror repo (separate from this source
# repo); locally, run the commands in CLAUDE.md to create them.
- cargo-sources.json
- npm-sources.json
- type: file
path: dev.psysonic.Psysonic.desktop
- type: file
path: dev.psysonic.Psysonic.metainfo.xml
+3
View File
@@ -94,6 +94,9 @@ function pickAsset(assets: GithubAsset[]): GithubAsset | undefined {
type DlState = 'idle' | 'downloading' | 'done' | 'error';
export default function AppUpdater() {
// Flathub manages updates for Flatpak installs — updater UI must not appear
if (import.meta.env.VITE_PSYSONIC_FLATPAK === '1') return null;
const { t } = useTranslation();
const [release, setRelease] = useState<ReleaseData | null>(null);
const [dismissed, setDismissed] = useState(false);