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
|
||||
soundfile
|
||||
numpy
|
||||
librosa
|
||||
requests
|
||||
python-socketio[client]
|
||||
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'.
|
||||
# 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})")
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
2066
techdj_qt.py
2066
techdj_qt.py
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue