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;