- name
- justanothermusicclient-tauri-youtube-music
- description
- Build and extend JustAnotherMusicClient, a Tauri-based desktop YouTube Music client for Windows, macOS, and Linux
- triggers
- ["how do I build JustAnotherMusicClient","set up development environment for YouTube Music desktop client","contribute to JustAnotherMusicClient project","create a Tauri music client like JustAnotherMusicClient","troubleshoot JustAnotherMusicClient build issues","add features to JustAnotherMusicClient","integrate Discord RPC in Tauri app","work with YouTube Music in Tauri"]
# JustAnotherMusicClient Development Skill
> Skill by [ara.so](https://ara.so) — Devtools Skills collection.
## What is JustAnotherMusicClient?
JustAnotherMusicClient is a desktop YouTube Music client built with **Tauri**, **React**, and **TypeScript**. It provides a native desktop experience for YouTube Music with features like multiple tabs, Discord RPC, synced lyrics, caching, and cross-platform support (Windows, macOS, Linux).
**Key Technologies:**
- **Tauri** - Rust-based desktop framework
- **React** - Frontend UI
- **TypeScript** - Type-safe development
- **YouTube Music Integration** - No official API, uses web scraping/injection
## Prerequisites
Before working with JustAnotherMusicClient, ensure you have:
```bash
# Node.js LTS and npm
node --version # v18+ recommended
npm --version
# Rust and Cargo
rustc --version
cargo --version
# Windows only: C++ build tools (Visual Studio Build Tools)
# macOS: Xcode Command Line Tools
# Linux: build-essential, libgtk-3-dev, libwebkit2gtk-4.0-dev, etc.
```
### Platform-Specific Requirements
**Windows:**
- Microsoft Edge WebView2 Runtime
- Visual Studio Build Tools with C++ workload
**macOS:**
- Xcode Command Line Tools: `xcode-select --install`
**Linux (Debian/Ubuntu):**
```bash
sudo apt update
sudo apt install libwebkit2gtk-4.0-dev \
build-essential \
curl \
wget \
libssl-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev
```
## Installation & Setup
### Clone and Install Dependencies
```bash
# Clone the repository
git clone https://github.com/2latemc/JustAnotherMusicClient.git
cd JustAnotherMusicClient
# Install npm dependencies (includes Tauri CLI)
npm install
```
### Project Structure
```
JustAnotherMusicClient/
├── src/ # React frontend source
│ ├── components/ # UI components
│ ├── hooks/ # Custom React hooks
│ ├── services/ # API/service layer
│ └── App.tsx # Main app component
├── src-tauri/ # Rust backend
│ ├── src/
│ │ └── main.rs # Tauri entry point
│ ├── Cargo.toml # Rust dependencies
│ └── tauri.conf.json # Tauri configuration
├── package.json # Node dependencies
└── tsconfig.json # TypeScript config
```
## Development Commands
### Run Development Server
```bash
# Start Tauri dev server with hot reload
npm run tauri dev
```
This will:
1. Start the React dev server (Vite)
2. Compile and run the Rust backend
3. Open the application window
4. Enable hot module replacement for frontend changes
### Build for Production
```bash
# Create production build
npm run tauri build
```
Outputs are in `src-tauri/target/release/bundle/`:
- **Windows:** `.msi` installer
- **macOS:** `.dmg` and `.app`
- **Linux:** `.deb`, `.AppImage`
### Lint and Type Check
```bash
# TypeScript type checking
npm run type-check
# ESLint
npm run lint
# Format with Prettier
npm run format
```
## Key Configuration Files
### tauri.conf.json
Located at `src-tauri/tauri.conf.json`, this controls app behavior:
```json
{
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devPath": "http://localhost:1420",
"distDir": "../dist"
},
"package": {
"productName": "JustAnotherMusicClient",
"version": "1.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"all": false,
"open": true
},
"window": {
"all": true,
"create": true,
"center": true,
"setDecorations": true
}
},
"bundle": {
"active": true,
"identifier": "com.2late.justanothermusicclient",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"security": {
"csp": "default-src 'self'; connect-src https://music.youtube.com"
},
"windows": [
{
"title": "JustAnotherMusicClient",
"width": 1200,
"height": 800,
"resizable": true,
"fullscreen": false
}
]
}
}
```
### Cargo.toml (Rust Dependencies)
Located at `src-tauri/Cargo.toml`:
```toml
[package]
name = "justanothermusicclient"
version = "1.0.0"
edition = "2021"
[dependencies]
tauri = { version = "1.5", features = ["shell-open", "window-all"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
[build-dependencies]
tauri-build = { version = "1.5" }
```
## Common Development Patterns
### Adding a Tauri Command (Rust Backend)
In `src-tauri/src/main.rs`:
```rust
use tauri::State;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Song {
title: String,
artist: String,
duration: u32,
}
// Define a Tauri command
#[tauri::command]
fn get_current_song() -> Result<Song, String> {
// Your logic here
Ok(Song {
title: "Song Title".to_string(),
artist: "Artist Name".to_string(),
duration: 180,
})
}
// State management example
struct AppState {
volume: std::sync::Mutex<f32>,
}
#[tauri::command]
fn set_volume(state: State<AppState>, volume: f32) -> Result<(), String> {
let mut vol = state.volume.lock().unwrap();
*vol = volume.clamp(0.0, 1.0);
Ok(())
}
fn main() {
tauri::Builder::default()
.manage(AppState {
volume: std::sync::Mutex::new(1.0),
})
.invoke_handler(tauri::generate_handler![
get_current_song,
set_volume
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
### Calling Tauri Commands from React/TypeScript
In your React components:
```typescript
import { invoke } from '@tauri-apps/api/tauri';
interface Song {
title: string;
artist: string;
duration: number;
}
// Call Rust command from frontend
async function fetchCurrentSong(): Promise<Song> {
try {
const song = await invoke<Song>('get_current_song');
return song;
} catch (error) {
console.error('Failed to fetch song:', error);
throw error;
}
}
// Set volume example
async function updateVolume(volume: number): Promise<void> {
await invoke('set_volume', { volume });
}
// Use in component
function NowPlaying() {
const [song, setSong] = React.useState<Song | null>(null);
React.useEffect(() => {
fetchCurrentSong().then(setSong);
}, []);
return (
<div>
{song && (
<>
<h2>{song.title}</h2>
<p>{song.artist}</p>
</>
)}
</div>
);
}
```
### Window Management
```typescript
import { appWindow } from '@tauri-apps/api/window';
// Minimize window
await appWindow.minimize();
// Maximize/restore toggle
await appWindow.toggleMaximize();
// Close window
await appWindow.close();
// Create new window
import { WebviewWindow } from '@tauri-apps/api/window';
const miniPlayer = new WebviewWindow('miniPlayer', {
url: '/mini-player',
title: 'Mini Player',
width: 300,
height: 100,
decorations: false,
alwaysOnTop: true,
});
```
### System Tray Integration
In `src-tauri/src/main.rs`:
```rust
use tauri::{CustomMenuItem, SystemTray, SystemTrayMenu, SystemTrayEvent};
use tauri::Manager;
fn main() {
let play_pause = CustomMenuItem::new("play_pause".to_string(), "Play/Pause");
let quit = CustomMenuItem::new("quit".to_string(), "Quit");
let tray_menu = SystemTrayMenu::new()
.add_item(play_pause)
.add_item(quit);
let system_tray = SystemTray::new()
.with_menu(tray_menu);
tauri::Builder::default()
.system_tray(system_tray)
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::MenuItemClick { id, .. } => {
match id.as_str() {
"play_pause" => {
// Emit event to frontend
app.emit_all("play-pause", ()).unwrap();
}
"quit" => {
std::process::exit(0);
}
_ => {}
}
}
_ => {}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
### Discord Rich Presence Integration
Add to `Cargo.toml`:
```toml
[dependencies]
discord-rich-presence = "0.2"
```
In Rust:
```rust
use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient};
#[tauri::command]
fn update_discord_rpc(song_title: String, artist: String) -> Result<(), String> {
let mut client = DiscordIpcClient::new("YOUR_APP_ID")
.map_err(|e| e.to_string())?;
client.connect().map_err(|e| e.to_string())?;
let activity = activity::Activity::new()
.state(&artist)
.details(&song_title)
.assets(
activity::Assets::new()
.large_image("music_icon")
.large_text("Listening to music")
);
client.set_activity(activity)
.map_err(|e| e.to_string())?;
Ok(())
}
```
### Local Storage / Cache Management
```typescript
import { BaseDirectory, createDir, writeTextFile, readTextFile } from '@tauri-apps/api/fs';
// Save cache
async function saveCachedData(key: string, data: any): Promise<void> {
const json = JSON.stringify(data);
await writeTextFile(`cache/${key}.json`, json, {
dir: BaseDirectory.AppData,
});
}
// Read cache
async function readCachedData<T>(key: string): Promise<T | null> {
try {
const json = await readTextFile(`cache/${key}.json`, {
dir: BaseDirectory.AppData,
});
return JSON.parse(json);
} catch {
return null;
}
}
// Initialize cache directory
async function initCache(): Promise<void> {
await createDir('cache', {
dir: BaseDirectory.AppData,
recursive: true,
});
}
```
## Working with YouTube Music Integration
### Injecting JavaScript into WebView
In `tauri.conf.json`, configure a WebView window for YouTube Music:
```json
{
"tauri": {
"windows": [
{
在 GitHub 查看