Remove Test Connection from source editing

The Test Connection buttons only probed API/server reachability, which
was misleading: a green result said nothing about whether videos would
actually play. Remove both the remote-server and WebDAV bandwidth test
buttons and all code exclusive to them (testBandwidth, BandwidthTestResult,
and orphaned localization keys). Add-time connectivity validation for
SMB/WebDAV sources is retained.
This commit is contained in:
Arkadiusz Fal
2026-05-19 20:40:28 +02:00
parent 858011c507
commit 88fafc5ada
6 changed files with 0 additions and 781 deletions

View File

@@ -476,51 +476,6 @@ extension SMBClient {
}
}
// MARK: - Bandwidth Testing
extension SMBClient {
/// Tests bandwidth to an SMB server.
///
/// Note: This is a placeholder implementation.
/// Real bandwidth testing would require file upload/download operations.
///
/// - Parameters:
/// - source: The media source configuration.
/// - password: The password for authentication.
/// - testFileSizeMB: Size of test file in megabytes.
/// - progressHandler: Optional callback for progress updates.
/// - Returns: BandwidthTestResult with speed measurements (same type as WebDAV).
func testBandwidth(
source: MediaSource,
password: String?,
testFileSizeMB: Int = 5,
progressHandler: (@Sendable (String) -> Void)? = nil
) async throws -> BandwidthTestResult {
guard source.type == .smb else {
throw MediaSourceError.unknown("Invalid source type for SMB client")
}
progressHandler?("Connecting...")
// Validate connection
_ = try await testConnection(source: source, password: password)
progressHandler?("Complete")
// TODO: Implement actual bandwidth testing
LoggingService.shared.logMediaSourcesWarning("SMB bandwidth testing not yet implemented")
return BandwidthTestResult(
hasWriteAccess: false,
uploadSpeed: nil,
downloadSpeed: nil,
testFileSize: 0,
warning: "Bandwidth testing not available for SMB sources"
)
}
}
// MARK: - URL Extension for Sanitization
extension URL {

View File

@@ -95,325 +95,6 @@ actor WebDAVClient {
return true
}
// MARK: - Bandwidth Testing
/// Tests bandwidth to a WebDAV server with auto-detection of write access.
/// - Parameters:
/// - source: The media source configuration.
/// - password: The password for authentication.
/// - testFileSizeMB: Size of test file in megabytes (default 5 MB).
/// - progressHandler: Optional callback for progress updates (status string).
/// - Returns: BandwidthTestResult with speed measurements.
func testBandwidth(
source: MediaSource,
password: String?,
testFileSizeMB: Int = 20,
progressHandler: (@Sendable (String) -> Void)? = nil
) async throws -> BandwidthTestResult {
let bandwidthTestSize = Int64(testFileSizeMB) * 1024 * 1024
guard source.type == .webdav else {
throw MediaSourceError.unknown("Invalid source type for WebDAV client")
}
// First, verify basic connectivity
progressHandler?("Connecting...")
_ = try await listFiles(at: "/", source: source, password: password)
// Try write test first
do {
return try await performWriteTest(source: source, password: password, testSize: bandwidthTestSize, progressHandler: progressHandler)
} catch let error as MediaSourceError {
// Check if it's a permission error - fall back to read-only
if case .connectionFailed(let message) = error,
message.contains("403") || message.contains("405") || message.contains("401") {
return try await performReadOnlyTest(source: source, password: password, testSize: bandwidthTestSize, progressHandler: progressHandler)
}
throw error
} catch {
// For other errors, try read-only test
return try await performReadOnlyTest(source: source, password: password, testSize: bandwidthTestSize, progressHandler: progressHandler)
}
}
/// Performs a write test: upload, download, and delete a test file.
private func performWriteTest(
source: MediaSource,
password: String?,
testSize: Int64,
progressHandler: (@Sendable (String) -> Void)?
) async throws -> BandwidthTestResult {
let testFileName = ".yattee-bandwidth-test-\(UUID().uuidString).tmp"
// Find a writable location - try first subfolder (root may not be writable, e.g. Synology shares listing)
let writablePath = try await findWritablePath(source: source, password: password)
let testPath = writablePath + testFileName
// Generate test data (zeros are fine for bandwidth testing)
let testData = Data(count: Int(testSize))
// Upload test
progressHandler?("Uploading...")
let uploadStart = CFAbsoluteTimeGetCurrent()
try await uploadFile(data: testData, to: testPath, source: source, password: password)
let uploadDuration = CFAbsoluteTimeGetCurrent() - uploadStart
let uploadSpeed = Double(testSize) / uploadDuration
progressHandler?("Downloading...")
// Download test
let downloadStart = CFAbsoluteTimeGetCurrent()
_ = try await downloadFile(from: testPath, source: source, password: password)
let downloadDuration = CFAbsoluteTimeGetCurrent() - downloadStart
let downloadSpeed = Double(testSize) / downloadDuration
progressHandler?("Cleaning up...")
// Delete test file (ignore errors - cleanup is best effort)
try? await deleteFile(at: testPath, source: source, password: password)
progressHandler?("Complete")
return BandwidthTestResult(
hasWriteAccess: true,
uploadSpeed: uploadSpeed,
downloadSpeed: downloadSpeed,
testFileSize: testSize,
warning: nil
)
}
/// Performs a read-only test by finding and downloading an existing file.
private func performReadOnlyTest(
source: MediaSource,
password: String?,
testSize: Int64,
progressHandler: (@Sendable (String) -> Void)?
) async throws -> BandwidthTestResult {
progressHandler?("Finding test file...")
// Find a file to download
guard let testFile = try await findTestFile(source: source, password: password) else {
// Server is empty or has no accessible files
progressHandler?("Complete")
return BandwidthTestResult(
hasWriteAccess: false,
uploadSpeed: nil,
downloadSpeed: nil,
testFileSize: 0,
warning: "No files available for speed test"
)
}
progressHandler?("Downloading...")
// Download the file (or first N MB of it based on test size)
let downloadStart = CFAbsoluteTimeGetCurrent()
let downloadedSize = try await downloadFilePartial(
from: testFile.path,
source: source,
password: password,
maxBytes: testSize
)
let downloadDuration = CFAbsoluteTimeGetCurrent() - downloadStart
let downloadSpeed = Double(downloadedSize) / downloadDuration
progressHandler?("Complete")
return BandwidthTestResult(
hasWriteAccess: false,
uploadSpeed: nil,
downloadSpeed: downloadSpeed,
testFileSize: Int64(downloadedSize),
warning: nil
)
}
/// Finds a writable path for the bandwidth test file.
/// Tries root first, then first available subfolder (useful for Synology where root is shares listing).
private func findWritablePath(
source: MediaSource,
password: String?
) async throws -> String {
// List root to find first subfolder
let rootFiles = try await listFiles(at: "/", source: source, password: password)
// Try first directory as writable location
if let firstDir = rootFiles.first(where: { $0.isDirectory }) {
return firstDir.path.hasSuffix("/") ? firstDir.path : firstDir.path + "/"
}
// Fall back to root if no subdirectories
return "/"
}
/// Finds a suitable file for download testing.
private func findTestFile(
source: MediaSource,
password: String?
) async throws -> MediaFile? {
return try await findFileRecursive(
in: "/",
source: source,
password: password,
depth: 0,
maxDepth: 2
)
}
/// Recursively searches for a suitable test file.
private func findFileRecursive(
in path: String,
source: MediaSource,
password: String?,
depth: Int,
maxDepth: Int
) async throws -> MediaFile? {
let files = try await listFiles(at: path, source: source, password: password)
// First, look for any file with reasonable size (> 100KB)
if let file = files.first(where: { !$0.isDirectory && ($0.size ?? 0) > 100_000 }) {
return file
}
// If at max depth, just return any file
if depth >= maxDepth {
return files.first(where: { !$0.isDirectory })
}
// Otherwise, recurse into directories
for dir in files.filter({ $0.isDirectory }) {
if let file = try? await findFileRecursive(
in: dir.path,
source: source,
password: password,
depth: depth + 1,
maxDepth: maxDepth
) {
return file
}
}
return nil
}
// MARK: - WebDAV Operations for Bandwidth Test
/// Uploads data to a WebDAV server.
private func uploadFile(
data: Data,
to path: String,
source: MediaSource,
password: String?
) async throws {
let normalizedPath = path.hasPrefix("/") ? path : "/\(path)"
let requestURL = source.url.appendingPathComponent(normalizedPath)
var request = URLRequest(url: requestURL)
request.httpMethod = "PUT"
request.httpBody = data
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
request.setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
request.timeoutInterval = 120 // 2 minutes for upload
if let authHeader = buildAuthHeader(username: source.username, password: password) {
request.setValue(authHeader, forHTTPHeaderField: "Authorization")
}
let (_, response) = try await session.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
guard (200...299).contains(httpResponse.statusCode) || httpResponse.statusCode == 201 else {
throw MediaSourceError.connectionFailed("Upload failed: HTTP \(httpResponse.statusCode)")
}
}
}
/// Downloads a file from a WebDAV server.
private func downloadFile(
from path: String,
source: MediaSource,
password: String?
) async throws -> Data {
let normalizedPath = path.hasPrefix("/") ? path : "/\(path)"
let requestURL = source.url.appendingPathComponent(normalizedPath)
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
request.timeoutInterval = 120
if let authHeader = buildAuthHeader(username: source.username, password: password) {
request.setValue(authHeader, forHTTPHeaderField: "Authorization")
}
let (data, response) = try await session.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
guard (200...299).contains(httpResponse.statusCode) else {
throw MediaSourceError.connectionFailed("Download failed: HTTP \(httpResponse.statusCode)")
}
}
return data
}
/// Downloads up to maxBytes of a file (using Range header if supported).
private func downloadFilePartial(
from path: String,
source: MediaSource,
password: String?,
maxBytes: Int64
) async throws -> Int {
let normalizedPath = path.hasPrefix("/") ? path : "/\(path)"
let requestURL = source.url.appendingPathComponent(normalizedPath)
var request = URLRequest(url: requestURL)
request.httpMethod = "GET"
request.setValue("bytes=0-\(maxBytes - 1)", forHTTPHeaderField: "Range")
request.timeoutInterval = 120
if let authHeader = buildAuthHeader(username: source.username, password: password) {
request.setValue(authHeader, forHTTPHeaderField: "Authorization")
}
let (data, response) = try await session.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
// Accept 200 (full file) or 206 (partial content)
guard httpResponse.statusCode == 200 || httpResponse.statusCode == 206 else {
throw MediaSourceError.connectionFailed("Download failed: HTTP \(httpResponse.statusCode)")
}
}
return data.count
}
/// Deletes a file from a WebDAV server.
private func deleteFile(
at path: String,
source: MediaSource,
password: String?
) async throws {
let normalizedPath = path.hasPrefix("/") ? path : "/\(path)"
let requestURL = source.url.appendingPathComponent(normalizedPath)
var request = URLRequest(url: requestURL)
request.httpMethod = "DELETE"
request.timeoutInterval = 30
if let authHeader = buildAuthHeader(username: source.username, password: password) {
request.setValue(authHeader, forHTTPHeaderField: "Authorization")
}
let (_, response) = try await session.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
// Accept 200, 204 (No Content), or 404 (already gone)
guard (200...299).contains(httpResponse.statusCode) || httpResponse.statusCode == 404 else {
throw MediaSourceError.connectionFailed("Delete failed: HTTP \(httpResponse.statusCode)")
}
}
}
/// Builds authentication headers for a WebDAV request.
/// - Parameters:
/// - source: The media source.
@@ -477,45 +158,6 @@ actor WebDAVClient {
}
}
// MARK: - Bandwidth Test Result
/// Result of a bandwidth test on a WebDAV server.
struct BandwidthTestResult: Sendable {
/// Whether the server allows write access (upload/delete).
let hasWriteAccess: Bool
/// Upload speed in bytes per second (nil if write access unavailable).
let uploadSpeed: Double?
/// Download speed in bytes per second.
let downloadSpeed: Double?
/// Size of the test file used (in bytes).
let testFileSize: Int64
/// Any warning message (e.g., "Server appears empty, could not test download speed").
let warning: String?
/// Formatted upload speed string (e.g., "12.5 MB/s").
var formattedUploadSpeed: String? {
guard let speed = uploadSpeed else { return nil }
return Self.formatSpeed(speed)
}
/// Formatted download speed string (e.g., "45.2 MB/s").
var formattedDownloadSpeed: String? {
guard let speed = downloadSpeed else { return nil }
return Self.formatSpeed(speed)
}
private static func formatSpeed(_ bytesPerSecond: Double) -> String {
let formatter = ByteCountFormatter()
formatter.countStyle = .file
formatter.allowedUnits = [.useKB, .useMB, .useGB]
return formatter.string(fromByteCount: Int64(bytesPerSecond)) + "/s"
}
}
// MARK: - WebDAV Response Parser
/// Parses WebDAV PROPFIND multi-status XML responses.