mirror of
https://github.com/yattee/yattee.git
synced 2026-07-19 05:42:04 +00:00
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:
@@ -15317,26 +15317,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sources.bandwidth.download %@" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Download: %@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sources.bandwidth.upload %@" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Upload: %@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sources.browsePeerTube" : {
|
||||
"comment" : "Menu item to browse PeerTube instances",
|
||||
"localizations" : {
|
||||
@@ -16318,17 +16298,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sources.status.connected" : {
|
||||
"comment" : "Status when connected",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Connected"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sources.status.disabled" : {
|
||||
"comment" : "Status when source is disabled",
|
||||
"localizations" : {
|
||||
@@ -16340,39 +16309,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sources.status.readOnly" : {
|
||||
"comment" : "Status when source is read-only",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Read-only access"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sources.test.success" : {
|
||||
"comment" : "Test connection success message",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Connected successfully"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sources.testConnection" : {
|
||||
"comment" : "Button to test connection",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Test Connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sources.testing" : {
|
||||
"comment" : "Message while testing connection",
|
||||
"localizations" : {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -11,8 +11,6 @@ import SwiftUI
|
||||
|
||||
/// Result of a connection test for WebDAV/SMB sources.
|
||||
enum SourceTestResult {
|
||||
case success
|
||||
case successWithBandwidth(BandwidthTestResult)
|
||||
case failure(String)
|
||||
}
|
||||
|
||||
@@ -25,38 +23,6 @@ struct SourceTestResultSection: View {
|
||||
var body: some View {
|
||||
Section {
|
||||
switch result {
|
||||
case .success:
|
||||
Label(String(localized: "sources.status.connected"), systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
|
||||
case .successWithBandwidth(let bandwidth):
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Label(String(localized: "sources.status.connected"), systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
if bandwidth.hasWriteAccess {
|
||||
if let upload = bandwidth.formattedUploadSpeed {
|
||||
Label(String(localized: "sources.bandwidth.upload \(upload)"), systemImage: "arrow.up.circle")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
if let download = bandwidth.formattedDownloadSpeed {
|
||||
Label(String(localized: "sources.bandwidth.download \(download)"), systemImage: "arrow.down.circle")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
if !bandwidth.hasWriteAccess {
|
||||
Label(String(localized: "sources.status.readOnly"), systemImage: "lock.fill")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
if let warning = bandwidth.warning {
|
||||
Text(warning)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
Label(error, systemImage: "xmark.circle.fill")
|
||||
.foregroundStyle(.red)
|
||||
|
||||
@@ -59,15 +59,6 @@ private struct EditRemoteServerContent: View {
|
||||
@State private var isLoadingServerInfo = false
|
||||
@State private var serverInfoError: String?
|
||||
|
||||
// Connection testing
|
||||
@State private var isTesting = false
|
||||
@State private var testResult: RemoteServerTestResult?
|
||||
|
||||
enum RemoteServerTestResult {
|
||||
case success
|
||||
case failure(String)
|
||||
}
|
||||
|
||||
init(instance: Instance) {
|
||||
self.instance = instance
|
||||
_name = State(initialValue: instance.name ?? "")
|
||||
@@ -309,30 +300,6 @@ private struct EditRemoteServerContent: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
testConnection()
|
||||
} label: {
|
||||
if isTesting {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
Text(String(localized: "sources.testing"))
|
||||
}
|
||||
} else {
|
||||
Label(String(localized: "sources.testConnection"), systemImage: "network")
|
||||
}
|
||||
}
|
||||
.disabled(isTesting)
|
||||
#if os(tvOS)
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
#endif
|
||||
}
|
||||
|
||||
if let result = testResult {
|
||||
testResultSection(result)
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
Section {
|
||||
Button {
|
||||
@@ -503,41 +470,6 @@ private struct EditRemoteServerContent: View {
|
||||
appEnvironment?.instancesManager.update(updated)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func testConnection() {
|
||||
guard let appEnvironment else { return }
|
||||
isTesting = true
|
||||
testResult = nil
|
||||
|
||||
Task {
|
||||
do {
|
||||
_ = try await appEnvironment.contentService.popular(for: instance)
|
||||
await MainActor.run {
|
||||
isTesting = false
|
||||
testResult = .success
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
isTesting = false
|
||||
testResult = .failure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func testResultSection(_ result: RemoteServerTestResult) -> some View {
|
||||
Section {
|
||||
switch result {
|
||||
case .success:
|
||||
Label(String(localized: "sources.test.success"), systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
case .failure(let error):
|
||||
Label(error, systemImage: "xmark.circle.fill")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - File Source Content
|
||||
@@ -554,18 +486,9 @@ private struct EditFileSourceContent: View {
|
||||
@State private var password: String
|
||||
@State private var allowInvalidCertificates: Bool
|
||||
@State private var smbProtocolVersion: SMBProtocol = .auto
|
||||
@State private var isTesting = false
|
||||
@State private var testResult: TestResult?
|
||||
@State private var testProgress: String?
|
||||
@State private var hasExistingPassword = false
|
||||
@State private var showingDeleteConfirmation = false
|
||||
|
||||
enum TestResult {
|
||||
case success
|
||||
case successWithBandwidth(BandwidthTestResult)
|
||||
case failure(String)
|
||||
}
|
||||
|
||||
init(source: MediaSource) {
|
||||
self.source = source
|
||||
_name = State(initialValue: source.name)
|
||||
@@ -738,30 +661,6 @@ private struct EditFileSourceContent: View {
|
||||
} footer: {
|
||||
Text(String(localized: "sources.footer.allowInvalidCertificates"))
|
||||
}
|
||||
|
||||
Section {
|
||||
Button {
|
||||
testConnection()
|
||||
} label: {
|
||||
if isTesting {
|
||||
HStack {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
Text(testProgress ?? String(localized: "sources.testing"))
|
||||
}
|
||||
} else {
|
||||
Label(String(localized: "sources.testConnection"), systemImage: "speedometer")
|
||||
}
|
||||
}
|
||||
.disabled(isTesting)
|
||||
#if os(tvOS)
|
||||
.buttonStyle(TVSettingsButtonStyle())
|
||||
#endif
|
||||
}
|
||||
|
||||
if let result = testResult {
|
||||
testResultSection(result)
|
||||
}
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
@@ -807,90 +706,6 @@ private struct EditFileSourceContent: View {
|
||||
.presentationCompactAdaptation(.sheet)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func testResultSection(_ result: TestResult) -> some View {
|
||||
Section {
|
||||
switch result {
|
||||
case .success:
|
||||
Label(String(localized: "sources.status.connected"), systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
case .successWithBandwidth(let bandwidth):
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Label(String(localized: "sources.status.connected"), systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
if bandwidth.hasWriteAccess {
|
||||
if let upload = bandwidth.formattedUploadSpeed {
|
||||
Label(String(localized: "sources.bandwidth.upload \(upload)"), systemImage: "arrow.up.circle")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
if let download = bandwidth.formattedDownloadSpeed {
|
||||
Label(String(localized: "sources.bandwidth.download \(download)"), systemImage: "arrow.down.circle")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
if !bandwidth.hasWriteAccess {
|
||||
Label(String(localized: "sources.status.readOnly"), systemImage: "lock.fill")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
if let warning = bandwidth.warning {
|
||||
Text(warning)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
Label(error, systemImage: "xmark.circle.fill")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func testConnection() {
|
||||
guard let appEnvironment else { return }
|
||||
|
||||
isTesting = true
|
||||
testResult = nil
|
||||
testProgress = nil
|
||||
|
||||
let testPassword = password.isEmpty
|
||||
? appEnvironment.mediaSourcesManager.password(for: source)
|
||||
: password
|
||||
|
||||
var updatedSource = source
|
||||
updatedSource.username = username.isEmpty ? nil : username
|
||||
updatedSource.allowInvalidCertificates = allowInvalidCertificates
|
||||
|
||||
// Use factory to create client with appropriate SSL settings
|
||||
let webDAVClient = appEnvironment.webDAVClientFactory.createClient(for: updatedSource)
|
||||
|
||||
Task {
|
||||
do {
|
||||
let bandwidthResult = try await webDAVClient.testBandwidth(
|
||||
source: updatedSource,
|
||||
password: testPassword
|
||||
) { status in
|
||||
Task { @MainActor in
|
||||
self.testProgress = status
|
||||
}
|
||||
}
|
||||
await MainActor.run {
|
||||
isTesting = false
|
||||
testProgress = nil
|
||||
testResult = .successWithBandwidth(bandwidthResult)
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
isTesting = false
|
||||
testProgress = nil
|
||||
testResult = .failure(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveChanges() {
|
||||
guard let appEnvironment else { return }
|
||||
|
||||
|
||||
@@ -154,101 +154,6 @@ struct LockedStorageTests {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BandwidthTestResult Tests
|
||||
|
||||
@Suite("BandwidthTestResult Tests")
|
||||
struct BandwidthTestResultTests {
|
||||
|
||||
@Test("BandwidthTestResult with write access")
|
||||
func bandwidthTestResultWithWrite() {
|
||||
let result = BandwidthTestResult(
|
||||
hasWriteAccess: true,
|
||||
uploadSpeed: 50_000_000, // 50 MB/s
|
||||
downloadSpeed: 100_000_000, // 100 MB/s
|
||||
testFileSize: 5 * 1024 * 1024, // 5 MB
|
||||
warning: nil
|
||||
)
|
||||
|
||||
#expect(result.hasWriteAccess == true)
|
||||
#expect(result.uploadSpeed == 50_000_000)
|
||||
#expect(result.downloadSpeed == 100_000_000)
|
||||
#expect(result.testFileSize == 5 * 1024 * 1024)
|
||||
#expect(result.warning == nil)
|
||||
}
|
||||
|
||||
@Test("BandwidthTestResult read-only mode")
|
||||
func bandwidthTestResultReadOnly() {
|
||||
let result = BandwidthTestResult(
|
||||
hasWriteAccess: false,
|
||||
uploadSpeed: nil,
|
||||
downloadSpeed: 75_000_000,
|
||||
testFileSize: 5 * 1024 * 1024,
|
||||
warning: "Server is read-only"
|
||||
)
|
||||
|
||||
#expect(result.hasWriteAccess == false)
|
||||
#expect(result.uploadSpeed == nil)
|
||||
#expect(result.downloadSpeed == 75_000_000)
|
||||
#expect(result.warning == "Server is read-only")
|
||||
}
|
||||
|
||||
@Test("BandwidthTestResult formatted speeds")
|
||||
func bandwidthTestResultFormattedSpeeds() {
|
||||
let result = BandwidthTestResult(
|
||||
hasWriteAccess: true,
|
||||
uploadSpeed: 50_000_000,
|
||||
downloadSpeed: 100_000_000,
|
||||
testFileSize: 5 * 1024 * 1024,
|
||||
warning: nil
|
||||
)
|
||||
|
||||
// Formatted strings should contain speed values (optional returns)
|
||||
#expect(result.formattedDownloadSpeed != nil)
|
||||
#expect(result.formattedUploadSpeed != nil)
|
||||
#expect(result.formattedDownloadSpeed?.contains("/s") == true)
|
||||
#expect(result.formattedUploadSpeed?.contains("/s") == true)
|
||||
}
|
||||
|
||||
@Test("formattedUploadSpeed nil when no upload")
|
||||
func formattedUploadSpeedNil() {
|
||||
let result = BandwidthTestResult(
|
||||
hasWriteAccess: false,
|
||||
uploadSpeed: nil,
|
||||
downloadSpeed: 50_000_000,
|
||||
testFileSize: 5_000_000,
|
||||
warning: nil
|
||||
)
|
||||
|
||||
#expect(result.formattedUploadSpeed == nil)
|
||||
}
|
||||
|
||||
@Test("formattedDownloadSpeed nil when no download")
|
||||
func formattedDownloadSpeedNil() {
|
||||
let result = BandwidthTestResult(
|
||||
hasWriteAccess: false,
|
||||
uploadSpeed: nil,
|
||||
downloadSpeed: nil,
|
||||
testFileSize: 0,
|
||||
warning: "No files available"
|
||||
)
|
||||
|
||||
#expect(result.formattedDownloadSpeed == nil)
|
||||
}
|
||||
|
||||
@Test("Warning message preserved")
|
||||
func warningPreserved() {
|
||||
let result = BandwidthTestResult(
|
||||
hasWriteAccess: false,
|
||||
uploadSpeed: nil,
|
||||
downloadSpeed: nil,
|
||||
testFileSize: 0,
|
||||
warning: "Server appears empty, could not test download speed"
|
||||
)
|
||||
|
||||
#expect(result.warning == "Server appears empty, could not test download speed")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MediaSourceError Tests
|
||||
|
||||
@Suite("MediaSourceError Tests")
|
||||
|
||||
Reference in New Issue
Block a user