Fix stream glitching, suppress ffmpeg noise, green progress bar, bug fixes
- StreamingWorker: replace -re with token-bucket pacing; -ss before -i for fast seek; add -write_xing 0; halve chunk size to 2048 bytes. Eliminates startup burst and glitchy audio on listener side. - Suppress Qt6 internal ffmpeg AV_LOG_WARNING noise via ctypes av_log_set_level - Progress bar colour changed from blue to green - server.py: drain ffmpeg stderr pipe in transcoder to prevent deadlock - Fix waveform/BPM thread signal race (disconnect before replacing thread) - Fix _roll_end: clamp real_pos to track duration - Fix open_settings: wrap file write in try/except - Fix hot cue initial tooltip text - Remove src-tauri (Tauri desktop wrapper removed)
This commit is contained in:
parent
0ec02507b3
commit
69c078071d
|
|
@ -10,6 +10,7 @@ PyQt6
|
||||||
sounddevice
|
sounddevice
|
||||||
soundfile
|
soundfile
|
||||||
numpy
|
numpy
|
||||||
|
librosa
|
||||||
requests
|
requests
|
||||||
python-socketio[client]
|
python-socketio[client]
|
||||||
yt-dlp
|
yt-dlp
|
||||||
|
|
|
||||||
14
server.py
14
server.py
|
|
@ -193,6 +193,20 @@ def _start_transcoder_if_needed(is_mp3_input=False):
|
||||||
# Define greenlets INSIDE so they close over THIS specific 'proc'.
|
# Define greenlets INSIDE so they close over THIS specific 'proc'.
|
||||||
# Blocking subprocess pipe I/O is delegated to eventlet.tpool so it runs
|
# Blocking subprocess pipe I/O is delegated to eventlet.tpool so it runs
|
||||||
# in a real OS thread, preventing it from stalling the eventlet hub.
|
# in a real OS thread, preventing it from stalling the eventlet hub.
|
||||||
|
|
||||||
|
def _stderr_drain(proc):
|
||||||
|
"""Drain ffmpeg's stderr pipe so it never fills the OS buffer (64 KB on
|
||||||
|
Linux) and deadlocks the ffmpeg process. Errors are printed and logged."""
|
||||||
|
try:
|
||||||
|
for raw in iter(proc.stderr.readline, b''):
|
||||||
|
line = raw.decode('utf-8', errors='replace').strip()
|
||||||
|
if line:
|
||||||
|
print(f'[FFMPEG] {line}')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
eventlet.spawn(_stderr_drain, _ffmpeg_proc)
|
||||||
|
|
||||||
def _writer(proc):
|
def _writer(proc):
|
||||||
global _transcoder_last_error
|
global _transcoder_last_error
|
||||||
print(f"[THREAD] Transcoder writer started (PID: {proc.pid})")
|
print(f"[THREAD] Transcoder writer started (PID: {proc.pid})")
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "techdj"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
tauri-build = { version = "2", features = [] }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tauri = { version = "2", features = [] }
|
|
||||||
tauri-plugin-fs = "2"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
serde_json = "1"
|
|
||||||
|
|
||||||
# Release optimisations — keep the binary small on the 4 GB machine
|
|
||||||
[profile.release]
|
|
||||||
panic = "abort"
|
|
||||||
codegen-units = 1
|
|
||||||
lto = true
|
|
||||||
opt-level = "s"
|
|
||||||
strip = true
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
fn main() {
|
|
||||||
tauri_build::build()
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://schema.tauri.app/config/2/acl/capability.json",
|
|
||||||
"identifier": "default",
|
|
||||||
"description": "TechDJ default permissions — read-only access to home directory for local audio",
|
|
||||||
"windows": ["main"],
|
|
||||||
"permissions": [
|
|
||||||
"core:default",
|
|
||||||
"fs:read-all",
|
|
||||||
"fs:scope-home-recursive"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::fs;
|
|
||||||
use serde_json::{json, Value};
|
|
||||||
use tauri::Manager;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Helpers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
fn home_dir() -> PathBuf {
|
|
||||||
std::env::var("HOME")
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(|_| PathBuf::from("/home"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn settings_file(app: &tauri::AppHandle) -> PathBuf {
|
|
||||||
app.path()
|
|
||||||
.app_local_data_dir()
|
|
||||||
.unwrap_or_else(|_| home_dir().join(".local/share/techdj"))
|
|
||||||
.join("settings.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_settings(app: &tauri::AppHandle) -> Value {
|
|
||||||
fs::read_to_string(settings_file(app))
|
|
||||||
.ok()
|
|
||||||
.and_then(|s| serde_json::from_str::<Value>(&s).ok())
|
|
||||||
.unwrap_or_else(|| json!({}))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Tauri commands
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Returns the current music folder path stored in app settings.
|
|
||||||
/// Falls back to ~/Music if nothing is saved.
|
|
||||||
#[tauri::command]
|
|
||||||
fn get_music_folder(app: tauri::AppHandle) -> String {
|
|
||||||
let settings = read_settings(&app);
|
|
||||||
if let Some(s) = settings["music_folder"].as_str() {
|
|
||||||
if !s.is_empty() {
|
|
||||||
return s.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
home_dir().join("Music").to_string_lossy().into_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Persists the chosen music folder to the Tauri app-local settings file.
|
|
||||||
#[tauri::command]
|
|
||||||
fn save_music_folder(app: tauri::AppHandle, path: String) -> Value {
|
|
||||||
let mut settings = read_settings(&app);
|
|
||||||
settings["music_folder"] = json!(path);
|
|
||||||
let sf = settings_file(&app);
|
|
||||||
if let Some(parent) = sf.parent() {
|
|
||||||
let _ = fs::create_dir_all(parent);
|
|
||||||
}
|
|
||||||
match fs::write(&sf, serde_json::to_string_pretty(&settings).unwrap_or_default()) {
|
|
||||||
Ok(_) => json!({ "success": true }),
|
|
||||||
Err(e) => json!({ "success": false, "error": e.to_string() }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recursively scans `music_dir` for supported audio files and returns an
|
|
||||||
/// array of `{ title, file, absolutePath }` objects — same shape as the
|
|
||||||
/// Flask `/library.json` endpoint so the front-end works without changes.
|
|
||||||
#[tauri::command]
|
|
||||||
fn scan_library(music_dir: String) -> Vec<Value> {
|
|
||||||
let mut tracks = Vec::new();
|
|
||||||
let p = Path::new(&music_dir);
|
|
||||||
if p.is_dir() {
|
|
||||||
scan_dir(p, &mut tracks);
|
|
||||||
}
|
|
||||||
tracks
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scan_dir(dir: &Path, tracks: &mut Vec<Value>) {
|
|
||||||
let Ok(rd) = fs::read_dir(dir) else { return };
|
|
||||||
let mut entries: Vec<_> = rd.flatten().collect();
|
|
||||||
entries.sort_by_key(|e| e.file_name());
|
|
||||||
for entry in entries {
|
|
||||||
let p = entry.path();
|
|
||||||
if p.is_dir() {
|
|
||||||
scan_dir(&p, tracks);
|
|
||||||
} else if let Some(ext) = p.extension() {
|
|
||||||
let ext_lc = ext.to_string_lossy().to_lowercase();
|
|
||||||
if matches!(ext_lc.as_str(), "mp3" | "m4a" | "wav" | "flac" | "ogg" | "aac") {
|
|
||||||
let title = p
|
|
||||||
.file_stem()
|
|
||||||
.map(|s| s.to_string_lossy().into_owned())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let file_name = p
|
|
||||||
.file_name()
|
|
||||||
.map(|s| s.to_string_lossy().into_owned())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let abs = p.to_string_lossy().into_owned();
|
|
||||||
tracks.push(json!({
|
|
||||||
"title": title,
|
|
||||||
"file": format!("music_proxy/{}", file_name),
|
|
||||||
"absolutePath": abs,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lists subdirectories at `path` — replaces the Flask `/browse_directories`
|
|
||||||
/// endpoint for the in-app folder picker.
|
|
||||||
#[tauri::command]
|
|
||||||
fn list_dirs(path: String) -> Value {
|
|
||||||
let dir = Path::new(&path);
|
|
||||||
let mut entries: Vec<Value> = Vec::new();
|
|
||||||
|
|
||||||
if let Some(parent) = dir.parent() {
|
|
||||||
entries.push(json!({ "name": "..", "path": parent.to_string_lossy(), "isDir": true }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(rd) = fs::read_dir(dir) {
|
|
||||||
let mut dirs: Vec<_> = rd.flatten().filter(|e| e.path().is_dir()).collect();
|
|
||||||
dirs.sort_by_key(|e| e.file_name());
|
|
||||||
for d in dirs {
|
|
||||||
entries.push(json!({
|
|
||||||
"name": d.file_name().to_string_lossy(),
|
|
||||||
"path": d.path().to_string_lossy(),
|
|
||||||
"isDir": true,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
json!({ "success": true, "path": path, "entries": entries })
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// App entry point
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
||||||
pub fn run() {
|
|
||||||
tauri::Builder::default()
|
|
||||||
.plugin(tauri_plugin_fs::init())
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
|
||||||
get_music_folder,
|
|
||||||
save_music_folder,
|
|
||||||
scan_library,
|
|
||||||
list_dirs,
|
|
||||||
])
|
|
||||||
.run(tauri::generate_context!())
|
|
||||||
.expect("error while running TechDJ");
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
// Hides the console window on Windows release builds; harmless on Linux.
|
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
techdj_lib::run()
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
|
||||||
"productName": "TechDJ",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"identifier": "dev.computertech.techdj",
|
|
||||||
"build": {
|
|
||||||
"frontendDist": "../"
|
|
||||||
},
|
|
||||||
"app": {
|
|
||||||
"withGlobalTauri": true,
|
|
||||||
"windows": [
|
|
||||||
{
|
|
||||||
"title": "TechDJ",
|
|
||||||
"width": 1280,
|
|
||||||
"height": 800,
|
|
||||||
"minWidth": 1024,
|
|
||||||
"minHeight": 600,
|
|
||||||
"decorations": true,
|
|
||||||
"fullscreen": false,
|
|
||||||
"resizable": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"security": {
|
|
||||||
"assetProtocol": {
|
|
||||||
"enable": true,
|
|
||||||
"scope": ["$HOME/**"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bundle": {
|
|
||||||
"active": true,
|
|
||||||
"targets": ["deb"],
|
|
||||||
"icon": ["../icon.png"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1864
techdj_qt.py
1864
techdj_qt.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue