Baseline: full tree snapshot after git repository loss

The previous .git directory was found gutted (only info/ remained), so
history is unrecoverable locally. This commit restores version control
with the tree exactly as it stands: the working app, the preview-tracks
launcher on the artist hero, the scrollable-track-previews design bundle
(docs + approved mockups), and the Phase 0 validation artifacts.
This commit is contained in:
KeelrLeek committed 2026-07-22 17:57:00 -05:00
commit a667b4b28e
161 files changed
+53170

No files matched your search

+311
View File
@@ -0,0 +1,311 @@
//! App config. Non-secret settings live in `config.json`; the Soulseek
//! password lives in the Windows Credential Manager (via `keyring`).
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use tauri::Manager;
const KEYRING_SERVICE: &str = "com.soulstream.app";
/// Pre-rename service name; stored passwords migrate on first read.
const LEGACY_KEYRING_SERVICE: &str = "com.trifecta.soulseek-plus";
/// Pre-rename app-data folder; migrated by `migrate_app_data` at startup.
const LEGACY_APP_DATA_DIR: &str = "com.trifecta.soulseek-plus";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ArtistHeroUpscaler {
/// Fixed 3840x2160 GPU master, mip-filtered to the visible window.
#[default]
#[serde(alias = "fsr")]
LiveGpu,
#[serde(rename = "fsr_legacy")]
Fsr,
Lanczos3,
}
/// Ceiling for peer-source formats. `LossyOnly` skips lossless files —
/// the "don't stream multi-hundred-MB FLACs on this connection" cap.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceQualityCap {
#[default]
Any,
LossyOnly,
}
/// Floor for peer-source quality. Lossless formats satisfy every floor; a
/// lossy file must carry a known bitrate at or above the named floor.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceQualityFloor {
#[default]
Any,
Kbps192,
Kbps256,
Kbps320,
Lossless,
}
/// What the resolver optimizes among sources that satisfy the cap/floor:
/// best format (current behavior) or fastest transfer (free slot, upload
/// speed, short queue — format only breaks ties).
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourcePriority {
#[default]
Quality,
Speed,
}
/// Per-mode source-selection preferences; streaming and downloading each
/// carry their own copy.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourcePrefs {
#[serde(default)]
pub max: SourceQualityCap,
#[serde(default)]
pub min: SourceQualityFloor,
#[serde(default)]
pub priority: SourcePriority,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AppConfig {
#[serde(default)]
pub username: String,
#[serde(default)]
pub listen_port: Option<u16>,
/// Where downloads are saved. None → default `<Music>\SoulSeek+`.
#[serde(default)]
pub download_dir: Option<String>,
/// Album-folder pattern below the managed library/download root.
/// Available placeholders: {artist}, {album}, {date}, {year}.
/// `\` or `/` in the pattern create subfolders (separators coming from
/// tag values are flattened to `_`).
#[serde(default)]
pub library_layout: Option<String>,
/// Extra folders to share (the downloads folder is always shared).
#[serde(default)]
pub shared_folders: Vec<String>,
/// Concurrent upload slots offered to peers.
#[serde(default)]
pub upload_slots: Option<u32>,
/// Named platform output endpoint. None follows Windows' default device.
#[serde(default)]
pub audio_output_device: Option<String>,
/// Spatial upscaler used for provider artist heroes below native 4K.
#[serde(default)]
pub artist_hero_upscaler: ArtistHeroUpscaler,
/// Source-selection preferences for streaming.
#[serde(default)]
pub stream_source_prefs: SourcePrefs,
/// Source-selection preferences for downloads/saves.
#[serde(default)]
pub download_source_prefs: SourcePrefs,
/// Replace whatever picture a download arrives with by the canonical
/// high-resolution cover. None → true. Startup-scanned library files are
/// never rewritten by this; it applies to fresh downloads only.
#[serde(default)]
pub replace_embedded_art: Option<bool>,
/// Longest edge for covers embedded into audio files. None → 1500.
/// Some(0) → embed the original untouched. The full-resolution original
/// is always kept in the art cache and the album folder regardless.
#[serde(default)]
pub embed_art_max_edge: Option<u32>,
/// Write the canonical cover as cover.jpg + folder.jpg in the organized
/// album folder so Explorer and other players show the same artwork.
/// None → true.
#[serde(default)]
pub write_folder_cover: Option<bool>,
/// Save every fully streamed track into the library. None → false:
/// streams stay only in the bounded stream cache below.
#[serde(default)]
pub stream_keep_forever: Option<bool>,
/// Days a streamed track stays in the stream cache after it was last
/// played. None → 30. Ignored when `stream_keep_forever` is on.
#[serde(default)]
pub stream_retention_days: Option<u32>,
/// Stream cache size cap in MB, oldest evicted first. None → 2048.
/// Some(0) → unlimited.
#[serde(default)]
pub stream_cache_max_mb: Option<u64>,
}
/// Default longest edge for embedded covers when the user hasn't chosen.
pub const DEFAULT_EMBED_ART_MAX_EDGE: u32 = 1500;
pub fn replace_embedded_art(cfg: &AppConfig) -> bool {
cfg.replace_embedded_art.unwrap_or(true)
}
/// 0 means "embed the original at full resolution".
pub fn embed_art_max_edge(cfg: &AppConfig) -> u32 {
cfg.embed_art_max_edge.unwrap_or(DEFAULT_EMBED_ART_MAX_EDGE)
}
pub fn write_folder_cover(cfg: &AppConfig) -> bool {
cfg.write_folder_cover.unwrap_or(true)
}
pub const DEFAULT_STREAM_RETENTION_DAYS: u32 = 30;
pub const DEFAULT_STREAM_CACHE_MAX_MB: u64 = 2048;
pub fn stream_keep_forever(cfg: &AppConfig) -> bool {
cfg.stream_keep_forever.unwrap_or(false)
}
pub fn stream_retention_days(cfg: &AppConfig) -> u32 {
cfg.stream_retention_days
.unwrap_or(DEFAULT_STREAM_RETENTION_DAYS)
.max(1)
}
/// 0 = unlimited.
pub fn stream_cache_max_mb(cfg: &AppConfig) -> u64 {
cfg.stream_cache_max_mb
.unwrap_or(DEFAULT_STREAM_CACHE_MAX_MB)
}
/// One-time migration after the identifier rename: the app-data dir moved
/// from `com.trifecta.soulseek-plus` to the new identifier's folder. Rename
/// the old dir into place so the library DB, config and stream cache
/// survive. Never overwrites existing data at the new location.
pub fn migrate_app_data(app: &tauri::AppHandle) {
let Ok(new_dir) = app.path().app_data_dir() else {
return;
};
let Some(parent) = new_dir.parent() else {
return;
};
let old_dir = parent.join(LEGACY_APP_DATA_DIR);
if !old_dir.is_dir() {
return;
}
let occupied = new_dir.exists()
&& std::fs::read_dir(&new_dir)
.map(|mut d| d.next().is_some())
.unwrap_or(true);
if occupied {
return;
}
if new_dir.exists() {
let _ = std::fs::remove_dir(&new_dir);
}
match std::fs::rename(&old_dir, &new_dir) {
Ok(()) => log::info!("migrated app data from {}", old_dir.display()),
Err(e) => log::warn!(
"app data migration failed ({e}); old data stays in {}",
old_dir.display()
),
}
}
fn config_path(app: &tauri::AppHandle) -> Option<PathBuf> {
let dir = app.path().app_data_dir().ok()?;
std::fs::create_dir_all(&dir).ok()?;
Some(dir.join("config.json"))
}
pub fn load(app: &tauri::AppHandle) -> AppConfig {
config_path(app)
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save(app: &tauri::AppHandle, cfg: &AppConfig) {
if let Some(p) = config_path(app) {
if let Ok(json) = serde_json::to_string_pretty(cfg) {
if let Err(e) = std::fs::write(&p, json) {
log::warn!("failed to save config to {}: {e}", p.display());
}
}
}
}
/// The effective downloads directory: configured, or `<Music>\SoulStream`.
/// A pre-rename `<Music>\SoulSeek+` folder keeps being used if it exists,
/// so an existing library doesn't fork into a new empty root.
pub fn download_dir(app: &tauri::AppHandle, cfg: &AppConfig) -> PathBuf {
if let Some(d) = &cfg.download_dir {
if !d.is_empty() {
return PathBuf::from(d);
}
}
let music = app
.path()
.audio_dir()
.or_else(|_| app.path().home_dir())
.unwrap_or_else(|_| PathBuf::from("."));
let legacy = music.join("SoulSeek+");
if legacy.is_dir() {
return legacy;
}
music.join("SoulStream")
}
pub const DEFAULT_LIBRARY_LAYOUT: &str = r"{artist}\{year} - {album}";
pub fn library_layout(cfg: &AppConfig) -> &str {
cfg.library_layout
.as_deref()
.map(str::trim)
.filter(|layout| !layout.is_empty())
.unwrap_or(DEFAULT_LIBRARY_LAYOUT)
}
// ---- password storage (Windows Credential Manager) ----
fn entry(username: &str) -> Option<keyring::Entry> {
keyring::Entry::new(KEYRING_SERVICE, username).ok()
}
pub fn save_password(username: &str, password: &str) {
if let Some(e) = entry(username) {
if let Err(err) = e.set_password(password) {
log::warn!("failed to store password in credential manager: {err}");
}
}
}
pub fn load_password(username: &str) -> Option<String> {
if let Some(pw) = entry(username).and_then(|e| e.get_password().ok()) {
return Some(pw);
}
// Fall back to the pre-rename service entry and migrate it over.
let legacy = keyring::Entry::new(LEGACY_KEYRING_SERVICE, username).ok()?;
let pw = legacy.get_password().ok()?;
save_password(username, &pw);
let _ = legacy.delete_credential();
Some(pw)
}
pub fn has_password(username: &str) -> bool {
!username.is_empty() && load_password(username).is_some()
}
pub fn delete_password(username: &str) {
if let Some(e) = entry(username) {
let _ = e.delete_credential();
}
}
#[cfg(test)]
mod tests {
use super::ArtistHeroUpscaler;
#[test]
fn legacy_fsr_setting_migrates_to_live_gpu() {
let migrated: ArtistHeroUpscaler = serde_json::from_str("\"fsr\"").unwrap();
assert_eq!(migrated, ArtistHeroUpscaler::LiveGpu);
assert_eq!(ArtistHeroUpscaler::default(), ArtistHeroUpscaler::LiveGpu);
assert_eq!(
serde_json::to_string(&ArtistHeroUpscaler::LiveGpu).unwrap(),
"\"live_gpu\""
);
assert_eq!(
serde_json::to_string(&ArtistHeroUpscaler::Fsr).unwrap(),
"\"fsr_legacy\""
);
}
}