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:
ComputerTech 2026-04-12 13:01:44 +01:00
parent 0ec02507b3
commit 69c078071d
9 changed files with 1594 additions and 710 deletions

View File

@ -10,6 +10,7 @@ PyQt6
sounddevice
soundfile
numpy
librosa
requests
python-socketio[client]
yt-dlp

View File

@ -193,6 +193,20 @@ def _start_transcoder_if_needed(is_mp3_input=False):
# Define greenlets INSIDE so they close over THIS specific 'proc'.
# 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.
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):
global _transcoder_last_error
print(f"[THREAD] Transcoder writer started (PID: {proc.pid})")

View File

@ -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

View File

@ -1,3 +0,0 @@
fn main() {
tauri_build::build()
}

View File

@ -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"
]
}

View File

@ -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");
}

View File

@ -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()
}

View File

@ -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"]
}
}

File diff suppressed because it is too large Load Diff