Troubleshooting
This document is compiled from the backend source code (internal/, pkg/tag/), the AGENTS.md summary of business pitfalls, the FAQ documentation, and the error definitions and configuration logic of each module.
Table of Contents
- Scanning Issues
- Playback Issues
- Authentication Issues
- Plugin Issues
- Cache Issues
- Database Issues
- Network Issues
- Tag Writing Issues
- Platform-Specific Issues
- Performance Tuning
- Debugging Tips
Scanning Issues
Section sources: internal/services/scanner.go, internal/services/scan_progress.go, internal/services/auto_scan.go, AGENTS.md summary of business pitfalls
Files Not Found / Empty Scan Results
| Symptom | Possible Cause | Solution |
|---|---|---|
| Song count is 0 after scanning | The music directory path is not configured or is incorrect | Set the correct absolute path via PUT /api/v1/settings/music-path |
| Some files do not appear | The file format is not in the supported list | Check the SupportedFormats configuration in scan_config |
| Subdirectory files are missing | The directory was matched by an exclusion rule | Check ExcludeDirs (matched by name) and ExcludePaths (exact path match) |
| Files not visible in the Docker environment | The volume mount path is incorrect | Mount with an absolute path: -v /absolute/path/to/music:/app/music |
Metadata Extraction Failure
Scanning relies on the pkg/tag pure Go library to extract audio metadata (no external dependencies). Optionally install ffprobe to obtain more accurate technical parameters (duration, bit rate, sample rate). The Docker image already includes ffprobe.
Common extraction failure scenarios:
- Corrupted or incomplete files: truncated files caused by interrupted downloads or transfer errors
- Non-standard tag formats: non-standard ID3v2 frames written by some software may fail to parse
- Oversized embedded cover art: extremely large cover art images may cause a significant increase in memory usage
Supported Audio Formats
Default support: MP3, FLAC, WAV, APE, OGG, M4A, WMA, AIF/AIFF. This can be customized via the SupportedFormats field of the database configuration scan_config.
Title Rule Pitfalls
Scan title extraction follows these rules:
- There is a
titlefield in the tag --> usetag.Titledirectly - There is no
titlefield in the tag --> use the filename with the extension removed
Note: no "longest common substring deduplication + concatenation" processing is done. A historical version once implemented this logic, which produced redundant titles like "Artist - Title", incorrectly mixing artist information into the title field.
Playback Issues
Section sources: internal/services/source/errors.go, internal/services/source/validator.go, AGENTS.md HLS radio proxy mode
Remote Song URL Expiration
The playback URL of a remote song may become invalid due to the source site's token expiring. The system handles this automatically through the source orchestration (Orchestrator) mechanism:
- The primary source URL returns a network error (
NetworkError) or the downloaded file fails validation (InvalidAudioError) - The system determines whether the error is fallbackable (the
IsFallbackablefunction) - If fallbackable, the Resolver fans out across plugins to search for alternative audio sources
- When all candidate sources fail, the terminal state
AllSourcesFailedErroris returned
Fallbackable error types:
InvalidAudioError: the file downloaded successfully but failed integrity validation (too short duration, too low bit rate, duration mismatch with expectation)NetworkError: HTTP-layer failure (DNS/connection/timeout/non-2xx status code)PluginInvocationError: the pluginmusic/urlinterface call failed
HLS Radio Cannot Play
| Symptom | Cause | Solution |
|---|---|---|
| Radio playback fails outright | Source site Referer/UA hotlink protection | Enable HLS proxy mode |
| Radio not playing in Web embedded mode | CORS restriction blocking | Enable HLS proxy mode |
| The player selects the wrong MediaSource | The URL lacks the .m3u8 suffix | The system already forces HLS radio URLs to carry the .m3u8 suffix |
Enable HLS proxy mode:
curl -X PUT http://localhost:58091/api/v1/settings/hls-proxy \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'In proxy mode, all segments are relayed through the server, so mind the bandwidth cost. It is off by default (false), and should be enabled only when source site hotlink protection or CORS causes playback failure.
Audio Validation Failure
The Validate function of source orchestration validates file integrity after download; the following situations trigger a validation failure and an attempt to fall back:
| Validation Reason | Meaning | Default Threshold |
|---|---|---|
probe_failed | ffprobe / tag cannot read the file | -- |
too_short | The measured duration is below the absolute lower bound | 30 seconds |
duration_mismatch_low | Measured duration < expected x tolerance ratio | expected x 0.85 |
duration_mismatch_high | Measured duration > expected x upper limit | expected x 1.5 |
bitrate_too_low | The average bit rate is too low | 8 kbps |
Thresholds can be adjusted via the source_validation config key, or validation can be gradually disabled by setting enabled: false.
Authentication Issues
Section sources: internal/services/auth_service.go, internal/middleware/auth.go
Token Expiration
| Token Type | Validity | Handling |
|---|---|---|
| Access Token | 7 days | Refresh with the Refresh Token |
| Refresh Token | 30 days | Log in again to obtain a new one |
| Plugin internal token | 100 years (permanent) | Automatically regenerated on process restart |
Refresh the token:
curl -X POST http://localhost:58091/api/v1/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "YOUR_REFRESH_TOKEN"}'Default Credentials
The default account and password are admin / admin. Ways to change them:
- Docker: environment variables
ADMIN_USERNAME/ADMIN_PASSWORD - Binary: command-line argument
-password your_password
JWT Secret Loss Recovery
The JWT secret is stored in the database configs table (with the key jwt_secret) and is automatically generated as a 32-byte random secret during the initial migration. If the database is corrupted and the secret is lost:
- At startup,
NewAuthServiceattempts to readjwt_secretfrom the database; on failure the service cannot start - Recovery method: restore the database from a backup, or delete
data/songloft.dbto re-initialize (all user data and configuration will be lost) - After the secret changes, all issued tokens automatically become invalid, and all clients need to log in again
Plugin Issues
Section sources: internal/jsplugin/health.go, internal/jsplugin/manager.go, AGENTS.md JS plugin section
Plugin Loading Failure
Common causes:
plugin.jsonhas a format error or is missing required fields- The file SHA256 fingerprint validation fails (two-layer validation: manifest validation + runtime load validation)
- QuickJS sandbox initialization failure (
onInitscript execution error) - Missing permission declaration: the
permissionsin the manifest does not declare the required capability (net,storage,fs:music, etc.)
Health Check Failure Causing Automatic Disabling
The plugin health checker performs a round of checks every 60 seconds, using a direct-to-VM probe (bypassing the scheduler's serial queue to avoid false positives caused by being blocked by a long fetch):
| Status | Meaning | Handling |
|---|---|---|
healthy | The VM is alive and responding normally | Reset the failure count |
busy | The VM is processing a long request (holding the lock) | Not counted as a failure; if still busy for 5 consecutive rounds (about 5 minutes), escalated to unhealthy |
unhealthy | The VM cannot respond or the env has been destroyed | After 3 consecutive failures, marked as error status and unloaded |
Automatic recovery mechanism: a plugin marked as error enters an exponential-backoff recovery sequence: 1 minute --> 5 minutes --> 15 minutes --> 30 minutes --> 60 minutes. After a successful recovery, it automatically switches back to active status.
Manual recovery: trigger recovery manually via POST /api/v1/plugins/{id}/recover, which clears the backoff count and starts over.
Insufficient Permissions
Plugin runtime permissions are declared by the permissions field in the manifest, and internal/jsplugin validates them at runtime. If a plugin attempts an undeclared operation (such as calling songloft.net.udpBind without declaring the net permission), the request is rejected.
Solution: check the permissions array in the plugin's plugin.json and ensure it contains the required permissions.
Hot Reload Not Taking Effect
Plugins use file fingerprints (SHA256) to detect changes and automatically trigger hot reload. If an update does not take effect:
- Confirm that the file fingerprint of the new version has indeed changed
- Check the plugin directory's write permission
- Check the logs for any hot-reload-related error messages
- Manually disable and re-enable the plugin to force a reload
Idle Plugin Auto-Unloading
After a plugin is idle for more than 10 minutes, its VM is automatically unloaded to free resources (the database status stays active, and it is lazily reloaded on the next request). A plugin with a timer wakes up 2 minutes before the timer fires. Plugins with an active WebSocket connection or a running child process are not put to sleep.
Cache Issues
Section sources: AGENTS.md music cache section, internal/services/file_move.go
Insufficient Disk Space
The cache service uses an LRU eviction policy to manage disk space:
- The default limit
max_sizeis 1 GB; when exceeded, the least recently used files are evicted by last access time - Setting
max_size=0means the cache size is unlimited - View cache status:
GET /api/v1/cache-manage/stats - Manual cleanup:
POST /api/v1/cache-manage/clean - Adjust the limit:
PUT /api/v1/cache-manage/config
Cache Directory Not Writable
Before setting a custom cache directory, use the validate-dir endpoint to verify it in advance:
curl -X POST http://localhost:58091/api/v1/cache-manage/validate-dir \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"cache_dir": "/mnt/data/music_cache"}'This endpoint performs three checks: automatically create the directory, test writability, and return the remaining disk space.
Cross-Device rename Error (EXDEV)
Typical scenario: the temp file is created in the system /tmp (tmpfs), the target cache directory is mounted on a separate disk or Docker volume, and os.Rename returns syscall.EXDEV (cross-device link).
Solution: the system internally uses internal/services.moveFile(src, dst) uniformly instead of raw os.Rename. This function tries rename first and automatically falls back to copy + remove on EXDEV.
Note: pkg/tag's atomic write is not affected by this issue, because it uses os.CreateTemp(dir, ...) to create the temp file in the same directory as the source file, so rename is always within the same device.
Database Issues
Section sources: internal/database/sqlite.go, internal/database/errors.go, AGENTS.md database standards
SQLITE_BUSY Error
SQLITE_BUSY may occur during concurrent SQLite writes. The system already mitigates it through the following measures:
- WAL mode: allows read/write concurrency, reads are not blocked by writes
- busy_timeout(10000): waits up to 10 seconds when encountering a lock, avoiding an immediate
SQLITE_BUSY - Connection pool limits:
MaxOpenConns=10,MaxIdleConns=5
If SQLITE_BUSY still occurs, it is usually because a cross-table write did not correctly use a transaction:
# Correct approach: use RunInTx to obtain the UnitOfWork under the same *sql.Tx
db.RunInTx(ctx, func(ctx context.Context, uow *UnitOfWork) error {
// uow.Songs / uow.Playlists share the same transaction
})
# Wrong approach: the service layer manually BeginTx (causes SQLITE_BUSY)Migration Failure
Database migration uses goose, running goose.Up automatically at startup. Common causes of migration failure:
| Cause | Solution |
|---|---|
| The database file is locked | Ensure no other process is accessing the same database file |
| Migration SQL syntax error | Check the corresponding SQL file in internal/database/migrations/ |
| Insufficient disk space | Free up disk space and restart |
| Manual ALTER caused schema inconsistency | Restore the database from a backup; manual ALTER data/songloft.db is prohibited |
Error Semantics
The repository layer uses unified sentinel errors:
database.ErrNotFound: the record does not existdatabase.ErrConflict: a UNIQUE constraint conflict or write conflict
The service layer should use errors.Is to distinguish them and translate them into business semantics.
Network Issues
Section sources: internal/services/whitelist.go, AGENTS.md HLS radio proxy mode / HTTP Proxy section
CORS Issues
In Web embedded mode, playing remote resources may encounter CORS restrictions. Solutions:
- HLS radio: enable HLS proxy mode (
PUT /api/v1/settings/hls-proxy), and the server proxies all requests - Remote songs: the cache service automatically caches the remote file locally, and after a cache hit, returns it via the same origin
SSRF Interception
The HLS proxy and related network features have built-in SSRF protection (services.IsHostnameAllowed), using a private network blocking strategy:
Blocked addresses:
localhost,*.localdomains- Loopback addresses:
127.0.0.0/8,::1 - Private addresses:
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,fc00::/7 - Link-local:
169.254.0.0/16,fe80::/10 - Unspecified addresses:
0.0.0.0,::
When DNS resolution fails, the request is allowed through, letting the subsequent HTTP request report the error on its own.
The HLS proxy additionally performs same-origin validation (scheme + host + port strictly equal to song.URL), keeping non-same-origin URLs unrewritten to avoid becoming an open proxy.
HTTP Proxy Configuration
Configure the backend's outbound requests to be forwarded through an HTTP proxy:
curl -X PUT http://localhost:58091/api/v1/settings/http-proxy \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"proxy": "http://192.168.1.1:7890"}'Supports HTTP/HTTPS/SOCKS5 proxy protocols. Takes effect immediately, no restart required.
Notes:
- Loopback addresses (
localhost/127.0.0.1/::1) automatically skip the proxy - Coexists with GitHub mirror acceleration (
github_proxyURL prefix concatenation): first concatenate the mirror prefix, then forward through the HTTP Proxy - Scope of effect: plugin registry fetching, plugin download/update, system upgrade check/download
Tag Writing Issues
Section sources: pkg/tag/write.go, AGENTS.md tag writing section
Format Support Status
tag.WriteTag(filePath, opts) dispatches the write implementation by file extension:
| Format | Write Method | Status |
|---|---|---|
.mp3 | ID3v2.3 (TIT2, TPE1, TPE2, TALB, TYER, TCON, USLT, APIC) | Supported |
.flac | Vorbis Comment + PICTURE block | Supported |
.ape | APEv2 tag | Supported |
.wav | RIFF INFO / ID3 | Supported |
.m4a / .mp4 / .m4b | iTunes-style atoms | Supported |
.ogg / .oga | Vorbis Comment (Ogg container) | Supported |
.aif / .aiff | ID3v2.3 (ID3 chunk) + NAME/AUTH native chunks | Supported |
| Other formats | -- | Returns ErrUnsupportedWrite |
Write Failure Handling
- Unsupported formats return
ErrUnsupportedWrite; the caller must degrade to a log entry and must not block the main flow - Writing uses an atomic strategy: first write to a temp file (created in the same directory as the source file), then
os.Renameto overwrite the original file - When the atomic write fails (such as insufficient disk space or insufficient permissions), the original file remains unchanged
Platform-Specific Issues
Section sources: AGENTS.md platform adaptation pitfalls section, docs/faq.md
macOS
| Issue | Cause | Solution |
|---|---|---|
| Token storage error | secure_storage cannot use the Keychain in an unsigned sandbox | The system automatically degrades to SharedPreferences; no action needed |
| Upgrade check unavailable | Only Docker deployment supports online upgrade | Manually download and replace the new version |
Android
| Issue | Cause | Solution |
|---|---|---|
| Build fails | SDK licenses not accepted | Run sdkmanager --licenses |
| Notifications not shown | Android 13+ requires runtime notification permission | Grant the notification permission in the app settings |
| Killed in the background on HyperOS3 | The system aggressively reclaims the foreground service | Configure androidStopForegroundOnPause: false |
Windows / Linux
| Issue | Cause | Solution |
|---|---|---|
| Audio playback silent/crashing | The audio backend depends on libmpv | Install the libmpv library required by just_audio_media_kit |
| Cannot change the password on Windows | Double-clicking the exe cannot pass arguments | Create songloft.bat with songloft.exe -password your_password |
Docker
| Issue | Cause | Solution |
|---|---|---|
| Scheduled task time is wrong | The container time zone is inconsistent with the host | Set the environment variable TZ=Asia/Shanghai |
| Music files not visible | The volume mount path is not an absolute path | Use -v /absolute/path:/app/music |
| Binary not updated | The hot-replacement rule decided to keep the data version | See the Docker hot-replacement rules table |
Performance Tuning
Section sources: internal/database/sqlite.go, AGENTS.md music cache section
SQLite Optimization Parameters
At startup, the system automatically configures the following optimizations via DSN parameters, which usually need no manual adjustment:
| Parameter | Value | Effect |
|---|---|---|
journal_mode | WAL | Read/write concurrency, reads not blocked by writes |
busy_timeout | 10000 (10s) | Wait on a lock instead of failing outright |
synchronous | NORMAL | Already safe enough in WAL mode, reduces fsync |
cache_size | 10000 | Page cache about 40 MB |
foreign_keys | 1 | Enable foreign key constraints |
MaxOpenConns | 10 | Connection pool upper limit |
MaxIdleConns | 5 | Idle connection upper limit |
ConnMaxLifetime | 30 min | Maximum connection lifetime |
Scanning Large Numbers of Files
Recommendations for scanning a large-scale music library:
- Use
ExcludeDirsto exclude non-music directories (such as.git,node_modules, thumbnail directories) - Use
ExcludePathsto exclude specific paths precisely - Scanning is executed asynchronously; you can view the status via the progress API and can also cancel an in-progress scan
- After the first scan, incremental scan speed improves significantly
Cache LRU Eviction Policy
| Configuration Item | Default | Description |
|---|---|---|
max_size | 1 GB | Total cache size upper limit; 0 means unlimited |
cache_dir | {data_dir}/music_cache/ | Can be customized to any absolute path |
When switching the cache directory, the LRU index is automatically rebuilt, but old files are not migrated. The inflight deduplication mechanism ensures concurrent requests for the same song are downloaded only once.
Debugging Tips
Section sources: internal/handlers/log.go, internal/services/source/metrics.go, internal/jsplugin/health.go
Runtime Log Level
Supports dynamically switching the log level at runtime, no restart required:
# View the current level
curl http://localhost:58091/api/v1/settings/log-level \
-H "Authorization: Bearer TOKEN"
# Switch to the debug level
curl -X PUT http://localhost:58091/api/v1/settings/log-level \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"level": "debug"}'Supported levels: debug, info (default), warn, error. Changes take effect immediately and are persisted to the database, automatically restored after a restart.
Access Logs
The backend uses the standard library slog to output structured logs. When troubleshooting, it is recommended to temporarily switch to the debug level to get detailed request-handling information, then switch back to info after troubleshooting to reduce log volume.
Plugin Health Check API
View the health status of all plugins:
curl http://localhost:58091/api/v1/plugins/health \
-H "Authorization: Bearer TOKEN"Returns each plugin's health classification (green/yellow/red), success rate, sample count, and recent failure records.
Source Orchestration Metrics API
View the Fetch success rate and health of each audio source plugin:
curl http://localhost:58091/api/v1/plugins/health \
-H "Authorization: Bearer TOKEN"The returned PluginHealthSnapshot contains:
| Field | Description |
|---|---|
class | Health classification: green (success rate >=80%) / yellow (intermediate state or insufficient samples) / red (success rate <40%) |
success_rate | Success rate of the most recent 200 attempts |
samples | Current sample count (when <10, it will not be judged as red, avoiding false kills on cold start) |
last_failures | Recent failure records and reasons (network_fail / probe_fail / validation_fail / plugin_invocation_fail) |
Health affects the Resolver's ranking weight: score = baseScore * (0.3 + 0.7 * successRate). When samples are insufficient, a neutral value of 0.8 is used, so cold-start plugins are not given zero priority.
Swagger Documentation
After starting in development mode (make run), visit http://localhost:58091/swagger/index.html to view the interactive API documentation. The production version does not include Swagger.
pprof Profiling
The development mode build (-tags dev) includes pprof endpoints, which can be used for CPU/memory/goroutine profiling. This feature is unavailable in the production version.
