MusicLib is a hybrid architecture combining a battle-tested shell script backend with modern Qt/KDE frontend interfaces. This design prioritizes stability, maintainability, and deep KDE integration while preserving the reliability of existing shell-based workflows.
Core Principle: Thin clients, authoritative shell backend. GUI and CLI are smart clients that read data directly but delegate all writes to shell scripts via process invocation.
┌──────────────────────────────────────────────────────────────┐
│ User Layer │
├───────────────┬──────────────────┬───────────────────────────┤
│ musiclib │ musiclib-cli │ Desktop Integration │
│ (Qt/KDE GUI) │ (C++ Dispatcher)│ (Dolphin, KRunner, Tray) │
│ │ │ │
│ • Library View│ • rate │ • Service Menus │
│ • Rating UI │ • mobile │ • Global Shortcuts │
│ • Maintenance │ • rebuild │ • Plasma Widget │
│ • Mobile Panel│ • tagclean │ • D-Bus Interface │
│ • Conky Panel │ • boost │ │
│ • Settings │ • scan │ │
│ │ • add-track │ │
└───────┬───────┴────────┬─────────┴───────────┬───────────────┘
│ │ │
│ QProcess │ exec() │ D-Bus / Scripts
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ Process Invocation Layer │
│ (QProcess, std::system, D-Bus method calls) │
└────────────────────────┬─────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ Shell Script Backend │
│ /usr/lib/musiclib/bin/ │
├──────────────────────────────────────────────────────────────┤
│ Utility Layer (sourced by all scripts): │
│ • musiclib_utils.sh (config, DB helpers, lock)│
│ • musiclib_utils_tag_functions.sh (tag repair/normalize) │
├──────────────────────────────────────────────────────────────┤
│ Operation Scripts (invoked by clients): │
│ • musiclib_rate.sh (rating → DSV + tags + Conky) │
│ • musiclib_mobile.sh (KDE Connect playlist sync) │
│ • musiclib_mpris_listen.sh (MPRIS2 D-Bus monitor) │
│ • musiclib_player_event.sh (song-change handler → Conky) │
│ • musiclib_new_tracks.sh (import pipeline) │
│ • musiclib_rebuild.sh (full DB rebuild from scan) │
│ • musiclib_tagrebuild.sh (repair tags from DB) │
│ • musiclib_tagclean.sh (ID3v1→v2, APE removal, art) │
│ • musiclib_boost.sh (ReplayGain loudness) │
│ • audpl_scanner.sh (playlist cross-reference) │
└────────────────────────┬─────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ External Tools Layer │
├──────────────────────────────────────────────────────────────┤
│ Tag/Metadata: kid3-cli, exiftool, rsgain │
│ Player Control: audtool (Audacious CLI) │
│ Mobile Sync: kdeconnect-cli │
│ File Operations: flock, bc, readlink, awk, sed │
│ Desktop: kdialog, notify-send (KNotifications) │
└────────────────────────┬─────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ Data Storage Layer │
├──────────────────────────────────────────────────────────────┤
│ Database: ~/.local/share/musiclib/data/musiclib.dsv │
│ Config: ~/.config/musiclib/musiclib.conf │
│ Music Files: /mnt/music/ (or user-configured MUSIC_REPO) │
│ Playlists: ~/.local/share/musiclib/playlists/ │
│ Conky Assets: ~/.local/share/musiclib/data/conky_output/ │
│ Logs: ~/.local/share/musiclib/logs/ │
│ Tag Backups: ~/.local/share/musiclib/data/tag_backups/ │
└──────────────────────────────────────────────────────────────┘
Example: GUI displays library view
User opens GUI
↓
musiclib reads ~/.local/share/musiclib/data/musiclib.dsv
↓
Parses DSV into QAbstractTableModel
↓
Displays in QTreeView (sortable, filterable)
↓
User sees library (no backend invocation needed)
Optimization: QFileSystemWatcher monitors DSV, triggers model refresh on change (debounced 500ms).
Example: User rates a track via GUI
User clicks 5th star on track in library view
↓
musiclib invokes QProcess:
/usr/lib/musiclib/bin/musiclib_rate.sh "/path/to/song.mp3" 5
↓
musiclib_rate.sh sources musiclib_utils.sh
↓
Calls with_db_lock() helper:
• Acquires flock on musiclib.dsv.lock
• Updates DSV (Rating column, GroupDesc column)
• Updates file tags (POPM via kid3-cli, Grouping)
• Regenerates Conky assets (starrating.png, detail.txt)
• Releases lock
↓
Returns exit code 0 (success) to QProcess
↓
musiclib receives exit code, shows KNotification:
"Rated: Artist – Title (★★★★★)"
↓
QFileSystemWatcher detects DSV change, triggers model refresh
↓
Library view updates (rating column shows 5 stars)
Write Atomicity: The DSV update uses two independent guarantees:
- Serialization (
flocklock): prevents two writers from interleaving their output - Crash safety (
rename(2)via.tmp+mv): if the process is killed mid-write, the original DSV file is untouched; the orphaned.tmpis discarded
These are distinct. The lock alone does not protect against a mid-write crash; the rename alone does not prevent two concurrent writers. Both are required for a safe write.
Error Handling:
musiclib_rate.sh exits with code 2 (lock timeout)
↓
Outputs JSON to stderr:
{"error": "Database lock timeout", "code": 2, ...}
↓
musiclib parses JSON, shows error dialog:
"Unable to rate track: Database lock timeout (another operation in progress)"
↓
Logs error to ~/.local/share/musiclib/logs/musiclib.log
Setup: musiclib_init_config.sh enables the musiclib-mpris.service systemd user unit automatically. No per-player configuration is required.
Flow:
MPRIS2 player (Strawberry, Audacious, Clementine, etc.) plays new track
↓
D-Bus emits PropertiesChanged signal
↓
musiclib_mpris_listen.sh (running as musiclib-mpris.service) receives signal
↓
Invokes musiclib_player_event.sh
↓
playerctl + qdbus6 resolve active player bus and xesam:url
↓
Looks up track in musiclib.dsv by SongPath
↓
Extracts album art (folder.jpg preferred; embedded via exiftool fallback)
↓
exiftool produces full tag dump (taginfofull.txt)
↓
Generates Conky assets:
• artist.txt, title.txt, album.txt, year.txt
• currbitrate.txt (numeric kbps, no unit suffix)
• currgpnum.txt (rating from Grouping tag)
• lastplayed.txt, songpath.txt, artloc.txt
↓
Forks scrobble polling loop (disowned — player never blocked):
• Checks every 3 s whether same track is still playing
• Scrobbles once 50% of track length elapsed (min 30 s, max 4 min)
• On pause: saves completed check count to state file
• On resume: subtracts saved checks, waits only remaining time
↓
Returns exit code 0
Conky Integration:
Conky reads ~/.local/share/musiclib/data/conky_output/artist.txt
↓
Displays "Now Playing: Pink Floyd – Time" on desktop
↓
Shows starrating.png (5-star image)
↓
Shows folder.jpg (album art)
Track-change defense: FILEPATH is captured once at script entry from the MPRIS2 metadata. The scrobble loop re-checks the active track on every 3-second tick; if the track has changed or playback stopped, the loop exits immediately without scrobbling.
Example: User uploads playlist to Android device
User selects "workout.audpl" in GUI Mobile panel
↓
Clicks "Upload" button
↓
musiclib invokes QProcess:
/usr/lib/musiclib/bin/musiclib_mobile.sh upload abc123def456 \
/home/user/.local/share/musiclib/playlists/workout.audpl
↓
musiclib_mobile.sh:
1. Parses workout.audpl (extracts file paths)
2. Generates .m3u with basenames only
3. Transfers .m3u + all MP3 files via kdeconnect-cli --share
4. Saves metadata:
• playlists/mobile/workout.tracks (file list)
• playlists/mobile/workout.meta (upload timestamp)
5. Processes previous playlist (if exists):
• Reads previous .tracks and .meta
• Calculates synthetic last-played times (evenly distributed)
• with_db_lock():
- Updates LastTimePlayed in DSV for each track
- Updates Songs-DB_Custom1 tags via kid3-cli
↓
Returns exit code 0 (success)
↓
musiclib shows success notification:
"Uploaded 42 tracks to Samsung Galaxy S21"
↓
Updated last-played times appear in library view after refresh
Synthetic Timestamp Logic:
Upload 1 (Monday): 42 tracks uploaded
Upload 2 (Friday): 37 tracks uploaded
↓
On Upload 2, process Upload 1 tracks:
• Time window: Monday → Friday (4 days = 345,600 seconds)
• Distribute 42 tracks evenly: 345,600 / 42 ≈ 8,229 seconds apart
• Assign timestamps: Monday + 8229s, Monday + 16458s, ...
↓
Result: Fabricated last-played timestamps in library view (evenly distributed, not
based on actual playback; real listening pattern during the mobile period is unknown)
Binaries (/usr/bin/):
musiclib-cli # C++ dispatcher, thin wrapper around scripts
musiclib # Qt/KDE GUI application
Backend Scripts (/usr/lib/musiclib/bin/):
musiclib_utils.sh # Shared utilities (config, DB, locking)
musiclib_utils_tag_functions.sh # Tag repair/normalization
musiclib_rate.sh # Rating operations
musiclib_mobile.sh # Mobile sync (upload/status subcommands)
musiclib_player_event.sh # Song-change handler (MPRIS2)
musiclib_mpris_listen.sh # MPRIS2 D-Bus monitor (systemd service)
musiclib_new_tracks.sh # Import pipeline
musiclib_rebuild.sh # Full DB rebuild from filesystem
musiclib_tagrebuild.sh # Repair corrupted tags from DB
musiclib_tagclean.sh # ID3v1→v2 merge, APE removal, art embed
musiclib_boost.sh # ReplayGain loudness targeting
audpl_scanner.sh # Playlist cross-reference CSV generator
Reference Config (/usr/lib/musiclib/config/):
tag_excludes.conf # Tags to exclude during operations
ID3v2_frame_excludes.txt # ID3v2 frames to strip
ID3v2_frames.txt # Valid ID3v2 frame reference
Shared Data (/usr/share/musiclib/):
conky_example.conf # Example Conky configuration
musiclib_example.dsv # Example database (for testing)
Desktop Integration (/usr/share/):
/usr/share/applications/org.musiclib.musiclib.desktop
/usr/share/kservices5/musiclib-dolphin.desktop # Or kio/servicemenus/
Configuration (~/.config/musiclib/):
musiclib.conf # User-specific config (DB path, music dir, device ID, etc.)
Application Data (~/.local/share/musiclib/):
data/
musiclib.dsv # Main database (^-delimited)
musiclib.dsv.lock # Lock file for flock
musiclib.dsv.backup.YYYYMMDD_* # Automatic backups
conky_output/ # Generated Conky assets
artist.txt, title.txt, album.txt, year.txt
lastplayed.txt, detail.txt
starrating.png # Composite star image (generated)
folder.jpg # Current album art
stars/ # Star image templates (5 levels)
blank.png, one.png, oneonehalf.png, ..., five.png
tag_backups/ # Tag backups before destructive ops
song.mp3.backup.YYYYMMDD_HHMMSS
playlists/
*.audpl # Audacious playlists
mobile/
*.tracks # Mobile sync file lists
*.meta # Mobile sync metadata (upload timestamps)
current_playlist # Symlink to most recent playlist
logs/
musiclib.log # Main operation log
audacious/
audacioushist.log # Playback history
mobile/
upload_YYYYMMDD.log # Per-upload logs
Music Files (user-configured):
/mnt/music/ # Or ~/Music/, configurable via MUSIC_REPO
Artist/
Album/
Track.mp3
Language: C++20 Frameworks:
- Qt 6.5+ (Core, Widgets, DBus, Concurrent)
- KDE Frameworks 6 (KConfig, KNotifications, KIO, KGlobalAccel, KXmlGui, KStatusNotifierItem)
Build System: CMake 3.20+ Compiler: GCC 11+ or Clang 14+
GUI Architecture:
KXmlGuiWindowmain window (menu bar, toolbar, status bar auto-configured)QTreeViewwithQSortFilterProxyModelfor library viewQStyledItemDelegatefor inline star rating widgetQFileSystemWatcherfor database change detectionQThreadfor non-blocking script executionQProcessfor script invocation with stdout/stderr capture
CLI Architecture:
- Argument parser (subcommand pattern:
musiclib-cli rate ...) - Script path resolution (
/usr/lib/musiclib/bin/with dev fallback) std::system()orfork()/exec()for script invocation- Exit code forwarding (transparent to user)
Language: Bash 4.4+ (with [[, ((, arrays, associative arrays)
Required Tools:
- Core:
bash,coreutils(awk,sed,grep,readlink,date,bc) - Locking:
flock(util-linux) - Tags:
kid3-cli,exiftool - Audio:
rsgain(ReplayGain),audtool(Audacious control) - Mobile:
kdeconnect-cli - Optional:
conky(desktop display),kdialog(GUI dialogs from scripts)
Design Patterns:
- Utility functions sourced from
musiclib_utils.sh - Strict
set -euo pipefailin all scripts flock-based locking via helper functions- JSON error output on stderr (exit codes 1, 2, 3)
- Logging to
musiclib.logvialog_message()helper
Database: Flat-file DSV (^ -delimited)
- Why not SQLite: Simplicity, shell-friendly, easy to backup/inspect, no schema migrations
- Concurrency:
flockfor write serialization - Performance: Linear scan acceptable for <100k tracks, awk/grep fast enough
Configuration: musiclib.conf (bash-sourceable key=value)
- Why not KConfig: Scripts need direct access without Qt dependencies
- Sync: GUI mirrors config to KConfig for Qt settings UI
Playlists: .audpl (Audacious), .m3u, .m3u8, .pls (mobile-compatible)
Multiple processes may attempt to write to musiclib.dsv concurrently:
- Audacious hook updates last-played
- User rates track in GUI
- Background rebuild runs
Without coordination, this causes:
- Corrupted DSV (partial writes, interleaved lines)
- Lost updates (last write wins)
- Inconsistent state (DSV vs. file tags diverge)
Mechanism:
# In musiclib_utils.sh
acquire_db_lock() {
exec 200>"${MUSICDB}.lock"
flock -w 5 200 || return 1
}
release_db_lock() {
flock -u 200
}
with_db_lock() {
local callback="$1"
shift
acquire_db_lock || { error_exit 2 "Database lock timeout"; return $?; }
trap release_db_lock EXIT
"$callback" "$@"
release_db_lock
}Usage in Scripts:
#!/bin/bash
source musiclib_utils.sh
update_rating_impl() {
local filepath="$1"
local rating="$2"
# Perform DSV + tag updates
}
with_db_lock update_rating_impl "/path/to/file.mp3" 4Timeout Policy:
- Default: 5 seconds
- Configurable via
LOCK_TIMEOUTinmusiclib.conf - On timeout: exit code 2, JSON error (future: exit code 3, queue operation)
Lock File Lifecycle:
- Created on first lock acquisition:
musiclib.dsv.lock - Persists across operations (never deleted)
- Safe on NFS with kernel ≥2.6.12 (advisory locking)
Design (not yet implemented):
# In musiclib_utils.sh (future)
with_db_lock_deferred() {
local callback="$1"
shift
if acquire_db_lock; then
trap release_db_lock EXIT
"$callback" "$@"
release_db_lock
return 0
else
# Timeout: queue operation
queue_operation "$callback" "$@"
error_exit 3 "Operation queued for retry"
return 3
fi
}
queue_operation() {
local callback="$1"
shift
local timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
jq -n \
--arg ts "$timestamp" \
--arg op "$callback" \
--argjson args "$(printf '%s\n' "$@" | jq -R . | jq -s .)" \
'{timestamp: $ts, operation: $op, args: $args, retry_count: 0}' \
>> ~/.local/share/musiclib/data/pending_ops.json
}Benefits:
- No user-facing lock timeout errors
- Operations never lost
- Background daemon (
musiclibd) or cron job retries pending ops
| Exit Code | Meaning | GUI Behavior | CLI Behavior |
|---|---|---|---|
| 0 | Success | KNotification (success), refresh view | Print "Success" |
| 1 | User error | Show error dialog (user-fixable) | Print error message, suggest fix |
| 2 | System error | Show error dialog (likely admin-fixable) | Print error message, suggest checking logs |
| 3 | Deferred | Show "pending" notification, log operation | Print "Queued for retry" |
Schema:
{
"error": "Human-readable error message",
"script": "scriptname.sh",
"code": 1,
"context": {
"key1": "value1",
"key2": "value2"
},
"timestamp": "2026-02-07T18:45:23Z"
}GUI Parsing:
QJsonDocument errorDoc = QJsonDocument::fromJson(process.readAllStandardError());
QJsonObject error = errorDoc.object();
QString message = error["error"].toString();
int code = error["code"].toInt();
QJsonObject context = error["context"].toObject();
if (code == 1) {
// User error: actionable message
QMessageBox::warning(this, "Input Error", message);
} else if (code == 2) {
// System error: suggest checking logs
QMessageBox::critical(this, "Operation Failed",
message + "\n\nCheck logs for details: ~/.local/share/musiclib/logs/musiclib.log");
} else if (code == 3) {
// Deferred: show pending notification
KNotification::event("pending", "Operation Queued", message);
}Purpose: Persistent access to rating and operations while Audacious plays.
Behavior:
- Tooltip:
"Artist – Title ★★★★☆" - Left-click: Open
musiclibto Maintenance Panel - Right-click menu:
- Quick Rate → 0–5 stars (submenu)
- Open Library
- Settings
- Quit
Implementation:
KStatusNotifierItem* trayIcon = new KStatusNotifierItem(this);
trayIcon->setIconByName("musiclib");
trayIcon->setToolTipTitle("MusicLib");
trayIcon->setToolTipSubTitle(getCurrentTrackInfo());
auto menu = trayIcon->contextMenu();
auto rateMenu = menu->addMenu("Quick Rate");
for (int i = 0; i <= 5; ++i) {
rateMenu->addAction(QString("%1 Stars").arg(i), [i]() {
rateCurrentTrack(i);
});
}Purpose: Rate tracks without focusing GUI (most-requested power-user feature).
Default Bindings (user-customizable):
Meta+1→ Rate current track 1 starMeta+2→ Rate current track 2 stars- ...
Meta+5→ Rate current track 5 starsMeta+0→ Clear rating
Implementation:
KGlobalAccel* globalAccel = KGlobalAccel::self();
for (int i = 0; i <= 5; ++i) {
QAction* rateAction = new QAction(QString("Rate %1 Stars").arg(i), this);
rateAction->setProperty("componentName", "musiclib");
rateAction->setProperty("componentDisplayName", "MusicLib");
globalAccel->setGlobalShortcut(rateAction,
QKeySequence(QString("Meta+%1").arg(i)));
connect(rateAction, &QAction::triggered, [i]() {
rateCurrentTrack(i);
});
}Purpose: Right-click audio files in Dolphin → "Add to MusicLib".
File: /usr/share/kservices5/musiclib-dolphin.desktop (or kio/servicemenus/)
[Desktop Entry]
Type=Service
ServiceTypes=KonqPopupMenu/Plugin
MimeType=audio/mpeg;audio/flac;audio/ogg;
Actions=AddToMusicLib
[Desktop Action AddToMusicLib]
Name=Add to MusicLib
Icon=musiclib
Exec=musiclib-cli add-track %fBehavior:
- User right-clicks
song.mp3in Dolphin - "Add to MusicLib" appears in context menu
- On click, invokes
musiclib-cli add-track /path/to/song.mp3 - KNotification shows success/failure
Service Name: org.musiclib
Interfaces:
<!-- org.musiclib.Library -->
<interface name="org.musiclib.Library">
<method name="Search">
<arg name="query" type="s" direction="in"/>
<arg name="results" type="a{sv}" direction="out"/>
</method>
<method name="GetTrackInfo">
<arg name="trackId" type="i" direction="in"/>
<arg name="info" type="a{sv}" direction="out"/>
</method>
</interface>
<!-- org.musiclib.Player -->
<interface name="org.musiclib.Player">
<method name="GetCurrentTrack">
<arg name="trackInfo" type="a{sv}" direction="out"/>
</method>
<method name="Rate">
<arg name="stars" type="i" direction="in"/>
</method>
</interface>Consumers:
- KRunner plugin:
qdbus org.musiclib /Library org.musiclib.Library.Search "dark side" - Plasma widget: Subscribe to
CurrentTrackChangedsignal - Custom scripts:
dbus-send --session --dest=org.musiclib ...
Example Queries:
ml: dark side → Search library, show results inline
ml:rate 5 → Rate current track 5 stars
ml:mobile upload → Trigger mobile sync dialog
ml:status → Show playback status inline
Implementation: C++ AbstractRunner subclass registered in /usr/share/kservices5/.
QML Widget: Desktop/panel widget showing now-playing from Conky output.
Features:
- Reads
~/.local/share/musiclib/data/conky_output/files - Displays album art, artist, title, rating
- Click to rate (invokes D-Bus method)
- Respects Plasma themes
Pros:
- ✅ Proven stability: 11 scripts, battle-tested in production
- ✅ Shell-native tools:
kid3-cli,exiftool,audtoolhave excellent CLI interfaces - ✅ Rapid prototyping: New features iterate faster in bash than C++
- ✅ Low maintenance: No library version conflicts, minimal dependencies
- ✅ Transparency: Operations are inspectable, debuggable, scriptable
Cons:
- ❌ Performance: <50ms latency not achievable (acceptable for non-critical-path)
- ❌ Type safety: Bash is untyped (mitigated by strict mode, tests)
- ❌ Error handling: Verbose compared to exceptions (mitigated by
error_exit())
Decision: Keep for v0.1–v0.3; migrate hot-path ops (rating, playback tracking) to C++ in v1.1+ if profiling shows need.
Alternative: CLI could reimplement operations in C++.
Rationale for Dispatcher:
- ✅ Code reuse: Scripts are single source of truth
- ✅ Faster development: No duplication of business logic
- ✅ Easier testing: Test one backend, two interfaces
- ✅ Unified error handling: Scripts already have JSON error contract
When to Migrate: If CLI usage exceeds GUI usage AND users complain about latency (unlikely for batch operations).
Pros:
- ✅ Shell-friendly: awk/grep queries are trivial
- ✅ No schema migrations: Append-only column evolution
- ✅ Easy backup:
cp musiclib.dsv musiclib.dsv.backup - ✅ Human-readable: Can inspect/edit in text editor (advanced users)
Cons:
- ❌ Performance: O(n) scans (acceptable for <100k tracks)
- ❌ Indexing: No B-trees (mitigated by sorting + binary search in GUI)
- ❌ Transactions: Manual locking (acceptable with
flock)
When to Migrate: If library size >100k tracks AND queries become slow (unlikely for target audience).
Risk: User-controlled input passed to shell scripts could enable command injection.
Mitigation:
- ✅ All script arguments are quoted in invocations:
"$filepath"not$filepath - ✅ GUI/CLI validate inputs before invoking scripts (e.g., rating 0–5, paths exist)
- ✅ Scripts use
[[conditionals (safer than[) - ✅ No use of
evalorsourceon user input
Example (safe):
musiclib_rate.sh "/path/with spaces/song.mp3" 4Example (vulnerable, never done):
eval "musiclib_rate.sh $user_input" # DON'T DO THISRisk: Unauthorized access to musiclib.dsv or config.
Mitigation:
- ✅ XDG directories default to
700(~/.config/,~/.local/share/) - ✅ DB file defaults to
600(owner read/write only) - ✅ Scripts check file permissions before operations
Risk: Untrusted device could receive sensitive playlists.
Mitigation:
- ✅ KDE Connect device pairing required (user must authorize device)
- ✅ GUI shows device name before upload (user confirms)
- ✅ No automatic sync; user-initiated only
| Operation | Complexity | Typical Latency | Notes |
|---|---|---|---|
| Read entire DB | O(n) | 50–200ms (10k tracks) | awk/grep parse, linear scan |
| Find track by path | O(n) | 10–50ms | grep early exit |
| Update rating | O(n) | 100–300ms | Rewrite DSV, update tag, regen Conky |
| Full rebuild | O(n log n) | 5–30s (10k tracks) | Filesystem scan + sort |
Optimization Opportunities (future):
- Sorted DSV + binary search → O(log n) lookups
- Incremental rebuild (only scan changed directories)
- In-memory cache in GUI (invalidate on change)
- QProcess spawn: ~10ms
- Shell script startup (source utils): ~5–10ms
- Total overhead: ~15–20ms per operation
Acceptable: Non-critical-path operations (rating, mobile sync) tolerate 100–300ms total latency.
Framework: Bash test harness (custom or BATS)
Coverage:
- Utility functions (
musiclib_utils.sh) - Tag functions (
musiclib_utils_tag_functions.sh) - Error handling (JSON output validation)
Example:
#!/bin/bash
source tests/test_framework.sh
source musiclib_utils.sh
test_epoch_to_sql_time() {
local epoch=1675000000
local expected="44955.574074"
local actual=$(epoch_to_sql_time $epoch)
assert_equals "$expected" "$actual"
}
run_testsFramework: Bash test scripts with fixtures
Coverage:
- Lock contention scenarios (
test_lock_contention.sh) - Tag corruption recovery (
test_tag_rebuild.sh) - Mobile upload workflow (
test_mobile_upload.sh) - Database rebuild accuracy (
test_rebuild.sh)
Framework: Qt Test
Coverage:
- Library view model (DSV parsing, filtering, sorting)
- Script invocation (QProcess mocking)
- Error handling (JSON parsing)
- Settings persistence (KConfig)
Example:
void TestLibraryModel::testDSVParsing() {
QFile testDb("tests/fixtures/test_db.dsv");
testDb.open(QIODevice::ReadOnly);
LibraryModel model;
model.loadFromStream(&testDb);
QCOMPARE(model.rowCount(), 10);
QCOMPARE(model.data(model.index(0, 0)).toString(), "1");
QCOMPARE(model.data(model.index(0, 5)).toString(), "Time");
}Framework: Bash script simulating user workflows
Coverage:
- Install packages
- Run first-run wizard
- Import test track
- Rate track via CLI
- Rate track via GUI
- Upload playlist to mock KDE Connect device
- Rebuild database
- Verify all operations logged
Goal: Migrate hot-path operations to C++ for <50ms latency while keeping shell for maintenance ops.
Proposed Architecture:
┌─────────────────────────────────────────┐
│ musiclib & musiclib-cli │
├─────────────────────────────────────────┤
│ Link against libmusiclib.so │
└─────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ libmusiclib.so (C++ Shared Library) │
├─────────────────────────────────────────┤
│ • DSV read/write (lock-aware) │
│ • Tag I/O (wrapper around kid3-cli) │
│ • Rating update (DSV + tags + Conky) │
│ • Playback tracking │
└─────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────┐
│ Shell Scripts (maintenance only) │
├─────────────────────────────────────────┤
│ • musiclib_rebuild.sh │
│ • musiclib_tagclean.sh │
│ • musiclib_tagrebuild.sh │
│ • musiclib_boost.sh │
│ • audpl_scanner.sh │
│ (Invoke libmusiclib for DB access) │
└─────────────────────────────────────────┘
Migration Path:
- Phase 1: Extract DSV read/write to C++ library
- Phase 2: Migrate rating logic to library
- Phase 3: Migrate playback tracking to library
- Phase 4: Scripts source library via FFI or invoke CLI helpers
- Phase 5: GUI/CLI link directly against library (bypass scripts for hot paths)
Document Version: 1.0
Last Updated: 2026-02-07
Status: Implementation-Ready