Compare commits
230 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f45892e975 | |||
| 16c511c167 | |||
| 31f72b0459 | |||
| e060737de9 | |||
| 28b23a9de1 | |||
| 649f5223b4 | |||
| 1cc674e4ee | |||
| 41e98d5783 | |||
| 6ffcd6f6fa | |||
| 33a15fd17a | |||
| 084543e59b | |||
| b0081e3d7a | |||
| ccc9f2cae5 | |||
| f34bd7c0f1 | |||
| 8cb5eb9384 | |||
| 02a4c43f61 | |||
| c49f6af38b | |||
| 69c0c5a907 | |||
| 4de577eede | |||
| c9dfbcc19f | |||
| 36f3d42dbe | |||
| 49f7fe5f6e | |||
| 28943f1ecb | |||
| 46cefb5712 | |||
| 74985fe331 | |||
| 4861d99cb1 | |||
| d2592839b0 | |||
| bbeb2f661e | |||
| 5f0fb5dcbd | |||
| 6f6cb0fd6b | |||
| 15dc970f53 | |||
| fc40d235d0 | |||
| f304589ea1 | |||
| 22f7de45e8 | |||
| 05da369aad | |||
| 9671f89a48 | |||
| 9567a94dea | |||
| 1ba8619a37 | |||
| 973b472c1f | |||
| 20bf93c344 | |||
| fd834314ba | |||
| a78c0fe9ac | |||
| 9c57d4f887 | |||
| 5d067b1f8b | |||
| 48d0145dc8 | |||
| 0da92c2fa1 | |||
| 4e3efa3814 | |||
| 77085a544e | |||
| 8ffecb4b7c | |||
| c569ff5f34 | |||
| d16a99a6f9 | |||
| 9b239892ef | |||
| ba670bd1e8 | |||
| c1e57b4c06 | |||
| fbe68116cc | |||
| dba0c26480 | |||
| 3643a78cd6 | |||
| 1e4b851e9e | |||
|
37799fb861
|
|||
| 099516121e | |||
| 9047a44480 | |||
| 47fcade3b3 | |||
| d832dfb253 | |||
| 041d946b58 | |||
| 64d8b1cbd3 | |||
| 5848c621fd | |||
| 0a0dde057d | |||
| 0119e27f6d | |||
| c7adb599ee | |||
| d6546e12ca | |||
| f0438cae5e | |||
| e64d151079 | |||
| f0b423ddc4 | |||
| 8f3b9ebe00 | |||
| d4b18fec5a | |||
| dbb53bfa70 | |||
| 737b057d4a | |||
| 0c2ac13fed | |||
| 1624880c2a | |||
| 6587c82e0c | |||
| 8a1d942128 | |||
| 829936a78d | |||
| ada7dd010f | |||
| 67de94d955 | |||
| 705c80ef07 | |||
| 504c53e71d | |||
| 5be7f7cabd | |||
| c1624342d3 | |||
| 4182b67732 | |||
| 7b4eeea0dc | |||
| 47cea7e3d6 | |||
| 5ed1b58d67 | |||
| 900853fedc | |||
| 2c3c89f078 | |||
| 45fa606ae1 | |||
| d49137475f | |||
| dd4e2f1162 | |||
| 78beb699a7 | |||
| 6f63d7020c | |||
| a606d1edd6 | |||
| 9ef8566d64 | |||
| 4b8b6ae797 | |||
| 9319c40fde | |||
| 63a3bcd0f4 | |||
| a4c400cc69 | |||
| e4aad22c03 | |||
| 489d50016f | |||
| 47490072ef | |||
| ff0a588014 | |||
| e4041287e2 | |||
| 70f0641439 | |||
| c779e8f587 | |||
| 95468fb137 | |||
| c36b1ea538 | |||
| 3f1b6fd92d | |||
| 1c2aa79e29 | |||
| f08619fb3d | |||
| 5ec8fa8ba3 | |||
| 51c118806e | |||
| fc653941c2 | |||
| 8add62a502 | |||
| 44287a7ceb | |||
| 0f3033d84e | |||
| d49af977ed | |||
| d73f348339 | |||
| 390e6e788d | |||
| ff950efb0c | |||
| 4ef21d6d78 | |||
| 7fc0550b65 | |||
| 5d06738ce1 | |||
| 1197c1f916 | |||
| b448c2bc82 | |||
| 6226383762 | |||
| 13a8f696d0 | |||
| 64e0948904 | |||
| 10b2bde5ef | |||
| 1c92f9ed74 | |||
| 443e0c0675 | |||
| 9cc8cfe087 | |||
| 7ac417fbfc | |||
| 50ac1b8284 | |||
| 29203803de | |||
| 83c36de091 | |||
| 1d0965708b | |||
| 2b5416f423 | |||
| 8cd4cdcd64 | |||
| a32ca64792 | |||
| 939abace35 | |||
| 4f7236e986 | |||
| 9be0d8dfa9 | |||
| 67f31b0700 | |||
| c873880a26 | |||
| 463b7483fd | |||
| 3d11ef91a1 | |||
| c365140870 | |||
| 651b3cb050 | |||
| e2ee9247ad | |||
| 74df7b6b88 | |||
| 27a6693c8c | |||
| a932e7c2db | |||
| 7263d93d42 | |||
| 95283d792b | |||
| 53d5888ebf | |||
| cad4338324 | |||
| f9bc67cb77 | |||
| 74c75d83ca | |||
| 005abae97d | |||
| a9c20dfbdf | |||
| 0b5db172bd | |||
| bf99a64baf | |||
| 523596e414 | |||
| 2fe35e3f9b | |||
| e9fa541933 | |||
| 746aa69405 | |||
| 205b2c1914 | |||
| 0086b3e310 | |||
| 55e7cb835b | |||
| d8da511a8f | |||
| 434ee0ecf1 | |||
| 7d1c66071e | |||
| 1adfda1daa | |||
| e65c476a76 | |||
| 560349819f | |||
| ada5327493 | |||
| c67d606f89 | |||
| 662cc94ca8 | |||
| 1eacaf678c | |||
| 4a8fb64c66 | |||
| 43c656dfc3 | |||
| 74b519f9f5 | |||
| 3d03b8d5a1 | |||
| d6f6e6466c | |||
| 95cdbc7fc7 | |||
| 42863877f6 | |||
| 7ed0fa4914 | |||
| 4f8e7d7bc7 | |||
| bb56269cd1 | |||
| 29a4363dca | |||
| e1d27798eb | |||
| b35539d3cf | |||
| a1b3022140 | |||
| b6fb66c46d | |||
| 936e548f40 | |||
| b67c198227 | |||
| 65a828e3fa | |||
| 6bdd6f3a59 | |||
| d62bffd082 | |||
| ff706104ab | |||
| 0abef4b266 | |||
| 3effad0830 | |||
| 361e9cfdb3 | |||
| 5516d95b52 | |||
| d927ef2082 | |||
| 867c5fbd3e | |||
| e550340565 | |||
| 57b70e6154 | |||
| 0b1ed8cc5a | |||
| c9c68a0e57 | |||
| 9d4997baac | |||
| a99e2e0657 | |||
| 7f85b587b4 | |||
| c8d5e9c028 | |||
| 2ba7845c79 | |||
| 9400a5fb2b | |||
| 0e88e8a5cd | |||
| 7de4b97df0 | |||
| 59115a09d2 | |||
| 18199a1f8a | |||
| d4e44199e9 | |||
| 9b4eb0982c |
@@ -0,0 +1,12 @@
|
||||
# Arch Linux's rust package bakes -fuse-ld=lld into the default rustflags.
|
||||
# ring (pulled in by tauri-plugin-updater) ships C/asm objects that lld cannot
|
||||
# resolve (ring_core_* symbols). Fix: force cc as linker driver and append
|
||||
# -fuse-ld=bfd so it overrides the hardcoded -fuse-ld=lld (last flag wins).
|
||||
# bfd is always available via binutils (part of base-devel on Arch).
|
||||
#
|
||||
# NOTE: When building via makepkg (AUR), RUSTFLAGS from /etc/makepkg.conf
|
||||
# overrides target-specific rustflags here. The PKGBUILD's build() applies
|
||||
# the same fix by patching the RUSTFLAGS env var directly.
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "cc"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=bfd"]
|
||||
@@ -19,23 +19,43 @@ jobs:
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'npm'
|
||||
- name: get version
|
||||
id: get-version
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
- name: extract changelog
|
||||
id: changelog
|
||||
run: |
|
||||
VERSION="${{ steps.get-version.outputs.version }}"
|
||||
# Extract the block between ## [VERSION] and the next ## heading
|
||||
BODY=$(awk "/^## \[$VERSION\]/{found=1; next} found && /^## \[/{exit} found{print}" CHANGELOG.md)
|
||||
# Store multiline output
|
||||
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
|
||||
echo "body<<$EOF" >> $GITHUB_OUTPUT
|
||||
echo "$BODY" >> $GITHUB_OUTPUT
|
||||
echo "$EOF" >> $GITHUB_OUTPUT
|
||||
- name: create release
|
||||
id: create-release
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ steps.get-version.outputs.version }}
|
||||
CHANGELOG_BODY: ${{ steps.changelog.outputs.body }}
|
||||
with:
|
||||
script: |
|
||||
const tag = `app-v${process.env.PACKAGE_VERSION}`;
|
||||
const body = process.env.CHANGELOG_BODY || 'See the assets to download this version and install.';
|
||||
try {
|
||||
const { data } = await github.rest.repos.getReleaseByTag({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag,
|
||||
});
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: data.id,
|
||||
body,
|
||||
});
|
||||
return data.id;
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
@@ -45,8 +65,8 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
tag_name: tag,
|
||||
name: `Psysonic v${process.env.PACKAGE_VERSION}`,
|
||||
body: 'See the assets to download this version and install.',
|
||||
draft: false,
|
||||
body,
|
||||
draft: true,
|
||||
prerelease: false
|
||||
});
|
||||
return data.id;
|
||||
@@ -64,7 +84,7 @@ jobs:
|
||||
- platform: 'macos-latest'
|
||||
args: '--target x86_64-apple-darwin'
|
||||
- platform: 'windows-latest'
|
||||
args: ''
|
||||
args: '--bundles nsis'
|
||||
runs-on: ${{ matrix.settings.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@@ -72,15 +92,22 @@ jobs:
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'npm'
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.settings.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
- name: cache cargo
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
- name: install npm dependencies
|
||||
run: npm install
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VITE_LASTFM_API_KEY: ${{ secrets.VITE_LASTFM_API_KEY }}
|
||||
VITE_LASTFM_API_SECRET: ${{ secrets.VITE_LASTFM_API_SECRET }}
|
||||
with:
|
||||
releaseId: ${{ needs.create-release.outputs.release_id }}
|
||||
args: ${{ matrix.settings.args }}
|
||||
@@ -98,43 +125,30 @@ jobs:
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf \
|
||||
libasound2-dev \
|
||||
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
|
||||
gstreamer1.0-plugins-bad gstreamer1.0-libav
|
||||
|
||||
- name: install linuxdeploy gstreamer plugin
|
||||
run: |
|
||||
wget -q "https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh" \
|
||||
-O /usr/local/bin/linuxdeploy-plugin-gstreamer.sh
|
||||
chmod +x /usr/local/bin/linuxdeploy-plugin-gstreamer.sh
|
||||
libasound2-dev
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: 'npm'
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: cache cargo
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: install npm dependencies
|
||||
run: npm install
|
||||
|
||||
- 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\nexport GDK_BACKEND=x11\nexport EGL_PLATFORM=x11' 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
|
||||
VITE_LASTFM_API_KEY: ${{ secrets.VITE_LASTFM_API_KEY }}
|
||||
VITE_LASTFM_API_SECRET: ${{ secrets.VITE_LASTFM_API_SECRET }}
|
||||
run: npm run tauri:build -- --bundles deb,rpm
|
||||
|
||||
- name: upload Linux artifacts
|
||||
env:
|
||||
@@ -142,5 +156,5 @@ jobs:
|
||||
run: |
|
||||
VERSION=${{ needs.create-release.outputs.package_version }}
|
||||
find src-tauri/target/release/bundle \
|
||||
\( -name "*.AppImage" -not -name "*.tar.gz" -o -name "*.deb" -o -name "*.rpm" \) \
|
||||
\( -name "*.deb" -o -name "*.rpm" \) \
|
||||
| xargs gh release upload "app-v${VERSION}" --clobber
|
||||
|
||||
@@ -7,6 +7,11 @@ yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Environment variables (API keys)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
dist
|
||||
@@ -29,3 +34,12 @@ src-tauri/target/
|
||||
|
||||
# Documentation
|
||||
CLAUDE.md
|
||||
|
||||
# Claude Code memory (local only)
|
||||
memory/
|
||||
|
||||
# Local scratchpad / notes (not committed)
|
||||
tmp/
|
||||
|
||||
# Third-party clones for local research (not committed)
|
||||
research/
|
||||
|
||||
@@ -1,53 +1,159 @@
|
||||
<div align="center">
|
||||
<img src="public/logo.png" alt="Psysonic Logo" width="200"/>
|
||||
<h1>Psysonic</h1>
|
||||
<img src="public/psysonic-inapp-logo.svg" alt="Psysonic Logo" width="300"/>
|
||||
|
||||
<p><strong>A modern, gorgeous, and blazing fast desktop client for Subsonic API compatible music servers (Navidrome, Gonic, etc.).</strong></p>
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/Psychotoxical/psysonic/releases/latest"><img alt="Latest Release" src="https://img.shields.io/github/v/release/Psychotoxical/psysonic?style=flat-square&color=8839ef"></a>
|
||||
<a href="https://github.com/Psychotoxical/psysonic/blob/main/LICENSE"><img alt="License: GPL v3" src="https://img.shields.io/badge/License-GPLv3-cba6f7?style=flat-square"></a>
|
||||
<a href="https://tauri.app/"><img alt="Built with Tauri" src="https://img.shields.io/badge/Built%20with-Tauri-242938?style=flat-square&logo=tauri"></a>
|
||||
<a href="https://aur.archlinux.org/packages/psysonic"><img alt="AUR" src="https://img.shields.io/aur/version/psysonic?style=flat-square&color=1793d1"></a>
|
||||
<a href="https://aur.archlinux.org/packages/psysonic-bin"><img alt="AUR (bin)" src="https://img.shields.io/aur/version/psysonic-bin?style=flat-square&color=1793d1&label=AUR%20(bin)"></a>
|
||||
<a href="https://discord.gg/ckVPGPMS"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20us-5865F2?style=flat-square&logo=discord&logoColor=white"></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
> [!WARNING]
|
||||
> **Psysonic is under heavy active development.** Bugs and rough edges are to be expected. We reserve the right to change, remove, or rework existing features at any time without prior notice.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<a href="https://discord.gg/ckVPGPMS">
|
||||
<img src="https://img.shields.io/badge/Join%20the%20Psysonic%20Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white" alt="Join Discord"/>
|
||||
</a>
|
||||
<p>Have questions, ideas, or just want to hang out? Come chat in our Discord server!</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Psysonic is a beautiful desktop music player built completely from the ground up for the modern era. Utilizing **Tauri v2** and **React**, it offers a native-feeling, lightweight, and incredibly fast experience with a stunning UI inspired by the [Catppuccin](https://github.com/catppuccin/catppuccin) aesthetic.
|
||||
Psysonic is a beautiful desktop music player built completely from the ground up for the modern era. Utilizing **Tauri v2** and **React**, it offers a native-feeling, lightweight, and incredibly fast experience with a stunning UI inspired by the [Catppuccin](https://github.com/catppuccin/catppuccin) and [Nord](https://www.nordtheme.com/) aesthetics.
|
||||
|
||||
Designed specifically for users hosting their own music via Navidrome or other Subsonic API servers, Psysonic aims to be the best way to interact with your personal library.
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🎨 **Gorgeous UI**: 8 deeply integrated themes (Catppuccin series + Nord series) with smooth glassmorphism effects and micro-animations.
|
||||
- 🎨 **Gorgeous UI**: 67 beautiful themes across 8 groups — Open Source Classics (Catppuccin, Nord, Gruvbox, Nightfox, Dracula), Operating Systems, Games, Movies, Series, Social Media, Psysonic originals, and Mediaplayer — with smooth glassmorphism effects and micro-animations. A time-based **Theme Scheduler** can automatically switch between a day and night theme.
|
||||
- ⚡ **Blazing Fast**: Built with Rust & Tauri — native audio engine (rodio), minimal RAM usage compared to typical Electron apps.
|
||||
- 🌍 **Internationalization (i18n)**: Fully translated into English and German.
|
||||
- 🌍 **Internationalization (i18n)**: Fully translated into English, German, French, Dutch, Chinese, Norwegian, and Russian.
|
||||
- 📻 **Live "Now Playing"**: See what other users on your server are currently listening to in real-time.
|
||||
- 🎵 **Last.fm Scrobbling**: Full integration for scrobbling your tracks via the Navidrome server.
|
||||
- 🎵 **Last.fm Integration**: Direct scrobbling, Now Playing updates, love/unlove, Similar Artists, and top stats — no Navidrome configuration required.
|
||||
- 🎤 **Synchronized Lyrics**: Lyrics pane in the sidebar and fullscreen player — powered by LRCLIB and your Navidrome server. Synced lyrics auto-scroll with line highlighting and click-to-seek; plain-text fallback for unsynced tracks.
|
||||
- 📻 **Smart Radio**: Start a Radio session from any song or artist. Playback begins instantly from top local tracks while similar artist tracks (via Last.fm) load in the background. Radio queues reload proactively so sessions never run dry.
|
||||
- ♾️ **Infinite Queue**: When the queue runs out with Repeat off, Psysonic silently appends more random tracks (optionally filtered by genre) so playback never stops. Auto-added tracks appear below a clear `— Auto —` divider.
|
||||
- 🎛️ **10-Band Graphic EQ**: Built-in EQ with presets and the ability to save custom presets. **AutoEQ** support lets you load headphone correction profiles automatically.
|
||||
- 🔀 **Gapless & Crossfade**: True gapless playback and configurable crossfade between tracks (mutually exclusive).
|
||||
- 📻 **Internet Radio**: Built-in internet radio player — browse and play any ICY/HLS stream directly within Psysonic.
|
||||
- ⭐ **Ratings**: Rate songs, albums, and artists with 1–5 stars via the context menu, player bar, or album detail view. Supports the OpenSubsonic ratings extension. Auto-rate-down songs you skip repeatedly (configurable threshold). Filter Random Mix and Random Albums by minimum star rating.
|
||||
- 🖥️ **Fullscreen Player**: A dedicated fullscreen view with album art, animated lyrics overlay, and artist image — toggled with a single click.
|
||||
- 📋 **Playlist Management**: Create, edit, rename, and delete playlists. Drag-and-drop track reordering, song search, and smart suggestions right inside the playlist view.
|
||||
- 💾 **IndexedDB Caching**: Ultra-fast loading times with persistent IndexedDB image caching for cover art and artist images.
|
||||
- 📀 **Album Downloads**: Support for downloading entire albums directly to your local machine.
|
||||
- 💿 **Album & Artist Views**: Beautiful grid displays and detailed artist pages with related albums and color-coded initial avatars for fast browsing.
|
||||
- 〰️ **Waveform Seekbar**: Canvas-based waveform with a blue-to-mauve gradient and glow effect — click or drag anywhere to seek.
|
||||
- 🌊 **MilkDrop Visualizer**: Full-screen Butterchurn/MilkDrop visualizer in the Ambient Stage with hundreds of presets and smooth transitions.
|
||||
- 💿 **Album & Artist Views**: Beautiful grid displays, multi-select album actions, and detailed artist pages with related albums.
|
||||
- 〰️ **Multi-Style Seekbar**: 10 canvas-drawn seekbar styles — Waveform, Bar, Thick Bar, Segmented, Line+Dot, Neon, Pulse Wave, Particle Trail, Liquid Fill, and Retro Tape.
|
||||
- 🎛️ **Queue Management**: Drag & drop reordering, shuffle, playlist saving/loading, and server-side queue synchronization.
|
||||
- ⌨️ **Configurable Keybindings**: Rebind any playback action (play/pause, next, seek, volume…) directly in Settings.
|
||||
- 🔤 **Font Picker & UI Scale**: 10 UI fonts and a global zoom slider (80–150%) to match your display and taste.
|
||||
- 🎼 **Random Mix**: Generate a random playlist from your entire library. Filter by keyword or pick a Super Genre (Metal, Rock, Electronic, Jazz…) for a focused mix with progressive loading.
|
||||
- 🔗 **In-App Browser**: Last.fm and Wikipedia links on artist pages open in a dedicated native window — no need to leave the app.
|
||||
- 🔄 **Update Notifications**: Built-in update checker (on startup + every 10 minutes) that notifies you when a new version is available on GitHub.
|
||||
- 🖥️ **Cross-Platform**: Available natively for Windows, macOS, and Linux (including Wayland support).
|
||||
- 🏷️ **Genres**: Browse your entire library by genre — coloured cards sorted by album count with a dedicated album view per genre. Multi-select genre filter available on Albums, New Releases, and Random Albums pages.
|
||||
- 🔔 **System Tray**: Minimize Psysonic to the system tray. Play/Pause, Prev, Next, and Show/Hide controls available from the tray icon.
|
||||
- 💾 **Backup & Restore**: Export and import all your settings, themes, and server profiles in one click.
|
||||
- 🔄 **In-App Auto-Update**: Checks for new releases on startup. macOS and Windows can install and relaunch directly in-app; Linux users get a link to the GitHub release page.
|
||||
- 🖥️ **Cross-Platform**: Available natively for Windows, macOS, and Linux (Arch AUR, .deb, .rpm).
|
||||
|
||||
## ● Known Limitations
|
||||
## 🗺️ Roadmap
|
||||
|
||||
- **Linux (drag & drop cursor feedback)**: Due to a WebKitGTK limitation, the drag cursor does not reflect the drop operation type — it may appear as a "forbidden" symbol or show no indicator at all, depending on the desktop environment. Drag and drop itself works correctly.
|
||||
### ✅ Completed
|
||||
- [x] Native Rust/rodio audio engine (replaces Howler.js)
|
||||
- [x] 10-band graphic EQ with built-in and custom presets
|
||||
- [x] AutoEQ — automatic headphone correction profile loader
|
||||
- [x] Crossfade between tracks
|
||||
- [x] Replay Gain (track + album mode)
|
||||
- [x] Gapless playback
|
||||
- [x] Multi-style seekbar (10 styles: Waveform, Bar, Thick, Segmented, Line+Dot, Neon, Pulse Wave, Particle Trail, Liquid Fill, Retro Tape)
|
||||
- [x] Last.fm scrobbling, Now Playing & love/unlove
|
||||
- [x] Similar Artists via Last.fm, filtered to library
|
||||
- [x] Statistics — Last.fm top charts, recent scrobbles, top-rated songs & artists
|
||||
- [x] Synchronized lyrics via LRCLIB and Navidrome server (in-sidebar + fullscreen, auto-scroll, click-to-seek)
|
||||
- [x] Smart Radio with proactive queue loading
|
||||
- [x] Infinite Queue (random auto-fill when queue runs out)
|
||||
- [x] OGG/Vorbis native playback
|
||||
- [x] Internet Radio (ICY/HLS streams)
|
||||
- [x] In-app auto-updater (macOS + Windows)
|
||||
- [x] Multi-server support
|
||||
- [x] IndexedDB image caching
|
||||
- [x] Random Mix with server-native Genre Mix (top genres by song count, shuffleable)
|
||||
- [x] Advanced Search (text + genre + year + result-type filters)
|
||||
- [x] 67 themes across 8 groups: Open Source Classics, Operating Systems, Games, Movies, Series, Social Media, Psysonic originals, Mediaplayer
|
||||
- [x] Time-based Theme Scheduler (auto day/night theme switching)
|
||||
- [x] Internationalization (English, German, French, Dutch, Chinese, Norwegian, Russian)
|
||||
- [x] AUR package (Arch / CachyOS)
|
||||
- [x] Configurable keybindings
|
||||
- [x] Font picker (10 UI fonts) + global UI scale slider
|
||||
- [x] Playlist management (create, edit, delete, drag-and-drop reorder, suggestions)
|
||||
- [x] Fullscreen player with synced lyrics overlay
|
||||
- [x] System tray icon with playback controls
|
||||
- [x] Song / Album / Artist ratings (1–5 stars, OpenSubsonic extension)
|
||||
- [x] Auto-rate-down on repeated skips + minimum-rating filter for mixes
|
||||
- [x] Album multi-select actions
|
||||
- [x] Custom Linux titlebar
|
||||
- [x] Backup & Restore (settings export/import)
|
||||
|
||||
### 📋 Planned
|
||||
- [ ] Theme contrast & legibility audit — systematic review of text/background contrast ratios across all 67 themes
|
||||
- [ ] Accessibility (a11y) — keyboard navigation, screen reader support, ARIA labels
|
||||
- [ ] More languages
|
||||
|
||||
---
|
||||
|
||||
## 📥 Installation
|
||||
|
||||
Navigate to the [Releases](https://github.com/Psychotoxical/psysonic/releases) page and download the installer for your operating system.
|
||||
|
||||
- **Windows**: `.exe` or `.msi`
|
||||
### 🐧 Linux
|
||||
|
||||
**Quick Install (Recommended):**
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Psychotoxical/psysonic/main/scripts/install.sh | sudo bash
|
||||
```
|
||||
|
||||
**Manual Installation:**
|
||||
- **Ubuntu / Debian**: `.deb` from GitHub Releases
|
||||
- **Fedora / RHEL**: `.rpm` from GitHub Releases
|
||||
|
||||
### 🍎 macOS
|
||||
|
||||
- **macOS**: `.dmg` (Universal or Apple Silicon)
|
||||
- **Linux**: `.AppImage` or `.deb`
|
||||
|
||||
> [!WARNING]
|
||||
> **Gatekeeper Note:**
|
||||
> Since the app is released without an Apple Developer certificate, macOS will block it by default. To bypass this, run the following command in the Terminal after moving the app to the Applications folder:
|
||||
> ```sh
|
||||
> xattr -cr /Applications/Psysonic.app
|
||||
> ```
|
||||
|
||||
### 🪟 Windows
|
||||
|
||||
- **Windows**: `.exe` (NSIS installer)
|
||||
|
||||
> [!WARNING]
|
||||
> **SmartScreen Note:**
|
||||
> Windows SmartScreen might show a warning because the installer isn't signed with an expensive developer certificate. Click on **"More info"** and then **"Run anyway"**.
|
||||
|
||||
## 📦 Installation (Arch Linux / AUR)
|
||||
|
||||
Psysonic is available in the **AUR** in two versions. Choose the one that best fits your needs:
|
||||
|
||||
| Package | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| [**psysonic**](https://aur.archlinux.org/packages/psysonic) | **Source** | Builds from source using your system's native **WebKitGTK** (no bundled libs, no EGL/Mesa compatibility issues). |
|
||||
| [**psysonic-bin**](https://aur.archlinux.org/packages/psysonic-bin) | **Binary** | Pre-compiled version for faster installation. |
|
||||
|
||||
> [!TIP]
|
||||
> The AUR binary package is kindly provided and maintained by [**kilyabin**](https://github.com/kilyabin).
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Psysonic – Dein Navidrome Desktop Player" />
|
||||
<title>Psysonic</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
{
|
||||
"name": "psysonic",
|
||||
"version": "1.3.0",
|
||||
"version": "1.34.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "psysonic",
|
||||
"version": "1.3.0",
|
||||
"version": "1.34.8",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/dm-sans": "^5.2.8",
|
||||
"@fontsource-variable/figtree": "^5.2.10",
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@fontsource-variable/lexend": "^5.2.11",
|
||||
"@fontsource-variable/manrope": "^5.2.8",
|
||||
"@fontsource-variable/nunito": "^5.2.7",
|
||||
"@fontsource-variable/outfit": "^5.2.8",
|
||||
"@fontsource-variable/plus-jakarta-sans": "^5.2.8",
|
||||
"@fontsource-variable/rubik": "^5.2.8",
|
||||
"@fontsource-variable/space-grotesk": "^5.2.10",
|
||||
"@tanstack/react-virtual": "^3.13.23",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2",
|
||||
"@tauri-apps/plugin-notification": "^2",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "^2",
|
||||
"@tauri-apps/plugin-store": "^2",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@tauri-apps/plugin-window-state": "^2.4.1",
|
||||
"axios": "^1.7.7",
|
||||
"butterchurn": "^2.6.7",
|
||||
"butterchurn-presets": "^2.4.7",
|
||||
"i18next": "^25.8.16",
|
||||
"lucide-react": "^0.462.0",
|
||||
"md5": "^2.3.0",
|
||||
@@ -35,7 +46,8 @@
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^6.0.3"
|
||||
"vite": "^6.0.3",
|
||||
"vitest": "^4.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -771,6 +783,96 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/dm-sans": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/dm-sans/-/dm-sans-5.2.8.tgz",
|
||||
"integrity": "sha512-AxkvMTvNWgfrmlyjiV05vlHYJa+nRQCf1EfvIrQAPBpFJW0O9VTz7oAFr9S3lvbWdmnFoBk7yFqQL86u64nl2g==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/figtree": {
|
||||
"version": "5.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/figtree/-/figtree-5.2.10.tgz",
|
||||
"integrity": "sha512-a5Gumbpy3mdd+Yg31g6Qb7CmjYbrfyutJa3bWfP5q8A4GclIOwX7mI+ZuSHsJnw/mHvW6r9oh1AHJcJTIxK4JA==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/inter": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.2.8.tgz",
|
||||
"integrity": "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/lexend": {
|
||||
"version": "5.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/lexend/-/lexend-5.2.11.tgz",
|
||||
"integrity": "sha512-0hgEQ4O7Nh8fxL/WWmspJf0BErbocRkZwtLRGey/V4mUUqxfF7QUwqhcdzwpjom3NYCniY4uzQ5wYD7r9/92tQ==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/manrope": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/manrope/-/manrope-5.2.8.tgz",
|
||||
"integrity": "sha512-nc9lOuCRz73UHnovDE2bwXUdghE2SEOc7Aii0qGe3CLyE03W1a7VnY5Z6euRiapiKbCkGS+eXbY3s/kvWeGeSw==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/nunito": {
|
||||
"version": "5.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/nunito/-/nunito-5.2.7.tgz",
|
||||
"integrity": "sha512-2N8QhatkyKgSUbAGZO2FYLioxA32+RyI1EplVLawbpkGjUeui9Qg9VMrpkCaik1ydjFjfLV+kzQ0cGEsMrMenQ==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/outfit": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/outfit/-/outfit-5.2.8.tgz",
|
||||
"integrity": "sha512-4oUDCZx/Tcz6HZP423w/niqEH31Gks5IsqHV2ZZz1qKHaVIZdj2f0/S1IK2n8jl6Xo0o3N+3RjNHlV9R73ozQA==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/plus-jakarta-sans": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/plus-jakarta-sans/-/plus-jakarta-sans-5.2.8.tgz",
|
||||
"integrity": "sha512-iQecBizIdZxezODNHzOn4SvvRMrZL/S8k4MEXGDynCmUrImVW0VmX+tIAMqnADwH4haXlHSXqMgU6+kcfBQJdw==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/rubik": {
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/rubik/-/rubik-5.2.8.tgz",
|
||||
"integrity": "sha512-vGDExLzB4a2Fj9mca5LqNoA2ZKcU9o+x5FEBLte/nxYkCB9hOQwZS6ZlItUv+Ssn7YMzKauGuI/Po+YueFuZbg==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/space-grotesk": {
|
||||
"version": "5.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/space-grotesk/-/space-grotesk-5.2.10.tgz",
|
||||
"integrity": "sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
@@ -1226,6 +1328,40 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.23",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz",
|
||||
"integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.23"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.23",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz",
|
||||
"integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
|
||||
@@ -1495,10 +1631,10 @@
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-notification": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz",
|
||||
"integrity": "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==",
|
||||
"node_modules/@tauri-apps/plugin-process": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz",
|
||||
"integrity": "sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
@@ -1522,6 +1658,24 @@
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-updater": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-updater/-/plugin-updater-2.10.0.tgz",
|
||||
"integrity": "sha512-ljN8jPlnT0aSn8ecYhuBib84alxfMx6Hc8vJSKMJyzGbTPFZAC44T2I1QNFZssgWKrAlofvJqCC6Rr472JWfkQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-window-state": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-window-state/-/plugin-window-state-2.4.1.tgz",
|
||||
"integrity": "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -1567,6 +1721,24 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/deep-eql": "*",
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -1640,6 +1812,129 @@
|
||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.3.tgz",
|
||||
"integrity": "sha512-CW8Q9KMtXDGHj0vCsqui0M5KqRsu0zm0GNDW7Gd3U7nZ2RFpPKSCpeCXoT+/+5zr1TNlsoQRDEz+LzZUyq6gnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"@types/chai": "^5.2.2",
|
||||
"@vitest/spy": "4.1.3",
|
||||
"@vitest/utils": "4.1.3",
|
||||
"chai": "^6.2.2",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.3.tgz",
|
||||
"integrity": "sha512-XN3TrycitDQSzGRnec/YWgoofkYRhouyVQj4YNsJ5r/STCUFqMrP4+oxEv3e7ZbLi4og5kIHrZwekDJgw6hcjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "4.1.3",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.21"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"msw": "^2.4.9",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"msw": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.3.tgz",
|
||||
"integrity": "sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.3.tgz",
|
||||
"integrity": "sha512-VwgOz5MmT0KhlUj40h02LWDpUBVpflZ/b7xZFA25F29AJzIrE+SMuwzFf0b7t4EXdwRNX61C3B6auIXQTR3ttA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.1.3",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.3.tgz",
|
||||
"integrity": "sha512-9l+k/J9KG5wPJDX9BcFFzhhwNjwkRb8RsnYhaT1vPY7OufxmQFc9sZzScRCPTiETzl37mrIWVY9zxzmdVeJwDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.3",
|
||||
"@vitest/utils": "4.1.3",
|
||||
"magic-string": "^0.30.21",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.3.tgz",
|
||||
"integrity": "sha512-ujj5Uwxagg4XUIfAUyRQxAg631BP6e9joRiN99mr48Bg9fRs+5mdUElhOoZ6rP5mBr8Bs3lmrREnkrQWkrsTCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.3.tgz",
|
||||
"integrity": "sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "4.1.3",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/assertion-error": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -1647,24 +1942,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz",
|
||||
"integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-runtime": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||
"integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "^2.4.0",
|
||||
"regenerator-runtime": "^0.11.0"
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
@@ -1714,27 +1999,6 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/butterchurn": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/butterchurn/-/butterchurn-2.6.7.tgz",
|
||||
"integrity": "sha512-BJiRA8L0L2+84uoG2SSfkp0kclBuN+vQKf217pK7pMlwEO2ZEg3MtO2/o+l8Qpr8Nbejg8tmL1ZHD1jmhiaaqg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"ecma-proposal-math-extensions": "0.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/butterchurn-presets": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/butterchurn-presets/-/butterchurn-presets-2.4.7.tgz",
|
||||
"integrity": "sha512-4MdM8ripz/VfH1BCldrIKdAc/1ryJFBDvqlyow6Ivo1frwj0H3duzvSMFC7/wIjAjxb1QpwVHVqGqS9uAFKhpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.26.0",
|
||||
"ecma-proposal-math-extensions": "0.0.2",
|
||||
"lodash": "^4.17.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -1769,6 +2033,16 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
||||
"integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/charenc": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||
@@ -1797,14 +2071,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
|
||||
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
|
||||
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/crypt": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
@@ -1862,12 +2128,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ecma-proposal-math-extensions": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ecma-proposal-math-extensions/-/ecma-proposal-math-extensions-0.0.2.tgz",
|
||||
"integrity": "sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.307",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz",
|
||||
@@ -1893,6 +2153,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
|
||||
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
@@ -1972,6 +2239,26 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -2226,12 +2513,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -2263,6 +2544,16 @@
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -2337,6 +2628,24 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -2345,9 +2654,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2387,10 +2696,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
@@ -2486,12 +2798,6 @@
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.59.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||
@@ -2556,6 +2862,13 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/siginfo": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -2566,6 +2879,37 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stackback": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
|
||||
"integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz",
|
||||
"integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@@ -2583,6 +2927,16 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||
"integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@@ -2645,9 +2999,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
|
||||
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2719,6 +3073,96 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.3.tgz",
|
||||
"integrity": "sha512-DBc4Tx0MPNsqb9isoyOq00lHftVx/KIU44QOm2q59npZyLUkENn8TMFsuzuO+4U2FUa9rgbbPt3udrP25GcjXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.1.3",
|
||||
"@vitest/mocker": "4.1.3",
|
||||
"@vitest/pretty-format": "4.1.3",
|
||||
"@vitest/runner": "4.1.3",
|
||||
"@vitest/snapshot": "4.1.3",
|
||||
"@vitest/spy": "4.1.3",
|
||||
"@vitest/utils": "4.1.3",
|
||||
"es-module-lexer": "^2.0.0",
|
||||
"expect-type": "^1.3.0",
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.1",
|
||||
"pathe": "^2.0.3",
|
||||
"picomatch": "^4.0.3",
|
||||
"std-env": "^4.0.0-rc.1",
|
||||
"tinybench": "^2.9.0",
|
||||
"tinyexec": "^1.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.1.0",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"vitest": "vitest.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
|
||||
"@vitest/browser-playwright": "4.1.3",
|
||||
"@vitest/browser-preview": "4.1.3",
|
||||
"@vitest/browser-webdriverio": "4.1.3",
|
||||
"@vitest/coverage-istanbul": "4.1.3",
|
||||
"@vitest/coverage-v8": "4.1.3",
|
||||
"@vitest/ui": "4.1.3",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*",
|
||||
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@edge-runtime/vm": {
|
||||
"optional": true
|
||||
},
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-preview": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-webdriverio": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-istanbul": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/coverage-v8": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/ui": {
|
||||
"optional": true
|
||||
},
|
||||
"happy-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"jsdom": {
|
||||
"optional": true
|
||||
},
|
||||
"vite": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
@@ -2728,6 +3172,23 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"siginfo": "^2.0.0",
|
||||
"stackback": "0.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"why-is-node-running": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "psysonic",
|
||||
"version": "1.4.3",
|
||||
"version": "1.34.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -8,19 +8,31 @@
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"tauri:dev": "GDK_BACKEND=x11 WEBKIT_DISABLE_COMPOSITING_MODE=1 tauri dev",
|
||||
"tauri:build": "tauri build"
|
||||
"tauri:build": "tauri build",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/dm-sans": "^5.2.8",
|
||||
"@fontsource-variable/figtree": "^5.2.10",
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@fontsource-variable/lexend": "^5.2.11",
|
||||
"@fontsource-variable/manrope": "^5.2.8",
|
||||
"@fontsource-variable/nunito": "^5.2.7",
|
||||
"@fontsource-variable/outfit": "^5.2.8",
|
||||
"@fontsource-variable/plus-jakarta-sans": "^5.2.8",
|
||||
"@fontsource-variable/rubik": "^5.2.8",
|
||||
"@fontsource-variable/space-grotesk": "^5.2.10",
|
||||
"@tanstack/react-virtual": "^3.13.23",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "^2.6.0",
|
||||
"@tauri-apps/plugin-fs": "^2.4.5",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2",
|
||||
"@tauri-apps/plugin-notification": "^2",
|
||||
"@tauri-apps/plugin-process": "^2.3.1",
|
||||
"@tauri-apps/plugin-shell": "^2",
|
||||
"@tauri-apps/plugin-store": "^2",
|
||||
"@tauri-apps/plugin-updater": "^2.10.0",
|
||||
"@tauri-apps/plugin-window-state": "^2.4.1",
|
||||
"axios": "^1.7.7",
|
||||
"butterchurn": "^2.6.7",
|
||||
"butterchurn-presets": "^2.4.7",
|
||||
"i18next": "^25.8.16",
|
||||
"lucide-react": "^0.462.0",
|
||||
"md5": "^2.3.0",
|
||||
@@ -38,6 +50,7 @@
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^6.0.3"
|
||||
"vite": "^6.0.3",
|
||||
"vitest": "^4.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
src/
|
||||
pkg/
|
||||
*.tar.zst
|
||||
*.tar.gz
|
||||
@@ -0,0 +1,80 @@
|
||||
# Maintainer: Psychotoxic <psychotoxic@gmx.de>
|
||||
pkgname=psysonic
|
||||
pkgver=1.34.8
|
||||
pkgrel=1
|
||||
pkgdesc="Desktop music player for Subsonic API-compatible servers (Navidrome, Gonic, etc.)"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/Psychotoxical/psysonic"
|
||||
license=('GPL-3.0-only')
|
||||
depends=(
|
||||
'webkit2gtk-4.1'
|
||||
'gtk3'
|
||||
'openssl'
|
||||
'alsa-lib'
|
||||
)
|
||||
makedepends=(
|
||||
'npm'
|
||||
'rust'
|
||||
'cargo'
|
||||
'clang'
|
||||
'nasm'
|
||||
)
|
||||
source=("$pkgname-$pkgver.tar.gz::https://github.com/Psychotoxical/psysonic/archive/refs/tags/v$pkgver.tar.gz")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
build() {
|
||||
cd "psysonic-$pkgver"
|
||||
|
||||
export CARGO_HOME="$srcdir/cargo-home"
|
||||
export npm_config_cache="$srcdir/npm-cache"
|
||||
|
||||
# ring (used by reqwest → rustls-tls) ships C/asm objects whose
|
||||
# symbols (ring_core_*) lld cannot resolve. On Arch/CachyOS, -fuse-ld=lld
|
||||
# is hardcoded into rustc itself (not just makepkg.conf RUSTFLAGS), so a
|
||||
# string substitution is a no-op. Appending -C link-arg=-fuse-ld=bfd works
|
||||
# because the last -fuse-ld=* flag passed to cc wins.
|
||||
export RUSTFLAGS="${RUSTFLAGS} -C link-arg=-fuse-ld=bfd"
|
||||
|
||||
# CachyOS sets -flto=auto in CFLAGS. ring compiles its C/asm objects via the
|
||||
# cc crate and picks up CFLAGS, producing fat-LTO objects. bfd cannot resolve
|
||||
# symbols from fat-LTO objects when linking against non-LTO Rust rlibs, causing
|
||||
# "undefined reference to ring_core_*" even though the symbols exist in the .a.
|
||||
# Strip CFLAGS/CXXFLAGS entirely so ring builds plain ELF objects.
|
||||
unset CFLAGS CXXFLAGS
|
||||
|
||||
npm install
|
||||
npm run tauri:build -- --no-bundle
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "psysonic-$pkgver"
|
||||
|
||||
# Binary (in /usr/lib to make room for the wrapper)
|
||||
install -Dm755 "src-tauri/target/release/psysonic" "$pkgdir/usr/lib/psysonic/psysonic"
|
||||
|
||||
# Wrapper script that sets necessary env vars for WebKitGTK on Wayland
|
||||
install -Dm755 /dev/stdin "$pkgdir/usr/bin/psysonic" <<EOF
|
||||
#!/bin/sh
|
||||
export GDK_BACKEND=x11
|
||||
export WEBKIT_DISABLE_COMPOSITING_MODE=1
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
exec /usr/lib/psysonic/psysonic "\$@"
|
||||
EOF
|
||||
|
||||
# Desktop entry
|
||||
install -Dm644 /dev/stdin "$pkgdir/usr/share/applications/psysonic.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=Psysonic
|
||||
Comment=Desktop music player for Subsonic API-compatible servers
|
||||
Exec=psysonic
|
||||
Icon=psysonic
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=AudioVideo;Audio;Music;Player;
|
||||
EOF
|
||||
|
||||
# Icons
|
||||
install -Dm644 "src-tauri/icons/32x32.png" "$pkgdir/usr/share/icons/hicolor/32x32/apps/psysonic.png"
|
||||
install -Dm644 "src-tauri/icons/128x128.png" "$pkgdir/usr/share/icons/hicolor/128x128/apps/psysonic.png"
|
||||
install -Dm644 "src-tauri/icons/128x128@2x.png" "$pkgdir/usr/share/icons/hicolor/256x256/apps/psysonic.png"
|
||||
}
|
||||
|
After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="115.549mm"
|
||||
height="130.30972mm"
|
||||
viewBox="0 0 115.549 130.30972"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
inkscape:export-filename="p-small.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"><inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="115.549"
|
||||
height="130.30972"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" /></sodipodi:namedview><defs
|
||||
id="defs1" /><g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(220.53237,27.789086)"><path
|
||||
style="fill:#ffffff"
|
||||
d="m -191.83501,87.581279 v -14.93937 l 1.01946,-0.029 c 1.8496,-0.0526 5.09881,-2.007 6.98453,-4.20123 2.13731,-2.48697 3.28384,-4.43657 4.52545,-7.69521 0.51751,-1.35819 1.078,-2.78694 1.24554,-3.175 0.16755,-0.38805 0.88173,-2.7693 1.58707,-5.29166 0.70533,-2.52236 1.41605,-4.90361 1.57937,-5.29167 0.16441,-0.39067 0.30759,11.85061 0.32081,27.42847 l 0.0239,28.134031 h -8.64306 -8.64305 z m -3.42317,-19.65031 c -0.81559,-0.16111 -1.84746,-0.48272 -2.29306,-0.71468 -1.09242,-0.5687 -2.72853,-2.16884 -2.74064,-2.68038 -0.005,-0.22765 -0.38465,-0.86265 -0.84281,-1.41111 -0.8626,-1.03264 -2.38323,-4.66133 -4.63113,-11.05137 -1.72997,-4.91772 -1.63358,-4.68451 -3.35352,-8.11389 -0.82714,-1.64924 -1.91998,-3.45186 -2.42853,-4.00582 -1.28805,-1.40307 -4.41406,-2.7715 -6.89485,-3.01827 l -2.08965,-0.20785 1.43221,-0.99035 c 1.5468,-1.06957 5.31147,-2.35399 6.9124,-2.35835 1.72563,-0.005 4.25283,0.7809 5.71247,1.77575 1.63175,1.11217 3.92377,3.83335 3.77488,4.48172 -0.0559,0.24344 0.11427,0.44261 0.37817,0.44261 0.53171,0 3.78445,6.24176 3.78445,7.26208 0,0.15195 0.30609,0.92171 0.6802,1.71057 0.37412,0.78887 1.08633,2.44854 1.5827,3.68817 1.00279,2.50434 2.57055,5.33152 2.95544,5.32962 0.85183,-0.004 3.83204,-7.97894 5.40479,-14.46266 1.9193,-7.91232 5.01161,-18.44694 6.10967,-20.81389 2.30114,-4.96024 4.60601,-7.03734 8.12223,-7.31959 1.95377,-0.15683 2.44243,-0.0601 4.01261,0.79453 2.49546,1.35819 3.31044,2.35029 5.40102,6.57479 0.93741,1.89425 3.29625,9.1126 4.36446,13.35583 0.51289,2.03729 1.21262,4.57729 1.55498,5.64444 0.34236,1.06716 0.83543,2.65466 1.09573,3.52778 0.96371,3.23267 3.75139,8.2344 5.51689,9.89856 2.09506,1.9748 4.10606,3.2977 5.85136,3.84922 0.72761,0.22993 1.32292,0.49404 1.32292,0.58692 0,0.0929 -0.71641,0.48577 -1.59202,0.87309 -2.29705,1.01609 -6.48839,1.02714 -8.75823,0.0231 -3.42674,-1.51581 -6.17101,-4.45149 -8.36088,-8.94406 -0.59782,-1.22642 -1.23412,-2.50231 -1.41401,-2.8353 -0.17988,-0.333 -0.47718,-1.20612 -0.66066,-1.94028 -0.74987,-3.00045 -6.42415,-19.25706 -6.99617,-20.04376 -0.79895,-1.09881 -0.87818,-1.08476 -1.55823,0.27628 -1.1693,2.3402 -2.07427,5.18987 -3.61302,11.37709 -3.03871,12.21839 -6.36478,22.38234 -8.0081,24.47148 -0.36655,0.466 -0.66646,0.99153 -0.66646,1.16785 0,0.86017 -2.61454,3.05174 -4.28395,3.59089 -1.94625,0.62857 -2.53141,0.65417 -4.78366,0.20926 z m 49.82815,-13.29265 c -2.77991,-0.70614 -6.29714,-6.05076 -8.15323,-12.38927 -0.30389,-1.03778 -0.47868,-1.96073 -0.38841,-2.051 0.0903,-0.0903 1.5695,-0.22877 3.28719,-0.30779 8.47079,-0.38969 9.78292,-0.63406 14.05919,-2.61837 3.78653,-1.75706 9.09259,-6.79386 10.56941,-10.03304 3.78708,-8.30644 4.33485,-14.20262 2.08448,-22.4376404 -1.15336,-4.22063002 -3.6401,-8.21361 -6.73205,-10.80969 -1.12271,-0.94265 -2.12066,-1.8146 -2.21767,-1.93765 -0.3794,-0.48123 -4.30858,-2.4333296 -6.41876,-3.1889796 -2.16778,-0.77628 -2.64336,-0.79956 -18.71666,-0.91597 l -16.49236,-0.11945 V -0.68605142 10.798429 h -0.8256 c -1.53109,0 -5.09758,2.09614 -6.79456,3.99338 -1.65639,1.85186 -4.54446,7.43871 -5.41264,10.47051 -0.25002,0.87312 -0.58222,1.98437 -0.73823,2.46944 -0.39136,1.2169 -2.0765,7.30176 -3.12634,11.28889 -0.2052,0.7793 -0.33685,-11.27627 -0.35693,-32.6846104 l -0.0318,-33.9193396 1.55319,-0.12371 c 0.85426,-0.068 12.32395,-0.10028 25.4882,-0.0716 20.69377,0.045 24.2694,0.12953 26.40444,0.62402 3.9887,0.92382 7.58472,2.04932 7.58472,2.3739 0,0.16576 0.52886,0.30139 1.17524,0.30139 2.09331,0 10.76432,4.87704 10.22435,5.75072 -0.12186,0.19718 -0.0447,0.24734 0.17328,0.11263 0.60692,-0.3751 4.21691,3.0333 6.9953,6.60467 2.06429,2.6534496 4.63504,8.4775396 5.94174,13.4611396 1.7681,6.7433 1.74625,15.8657704 -0.0549,22.9305504 -2.11084,8.27937 -4.97852,13.41407 -10.75456,19.25647 -2.59968,2.62955 -8.78375,7.02548 -9.88326,7.02548 -0.27557,0 -0.68644,0.1854 -0.91304,0.412 -0.39593,0.39593 -0.78905,0.56749 -4.31522,1.88319 -3.68968,1.37672 -10.83412,2.28545 -13.21446,1.68081 z m 7.57002,-15.26489 c 0,-0.19403 -0.07,-0.35278 -0.15557,-0.35278 -0.0856,0 -0.25368,0.15875 -0.3736,0.35278 -0.11992,0.19403 -0.0499,0.35278 0.15557,0.35278 0.20548,0 0.3736,-0.15875 0.3736,-0.35278 z"
|
||||
id="path1" /></g></svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 558 KiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 937 KiB |
|
After Width: | Height: | Size: 566 KiB |
|
After Width: | Height: | Size: 365 KiB |
@@ -0,0 +1,52 @@
|
||||
# Scripts
|
||||
|
||||
## install.sh - Auto-Installer for Debian and RHEL-based Systems
|
||||
|
||||
This script automatically downloads and installs the latest Psysonic release from GitHub Releases.
|
||||
|
||||
### Supported Distributions
|
||||
|
||||
- **Debian/Ubuntu**: Downloads and installs `.deb` package
|
||||
- **RHEL/Fedora/CentOS**: Downloads and installs `.rpm` package
|
||||
|
||||
### Usage
|
||||
|
||||
#### Quick Install (Recommended)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/Psychotoxical/psysonic/main/scripts/install.sh | sudo bash
|
||||
```
|
||||
|
||||
#### Manual Installation
|
||||
|
||||
```bash
|
||||
# Download the script
|
||||
wget https://raw.githubusercontent.com/Psychotoxical/psysonic/main/scripts/install.sh
|
||||
|
||||
# Make it executable
|
||||
chmod +x install.sh
|
||||
|
||||
# Run with sudo
|
||||
sudo ./install.sh
|
||||
```
|
||||
|
||||
### What it does
|
||||
|
||||
1. Detects your OS type (Debian or RHEL-based)
|
||||
2. Fetches the latest release from GitHub
|
||||
3. Downloads the appropriate package (.deb or .rpm)
|
||||
4. Installs it using your system's package manager
|
||||
5. Cleans up temporary files
|
||||
|
||||
### Requirements
|
||||
|
||||
- `curl` - for downloading packages
|
||||
- `sudo` or root access
|
||||
- Internet connection
|
||||
- Supported package manager (apt-get, dnf, or yum)
|
||||
|
||||
### Notes
|
||||
|
||||
- If Psysonic is already installed, the script will ask if you want to reinstall
|
||||
- The script automatically handles dependency installation for Debian systems
|
||||
- After installation, you can launch Psysonic from your application menu or by running `psysonic` in the terminal
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Psysonic Auto-Installer
|
||||
# Automatically detects your OS and installs the latest release from GitHub
|
||||
|
||||
REPO="Psychotoxical/psysonic"
|
||||
APP_NAME="psysonic"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
check_root() {
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
error "Please run this script as root (use sudo)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Detect package manager and OS type
|
||||
detect_os() {
|
||||
if command -v apt &> /dev/null; then
|
||||
OS_TYPE="debian"
|
||||
PACKAGE_MANAGER="apt"
|
||||
info "Detected Debian/Ubuntu-based system (apt)"
|
||||
elif command -v dnf &> /dev/null; then
|
||||
OS_TYPE="rhel"
|
||||
PACKAGE_MANAGER="dnf"
|
||||
info "Detected RHEL/Fedora-based system (dnf)"
|
||||
elif command -v yum &> /dev/null; then
|
||||
OS_TYPE="rhel"
|
||||
PACKAGE_MANAGER="yum"
|
||||
info "Detected RHEL/CentOS-based system (yum)"
|
||||
else
|
||||
error "Unsupported package manager. This installer supports Debian/Ubuntu and RHEL/Fedora/CentOS systems."
|
||||
fi
|
||||
}
|
||||
|
||||
# Get the latest release download URL for the specific package type
|
||||
get_download_url() {
|
||||
local api_url="https://api.github.com/repos/${REPO}/releases/latest"
|
||||
|
||||
info "Fetching latest release information..."
|
||||
|
||||
local release_info
|
||||
release_info=$(curl -s "$api_url")
|
||||
|
||||
if echo "$release_info" | grep -q "message.*Not Found"; then
|
||||
error "Could not fetch release information. Please check your internet connection."
|
||||
fi
|
||||
|
||||
local tag_name
|
||||
tag_name=$(echo "$release_info" | grep -o '"tag_name": *"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$tag_name" ]; then
|
||||
error "Could not determine latest release version."
|
||||
fi
|
||||
|
||||
info "Latest version: $tag_name"
|
||||
|
||||
local download_url=""
|
||||
|
||||
if [ "$OS_TYPE" = "debian" ]; then
|
||||
download_url=$(echo "$release_info" | grep -o '"browser_download_url": *"[^"]*\.deb"' | head -1 | cut -d'"' -f4)
|
||||
elif [ "$OS_TYPE" = "rhel" ]; then
|
||||
download_url=$(echo "$release_info" | grep -o '"browser_download_url": *"[^"]*\.rpm"' | head -1 | cut -d'"' -f4)
|
||||
fi
|
||||
|
||||
if [ -z "$download_url" ]; then
|
||||
error "Could not find download URL for $OS_TYPE package."
|
||||
fi
|
||||
|
||||
echo "$download_url"
|
||||
}
|
||||
|
||||
# Install the package
|
||||
install_package() {
|
||||
local download_url="$1"
|
||||
local temp_dir
|
||||
temp_dir=$(mktemp -d)
|
||||
local package_file="$temp_dir/${APP_NAME}_latest"
|
||||
|
||||
info "Downloading package..."
|
||||
|
||||
if [ "$OS_TYPE" = "debian" ]; then
|
||||
package_file="${package_file}.deb"
|
||||
curl -L -o "$package_file" "$download_url"
|
||||
|
||||
info "Installing package..."
|
||||
$PACKAGE_MANAGER install -y "$package_file" || {
|
||||
warn "Trying to fix broken dependencies..."
|
||||
$PACKAGE_MANAGER install -f -y
|
||||
}
|
||||
elif [ "$OS_TYPE" = "rhel" ]; then
|
||||
package_file="${package_file}.rpm"
|
||||
curl -L -o "$package_file" "$download_url"
|
||||
|
||||
info "Installing package..."
|
||||
$PACKAGE_MANAGER install -y "$package_file"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -rf "$temp_dir"
|
||||
}
|
||||
|
||||
# Check if app is already installed
|
||||
check_installed() {
|
||||
if command -v $APP_NAME &> /dev/null || command -v ${APP_NAME^} &> /dev/null; then
|
||||
warn "${APP_NAME} appears to be already installed."
|
||||
read -p "Do you want to reinstall? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
info "Installation cancelled."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Main installation flow
|
||||
main() {
|
||||
echo -e "${GREEN}"
|
||||
echo "╔══════════════════════════════════════════════════════════╗"
|
||||
echo "║ Psysonic Auto-Installer ║"
|
||||
echo "║ Install the latest release from GitHub Releases ║"
|
||||
echo "╚══════════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
|
||||
check_root
|
||||
detect_os
|
||||
check_installed
|
||||
|
||||
local download_url
|
||||
download_url=$(get_download_url)
|
||||
|
||||
if [ -z "$download_url" ]; then
|
||||
error "Failed to get download URL."
|
||||
fi
|
||||
|
||||
info "Download URL: $download_url"
|
||||
|
||||
install_package "$download_url"
|
||||
|
||||
echo ""
|
||||
success "Psysonic has been installed successfully!"
|
||||
echo -e "${BLUE}You can launch it from your application menu or by running:${NC} psysonic"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "psysonic"
|
||||
version = "1.4.2"
|
||||
version = "1.34.8"
|
||||
description = "Psysonic Desktop Music Player"
|
||||
authors = []
|
||||
license = ""
|
||||
@@ -22,8 +22,8 @@ tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||
tauri-plugin-single-instance = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-store = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
@@ -31,5 +31,37 @@ tauri-plugin-fs = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rodio = { version = "0.19", default-features = false, features = ["symphonia-all"] }
|
||||
reqwest = { version = "0.12", features = ["stream"] }
|
||||
tokio = { version = "1", features = ["rt", "time"] }
|
||||
symphonia = { version = "0.5", default-features = false, features = ["flac", "mp3", "pcm", "aac", "alac", "isomp4", "vorbis", "ogg", "wav", "adpcm"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["stream", "json", "multipart", "rustls-tls", "blocking"] }
|
||||
futures-util = "0.3"
|
||||
md5 = "0.7"
|
||||
tokio = { version = "1", features = ["rt", "time", "sync"] }
|
||||
biquad = "0.4"
|
||||
ringbuf = "0.3"
|
||||
tauri-plugin-window-state = "2.4.1"
|
||||
tauri-plugin-process = "2"
|
||||
souvlaki = { version = "0.8", default-features = false, features = ["use_zbus"] }
|
||||
discord-rich-presence = "0.2"
|
||||
url = "2"
|
||||
thread-priority = "1"
|
||||
lofty = "0.22"
|
||||
id3 = "1.16.4"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows = { version = "0.58", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_System_Com",
|
||||
"Win32_UI_Controls",
|
||||
"Win32_UI_Shell",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[patch.crates-io]
|
||||
# Local patch for Symphonia's isomp4 demuxer:
|
||||
# - Fixes descriptor.unwrap() panic on malformed esds atoms (older iTunes M4A)
|
||||
# - Tolerates SL predefined=0x01 (null) used by some older iTunes-purchased files
|
||||
# - Gracefully skips malformed trak atoms (e.g. MJPEG cover-art streams) instead
|
||||
# of failing the entire probe
|
||||
symphonia-format-isomp4 = { path = "patches/symphonia-format-isomp4" }
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Sandbox disabled: app is unsigned/ad-hoc signed, so an active sandbox
|
||||
would restrict network access more than without it. macOS users bypass
|
||||
Gatekeeper via: xattr -cr /Applications/Psysonic.app -->
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<false/>
|
||||
|
||||
<!-- Allow outbound TCP connections to the Navidrome server (reqwest in audio.rs).
|
||||
Respected by ad-hoc signatures on some macOS versions. -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
|
||||
<!-- Psysonic is a music PLAYER only — no microphone access needed.
|
||||
This suppresses the macOS microphone permission prompt triggered
|
||||
by cpal/CoreAudio enumerating input devices during audio init. -->
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Psysonic is a music player only — it does not record audio.
|
||||
This description is shown if macOS prompts for microphone access
|
||||
(triggered by CoreAudio enumerating input devices during init). -->
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Psysonic does not use the microphone. This prompt is triggered by the audio subsystem initializing output devices.</string>
|
||||
|
||||
<!-- Allow HTTP (non-TLS) connections so that internet radio streams and
|
||||
Navidrome servers running without HTTPS can load media in WKWebView.
|
||||
Without this, macOS App Transport Security blocks HTTP audio src. -->
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -8,7 +8,6 @@
|
||||
"core:default",
|
||||
"shell:default",
|
||||
{ "identifier": "shell:allow-open", "allow": [{ "url": "https://**" }] },
|
||||
"notification:default",
|
||||
"global-shortcut:allow-register",
|
||||
"global-shortcut:allow-unregister",
|
||||
"store:default",
|
||||
@@ -18,17 +17,26 @@
|
||||
"store:allow-save",
|
||||
"dialog:default",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"fs:default",
|
||||
"fs:allow-write-file",
|
||||
"fs:allow-read-file",
|
||||
"fs:allow-mkdir",
|
||||
"fs:scope-download-recursive",
|
||||
"fs:scope-home-recursive",
|
||||
"window-state:allow-save-window-state",
|
||||
"window-state:allow-restore-state",
|
||||
"core:window:allow-set-title",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-set-fullscreen",
|
||||
"core:window:allow-is-fullscreen",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-create",
|
||||
"core:webview:allow-create-webview-window"
|
||||
"core:webview:allow-create-webview-window",
|
||||
"process:allow-restart"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default capabilities for Psysonic","local":true,"windows":["main"],"permissions":["core:default","shell:default","shell:allow-open","notification:default","global-shortcut:allow-register","global-shortcut:allow-unregister","store:default","store:allow-load","store:allow-set","store:allow-get","store:allow-save","dialog:default","dialog:allow-open","fs:default","fs:allow-write-file","fs:allow-mkdir","fs:scope-download-recursive","core:window:allow-set-title","core:window:allow-close","core:window:allow-hide","core:window:allow-show","core:window:allow-set-fullscreen","core:window:allow-is-fullscreen","core:window:allow-create","core:webview:allow-create-webview-window"],"platforms":["linux","macOS","windows"]}}
|
||||
{"default":{"identifier":"default","description":"Default capabilities for Psysonic","local":true,"windows":["main"],"permissions":["core:default","shell:default",{"identifier":"shell:allow-open","allow":[{"url":"https://**"}]},"global-shortcut:allow-register","global-shortcut:allow-unregister","store:default","store:allow-load","store:allow-set","store:allow-get","store:allow-save","dialog:default","dialog:allow-open","dialog:allow-save","fs:default","fs:allow-write-file","fs:allow-read-file","fs:allow-mkdir","fs:scope-download-recursive","fs:scope-home-recursive","window-state:allow-save-window-state","window-state:allow-restore-state","core:window:allow-set-title","core:window:allow-close","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-hide","core:window:allow-show","core:window:allow-set-fullscreen","core:window:allow-is-fullscreen","core:window:allow-start-dragging","core:window:allow-create","core:webview:allow-create-webview-window","process:allow-restart"],"platforms":["linux","macOS","windows"]}}
|
||||
@@ -6015,202 +6015,34 @@
|
||||
"markdownDescription": "Denies the unregister_all command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||
"description": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`",
|
||||
"type": "string",
|
||||
"const": "notification:default",
|
||||
"markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`"
|
||||
"const": "process:default",
|
||||
"markdownDescription": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the batch command without any pre-configured scope.",
|
||||
"description": "Enables the exit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-batch",
|
||||
"markdownDescription": "Enables the batch command without any pre-configured scope."
|
||||
"const": "process:allow-exit",
|
||||
"markdownDescription": "Enables the exit command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the cancel command without any pre-configured scope.",
|
||||
"description": "Enables the restart command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-cancel",
|
||||
"markdownDescription": "Enables the cancel command without any pre-configured scope."
|
||||
"const": "process:allow-restart",
|
||||
"markdownDescription": "Enables the restart command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||
"description": "Denies the exit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-check-permissions",
|
||||
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||
"const": "process:deny-exit",
|
||||
"markdownDescription": "Denies the exit command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the create_channel command without any pre-configured scope.",
|
||||
"description": "Denies the restart command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-create-channel",
|
||||
"markdownDescription": "Enables the create_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the delete_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-delete-channel",
|
||||
"markdownDescription": "Enables the delete_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-get-active",
|
||||
"markdownDescription": "Enables the get_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_pending command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-get-pending",
|
||||
"markdownDescription": "Enables the get_pending command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_permission_granted command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-is-permission-granted",
|
||||
"markdownDescription": "Enables the is_permission_granted command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the list_channels command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-list-channels",
|
||||
"markdownDescription": "Enables the list_channels command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the notify command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-notify",
|
||||
"markdownDescription": "Enables the notify command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the permission_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-permission-state",
|
||||
"markdownDescription": "Enables the permission_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_action_types command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-register-action-types",
|
||||
"markdownDescription": "Enables the register_action_types command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-remove-active",
|
||||
"markdownDescription": "Enables the remove_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-request-permission",
|
||||
"markdownDescription": "Enables the request_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the show command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-show",
|
||||
"markdownDescription": "Enables the show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the batch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-batch",
|
||||
"markdownDescription": "Denies the batch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-cancel",
|
||||
"markdownDescription": "Denies the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-check-permissions",
|
||||
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the create_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-create-channel",
|
||||
"markdownDescription": "Denies the create_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the delete_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-delete-channel",
|
||||
"markdownDescription": "Denies the delete_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-get-active",
|
||||
"markdownDescription": "Denies the get_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_pending command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-get-pending",
|
||||
"markdownDescription": "Denies the get_pending command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_permission_granted command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-is-permission-granted",
|
||||
"markdownDescription": "Denies the is_permission_granted command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the list_channels command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-list-channels",
|
||||
"markdownDescription": "Denies the list_channels command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the notify command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-notify",
|
||||
"markdownDescription": "Denies the notify command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the permission_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-permission-state",
|
||||
"markdownDescription": "Denies the permission_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_action_types command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-register-action-types",
|
||||
"markdownDescription": "Denies the register_action_types command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-remove-active",
|
||||
"markdownDescription": "Denies the remove_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-request-permission",
|
||||
"markdownDescription": "Denies the request_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the show command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-show",
|
||||
"markdownDescription": "Denies the show command without any pre-configured scope."
|
||||
"const": "process:deny-restart",
|
||||
"markdownDescription": "Denies the restart command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
||||
@@ -6451,6 +6283,48 @@
|
||||
"type": "string",
|
||||
"const": "store:deny-values",
|
||||
"markdownDescription": "Denies the values command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-filename`\n- `allow-restore-state`\n- `allow-save-window-state`",
|
||||
"type": "string",
|
||||
"const": "window-state:default",
|
||||
"markdownDescription": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-filename`\n- `allow-restore-state`\n- `allow-save-window-state`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the filename command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:allow-filename",
|
||||
"markdownDescription": "Enables the filename command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the restore_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:allow-restore-state",
|
||||
"markdownDescription": "Enables the restore_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the save_window_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:allow-save-window-state",
|
||||
"markdownDescription": "Enables the save_window_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the filename command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:deny-filename",
|
||||
"markdownDescription": "Denies the filename command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the restore_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:deny-restore-state",
|
||||
"markdownDescription": "Denies the restore_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the save_window_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:deny-save-window-state",
|
||||
"markdownDescription": "Denies the save_window_state command without any pre-configured scope."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -6015,202 +6015,34 @@
|
||||
"markdownDescription": "Denies the unregister_all command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||
"description": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`",
|
||||
"type": "string",
|
||||
"const": "notification:default",
|
||||
"markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`"
|
||||
"const": "process:default",
|
||||
"markdownDescription": "This permission set configures which\nprocess features are by default exposed.\n\n#### Granted Permissions\n\nThis enables to quit via `allow-exit` and restart via `allow-restart`\nthe application.\n\n#### This default permission set includes:\n\n- `allow-exit`\n- `allow-restart`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the batch command without any pre-configured scope.",
|
||||
"description": "Enables the exit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-batch",
|
||||
"markdownDescription": "Enables the batch command without any pre-configured scope."
|
||||
"const": "process:allow-exit",
|
||||
"markdownDescription": "Enables the exit command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the cancel command without any pre-configured scope.",
|
||||
"description": "Enables the restart command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-cancel",
|
||||
"markdownDescription": "Enables the cancel command without any pre-configured scope."
|
||||
"const": "process:allow-restart",
|
||||
"markdownDescription": "Enables the restart command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||
"description": "Denies the exit command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-check-permissions",
|
||||
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||
"const": "process:deny-exit",
|
||||
"markdownDescription": "Denies the exit command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the create_channel command without any pre-configured scope.",
|
||||
"description": "Denies the restart command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-create-channel",
|
||||
"markdownDescription": "Enables the create_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the delete_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-delete-channel",
|
||||
"markdownDescription": "Enables the delete_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-get-active",
|
||||
"markdownDescription": "Enables the get_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_pending command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-get-pending",
|
||||
"markdownDescription": "Enables the get_pending command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_permission_granted command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-is-permission-granted",
|
||||
"markdownDescription": "Enables the is_permission_granted command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the list_channels command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-list-channels",
|
||||
"markdownDescription": "Enables the list_channels command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the notify command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-notify",
|
||||
"markdownDescription": "Enables the notify command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the permission_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-permission-state",
|
||||
"markdownDescription": "Enables the permission_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_action_types command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-register-action-types",
|
||||
"markdownDescription": "Enables the register_action_types command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-remove-active",
|
||||
"markdownDescription": "Enables the remove_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-request-permission",
|
||||
"markdownDescription": "Enables the request_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the show command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-show",
|
||||
"markdownDescription": "Enables the show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the batch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-batch",
|
||||
"markdownDescription": "Denies the batch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-cancel",
|
||||
"markdownDescription": "Denies the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-check-permissions",
|
||||
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the create_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-create-channel",
|
||||
"markdownDescription": "Denies the create_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the delete_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-delete-channel",
|
||||
"markdownDescription": "Denies the delete_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-get-active",
|
||||
"markdownDescription": "Denies the get_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_pending command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-get-pending",
|
||||
"markdownDescription": "Denies the get_pending command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_permission_granted command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-is-permission-granted",
|
||||
"markdownDescription": "Denies the is_permission_granted command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the list_channels command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-list-channels",
|
||||
"markdownDescription": "Denies the list_channels command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the notify command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-notify",
|
||||
"markdownDescription": "Denies the notify command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the permission_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-permission-state",
|
||||
"markdownDescription": "Denies the permission_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_action_types command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-register-action-types",
|
||||
"markdownDescription": "Denies the register_action_types command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-remove-active",
|
||||
"markdownDescription": "Denies the remove_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-request-permission",
|
||||
"markdownDescription": "Denies the request_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the show command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-show",
|
||||
"markdownDescription": "Denies the show command without any pre-configured scope."
|
||||
"const": "process:deny-restart",
|
||||
"markdownDescription": "Denies the restart command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
||||
@@ -6451,6 +6283,48 @@
|
||||
"type": "string",
|
||||
"const": "store:deny-values",
|
||||
"markdownDescription": "Denies the values command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-filename`\n- `allow-restore-state`\n- `allow-save-window-state`",
|
||||
"type": "string",
|
||||
"const": "window-state:default",
|
||||
"markdownDescription": "This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n\n#### This default permission set includes:\n\n- `allow-filename`\n- `allow-restore-state`\n- `allow-save-window-state`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the filename command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:allow-filename",
|
||||
"markdownDescription": "Enables the filename command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the restore_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:allow-restore-state",
|
||||
"markdownDescription": "Enables the restore_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the save_window_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:allow-save-window-state",
|
||||
"markdownDescription": "Enables the save_window_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the filename command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:deny-filename",
|
||||
"markdownDescription": "Denies the filename command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the restore_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:deny-restore-state",
|
||||
"markdownDescription": "Denies the restore_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the save_window_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "window-state:deny-save-window-state",
|
||||
"markdownDescription": "Denies the save_window_state command without any pre-configured scope."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 998 B After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 985 KiB After Width: | Height: | Size: 829 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1 @@
|
||||
{"v":1}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "6d533f26150953a882a6a111ebd13f0abf7129d5"
|
||||
},
|
||||
"path_in_vcs": "symphonia-format-isomp4"
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-core"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags",
|
||||
"bytemuck",
|
||||
"lazy_static",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-isomp4"
|
||||
version = "0.5.5"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
"symphonia-utils-xiph",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-metadata"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-utils-xiph"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16"
|
||||
dependencies = [
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
@@ -0,0 +1,59 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2018"
|
||||
rust-version = "1.53"
|
||||
name = "symphonia-format-isomp4"
|
||||
version = "0.5.5"
|
||||
authors = ["Philip Deljanov <philip.deljanov@gmail.com>"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Pure Rust ISO/MP4 demuxer (a part of project Symphonia)."
|
||||
homepage = "https://github.com/pdeljanov/Symphonia"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"audio",
|
||||
"media",
|
||||
"demuxer",
|
||||
"mp4",
|
||||
"iso",
|
||||
]
|
||||
categories = [
|
||||
"multimedia",
|
||||
"multimedia::audio",
|
||||
"multimedia::encoding",
|
||||
]
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/pdeljanov/Symphonia"
|
||||
|
||||
[lib]
|
||||
name = "symphonia_format_isomp4"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.encoding_rs]
|
||||
version = "0.8.17"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4"
|
||||
|
||||
[dependencies.symphonia-core]
|
||||
version = "0.5.5"
|
||||
|
||||
[dependencies.symphonia-metadata]
|
||||
version = "0.5.5"
|
||||
|
||||
[dependencies.symphonia-utils-xiph]
|
||||
version = "0.5.5"
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "symphonia-format-isomp4"
|
||||
version = "0.5.5"
|
||||
description = "Pure Rust ISO/MP4 demuxer (a part of project Symphonia)."
|
||||
homepage = "https://github.com/pdeljanov/Symphonia"
|
||||
repository = "https://github.com/pdeljanov/Symphonia"
|
||||
authors = ["Philip Deljanov <philip.deljanov@gmail.com>"]
|
||||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
categories = ["multimedia", "multimedia::audio", "multimedia::encoding"]
|
||||
keywords = ["audio", "media", "demuxer", "mp4", "iso"]
|
||||
edition = "2018"
|
||||
rust-version = "1.53"
|
||||
|
||||
[dependencies]
|
||||
encoding_rs = "0.8.17"
|
||||
log = "0.4"
|
||||
symphonia-core = { version = "0.5.5", path = "../symphonia-core" }
|
||||
symphonia-metadata = { version = "0.5.5", path = "../symphonia-metadata" }
|
||||
symphonia-utils-xiph = { version = "0.5.5", path = "../symphonia-utils-xiph" }
|
||||
@@ -0,0 +1,15 @@
|
||||
# Symphonia ISO/MP4 Demuxer
|
||||
|
||||
[](https://docs.rs/symphonia-format-isomp4)
|
||||
|
||||
ISO/MP4 demuxer for Project Symphonia.
|
||||
|
||||
**Note:** This crate is part of Project Symphonia. Please use the [`symphonia`](https://crates.io/crates/symphonia) crate instead of this one directly.
|
||||
|
||||
## License
|
||||
|
||||
Symphonia is provided under the MPL v2.0 license. Please refer to the LICENSE file for more details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Symphonia is a free and open-source project that welcomes contributions! To get started, please read our [Contribution Guidelines](https://github.com/pdeljanov/Symphonia/tree/master/CONTRIBUTING.md).
|
||||
@@ -0,0 +1,60 @@
|
||||
// Symphonia
|
||||
// Copyright (c) 2019-2022 The Project Symphonia Developers.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use symphonia_core::codecs::{CodecParameters, CODEC_TYPE_ALAC};
|
||||
use symphonia_core::errors::{decode_error, unsupported_error, Result};
|
||||
use symphonia_core::io::ReadBytes;
|
||||
|
||||
use crate::atoms::{Atom, AtomHeader};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct AlacAtom {
|
||||
/// Atom header.
|
||||
header: AtomHeader,
|
||||
/// ALAC extra data (magic cookie).
|
||||
extra_data: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl Atom for AlacAtom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
let (version, flags) = AtomHeader::read_extra(reader)?;
|
||||
|
||||
if version != 0 {
|
||||
return unsupported_error("isomp4 (alac): unsupported alac version");
|
||||
}
|
||||
|
||||
if flags != 0 {
|
||||
return decode_error("isomp4 (alac): flags not zero");
|
||||
}
|
||||
|
||||
if header.data_len <= AtomHeader::EXTRA_DATA_SIZE {
|
||||
return decode_error("isomp4 (alac): invalid alac atom length");
|
||||
}
|
||||
|
||||
// The ALAC magic cookie (aka extra data) is either 24 or 48 bytes long.
|
||||
let magic_len = match header.data_len - AtomHeader::EXTRA_DATA_SIZE {
|
||||
len @ 24 | len @ 48 => len as usize,
|
||||
_ => return decode_error("isomp4 (alac): invalid magic cookie length"),
|
||||
};
|
||||
|
||||
// Read the magic cookie.
|
||||
let extra_data = reader.read_boxed_slice_exact(magic_len)?;
|
||||
|
||||
Ok(AlacAtom { header, extra_data })
|
||||
}
|
||||
}
|
||||
|
||||
impl AlacAtom {
|
||||
pub fn fill_codec_params(&self, codec_params: &mut CodecParameters) {
|
||||
codec_params.for_codec(CODEC_TYPE_ALAC).with_extra_data(self.extra_data.clone());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Symphonia
|
||||
// Copyright (c) 2019-2022 The Project Symphonia Developers.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use symphonia_core::errors::Result;
|
||||
use symphonia_core::io::ReadBytes;
|
||||
|
||||
use crate::atoms::{Atom, AtomHeader};
|
||||
|
||||
/// Chunk offset atom (64-bit version).
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct Co64Atom {
|
||||
/// Atom header.
|
||||
header: AtomHeader,
|
||||
pub chunk_offsets: Vec<u64>,
|
||||
}
|
||||
|
||||
impl Atom for Co64Atom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
let (_, _) = AtomHeader::read_extra(reader)?;
|
||||
|
||||
let entry_count = reader.read_be_u32()?;
|
||||
|
||||
// TODO: Apply a limit.
|
||||
let mut chunk_offsets = Vec::with_capacity(entry_count as usize);
|
||||
|
||||
for _ in 0..entry_count {
|
||||
chunk_offsets.push(reader.read_be_u64()?);
|
||||
}
|
||||
|
||||
Ok(Co64Atom { header, chunk_offsets })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Symphonia
|
||||
// Copyright (c) 2019-2022 The Project Symphonia Developers.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use symphonia_core::errors::Result;
|
||||
use symphonia_core::io::ReadBytes;
|
||||
|
||||
use crate::atoms::{Atom, AtomHeader};
|
||||
|
||||
/// Composition time atom.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct CttsAtom {
|
||||
/// Atom header.
|
||||
header: AtomHeader,
|
||||
}
|
||||
|
||||
impl Atom for CttsAtom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
fn read<B: ReadBytes>(_reader: &mut B, _header: AtomHeader) -> Result<Self> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Symphonia
|
||||
// Copyright (c) 2019-2022 The Project Symphonia Developers.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use symphonia_core::errors::Result;
|
||||
use symphonia_core::io::ReadBytes;
|
||||
|
||||
use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType, ElstAtom};
|
||||
|
||||
/// Edits atom.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct EdtsAtom {
|
||||
header: AtomHeader,
|
||||
pub elst: Option<ElstAtom>,
|
||||
}
|
||||
|
||||
impl Atom for EdtsAtom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
#[allow(clippy::single_match)]
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
let mut iter = AtomIterator::new(reader, header);
|
||||
|
||||
let mut elst = None;
|
||||
|
||||
while let Some(header) = iter.next()? {
|
||||
match header.atype {
|
||||
AtomType::EditList => {
|
||||
elst = Some(iter.read_atom::<ElstAtom>()?);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(EdtsAtom { header, elst })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Symphonia
|
||||
// Copyright (c) 2019-2022 The Project Symphonia Developers.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use symphonia_core::errors::{decode_error, Result};
|
||||
use symphonia_core::io::ReadBytes;
|
||||
use symphonia_core::util::bits;
|
||||
|
||||
use crate::atoms::{Atom, AtomHeader};
|
||||
|
||||
/// Edit list entry.
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ElstEntry {
|
||||
segment_duration: u64,
|
||||
media_time: i64,
|
||||
media_rate_int: i16,
|
||||
media_rate_frac: i16,
|
||||
}
|
||||
|
||||
/// Edit list atom.
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct ElstAtom {
|
||||
header: AtomHeader,
|
||||
entries: Vec<ElstEntry>,
|
||||
}
|
||||
|
||||
impl Atom for ElstAtom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
let (version, _) = AtomHeader::read_extra(reader)?;
|
||||
|
||||
// TODO: Apply a limit.
|
||||
let entry_count = reader.read_be_u32()?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for _ in 0..entry_count {
|
||||
let (segment_duration, media_time) = match version {
|
||||
0 => (
|
||||
u64::from(reader.read_be_u32()?),
|
||||
i64::from(bits::sign_extend_leq32_to_i32(reader.read_be_u32()?, 32)),
|
||||
),
|
||||
1 => (
|
||||
reader.read_be_u64()?,
|
||||
bits::sign_extend_leq64_to_i64(reader.read_be_u64()?, 64),
|
||||
),
|
||||
_ => return decode_error("isomp4: invalid tkhd version"),
|
||||
};
|
||||
|
||||
let media_rate_int = bits::sign_extend_leq16_to_i16(reader.read_be_u16()?, 16);
|
||||
let media_rate_frac = bits::sign_extend_leq16_to_i16(reader.read_be_u16()?, 16);
|
||||
|
||||
entries.push(ElstEntry {
|
||||
segment_duration,
|
||||
media_time,
|
||||
media_rate_int,
|
||||
media_rate_frac,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ElstAtom { header, entries })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
// Symphonia
|
||||
// Copyright (c) 2019-2022 The Project Symphonia Developers.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use symphonia_core::codecs::{
|
||||
CodecParameters, CodecType, CODEC_TYPE_AAC, CODEC_TYPE_MP3, CODEC_TYPE_NULL,
|
||||
};
|
||||
use symphonia_core::errors::{decode_error, unsupported_error, Result};
|
||||
use symphonia_core::io::{FiniteStream, ReadBytes, ScopedStream};
|
||||
|
||||
use crate::atoms::{Atom, AtomHeader};
|
||||
|
||||
use log::{debug, warn};
|
||||
|
||||
const ES_DESCRIPTOR: u8 = 0x03;
|
||||
const DECODER_CONFIG_DESCRIPTOR: u8 = 0x04;
|
||||
const DECODER_SPECIFIC_DESCRIPTOR: u8 = 0x05;
|
||||
const SL_CONFIG_DESCRIPTOR: u8 = 0x06;
|
||||
|
||||
const MIN_DESCRIPTOR_SIZE: u64 = 2;
|
||||
|
||||
fn read_descriptor_header<B: ReadBytes>(reader: &mut B) -> Result<(u8, u32)> {
|
||||
let tag = reader.read_u8()?;
|
||||
|
||||
let mut size = 0;
|
||||
|
||||
for _ in 0..4 {
|
||||
let val = reader.read_u8()?;
|
||||
size = (size << 7) | u32::from(val & 0x7f);
|
||||
if val & 0x80 == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((tag, size))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct EsdsAtom {
|
||||
/// Atom header.
|
||||
header: AtomHeader,
|
||||
/// Elementary stream descriptor.
|
||||
descriptor: ESDescriptor,
|
||||
}
|
||||
|
||||
impl Atom for EsdsAtom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
let (_, _) = AtomHeader::read_extra(reader)?;
|
||||
|
||||
let mut descriptor = None;
|
||||
|
||||
let mut scoped = ScopedStream::new(reader, header.data_len - AtomHeader::EXTRA_DATA_SIZE);
|
||||
|
||||
while scoped.bytes_available() > MIN_DESCRIPTOR_SIZE {
|
||||
let (desc, desc_len) = read_descriptor_header(&mut scoped)?;
|
||||
|
||||
match desc {
|
||||
ES_DESCRIPTOR => {
|
||||
descriptor = Some(ESDescriptor::read(&mut scoped, desc_len)?);
|
||||
}
|
||||
_ => {
|
||||
warn!("unknown descriptor in esds atom, desc={}", desc);
|
||||
scoped.ignore_bytes(desc_len as u64)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore remainder of the atom.
|
||||
scoped.ignore()?;
|
||||
|
||||
// Guard against malformed esds atoms that contain no ES_DESCRIPTOR (old iTunes files).
|
||||
let descriptor = descriptor
|
||||
.ok_or_else(|| symphonia_core::errors::Error::DecodeError("isomp4: missing es descriptor in esds atom"))?;
|
||||
|
||||
Ok(EsdsAtom { header, descriptor })
|
||||
}
|
||||
}
|
||||
|
||||
impl EsdsAtom {
|
||||
pub fn fill_codec_params(&self, codec_params: &mut CodecParameters) {
|
||||
codec_params.for_codec(self.descriptor.dec_config.codec_type);
|
||||
|
||||
if let Some(ds_config) = &self.descriptor.dec_config.dec_specific_info {
|
||||
codec_params.with_extra_data(ds_config.extra_data.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ObjectDescriptor: Sized {
|
||||
fn read<B: ReadBytes>(reader: &mut B, len: u32) -> Result<Self>;
|
||||
}
|
||||
|
||||
/*
|
||||
class ES_Descriptor extends BaseDescriptor : bit(8) tag=ES_DescrTag {
|
||||
bit(16) ES_ID;
|
||||
bit(1) streamDependenceFlag;
|
||||
bit(1) URL_Flag;
|
||||
bit(1) OCRstreamFlag;
|
||||
bit(5) streamPriority;
|
||||
if (streamDependenceFlag)
|
||||
bit(16) dependsOn_ES_ID;
|
||||
if (URL_Flag) {
|
||||
bit(8) URLlength;
|
||||
bit(8) URLstring[URLlength];
|
||||
}
|
||||
if (OCRstreamFlag)
|
||||
bit(16) OCR_ES_Id;
|
||||
DecoderConfigDescriptor decConfigDescr;
|
||||
SLConfigDescriptor slConfigDescr;
|
||||
IPI_DescrPointer ipiPtr[0 .. 1];
|
||||
IP_IdentificationDataSet ipIDS[0 .. 255];
|
||||
IPMP_DescriptorPointer ipmpDescrPtr[0 .. 255];
|
||||
LanguageDescriptor langDescr[0 .. 255];
|
||||
QoS_Descriptor qosDescr[0 .. 1];
|
||||
RegistrationDescriptor regDescr[0 .. 1];
|
||||
ExtensionDescriptor extDescr[0 .. 255];
|
||||
}
|
||||
*/
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct ESDescriptor {
|
||||
pub es_id: u16,
|
||||
pub dec_config: DecoderConfigDescriptor,
|
||||
pub sl_config: SLDescriptor,
|
||||
}
|
||||
|
||||
impl ObjectDescriptor for ESDescriptor {
|
||||
fn read<B: ReadBytes>(reader: &mut B, len: u32) -> Result<Self> {
|
||||
let es_id = reader.read_be_u16()?;
|
||||
let es_flags = reader.read_u8()?;
|
||||
|
||||
// Stream dependence flag.
|
||||
if es_flags & 0x80 != 0 {
|
||||
let _depends_on_es_id = reader.read_u16()?;
|
||||
}
|
||||
|
||||
// URL flag.
|
||||
if es_flags & 0x40 != 0 {
|
||||
let url_len = reader.read_u8()?;
|
||||
reader.ignore_bytes(u64::from(url_len))?;
|
||||
}
|
||||
|
||||
// OCR stream flag.
|
||||
if es_flags & 0x20 != 0 {
|
||||
let _ocr_es_id = reader.read_u16()?;
|
||||
}
|
||||
|
||||
let mut dec_config = None;
|
||||
let mut sl_config = None;
|
||||
|
||||
let mut scoped = ScopedStream::new(reader, u64::from(len) - 3);
|
||||
|
||||
// Multiple descriptors follow, but only the decoder configuration descriptor is useful.
|
||||
while scoped.bytes_available() > MIN_DESCRIPTOR_SIZE {
|
||||
let (desc, desc_len) = read_descriptor_header(&mut scoped)?;
|
||||
|
||||
match desc {
|
||||
DECODER_CONFIG_DESCRIPTOR => {
|
||||
dec_config = Some(DecoderConfigDescriptor::read(&mut scoped, desc_len)?);
|
||||
}
|
||||
SL_CONFIG_DESCRIPTOR => {
|
||||
sl_config = Some(SLDescriptor::read(&mut scoped, desc_len)?);
|
||||
}
|
||||
_ => {
|
||||
debug!("skipping {} object in es descriptor", desc);
|
||||
scoped.ignore_bytes(u64::from(desc_len))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Consume remaining bytes.
|
||||
scoped.ignore()?;
|
||||
|
||||
// Decoder configuration descriptor is mandatory.
|
||||
if dec_config.is_none() {
|
||||
return decode_error("isomp4: missing decoder config descriptor");
|
||||
}
|
||||
|
||||
// SL descriptor is mandatory.
|
||||
if sl_config.is_none() {
|
||||
return decode_error("isomp4: missing sl config descriptor");
|
||||
}
|
||||
|
||||
Ok(ESDescriptor { es_id, dec_config: dec_config.unwrap(), sl_config: sl_config.unwrap() })
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
class DecoderConfigDescriptor extends BaseDescriptor : bit(8) tag=DecoderConfigDescrTag {
|
||||
bit(8) objectTypeIndication;
|
||||
bit(6) streamType;
|
||||
bit(1) upStream;
|
||||
const bit(1) reserved=1;
|
||||
bit(24) bufferSizeDB;
|
||||
bit(32) maxBitrate;
|
||||
bit(32) avgBitrate;
|
||||
DecoderSpecificInfo decSpecificInfo[0 .. 1];
|
||||
profileLevelIndicationIndexDescriptor profileLevelIndicationIndexDescr [0..255];
|
||||
}
|
||||
*/
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct DecoderConfigDescriptor {
|
||||
pub codec_type: CodecType,
|
||||
pub object_type_indication: u8,
|
||||
pub dec_specific_info: Option<DecoderSpecificInfo>,
|
||||
}
|
||||
|
||||
impl ObjectDescriptor for DecoderConfigDescriptor {
|
||||
fn read<B: ReadBytes>(reader: &mut B, len: u32) -> Result<Self> {
|
||||
// AAC
|
||||
const OBJECT_TYPE_ISO14496_3: u8 = 0x40;
|
||||
const OBJECT_TYPE_ISO13818_7_MAIN: u8 = 0x66;
|
||||
const OBJECT_TYPE_ISO13818_7_LC: u8 = 0x67;
|
||||
// MP3
|
||||
const OBJECT_TYPE_ISO13818_3: u8 = 0x69;
|
||||
const OBJECT_TYPE_ISO11172_3: u8 = 0x6b;
|
||||
|
||||
let object_type_indication = reader.read_u8()?;
|
||||
|
||||
let (_stream_type, _upstream) = {
|
||||
let val = reader.read_u8()?;
|
||||
|
||||
if val & 0x1 != 1 {
|
||||
debug!("decoder config descriptor reserved bit is not 1");
|
||||
}
|
||||
|
||||
((val & 0xfc) >> 2, (val & 0x2) >> 1)
|
||||
};
|
||||
|
||||
let _buffer_size = reader.read_be_u24()?;
|
||||
let _max_bitrate = reader.read_be_u32()?;
|
||||
let _avg_bitrate = reader.read_be_u32()?;
|
||||
|
||||
let mut dec_specific_config = None;
|
||||
|
||||
let mut scoped = ScopedStream::new(reader, u64::from(len) - 13);
|
||||
|
||||
// Multiple descriptors follow, but only the decoder specific info descriptor is useful.
|
||||
while scoped.bytes_available() > MIN_DESCRIPTOR_SIZE {
|
||||
let (desc, desc_len) = read_descriptor_header(&mut scoped)?;
|
||||
|
||||
match desc {
|
||||
DECODER_SPECIFIC_DESCRIPTOR => {
|
||||
dec_specific_config = Some(DecoderSpecificInfo::read(&mut scoped, desc_len)?);
|
||||
}
|
||||
_ => {
|
||||
debug!("skipping {} object in decoder config descriptor", desc);
|
||||
scoped.ignore_bytes(u64::from(desc_len))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let codec_type = match object_type_indication {
|
||||
OBJECT_TYPE_ISO14496_3 | OBJECT_TYPE_ISO13818_7_LC | OBJECT_TYPE_ISO13818_7_MAIN => {
|
||||
CODEC_TYPE_AAC
|
||||
}
|
||||
OBJECT_TYPE_ISO13818_3 | OBJECT_TYPE_ISO11172_3 => CODEC_TYPE_MP3,
|
||||
_ => {
|
||||
debug!(
|
||||
"unknown object type indication {:#x} for decoder config descriptor",
|
||||
object_type_indication
|
||||
);
|
||||
|
||||
CODEC_TYPE_NULL
|
||||
}
|
||||
};
|
||||
|
||||
// Consume remaining bytes.
|
||||
scoped.ignore()?;
|
||||
|
||||
Ok(DecoderConfigDescriptor {
|
||||
codec_type,
|
||||
object_type_indication,
|
||||
dec_specific_info: dec_specific_config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DecoderSpecificInfo {
|
||||
pub extra_data: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl ObjectDescriptor for DecoderSpecificInfo {
|
||||
fn read<B: ReadBytes>(reader: &mut B, len: u32) -> Result<Self> {
|
||||
Ok(DecoderSpecificInfo { extra_data: reader.read_boxed_slice_exact(len as usize)? })
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
class SLConfigDescriptor extends BaseDescriptor : bit(8) tag=SLConfigDescrTag {
|
||||
bit(8) predefined;
|
||||
if (predefined==0) {
|
||||
bit(1) useAccessUnitStartFlag;
|
||||
bit(1) useAccessUnitEndFlag;
|
||||
bit(1) useRandomAccessPointFlag;
|
||||
bit(1) hasRandomAccessUnitsOnlyFlag;
|
||||
bit(1) usePaddingFlag;
|
||||
bit(1) useTimeStampsFlag;
|
||||
bit(1) useIdleFlag;
|
||||
bit(1) durationFlag;
|
||||
bit(32) timeStampResolution;
|
||||
bit(32) OCRResolution;
|
||||
bit(8) timeStampLength; // must be 64
|
||||
bit(8) OCRLength; // must be 64
|
||||
bit(8) AU_Length; // must be 32
|
||||
bit(8) instantBitrateLength;
|
||||
bit(4) degradationPriorityLength;
|
||||
bit(5) AU_seqNumLength; // must be 16
|
||||
bit(5) packetSeqNumLength; // must be 16
|
||||
bit(2) reserved=0b11;
|
||||
}
|
||||
if (durationFlag) {
|
||||
bit(32) timeScale;
|
||||
bit(16) accessUnitDuration;
|
||||
bit(16) compositionUnitDuration;
|
||||
}
|
||||
if (!useTimeStampsFlag) {
|
||||
bit(timeStampLength) startDecodingTimeStamp;
|
||||
bit(timeStampLength) startCompositionTimeStamp;
|
||||
}
|
||||
}
|
||||
|
||||
timeStampLength == 32, for predefined == 0x1
|
||||
timeStampLength == 0, for predefined == 0x2
|
||||
*/
|
||||
#[derive(Debug)]
|
||||
pub struct SLDescriptor;
|
||||
|
||||
impl ObjectDescriptor for SLDescriptor {
|
||||
fn read<B: ReadBytes>(reader: &mut B, len: u32) -> Result<Self> {
|
||||
// const SLCONFIG_PREDEFINED_CUSTOM: u8 = 0x0;
|
||||
const SLCONFIG_PREDEFINED_NULL: u8 = 0x1; // older iTunes M4A
|
||||
const SLCONFIG_PREDEFINED_MP4: u8 = 0x2;
|
||||
|
||||
let predefined = reader.read_u8()?;
|
||||
|
||||
match predefined {
|
||||
SLCONFIG_PREDEFINED_MP4 => {
|
||||
// Standard MP4: no extra fields. Nothing to read.
|
||||
}
|
||||
SLCONFIG_PREDEFINED_NULL => {
|
||||
// Older iTunes files use predefined=0x1. The SL descriptor in
|
||||
// this mode has no additional fields beyond the predefined byte,
|
||||
// so we just ignore the remaining bytes.
|
||||
if len > 1 {
|
||||
reader.ignore_bytes(u64::from(len - 1))?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return unsupported_error("isomp4: sl descriptor predefined not mp4 or null");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SLDescriptor {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Symphonia
|
||||
// Copyright (c) 2019-2022 The Project Symphonia Developers.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use symphonia_core::codecs::{CodecParameters, VerificationCheck, CODEC_TYPE_FLAC};
|
||||
use symphonia_core::errors::{decode_error, unsupported_error, Result};
|
||||
use symphonia_core::io::{BufReader, ReadBytes};
|
||||
|
||||
use symphonia_utils_xiph::flac::metadata::{MetadataBlockHeader, MetadataBlockType, StreamInfo};
|
||||
|
||||
use crate::atoms::{Atom, AtomHeader};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct FlacAtom {
|
||||
/// Atom header.
|
||||
header: AtomHeader,
|
||||
/// FLAC stream info block.
|
||||
stream_info: StreamInfo,
|
||||
/// FLAC extra data.
|
||||
extra_data: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl Atom for FlacAtom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
let (version, flags) = AtomHeader::read_extra(reader)?;
|
||||
|
||||
if version != 0 {
|
||||
return unsupported_error("isomp4 (flac): unsupported flac version");
|
||||
}
|
||||
|
||||
if flags != 0 {
|
||||
return decode_error("isomp4 (flac): flags not zero");
|
||||
}
|
||||
|
||||
// The first block must be the stream information block.
|
||||
let block_header = MetadataBlockHeader::read(reader)?;
|
||||
|
||||
if block_header.block_type != MetadataBlockType::StreamInfo {
|
||||
return decode_error("isomp4 (flac): first block is not stream info");
|
||||
}
|
||||
|
||||
// Ensure the block length is correct for a stream information block before allocating a
|
||||
// buffer for it.
|
||||
if !StreamInfo::is_valid_size(u64::from(block_header.block_len)) {
|
||||
return decode_error("isomp4 (flac): invalid stream info block length");
|
||||
}
|
||||
|
||||
let extra_data = reader.read_boxed_slice_exact(block_header.block_len as usize)?;
|
||||
let stream_info = StreamInfo::read(&mut BufReader::new(&extra_data))?;
|
||||
|
||||
Ok(FlacAtom { header, stream_info, extra_data })
|
||||
}
|
||||
}
|
||||
|
||||
impl FlacAtom {
|
||||
pub fn fill_codec_params(&self, codec_params: &mut CodecParameters) {
|
||||
codec_params
|
||||
.for_codec(CODEC_TYPE_FLAC)
|
||||
.with_sample_rate(self.stream_info.sample_rate)
|
||||
.with_bits_per_sample(self.stream_info.bits_per_sample)
|
||||
.with_channels(self.stream_info.channels)
|
||||
.with_packet_data_integrity(true)
|
||||
.with_extra_data(self.extra_data.clone());
|
||||
|
||||
if let Some(md5) = self.stream_info.md5 {
|
||||
codec_params.with_verification_code(VerificationCheck::Md5(md5));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Symphonia
|
||||
// Copyright (c) 2019-2022 The Project Symphonia Developers.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use symphonia_core::errors::{decode_error, Result};
|
||||
use symphonia_core::io::ReadBytes;
|
||||
|
||||
use crate::atoms::{Atom, AtomHeader};
|
||||
use crate::fourcc::FourCc;
|
||||
|
||||
/// File type atom.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct FtypAtom {
|
||||
header: AtomHeader,
|
||||
pub major: FourCc,
|
||||
pub minor: [u8; 4],
|
||||
pub compatible: Vec<FourCc>,
|
||||
}
|
||||
|
||||
impl Atom for FtypAtom {
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
// The Ftyp atom must be have a data length that is known, and it must be a multiple of 4
|
||||
// since it only stores FourCCs.
|
||||
if header.data_len < 8 || header.data_len & 0x3 != 0 {
|
||||
return decode_error("isomp4: invalid ftyp data length");
|
||||
}
|
||||
|
||||
// Major
|
||||
let major = FourCc::new(reader.read_quad_bytes()?);
|
||||
|
||||
// Minor
|
||||
let minor = reader.read_quad_bytes()?;
|
||||
|
||||
// The remainder of the Ftyp atom contains the FourCCs of compatible brands.
|
||||
let n_brands = (header.data_len - 8) / 4;
|
||||
|
||||
let mut compatible = Vec::new();
|
||||
|
||||
for _ in 0..n_brands {
|
||||
let brand = reader.read_quad_bytes()?;
|
||||
compatible.push(FourCc::new(brand));
|
||||
}
|
||||
|
||||
Ok(FtypAtom { header, major, minor, compatible })
|
||||
}
|
||||
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Symphonia
|
||||
// Copyright (c) 2019-2022 The Project Symphonia Developers.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use symphonia_core::errors::Result;
|
||||
use symphonia_core::io::ReadBytes;
|
||||
|
||||
use crate::{
|
||||
atoms::{Atom, AtomHeader},
|
||||
fourcc::FourCc,
|
||||
};
|
||||
|
||||
use log::warn;
|
||||
|
||||
/// Handler type.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum HandlerType {
|
||||
/// Video handler.
|
||||
Video,
|
||||
/// Audio handler.
|
||||
Sound,
|
||||
/// Subtitle handler.
|
||||
Subtitle,
|
||||
/// Metadata handler.
|
||||
Metadata,
|
||||
/// Text handler.
|
||||
Text,
|
||||
/// Unknown handler type.
|
||||
Other([u8; 4]),
|
||||
}
|
||||
|
||||
/// Handler atom.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct HdlrAtom {
|
||||
/// Atom header.
|
||||
header: AtomHeader,
|
||||
/// Handler type.
|
||||
pub handler_type: HandlerType,
|
||||
/// Human-readable handler name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Atom for HdlrAtom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
let (_, _) = AtomHeader::read_extra(reader)?;
|
||||
|
||||
// Always 0 for MP4, but for Quicktime this contains the component type.
|
||||
let _ = reader.read_quad_bytes()?;
|
||||
|
||||
let handler_type = match &reader.read_quad_bytes()? {
|
||||
b"vide" => HandlerType::Video,
|
||||
b"soun" => HandlerType::Sound,
|
||||
b"meta" => HandlerType::Metadata,
|
||||
b"subt" => HandlerType::Subtitle,
|
||||
b"text" => HandlerType::Text,
|
||||
&hdlr => {
|
||||
warn!("unknown handler type {:?}", FourCc::new(hdlr));
|
||||
HandlerType::Other(hdlr)
|
||||
}
|
||||
};
|
||||
|
||||
// These bytes are reserved for MP4, but for QuickTime they contain the component
|
||||
// manufacturer, flags, and flags mask.
|
||||
reader.ignore_bytes(4 * 3)?;
|
||||
|
||||
// Human readable UTF-8 string of the track type.
|
||||
let buf = reader.read_boxed_slice_exact((header.data_len - 24) as usize)?;
|
||||
let name = String::from_utf8_lossy(&buf).to_string();
|
||||
|
||||
Ok(HdlrAtom { header, handler_type, name })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,767 @@
|
||||
// Symphonia
|
||||
// Copyright (c) 2019-2022 The Project Symphonia Developers.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
use symphonia_core::errors::{decode_error, Result};
|
||||
use symphonia_core::io::{BufReader, ReadBytes};
|
||||
use symphonia_core::meta::{
|
||||
MetadataBuilder, MetadataRevision, StandardTagKey, StandardVisualKey, Tag,
|
||||
};
|
||||
use symphonia_core::meta::{Value, Visual};
|
||||
use symphonia_core::util::bits;
|
||||
use symphonia_metadata::{id3v1, itunes};
|
||||
|
||||
use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType};
|
||||
|
||||
use encoding_rs::{SHIFT_JIS, UTF_16BE};
|
||||
use log::warn;
|
||||
|
||||
/// Data type enumeration for metadata value atoms as defined in the QuickTime File Format standard.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum DataType {
|
||||
AffineTransformF64,
|
||||
Bmp,
|
||||
DimensionsF32,
|
||||
Float32,
|
||||
Float64,
|
||||
Jpeg,
|
||||
/// The data type is implicit to the atom.
|
||||
NoType,
|
||||
Png,
|
||||
PointF32,
|
||||
QuickTimeMetadata,
|
||||
RectF32,
|
||||
ShiftJis,
|
||||
SignedInt16,
|
||||
SignedInt32,
|
||||
SignedInt64,
|
||||
SignedInt8,
|
||||
SignedIntVariable,
|
||||
UnsignedInt16,
|
||||
UnsignedInt32,
|
||||
UnsignedInt64,
|
||||
UnsignedInt8,
|
||||
UnsignedIntVariable,
|
||||
Utf16,
|
||||
Utf16Sort,
|
||||
Utf8,
|
||||
Utf8Sort,
|
||||
Unknown(u32),
|
||||
}
|
||||
|
||||
impl From<u32> for DataType {
|
||||
fn from(value: u32) -> Self {
|
||||
match value {
|
||||
0 => DataType::NoType,
|
||||
1 => DataType::Utf8,
|
||||
2 => DataType::Utf16,
|
||||
3 => DataType::ShiftJis,
|
||||
4 => DataType::Utf8Sort,
|
||||
5 => DataType::Utf16Sort,
|
||||
13 => DataType::Jpeg,
|
||||
14 => DataType::Png,
|
||||
21 => DataType::SignedIntVariable,
|
||||
22 => DataType::UnsignedIntVariable,
|
||||
23 => DataType::Float32,
|
||||
24 => DataType::Float64,
|
||||
27 => DataType::Bmp,
|
||||
28 => DataType::QuickTimeMetadata,
|
||||
65 => DataType::SignedInt8,
|
||||
66 => DataType::SignedInt16,
|
||||
67 => DataType::SignedInt32,
|
||||
70 => DataType::PointF32,
|
||||
71 => DataType::DimensionsF32,
|
||||
72 => DataType::RectF32,
|
||||
74 => DataType::SignedInt64,
|
||||
75 => DataType::UnsignedInt8,
|
||||
76 => DataType::UnsignedInt16,
|
||||
77 => DataType::UnsignedInt32,
|
||||
78 => DataType::UnsignedInt64,
|
||||
79 => DataType::AffineTransformF64,
|
||||
_ => DataType::Unknown(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_no_type(data: &[u8]) -> Option<Value> {
|
||||
// Latin1, potentially null-terminated.
|
||||
let end = data.iter().position(|&c| c == b'\0').unwrap_or(data.len());
|
||||
let text = String::from_utf8_lossy(&data[..end]);
|
||||
Some(Value::from(text))
|
||||
}
|
||||
|
||||
fn parse_utf8(data: &[u8]) -> Option<Value> {
|
||||
// UTF8, no null-terminator or count.
|
||||
let text = String::from_utf8_lossy(data);
|
||||
Some(Value::from(text))
|
||||
}
|
||||
|
||||
fn parse_utf16(data: &[u8]) -> Option<Value> {
|
||||
// UTF16 BE
|
||||
let text = UTF_16BE.decode(data).0;
|
||||
Some(Value::from(text))
|
||||
}
|
||||
|
||||
fn parse_shift_jis(data: &[u8]) -> Option<Value> {
|
||||
// Shift-JIS
|
||||
let text = SHIFT_JIS.decode(data).0;
|
||||
Some(Value::from(text))
|
||||
}
|
||||
|
||||
fn parse_signed_int8(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
1 => {
|
||||
let s = bits::sign_extend_leq8_to_i8(data[0], 8);
|
||||
Some(Value::from(s))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_signed_int16(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
2 => {
|
||||
let u = BufReader::new(data).read_be_u16().ok()?;
|
||||
let s = bits::sign_extend_leq16_to_i16(u, 16);
|
||||
Some(Value::from(s))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_signed_int32(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
4 => {
|
||||
let u = BufReader::new(data).read_be_u32().ok()?;
|
||||
let s = bits::sign_extend_leq32_to_i32(u, 32);
|
||||
Some(Value::from(s))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_signed_int64(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
8 => {
|
||||
let u = BufReader::new(data).read_be_u64().ok()?;
|
||||
let s = bits::sign_extend_leq64_to_i64(u, 64);
|
||||
Some(Value::from(s))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_var_signed_int(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
1 => parse_signed_int8(data),
|
||||
2 => parse_signed_int16(data),
|
||||
4 => parse_signed_int32(data),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_unsigned_int8(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
1 => Some(Value::from(data[0])),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_unsigned_int16(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
2 => {
|
||||
let u = BufReader::new(data).read_be_u16().ok()?;
|
||||
Some(Value::from(u))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_unsigned_int32(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
4 => {
|
||||
let u = BufReader::new(data).read_be_u32().ok()?;
|
||||
Some(Value::from(u))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_unsigned_int64(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
8 => {
|
||||
let u = BufReader::new(data).read_be_u64().ok()?;
|
||||
Some(Value::from(u))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_var_unsigned_int(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
1 => parse_unsigned_int8(data),
|
||||
2 => parse_unsigned_int16(data),
|
||||
4 => parse_unsigned_int32(data),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_float32(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
4 => {
|
||||
let f = BufReader::new(data).read_be_f32().ok()?;
|
||||
Some(Value::Float(f64::from(f)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_float64(data: &[u8]) -> Option<Value> {
|
||||
match data.len() {
|
||||
8 => {
|
||||
let f = BufReader::new(data).read_be_f64().ok()?;
|
||||
Some(Value::Float(f))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_tag_value(data_type: DataType, data: &[u8]) -> Option<Value> {
|
||||
match data_type {
|
||||
DataType::NoType => parse_no_type(data),
|
||||
DataType::Utf8 | DataType::Utf8Sort => parse_utf8(data),
|
||||
DataType::Utf16 | DataType::Utf16Sort => parse_utf16(data),
|
||||
DataType::ShiftJis => parse_shift_jis(data),
|
||||
DataType::UnsignedInt8 => parse_unsigned_int8(data),
|
||||
DataType::UnsignedInt16 => parse_unsigned_int16(data),
|
||||
DataType::UnsignedInt32 => parse_unsigned_int32(data),
|
||||
DataType::UnsignedInt64 => parse_unsigned_int64(data),
|
||||
DataType::UnsignedIntVariable => parse_var_unsigned_int(data),
|
||||
DataType::SignedInt8 => parse_signed_int8(data),
|
||||
DataType::SignedInt16 => parse_signed_int16(data),
|
||||
DataType::SignedInt32 => parse_signed_int32(data),
|
||||
DataType::SignedInt64 => parse_signed_int64(data),
|
||||
DataType::SignedIntVariable => parse_var_signed_int(data),
|
||||
DataType::Float32 => parse_float32(data),
|
||||
DataType::Float64 => parse_float64(data),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads and parses a `MetaTagAtom` from the provided iterator and adds it to the `MetadataBuilder`
|
||||
/// if there are no errors.
|
||||
fn add_generic_tag<B: ReadBytes>(
|
||||
iter: &mut AtomIterator<B>,
|
||||
builder: &mut MetadataBuilder,
|
||||
std_key: Option<StandardTagKey>,
|
||||
) -> Result<()> {
|
||||
let tag = iter.read_atom::<MetaTagAtom>()?;
|
||||
|
||||
for value_atom in tag.values.iter() {
|
||||
// Parse the value atom data into a string, if possible.
|
||||
if let Some(value) = parse_tag_value(value_atom.data_type, &value_atom.data) {
|
||||
builder.add_tag(Tag::new(std_key, "", value));
|
||||
}
|
||||
else {
|
||||
warn!("unsupported data type {:?} for {:?} tag", value_atom.data_type, std_key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_var_unsigned_int_tag<B: ReadBytes>(
|
||||
iter: &mut AtomIterator<B>,
|
||||
builder: &mut MetadataBuilder,
|
||||
std_key: StandardTagKey,
|
||||
) -> Result<()> {
|
||||
let tag = iter.read_atom::<MetaTagAtom>()?;
|
||||
|
||||
if let Some(value_atom) = tag.values.first() {
|
||||
if let Some(value) = parse_var_unsigned_int(&value_atom.data) {
|
||||
builder.add_tag(Tag::new(Some(std_key), "", value));
|
||||
}
|
||||
else {
|
||||
warn!("got unexpected data for {:?} tag", std_key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_var_signed_int_tag<B: ReadBytes>(
|
||||
iter: &mut AtomIterator<B>,
|
||||
builder: &mut MetadataBuilder,
|
||||
std_key: StandardTagKey,
|
||||
) -> Result<()> {
|
||||
let tag = iter.read_atom::<MetaTagAtom>()?;
|
||||
|
||||
if let Some(value_atom) = tag.values.first() {
|
||||
if let Some(value) = parse_var_signed_int(&value_atom.data) {
|
||||
builder.add_tag(Tag::new(Some(std_key), "", value));
|
||||
}
|
||||
else {
|
||||
warn!("got unexpected data for {:?} tag", std_key);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_boolean_tag<B: ReadBytes>(
|
||||
iter: &mut AtomIterator<B>,
|
||||
builder: &mut MetadataBuilder,
|
||||
std_key: StandardTagKey,
|
||||
) -> Result<()> {
|
||||
let tag = iter.read_atom::<MetaTagAtom>()?;
|
||||
|
||||
// There should only be 1 value.
|
||||
if let Some(value) = tag.values.first() {
|
||||
// Boolean tags are just "flags", only add a tag if the boolean is true (1).
|
||||
if let Some(bool_value) = value.data.first() {
|
||||
if *bool_value == 1 {
|
||||
builder.add_tag(Tag::new(Some(std_key), "", Value::Flag));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_m_of_n_tag<B: ReadBytes>(
|
||||
iter: &mut AtomIterator<B>,
|
||||
builder: &mut MetadataBuilder,
|
||||
m_key: StandardTagKey,
|
||||
n_key: StandardTagKey,
|
||||
) -> Result<()> {
|
||||
let tag = iter.read_atom::<MetaTagAtom>()?;
|
||||
|
||||
// There should only be 1 value.
|
||||
if let Some(value) = tag.values.first() {
|
||||
// The trkn and disk atoms contains an 8 byte value buffer, where the 4th and 6th bytes
|
||||
// indicate the track/disk number and total number of tracks/disks, respectively. Odd.
|
||||
if value.data.len() == 8 {
|
||||
let m = value.data[3];
|
||||
let n = value.data[5];
|
||||
|
||||
builder.add_tag(Tag::new(Some(m_key), "", Value::from(m)));
|
||||
builder.add_tag(Tag::new(Some(n_key), "", Value::from(n)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_visual_tag<B: ReadBytes>(
|
||||
iter: &mut AtomIterator<B>,
|
||||
builder: &mut MetadataBuilder,
|
||||
) -> Result<()> {
|
||||
let tag = iter.read_atom::<MetaTagAtom>()?;
|
||||
|
||||
// There could be more than one attached image.
|
||||
for value in tag.values {
|
||||
let media_type = match value.data_type {
|
||||
DataType::Bmp => "image/bmp",
|
||||
DataType::Jpeg => "image/jpeg",
|
||||
DataType::Png => "image/png",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
builder.add_visual(Visual {
|
||||
media_type: media_type.into(),
|
||||
dimensions: None,
|
||||
bits_per_pixel: None,
|
||||
color_mode: None,
|
||||
usage: Some(StandardVisualKey::FrontCover),
|
||||
tags: Default::default(),
|
||||
data: value.data,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_advisory_tag<B: ReadBytes>(
|
||||
_iter: &mut AtomIterator<B>,
|
||||
_builder: &mut MetadataBuilder,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_media_type_tag<B: ReadBytes>(
|
||||
iter: &mut AtomIterator<B>,
|
||||
builder: &mut MetadataBuilder,
|
||||
) -> Result<()> {
|
||||
let tag = iter.read_atom::<MetaTagAtom>()?;
|
||||
|
||||
// There should only be 1 value.
|
||||
if let Some(value) = tag.values.first() {
|
||||
if let Some(media_type_value) = value.data.first() {
|
||||
let media_type = match media_type_value {
|
||||
0 => "Movie",
|
||||
1 => "Normal",
|
||||
2 => "Audio Book",
|
||||
5 => "Whacked Bookmark",
|
||||
6 => "Music Video",
|
||||
9 => "Short Film",
|
||||
10 => "TV Show",
|
||||
11 => "Booklet",
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
builder.add_tag(Tag::new(
|
||||
Some(StandardTagKey::MediaFormat),
|
||||
"",
|
||||
Value::from(media_type),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_id3v1_genre_tag<B: ReadBytes>(
|
||||
iter: &mut AtomIterator<B>,
|
||||
builder: &mut MetadataBuilder,
|
||||
) -> Result<()> {
|
||||
let tag = iter.read_atom::<MetaTagAtom>()?;
|
||||
|
||||
// There should only be 1 value.
|
||||
if let Some(value) = tag.values.first() {
|
||||
// The ID3v1 genre is stored as a unsigned 16-bit big-endian integer.
|
||||
let index = BufReader::new(&value.data).read_be_u16()?;
|
||||
|
||||
// The stored index uses 1-based indexing, but the ID3v1 genre list is 0-based.
|
||||
if index > 0 {
|
||||
if let Some(genre) = id3v1::util::genre_name((index - 1) as u8) {
|
||||
builder.add_tag(Tag::new(Some(StandardTagKey::Genre), "", Value::from(*genre)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_freeform_tag<B: ReadBytes>(
|
||||
iter: &mut AtomIterator<B>,
|
||||
builder: &mut MetadataBuilder,
|
||||
) -> Result<()> {
|
||||
let tag = iter.read_atom::<MetaTagAtom>()?;
|
||||
|
||||
// A user-defined tag should only have 1 value.
|
||||
for value_atom in tag.values.iter() {
|
||||
// Parse the value atom data into a string, if possible.
|
||||
if let Some(value) = parse_tag_value(value_atom.data_type, &value_atom.data) {
|
||||
// Gets the fully qualified tag name.
|
||||
let full_name = tag.full_name();
|
||||
|
||||
// Try to map iTunes freeform tags to standard tag keys.
|
||||
let std_key = itunes::std_key_from_tag(&full_name);
|
||||
|
||||
builder.add_tag(Tag::new(std_key, &full_name, value));
|
||||
}
|
||||
else {
|
||||
warn!("unsupported data type {:?} for free-form tag", value_atom.data_type);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Metadata tag data atom.
|
||||
#[allow(dead_code)]
|
||||
pub struct MetaTagDataAtom {
|
||||
/// Atom header.
|
||||
header: AtomHeader,
|
||||
/// Tag data.
|
||||
pub data: Box<[u8]>,
|
||||
/// The data type contained in buf.
|
||||
pub data_type: DataType,
|
||||
}
|
||||
|
||||
impl Atom for MetaTagDataAtom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
let (version, flags) = AtomHeader::read_extra(reader)?;
|
||||
|
||||
// For the mov brand, this a data type indicator and must always be 0 (well-known type). It
|
||||
// specifies the table in which the next 24-bit integer specifying the actual data type
|
||||
// indexes. For iso/mp4, this is a version, and there is only one version, 0. Therefore,
|
||||
// flags are interpreted as the actual data type index.
|
||||
if version != 0 {
|
||||
return decode_error("isomp4: invalid data atom version");
|
||||
}
|
||||
|
||||
let data_type = DataType::from(flags);
|
||||
|
||||
// For the mov brand, the next four bytes are country and languages code. However, for
|
||||
// iso/mp4 these codes should be ignored.
|
||||
let _country = reader.read_be_u16()?;
|
||||
let _language = reader.read_be_u16()?;
|
||||
|
||||
// The data payload is the remainder of the atom.
|
||||
// TODO: Apply a limit.
|
||||
let data = reader
|
||||
.read_boxed_slice_exact((header.data_len - AtomHeader::EXTRA_DATA_SIZE - 4) as usize)?;
|
||||
|
||||
Ok(MetaTagDataAtom { header, data, data_type })
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata tag name and mean atom.
|
||||
#[allow(dead_code)]
|
||||
pub struct MetaTagNamespaceAtom {
|
||||
/// Atom header.
|
||||
header: AtomHeader,
|
||||
/// For 'mean' atoms, this is the key namespace. For 'name' atom, this is the key name.
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
impl Atom for MetaTagNamespaceAtom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
let (_, _) = AtomHeader::read_extra(reader)?;
|
||||
|
||||
let buf = reader
|
||||
.read_boxed_slice_exact((header.data_len - AtomHeader::EXTRA_DATA_SIZE) as usize)?;
|
||||
|
||||
// Do a lossy conversion because metadata should not prevent the demuxer from working.
|
||||
let value = String::from_utf8_lossy(&buf).to_string();
|
||||
|
||||
Ok(MetaTagNamespaceAtom { header, value })
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic metadata tag atom.
|
||||
#[allow(dead_code)]
|
||||
pub struct MetaTagAtom {
|
||||
/// Atom header.
|
||||
header: AtomHeader,
|
||||
/// Tag value(s).
|
||||
pub values: Vec<MetaTagDataAtom>,
|
||||
/// Optional, tag key namespace.
|
||||
pub mean: Option<MetaTagNamespaceAtom>,
|
||||
/// Optional, tag key name.
|
||||
pub name: Option<MetaTagNamespaceAtom>,
|
||||
}
|
||||
|
||||
impl MetaTagAtom {
|
||||
pub fn full_name(&self) -> String {
|
||||
let mut full_name = String::new();
|
||||
|
||||
if self.mean.is_some() || self.name.is_some() {
|
||||
// full_name.push_str("----:");
|
||||
|
||||
if let Some(mean) = &self.mean {
|
||||
full_name.push_str(&mean.value);
|
||||
}
|
||||
|
||||
full_name.push(':');
|
||||
|
||||
if let Some(name) = &self.name {
|
||||
full_name.push_str(&name.value);
|
||||
}
|
||||
}
|
||||
|
||||
full_name
|
||||
}
|
||||
}
|
||||
|
||||
impl Atom for MetaTagAtom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
let mut iter = AtomIterator::new(reader, header);
|
||||
|
||||
let mut mean = None;
|
||||
let mut name = None;
|
||||
let mut values = Vec::new();
|
||||
|
||||
while let Some(header) = iter.next()? {
|
||||
match header.atype {
|
||||
AtomType::MetaTagData => {
|
||||
values.push(iter.read_atom::<MetaTagDataAtom>()?);
|
||||
}
|
||||
AtomType::MetaTagName => {
|
||||
name = Some(iter.read_atom::<MetaTagNamespaceAtom>()?);
|
||||
}
|
||||
AtomType::MetaTagMeaning => {
|
||||
mean = Some(iter.read_atom::<MetaTagNamespaceAtom>()?);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MetaTagAtom { header, values, mean, name })
|
||||
}
|
||||
}
|
||||
|
||||
/// User data atom.
|
||||
#[allow(dead_code)]
|
||||
pub struct IlstAtom {
|
||||
/// Atom header.
|
||||
header: AtomHeader,
|
||||
/// Metadata revision.
|
||||
pub metadata: MetadataRevision,
|
||||
}
|
||||
|
||||
impl Atom for IlstAtom {
|
||||
fn header(&self) -> AtomHeader {
|
||||
self.header
|
||||
}
|
||||
|
||||
fn read<B: ReadBytes>(reader: &mut B, header: AtomHeader) -> Result<Self> {
|
||||
let mut iter = AtomIterator::new(reader, header);
|
||||
|
||||
let mut mb = MetadataBuilder::new();
|
||||
|
||||
while let Some(header) = iter.next()? {
|
||||
// Ignore standard atoms, check if other is a metadata atom.
|
||||
match &header.atype {
|
||||
AtomType::AdvisoryTag => add_advisory_tag(&mut iter, &mut mb)?,
|
||||
AtomType::AlbumArtistTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::AlbumArtist))?
|
||||
}
|
||||
AtomType::AlbumTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Album))?
|
||||
}
|
||||
AtomType::ArtistLowerTag => (),
|
||||
AtomType::ArtistTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Artist))?
|
||||
}
|
||||
AtomType::CategoryTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::PodcastCategory))?
|
||||
}
|
||||
AtomType::CommentTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Comment))?
|
||||
}
|
||||
AtomType::CompilationTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Compilation))?
|
||||
}
|
||||
AtomType::ComposerTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Composer))?
|
||||
}
|
||||
AtomType::CopyrightTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Copyright))?
|
||||
}
|
||||
AtomType::CoverTag => add_visual_tag(&mut iter, &mut mb)?,
|
||||
AtomType::CustomGenreTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Genre))?
|
||||
}
|
||||
AtomType::DateTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Date))?
|
||||
}
|
||||
AtomType::DescriptionTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Description))?
|
||||
}
|
||||
AtomType::DiskNumberTag => add_m_of_n_tag(
|
||||
&mut iter,
|
||||
&mut mb,
|
||||
StandardTagKey::DiscNumber,
|
||||
StandardTagKey::DiscTotal,
|
||||
)?,
|
||||
AtomType::EncodedByTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::EncodedBy))?
|
||||
}
|
||||
AtomType::EncoderTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Encoder))?
|
||||
}
|
||||
AtomType::GaplessPlaybackTag => {
|
||||
// TODO: Need standard tag key for gapless playback.
|
||||
// add_boolean_tag(&mut iter, &mut mb, )?
|
||||
}
|
||||
AtomType::GenreTag => add_id3v1_genre_tag(&mut iter, &mut mb)?,
|
||||
AtomType::GroupingTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::ContentGroup))?
|
||||
}
|
||||
AtomType::HdVideoTag => (),
|
||||
AtomType::IdentPodcastTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::IdentPodcast))?
|
||||
}
|
||||
AtomType::KeywordTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::PodcastKeywords))?
|
||||
}
|
||||
AtomType::LongDescriptionTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Description))?
|
||||
}
|
||||
AtomType::LyricsTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Lyrics))?
|
||||
}
|
||||
AtomType::MediaTypeTag => add_media_type_tag(&mut iter, &mut mb)?,
|
||||
AtomType::OwnerTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Owner))?
|
||||
}
|
||||
AtomType::PodcastTag => {
|
||||
add_boolean_tag(&mut iter, &mut mb, StandardTagKey::Podcast)?
|
||||
}
|
||||
AtomType::PurchaseDateTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::PurchaseDate))?
|
||||
}
|
||||
AtomType::RatingTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Rating))?
|
||||
}
|
||||
AtomType::SortAlbumArtistTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::SortAlbumArtist))?
|
||||
}
|
||||
AtomType::SortAlbumTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::SortAlbum))?
|
||||
}
|
||||
AtomType::SortArtistTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::SortArtist))?
|
||||
}
|
||||
AtomType::SortComposerTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::SortComposer))?
|
||||
}
|
||||
AtomType::SortNameTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::SortTrackTitle))?
|
||||
}
|
||||
AtomType::TempoTag => {
|
||||
add_var_signed_int_tag(&mut iter, &mut mb, StandardTagKey::Bpm)?
|
||||
}
|
||||
AtomType::TrackNumberTag => add_m_of_n_tag(
|
||||
&mut iter,
|
||||
&mut mb,
|
||||
StandardTagKey::TrackNumber,
|
||||
StandardTagKey::TrackTotal,
|
||||
)?,
|
||||
AtomType::TrackTitleTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::TrackTitle))?
|
||||
}
|
||||
AtomType::TvEpisodeNameTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::TvEpisodeTitle))?
|
||||
}
|
||||
AtomType::TvEpisodeNumberTag => {
|
||||
add_var_unsigned_int_tag(&mut iter, &mut mb, StandardTagKey::TvEpisode)?
|
||||
}
|
||||
AtomType::TvNetworkNameTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::TvNetwork))?
|
||||
}
|
||||
AtomType::TvSeasonNumberTag => {
|
||||
add_var_unsigned_int_tag(&mut iter, &mut mb, StandardTagKey::TvSeason)?
|
||||
}
|
||||
AtomType::TvShowNameTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::TvShowTitle))?
|
||||
}
|
||||
AtomType::UrlPodcastTag => {
|
||||
add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::UrlPodcast))?
|
||||
}
|
||||
AtomType::FreeFormTag => add_freeform_tag(&mut iter, &mut mb)?,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(IlstAtom { header, metadata: mb.metadata() })
|
||||
}
|
||||
}
|
||||