diff --git a/octo-fiesta.Tests/DeezerDownloadServiceTests.cs b/octo-fiesta.Tests/DeezerDownloadServiceTests.cs index a740392..2134cde 100644 --- a/octo-fiesta.Tests/DeezerDownloadServiceTests.cs +++ b/octo-fiesta.Tests/DeezerDownloadServiceTests.cs @@ -1,6 +1,7 @@ using octo_fiesta.Services; using octo_fiesta.Services.Deezer; using octo_fiesta.Services.Local; +using octo_fiesta.Services.Common; using octo_fiesta.Models.Domain; using octo_fiesta.Models.Settings; using octo_fiesta.Models.Download; diff --git a/octo-fiesta/Services/Common/BaseDownloadService.cs b/octo-fiesta/Services/Common/BaseDownloadService.cs index 15b477e..050d651 100644 --- a/octo-fiesta/Services/Common/BaseDownloadService.cs +++ b/octo-fiesta/Services/Common/BaseDownloadService.cs @@ -4,7 +4,6 @@ using octo_fiesta.Models.Download; using octo_fiesta.Models.Search; using octo_fiesta.Models.Subsonic; using octo_fiesta.Services.Local; -using octo_fiesta.Services.Deezer; using TagLib; using IOFile = System.IO.File; diff --git a/octo-fiesta/Services/Common/PathHelper.cs b/octo-fiesta/Services/Common/PathHelper.cs new file mode 100644 index 0000000..93f43ad --- /dev/null +++ b/octo-fiesta/Services/Common/PathHelper.cs @@ -0,0 +1,125 @@ +using IOFile = System.IO.File; + +namespace octo_fiesta.Services.Common; + +/// +/// Helper class for path building and sanitization. +/// Provides utilities for creating safe file and folder paths for downloaded music files. +/// +public static class PathHelper +{ + /// + /// Builds the output path for a downloaded track following the Artist/Album/Track structure. + /// + /// Base download directory path. + /// Artist name (will be sanitized). + /// Album name (will be sanitized). + /// Track title (will be sanitized). + /// Optional track number for prefix. + /// File extension (e.g., ".flac", ".mp3"). + /// Full path for the track file. + public static string BuildTrackPath(string downloadPath, string artist, string album, string title, int? trackNumber, string extension) + { + var safeArtist = SanitizeFolderName(artist); + var safeAlbum = SanitizeFolderName(album); + var safeTitle = SanitizeFileName(title); + + var artistFolder = Path.Combine(downloadPath, safeArtist); + var albumFolder = Path.Combine(artistFolder, safeAlbum); + + var trackPrefix = trackNumber.HasValue ? $"{trackNumber:D2} - " : ""; + var fileName = $"{trackPrefix}{safeTitle}{extension}"; + + return Path.Combine(albumFolder, fileName); + } + + /// + /// Sanitizes a file name by removing invalid characters. + /// + /// Original file name. + /// Sanitized file name safe for all file systems. + public static string SanitizeFileName(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + return "Unknown"; + } + + var invalidChars = Path.GetInvalidFileNameChars(); + var sanitized = new string(fileName + .Select(c => invalidChars.Contains(c) ? '_' : c) + .ToArray()); + + if (sanitized.Length > 100) + { + sanitized = sanitized[..100]; + } + + return sanitized.Trim(); + } + + /// + /// Sanitizes a folder name by removing invalid path characters. + /// + /// Original folder name. + /// Sanitized folder name safe for all file systems. + public static string SanitizeFolderName(string folderName) + { + if (string.IsNullOrWhiteSpace(folderName)) + { + return "Unknown"; + } + + var invalidChars = Path.GetInvalidFileNameChars() + .Concat(Path.GetInvalidPathChars()) + .Distinct() + .ToArray(); + + var sanitized = new string(folderName + .Select(c => invalidChars.Contains(c) ? '_' : c) + .ToArray()); + + // Remove leading/trailing dots and spaces (Windows folder restrictions) + sanitized = sanitized.Trim().TrimEnd('.'); + + if (sanitized.Length > 100) + { + sanitized = sanitized[..100].TrimEnd('.'); + } + + // Ensure we have a valid name + if (string.IsNullOrWhiteSpace(sanitized)) + { + return "Unknown"; + } + + return sanitized; + } + + /// + /// Resolves a unique file path by appending a counter if the file already exists. + /// + /// Desired file path. + /// Unique file path that does not exist yet. + public static string ResolveUniquePath(string basePath) + { + if (!IOFile.Exists(basePath)) + { + return basePath; + } + + var directory = Path.GetDirectoryName(basePath)!; + var extension = Path.GetExtension(basePath); + var fileNameWithoutExt = Path.GetFileNameWithoutExtension(basePath); + + var counter = 1; + string uniquePath; + do + { + uniquePath = Path.Combine(directory, $"{fileNameWithoutExt} ({counter}){extension}"); + counter++; + } while (IOFile.Exists(uniquePath)); + + return uniquePath; + } +} diff --git a/octo-fiesta/Services/Deezer/DeezerDownloadService.cs b/octo-fiesta/Services/Deezer/DeezerDownloadService.cs index 73a5e60..0bc843d 100644 --- a/octo-fiesta/Services/Deezer/DeezerDownloadService.cs +++ b/octo-fiesta/Services/Deezer/DeezerDownloadService.cs @@ -523,112 +523,3 @@ public class DeezerDownloadService : BaseDownloadService public string Artist { get; set; } = string.Empty; } } - -/// -/// Helper class for path building and sanitization. -/// Extracted for testability. -/// -public static class PathHelper -{ - /// - /// Builds the output path for a downloaded track following the Artist/Album/Track structure. - /// - public static string BuildTrackPath(string downloadPath, string artist, string album, string title, int? trackNumber, string extension) - { - var safeArtist = SanitizeFolderName(artist); - var safeAlbum = SanitizeFolderName(album); - var safeTitle = SanitizeFileName(title); - - var artistFolder = Path.Combine(downloadPath, safeArtist); - var albumFolder = Path.Combine(artistFolder, safeAlbum); - - var trackPrefix = trackNumber.HasValue ? $"{trackNumber:D2} - " : ""; - var fileName = $"{trackPrefix}{safeTitle}{extension}"; - - return Path.Combine(albumFolder, fileName); - } - - /// - /// Sanitizes a file name by removing invalid characters. - /// - public static string SanitizeFileName(string fileName) - { - if (string.IsNullOrWhiteSpace(fileName)) - { - return "Unknown"; - } - - var invalidChars = Path.GetInvalidFileNameChars(); - var sanitized = new string(fileName - .Select(c => invalidChars.Contains(c) ? '_' : c) - .ToArray()); - - if (sanitized.Length > 100) - { - sanitized = sanitized[..100]; - } - - return sanitized.Trim(); - } - - /// - /// Sanitizes a folder name by removing invalid path characters. - /// - public static string SanitizeFolderName(string folderName) - { - if (string.IsNullOrWhiteSpace(folderName)) - { - return "Unknown"; - } - - var invalidChars = Path.GetInvalidFileNameChars() - .Concat(Path.GetInvalidPathChars()) - .Distinct() - .ToArray(); - - var sanitized = new string(folderName - .Select(c => invalidChars.Contains(c) ? '_' : c) - .ToArray()); - - // Remove leading/trailing dots and spaces (Windows folder restrictions) - sanitized = sanitized.Trim().TrimEnd('.'); - - if (sanitized.Length > 100) - { - sanitized = sanitized[..100].TrimEnd('.'); - } - - // Ensure we have a valid name - if (string.IsNullOrWhiteSpace(sanitized)) - { - return "Unknown"; - } - - return sanitized; - } - - /// - /// Resolves a unique file path by appending a counter if the file already exists. - /// - public static string ResolveUniquePath(string basePath) - { - if (!IOFile.Exists(basePath)) - { - return basePath; - } - - var directory = Path.GetDirectoryName(basePath)!; - var extension = Path.GetExtension(basePath); - var fileNameWithoutExt = Path.GetFileNameWithoutExtension(basePath); - - var counter = 1; - string uniquePath; - do - { - uniquePath = Path.Combine(directory, $"{fileNameWithoutExt} ({counter}){extension}"); - counter++; - } while (IOFile.Exists(uniquePath)); - - return uniquePath; - } -} diff --git a/octo-fiesta/Services/Qobuz/QobuzDownloadService.cs b/octo-fiesta/Services/Qobuz/QobuzDownloadService.cs index 7e26ddc..5abddfa 100644 --- a/octo-fiesta/Services/Qobuz/QobuzDownloadService.cs +++ b/octo-fiesta/Services/Qobuz/QobuzDownloadService.cs @@ -8,7 +8,6 @@ using octo_fiesta.Models.Search; using octo_fiesta.Models.Subsonic; using octo_fiesta.Services.Local; using octo_fiesta.Services.Common; -using octo_fiesta.Services.Deezer; using Microsoft.Extensions.Options; using IOFile = System.IO.File;