mirror of
https://github.com/markuryy/shark.git
synced 2026-02-01 12:31:02 +00:00
feat(sync): support playlists
This commit is contained in:
@@ -66,57 +66,19 @@ fn normalize_path(path: &Path) -> String {
|
|||||||
path.to_string_lossy().nfc().collect::<String>()
|
path.to_string_lossy().nfc().collect::<String>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Index device and compare with library
|
/// Helper function to index and compare a single folder pair
|
||||||
#[tauri::command]
|
fn compare_folder_pair(
|
||||||
pub async fn index_and_compare(
|
library_path: &Path,
|
||||||
library_path: String,
|
device_files: &HashMap<String, FileMetadata>,
|
||||||
device_path: String,
|
overwrite_mode: &str,
|
||||||
overwrite_mode: String, // "skip" | "different" | "always"
|
) -> Result<(Vec<FileInfo>, usize, usize, usize, u64), String> {
|
||||||
) -> Result<SyncDiff, String> {
|
|
||||||
let library_path = PathBuf::from(library_path);
|
|
||||||
let device_path = PathBuf::from(device_path);
|
|
||||||
|
|
||||||
// Validate paths exist
|
|
||||||
if !library_path.exists() {
|
|
||||||
return Err(format!("Library path does not exist: {}", library_path.display()));
|
|
||||||
}
|
|
||||||
if !device_path.exists() {
|
|
||||||
return Err(format!("Device path does not exist: {}", device_path.display()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 1: Index device - build HashMap of existing files with normalized paths
|
|
||||||
let mut device_files: HashMap<String, FileMetadata> = HashMap::new();
|
|
||||||
|
|
||||||
for entry in WalkDir::new(&device_path)
|
|
||||||
.follow_links(false)
|
|
||||||
.into_iter()
|
|
||||||
.filter_entry(|e| !should_skip_dir(e.path()))
|
|
||||||
{
|
|
||||||
let entry = entry.map_err(|e| format!("Error reading device: {}", e))?;
|
|
||||||
|
|
||||||
if entry.file_type().is_file() && !should_skip_file(entry.path()) {
|
|
||||||
let relative_path = entry.path()
|
|
||||||
.strip_prefix(&device_path)
|
|
||||||
.map_err(|e| format!("Path error: {}", e))?
|
|
||||||
.to_path_buf();
|
|
||||||
|
|
||||||
if let Ok(metadata) = entry.metadata() {
|
|
||||||
let normalized = normalize_path(&relative_path);
|
|
||||||
device_files.insert(normalized, FileMetadata {
|
|
||||||
size: metadata.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Walk library and compare
|
|
||||||
let mut files_to_copy = Vec::new();
|
let mut files_to_copy = Vec::new();
|
||||||
let mut new_count = 0;
|
let mut new_count = 0;
|
||||||
let mut updated_count = 0;
|
let mut updated_count = 0;
|
||||||
let mut unchanged_count = 0;
|
let mut unchanged_count = 0;
|
||||||
let mut total_size = 0u64;
|
let mut total_size = 0u64;
|
||||||
|
|
||||||
for entry in WalkDir::new(&library_path)
|
for entry in WalkDir::new(library_path)
|
||||||
.follow_links(false)
|
.follow_links(false)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_entry(|e| !should_skip_dir(e.path()))
|
.filter_entry(|e| !should_skip_dir(e.path()))
|
||||||
@@ -125,7 +87,7 @@ pub async fn index_and_compare(
|
|||||||
|
|
||||||
if entry.file_type().is_file() && !should_skip_file(entry.path()) {
|
if entry.file_type().is_file() && !should_skip_file(entry.path()) {
|
||||||
let relative_path = entry.path()
|
let relative_path = entry.path()
|
||||||
.strip_prefix(&library_path)
|
.strip_prefix(library_path)
|
||||||
.map_err(|e| format!("Path error: {}", e))?
|
.map_err(|e| format!("Path error: {}", e))?
|
||||||
.to_path_buf();
|
.to_path_buf();
|
||||||
|
|
||||||
@@ -140,11 +102,11 @@ pub async fn index_and_compare(
|
|||||||
// File exists on device
|
// File exists on device
|
||||||
let size_different = device_meta.size != file_size;
|
let size_different = device_meta.size != file_size;
|
||||||
|
|
||||||
let should_copy = match overwrite_mode.as_str() {
|
let should_copy = match overwrite_mode {
|
||||||
"skip" => false, // Never overwrite
|
"skip" => false,
|
||||||
"different" => size_different, // Only if different size
|
"different" => size_different,
|
||||||
"always" => true, // Always overwrite
|
"always" => true,
|
||||||
_ => size_different, // Default to "different"
|
_ => size_different,
|
||||||
};
|
};
|
||||||
|
|
||||||
if should_copy {
|
if should_copy {
|
||||||
@@ -171,13 +133,116 @@ pub async fn index_and_compare(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok((files_to_copy, new_count, updated_count, unchanged_count, total_size))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Index device and compare with library
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn index_and_compare(
|
||||||
|
music_library_path: String,
|
||||||
|
music_device_path: String,
|
||||||
|
playlists_library_path: Option<String>,
|
||||||
|
playlists_device_path: Option<String>,
|
||||||
|
overwrite_mode: String, // "skip" | "different" | "always"
|
||||||
|
) -> Result<SyncDiff, String> {
|
||||||
|
let music_library = PathBuf::from(music_library_path);
|
||||||
|
let music_device = PathBuf::from(music_device_path);
|
||||||
|
|
||||||
|
// Validate music paths exist
|
||||||
|
if !music_library.exists() {
|
||||||
|
return Err(format!("Music library path does not exist: {}", music_library.display()));
|
||||||
|
}
|
||||||
|
if !music_device.exists() {
|
||||||
|
return Err(format!("Music device path does not exist: {}", music_device.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse optional playlists paths
|
||||||
|
let playlists_pair = match (playlists_library_path, playlists_device_path) {
|
||||||
|
(Some(lib), Some(dev)) => {
|
||||||
|
let lib_path = PathBuf::from(lib);
|
||||||
|
let dev_path = PathBuf::from(dev);
|
||||||
|
if lib_path.exists() && dev_path.exists() {
|
||||||
|
Some((lib_path, dev_path))
|
||||||
|
} else {
|
||||||
|
None // Skip if either path doesn't exist
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step 1: Index device music folder
|
||||||
|
let mut device_files: HashMap<String, FileMetadata> = HashMap::new();
|
||||||
|
|
||||||
|
for entry in WalkDir::new(&music_device)
|
||||||
|
.follow_links(false)
|
||||||
|
.into_iter()
|
||||||
|
.filter_entry(|e| !should_skip_dir(e.path()))
|
||||||
|
{
|
||||||
|
let entry = entry.map_err(|e| format!("Error reading device music: {}", e))?;
|
||||||
|
|
||||||
|
if entry.file_type().is_file() && !should_skip_file(entry.path()) {
|
||||||
|
let relative_path = entry.path()
|
||||||
|
.strip_prefix(&music_device)
|
||||||
|
.map_err(|e| format!("Path error: {}", e))?
|
||||||
|
.to_path_buf();
|
||||||
|
|
||||||
|
if let Ok(metadata) = entry.metadata() {
|
||||||
|
let normalized = normalize_path(&relative_path);
|
||||||
|
device_files.insert(normalized, FileMetadata {
|
||||||
|
size: metadata.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index device playlists folder if it exists
|
||||||
|
if let Some((_, ref playlists_dev)) = playlists_pair {
|
||||||
|
for entry in WalkDir::new(playlists_dev)
|
||||||
|
.follow_links(false)
|
||||||
|
.into_iter()
|
||||||
|
.filter_entry(|e| !should_skip_dir(e.path()))
|
||||||
|
{
|
||||||
|
let entry = entry.map_err(|e| format!("Error reading device playlists: {}", e))?;
|
||||||
|
|
||||||
|
if entry.file_type().is_file() && !should_skip_file(entry.path()) {
|
||||||
|
let relative_path = entry.path()
|
||||||
|
.strip_prefix(playlists_dev)
|
||||||
|
.map_err(|e| format!("Path error: {}", e))?
|
||||||
|
.to_path_buf();
|
||||||
|
|
||||||
|
if let Ok(metadata) = entry.metadata() {
|
||||||
|
let normalized = normalize_path(&relative_path);
|
||||||
|
device_files.insert(normalized, FileMetadata {
|
||||||
|
size: metadata.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Compare music library
|
||||||
|
let (mut all_files, mut total_new, mut total_updated, mut total_unchanged, mut total_bytes) =
|
||||||
|
compare_folder_pair(&music_library, &device_files, &overwrite_mode)?;
|
||||||
|
|
||||||
|
// Step 3: Compare playlists library if configured
|
||||||
|
if let Some((ref playlists_lib, _)) = playlists_pair {
|
||||||
|
let (pl_files, pl_new, pl_updated, pl_unchanged, pl_bytes) =
|
||||||
|
compare_folder_pair(playlists_lib, &device_files, &overwrite_mode)?;
|
||||||
|
|
||||||
|
all_files.extend(pl_files);
|
||||||
|
total_new += pl_new;
|
||||||
|
total_updated += pl_updated;
|
||||||
|
total_unchanged += pl_unchanged;
|
||||||
|
total_bytes += pl_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(SyncDiff {
|
Ok(SyncDiff {
|
||||||
files_to_copy,
|
files_to_copy: all_files,
|
||||||
stats: SyncStats {
|
stats: SyncStats {
|
||||||
new_files: new_count,
|
new_files: total_new,
|
||||||
updated_files: updated_count,
|
updated_files: total_updated,
|
||||||
unchanged_files: unchanged_count,
|
unchanged_files: total_unchanged,
|
||||||
total_size,
|
total_size: total_bytes,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -186,18 +251,43 @@ pub async fn index_and_compare(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn sync_to_device(
|
pub async fn sync_to_device(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
library_path: String,
|
music_library_path: String,
|
||||||
device_path: String,
|
music_device_path: String,
|
||||||
|
playlists_library_path: Option<String>,
|
||||||
|
playlists_device_path: Option<String>,
|
||||||
files_to_copy: Vec<FileInfo>,
|
files_to_copy: Vec<FileInfo>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let library_path = PathBuf::from(library_path);
|
let music_library = PathBuf::from(music_library_path);
|
||||||
let device_path = PathBuf::from(device_path);
|
let music_device = PathBuf::from(music_device_path);
|
||||||
|
|
||||||
|
let playlists_pair = match (playlists_library_path, playlists_device_path) {
|
||||||
|
(Some(lib), Some(dev)) => Some((PathBuf::from(lib), PathBuf::from(dev))),
|
||||||
|
_ => None
|
||||||
|
};
|
||||||
|
|
||||||
let total = files_to_copy.len();
|
let total = files_to_copy.len();
|
||||||
|
|
||||||
for (index, file_info) in files_to_copy.iter().enumerate() {
|
for (index, file_info) in files_to_copy.iter().enumerate() {
|
||||||
let source = library_path.join(&file_info.relative_path);
|
// Determine source and destination by checking if file exists in music or playlists library
|
||||||
let dest = device_path.join(&file_info.relative_path);
|
let rel_path = PathBuf::from(&file_info.relative_path);
|
||||||
|
let music_source = music_library.join(&rel_path);
|
||||||
|
|
||||||
|
let (source, dest) = if music_source.exists() {
|
||||||
|
// File is in music library
|
||||||
|
(music_source, music_device.join(&rel_path))
|
||||||
|
} else if let Some((ref pl_lib, ref pl_dev)) = playlists_pair {
|
||||||
|
// Check playlists library
|
||||||
|
let playlist_source = pl_lib.join(&rel_path);
|
||||||
|
if playlist_source.exists() {
|
||||||
|
(playlist_source, pl_dev.join(&rel_path))
|
||||||
|
} else {
|
||||||
|
// Fallback to music (original behavior)
|
||||||
|
(music_source, music_device.join(&rel_path))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No playlists configured, use music paths
|
||||||
|
(music_source, music_device.join(&rel_path))
|
||||||
|
};
|
||||||
|
|
||||||
// Emit progress
|
// Emit progress
|
||||||
app.emit("sync-progress", SyncProgress {
|
app.emit("sync-progress", SyncProgress {
|
||||||
@@ -222,7 +312,7 @@ pub async fn sync_to_device(
|
|||||||
format!("Permission denied: {}", dest.display())
|
format!("Permission denied: {}", dest.display())
|
||||||
}
|
}
|
||||||
std::io::ErrorKind::NotFound => {
|
std::io::ErrorKind::NotFound => {
|
||||||
format!("Device disconnected or path not found: {}", dest.display())
|
format!("Source file not found or device disconnected: {} -> {}", source.display(), dest.display())
|
||||||
}
|
}
|
||||||
_ => format!("Failed to copy {}: {}", file_info.relative_path, e)
|
_ => format!("Failed to copy {}: {}", file_info.relative_path, e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,18 @@ export type ProgressCallback = (progress: SyncProgress) => void;
|
|||||||
* Returns a diff showing which files need to be synced
|
* Returns a diff showing which files need to be synced
|
||||||
*/
|
*/
|
||||||
export async function indexAndCompare(
|
export async function indexAndCompare(
|
||||||
libraryPath: string,
|
musicLibraryPath: string,
|
||||||
devicePath: string,
|
musicDevicePath: string,
|
||||||
|
playlistsLibraryPath: string | null | undefined,
|
||||||
|
playlistsDevicePath: string | null | undefined,
|
||||||
overwriteMode: string
|
overwriteMode: string
|
||||||
): Promise<SyncDiff> {
|
): Promise<SyncDiff> {
|
||||||
try {
|
try {
|
||||||
const result = await invoke<SyncDiff>('index_and_compare', {
|
const result = await invoke<SyncDiff>('index_and_compare', {
|
||||||
libraryPath,
|
musicLibraryPath,
|
||||||
devicePath,
|
musicDevicePath,
|
||||||
|
playlistsLibraryPath: playlistsLibraryPath || undefined,
|
||||||
|
playlistsDevicePath: playlistsDevicePath || undefined,
|
||||||
overwriteMode
|
overwriteMode
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
@@ -59,8 +63,10 @@ export async function indexAndCompare(
|
|||||||
* Sync files to device with progress updates
|
* Sync files to device with progress updates
|
||||||
*/
|
*/
|
||||||
export async function syncToDevice(
|
export async function syncToDevice(
|
||||||
libraryPath: string,
|
musicLibraryPath: string,
|
||||||
devicePath: string,
|
musicDevicePath: string,
|
||||||
|
playlistsLibraryPath: string | null | undefined,
|
||||||
|
playlistsDevicePath: string | null | undefined,
|
||||||
filesToCopy: FileInfo[],
|
filesToCopy: FileInfo[],
|
||||||
onProgress?: ProgressCallback
|
onProgress?: ProgressCallback
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
@@ -76,8 +82,10 @@ export async function syncToDevice(
|
|||||||
|
|
||||||
// Start sync operation
|
// Start sync operation
|
||||||
const result = await invoke<string>('sync_to_device', {
|
const result = await invoke<string>('sync_to_device', {
|
||||||
libraryPath,
|
musicLibraryPath,
|
||||||
devicePath,
|
musicDevicePath,
|
||||||
|
playlistsLibraryPath: playlistsLibraryPath || undefined,
|
||||||
|
playlistsDevicePath: playlistsDevicePath || undefined,
|
||||||
filesToCopy
|
filesToCopy
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,8 @@
|
|||||||
const result = await indexAndCompare(
|
const result = await indexAndCompare(
|
||||||
$settings.musicFolder,
|
$settings.musicFolder,
|
||||||
$deviceSyncSettings.musicPath,
|
$deviceSyncSettings.musicPath,
|
||||||
|
$settings.playlistsFolder,
|
||||||
|
$deviceSyncSettings.playlistsPath,
|
||||||
$deviceSyncSettings.overwriteMode
|
$deviceSyncSettings.overwriteMode
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -160,6 +162,8 @@
|
|||||||
const result = await syncToDevice(
|
const result = await syncToDevice(
|
||||||
$settings.musicFolder,
|
$settings.musicFolder,
|
||||||
$deviceSyncSettings.musicPath,
|
$deviceSyncSettings.musicPath,
|
||||||
|
$settings.playlistsFolder,
|
||||||
|
$deviceSyncSettings.playlistsPath,
|
||||||
syncDiff.filesToCopy,
|
syncDiff.filesToCopy,
|
||||||
(progress) => {
|
(progress) => {
|
||||||
syncProgress = progress;
|
syncProgress = progress;
|
||||||
|
|||||||
Reference in New Issue
Block a user