feat: album download support and GPU compatibility fixes (v1.0.4)

This commit is contained in:
Psychotoxical
2026-03-12 20:04:28 +01:00
parent 04773e83f7
commit 7e0cffc892
8 changed files with 170 additions and 31 deletions
+85 -19
View File
@@ -6,7 +6,42 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
release: create-release:
permissions:
contents: write
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.create-release.outputs.result }}
package_version: ${{ steps.get-version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: get version
id: get-version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: create release
id: create-release
uses: actions/github-script@v7
env:
PACKAGE_VERSION: ${{ steps.get-version.outputs.version }}
with:
script: |
const { data } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `app-v${process.env.PACKAGE_VERSION}`,
name: `Psysonic v${process.env.PACKAGE_VERSION}`,
body: 'See the assets to download this version and install.',
draft: false,
prerelease: false
})
return data.id
build-macos-windows:
needs: create-release
permissions: permissions:
contents: write contents: write
strategy: strategy:
@@ -17,17 +52,37 @@ jobs:
args: '--target aarch64-apple-darwin' args: '--target aarch64-apple-darwin'
- platform: 'macos-latest' - platform: 'macos-latest'
args: '--target x86_64-apple-darwin' args: '--target x86_64-apple-darwin'
- platform: 'ubuntu-22.04'
args: ''
- platform: 'windows-latest' - platform: 'windows-latest'
args: '' args: ''
runs-on: ${{ matrix.settings.platform }} runs-on: ${{ matrix.settings.platform }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: install npm dependencies
run: npm install
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
releaseId: ${{ needs.create-release.outputs.release_id }}
args: ${{ matrix.settings.args }}
- name: install dependencies (ubuntu only) build-linux:
if: matrix.settings.platform == 'ubuntu-22.04' needs: create-release
permissions:
contents: write
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y \ sudo apt-get install -y \
@@ -35,8 +90,7 @@ jobs:
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad gstreamer1.0-libav gstreamer1.0-plugins-bad gstreamer1.0-libav
- name: install linuxdeploy gstreamer plugin (ubuntu only) - name: install linuxdeploy gstreamer plugin
if: matrix.settings.platform == 'ubuntu-22.04'
run: | run: |
wget -q "https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh" \ wget -q "https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh" \
-O /usr/local/bin/linuxdeploy-plugin-gstreamer.sh -O /usr/local/bin/linuxdeploy-plugin-gstreamer.sh
@@ -49,20 +103,32 @@ jobs:
- name: install Rust stable - name: install Rust stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- name: install npm dependencies - name: install npm dependencies
run: npm install run: npm install
- uses: tauri-apps/tauri-action@v0 - name: build
run: npm run tauri:build
env:
APPIMAGE_EXTRACT_AND_RUN: 1
- name: patch AppImage AppRun
run: |
APPIMAGE=$(find src-tauri/target/release/bundle/appimage -name "*.AppImage" -not -name "*.tar.gz" | head -1)
echo "Patching: $APPIMAGE"
APPIMAGE_EXTRACT_AND_RUN=1 ./"$APPIMAGE" --appimage-extract
sed -i '/^exec /i export WEBKIT_DISABLE_COMPOSITING_MODE=1\nexport WEBKIT_DISABLE_DMABUF_RENDERER=1' squashfs-root/AppRun
wget -q "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" \
-O appimagetool
chmod +x appimagetool
APPIMAGE_EXTRACT_AND_RUN=1 ./appimagetool squashfs-root "$APPIMAGE"
rm -rf squashfs-root appimagetool
- name: upload Linux artifacts
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPIMAGE_EXTRACT_AND_RUN: 1 run: |
with: VERSION=${{ needs.create-release.outputs.package_version }}
tagName: app-v__VERSION__ find src-tauri/target/release/bundle \
releaseName: 'Psysonic v__VERSION__' \( -name "*.AppImage" -not -name "*.tar.gz" -o -name "*.deb" -o -name "*.rpm" \) \
releaseBody: 'See the assets to download this version and install.' | xargs gh release upload "app-v${VERSION}" --clobber
releaseDraft: false
prerelease: false
args: ${{ matrix.settings.args }}
+9
View File
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.4] - 2026-03-12
### Added
- **Album Downloads**: Support for downloading entire albums with real-time progress tracking.
### Fixed
- **Linux GPU Compatibility**: Patched AppImage to disable DMABUF renderer, fixing EGL/GPU crashes on older hardware.
- **CI/CD Reliability**: Optimized release workflow with split jobs for better stability across platforms.
## [1.0.3] - 2026-03-12 ## [1.0.3] - 2026-03-12
### Fixed ### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "psysonic", "name": "psysonic",
"version": "1.0.3", "version": "1.0.4",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+1 -1
View File
@@ -2732,7 +2732,7 @@ dependencies = [
[[package]] [[package]]
name = "psysonic" name = "psysonic"
version = "1.0.0" version = "1.0.4"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "psysonic" name = "psysonic"
version = "1.0.3" version = "1.0.4"
description = "Psysonic Desktop Music Player" description = "Psysonic Desktop Music Player"
authors = [] authors = []
license = "" license = ""
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Psysonic", "productName": "Psysonic",
"version": "1.0.3", "version": "1.0.4",
"identifier": "dev.psysonic.app", "identifier": "dev.psysonic.app",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",
+38 -6
View File
@@ -106,7 +106,7 @@ export default function AlbumDetail() {
const [bio, setBio] = useState<string | null>(null); const [bio, setBio] = useState<string | null>(null);
const [bioOpen, setBioOpen] = useState(false); const [bioOpen, setBioOpen] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [downloading, setDownloading] = useState(false); const [downloadProgress, setDownloadProgress] = useState<number | null>(null);
const [isStarred, setIsStarred] = useState(false); const [isStarred, setIsStarred] = useState(false);
const [starredSongs, setStarredSongs] = useState<Set<string>>(new Set()); const [starredSongs, setStarredSongs] = useState<Set<string>>(new Set());
@@ -174,13 +174,33 @@ export default function AlbumDetail() {
}; };
const handleDownload = async (albumName: string, albumId: string) => { const handleDownload = async (albumName: string, albumId: string) => {
setDownloading(true); setDownloadProgress(0);
try { try {
const url = buildDownloadUrl(albumId); const url = buildDownloadUrl(albumId);
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`); if (!response.ok) throw new Error(`HTTP ${response.status}`);
const blob = await response.blob();
const contentLength = response.headers.get('Content-Length');
const total = contentLength ? parseInt(contentLength, 10) : 0;
const chunks: Uint8Array<ArrayBuffer>[] = [];
if (total && response.body) {
const reader = response.body.getReader();
let received = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.length;
setDownloadProgress(Math.round((received / total) * 100));
}
} else {
const buffer = await response.arrayBuffer() as ArrayBuffer;
chunks.push(new Uint8Array(buffer));
setDownloadProgress(100);
}
const blob = new Blob(chunks);
if (auth.downloadFolder) { if (auth.downloadFolder) {
const buffer = await blob.arrayBuffer(); const buffer = await blob.arrayBuffer();
const path = await join(auth.downloadFolder, `${sanitizeFilename(albumName)}.zip`); const path = await join(auth.downloadFolder, `${sanitizeFilename(albumName)}.zip`);
@@ -197,8 +217,10 @@ export default function AlbumDetail() {
} }
} catch (e) { } catch (e) {
console.error('Download failed:', e); console.error('Download failed:', e);
setDownloadProgress(null);
} finally { } finally {
setDownloading(false); // keep bar visible at 100% for 3 seconds so user sees completion
setTimeout(() => setDownloadProgress(null), 60000);
} }
}; };
@@ -320,9 +342,19 @@ export default function AlbumDetail() {
<button className="btn btn-ghost" id="album-bio-btn" onClick={handleBio}> <button className="btn btn-ghost" id="album-bio-btn" onClick={handleBio}>
<ExternalLink size={16} /> {t('albumDetail.artistBio')} <ExternalLink size={16} /> {t('albumDetail.artistBio')}
</button> </button>
<button className="btn btn-ghost" id="album-download-btn" onClick={() => handleDownload(info.name, info.id)} disabled={downloading}> {downloadProgress !== null ? (
<Download size={16} /> {downloading ? t('albumDetail.downloading') : t('albumDetail.download')} <div className="download-progress-wrap">
<Download size={14} />
<div className="download-progress-bar">
<div className="download-progress-fill" style={{ width: `${downloadProgress}%` }} />
</div>
<span className="download-progress-pct">{downloadProgress}%</span>
</div>
) : (
<button className="btn btn-ghost" id="album-download-btn" onClick={() => handleDownload(info.name, info.id)}>
<Download size={16} /> {t('albumDetail.download')}
</button> </button>
)}
</div> </div>
</div> </div>
</div> </div>
+33 -1
View File
@@ -465,7 +465,39 @@
text-decoration: underline; text-decoration: underline;
} }
.album-detail-info { display: flex; gap: var(--space-3); color: var(--text-muted); font-size: 13px; margin: var(--space-2) 0 var(--space-4); } .album-detail-info { display: flex; gap: var(--space-3); color: var(--text-muted); font-size: 13px; margin: var(--space-2) 0 var(--space-4); }
.album-detail-actions { display: flex; gap: var(--space-3); } .album-detail-actions { display: flex; gap: var(--space-3); align-items: center; }
.download-progress-wrap {
display: flex;
align-items: center;
gap: var(--space-2);
padding: 0 var(--space-3);
height: 36px;
border-radius: var(--radius-md);
background: var(--surface-1);
border: 1px solid var(--border);
min-width: 180px;
color: var(--text-secondary);
font-size: 12px;
}
.download-progress-bar {
flex: 1;
height: 4px;
background: var(--surface-2);
border-radius: 2px;
overflow: hidden;
}
.download-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--ctp-mauve), var(--ctp-blue));
border-radius: 2px;
transition: width 0.2s ease;
}
.download-progress-pct {
min-width: 28px;
text-align: right;
font-variant-numeric: tabular-nums;
}
/* ─ Tracklist ─ */ /* ─ Tracklist ─ */
.tracklist { padding: 0 var(--space-6) var(--space-6); } .tracklist { padding: 0 var(--space-6) var(--space-6); }