Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions Core/Services/AI/AIErrorHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,44 @@ enum AIErrorHandler {
/// Handles specific GenerationError cases.
private static func handleGenerationError(_ error: LanguageModelSession.GenerationError) -> AIError {
switch error {
case let .exceededContextWindowSize(info):
self.logger.warning("Context window exceeded: \\(String(describing: info))")
case .exceededContextWindowSize:
self.logger.warning("Context window exceeded")
return .contextWindowExceeded

case .guardrailViolation:
self.logger.warning("Content blocked by guardrails")
return .contentBlocked

case .assetsUnavailable:
self.logger.warning("Model assets unavailable")
return .notAvailable(reason: "Model assets are not available")

case .unsupportedGuide:
self.logger.warning("Unsupported generation guide")
return .unknown(underlying: error)

case .unsupportedLanguageOrLocale:
self.logger.warning("Unsupported language or locale")
return .notAvailable(reason: "Language not supported")

case .decodingFailure:
self.logger.warning("Failed to decode model response")
return .unknown(underlying: error)

case .rateLimited:
self.logger.warning("Rate limited by model")
return .sessionBusy

case .concurrentRequests:
self.logger.warning("Concurrent request limit exceeded")
return .sessionBusy

case .refusal:
self.logger.warning("Model refused to respond")
return .contentBlocked

@unknown default:
self.logger.error("Unknown generation error: \\(error.localizedDescription)")
self.logger.error("Unknown generation error: \(error.localizedDescription)")
return .unknown(underlying: error)
}
}
Expand All @@ -145,9 +173,9 @@ enum AIErrorHandler {
/// - Returns: A formatted string suitable for UI display.
static func userMessage(for error: AIError) -> String {
if let suggestion = error.recoverySuggestion {
return "\(error.localizedDescription ?? "Error"). \(suggestion)"
return "\(error.errorDescription ?? "Error"). \(suggestion)"
}
return error.localizedDescription ?? "An error occurred"
return error.errorDescription ?? "An error occurred"
}

/// Logs an error with appropriate severity and returns a display message.
Expand Down
10 changes: 3 additions & 7 deletions Core/Services/AI/FoundationModelsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,8 @@ final class FoundationModelsService {

let session = LanguageModelSession()

do {
// Use the official prewarm API instead of sending a dummy prompt
try await session.prewarm()
self.logger.debug("Foundation Models prewarm completed successfully")
} catch {
self.logger.error("Failed to prewarm session: \(error.localizedDescription)")
}
// Use the official prewarm API to load model resources
await session.prewarm()
self.logger.debug("Foundation Models prewarm completed successfully")
}
}
3 changes: 3 additions & 0 deletions Core/Services/API/Parsers/LyricsParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Foundation

/// Parses lyrics responses from YouTube Music API.
enum LyricsParser {
private static let logger = DiagnosticsLogger.api

/// Extracts the lyrics browse ID from the "next" endpoint response.
/// - Parameter data: The response from the "next" endpoint
/// - Returns: The browse ID for fetching lyrics, or nil if unavailable
Expand All @@ -12,6 +14,7 @@ enum LyricsParser {
let watchNextTabbedResults = tabbedRenderer["watchNextTabbedResultsRenderer"] as? [String: Any],
let tabs = watchNextTabbedResults["tabs"] as? [[String: Any]]
else {
self.logger.debug("LyricsParser: Failed to extract lyrics browse ID structure")
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parse failure logging uses self.logger.debug but should be Self.logger.debug for consistency with static context (enum with static methods). The capital 'S' in 'Self' is the convention for static contexts.

Copilot uses AI. Check for mistakes.
return nil
}

Expand Down
3 changes: 3 additions & 0 deletions Core/Services/API/Parsers/RadioQueueParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ struct RadioQueueResult {

/// Parses radio queue responses from YouTube Music API.
enum RadioQueueParser {
private static let logger = DiagnosticsLogger.api

/// Parses the radio queue from the "next" endpoint response.
/// - Parameter data: The response from the "next" endpoint with a radio playlist ID
/// - Returns: RadioQueueResult containing songs and optional continuation token
Expand All @@ -30,6 +32,7 @@ enum RadioQueueParser {
let playlistPanelRenderer = queueContent["playlistPanelRenderer"] as? [String: Any],
let playlistContents = playlistPanelRenderer["contents"] as? [[String: Any]]
else {
self.logger.debug("RadioQueueParser: Failed to parse radio queue structure. Top keys: \(data.keys.sorted())")
return RadioQueueResult(songs: [], continuationToken: nil)
}

Expand Down
3 changes: 3 additions & 0 deletions Core/Services/API/Parsers/SearchResponseParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Foundation

/// Parser for search responses from YouTube Music API.
enum SearchResponseParser {
private static let logger = DiagnosticsLogger.api

/// Parses a search response.
static func parse(_ data: [String: Any]) -> SearchResponse {
var songs: [Song] = []
Expand All @@ -19,6 +21,7 @@ enum SearchResponseParser {
let sectionListRenderer = tabContent["sectionListRenderer"] as? [String: Any],
let sectionContents = sectionListRenderer["contents"] as? [[String: Any]]
else {
Self.logger.debug("SearchResponseParser: Failed to parse response structure. Top keys: \(data.keys.sorted())")
return SearchResponse.empty
}

Expand Down
3 changes: 3 additions & 0 deletions Core/Services/API/Parsers/SearchSuggestionsParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Foundation

/// Parser for search suggestions responses from YouTube Music API.
enum SearchSuggestionsParser {
private static let logger = DiagnosticsLogger.api

/// Parses a search suggestions response.
/// - Parameter data: The raw JSON response from the API.
/// - Returns: An array of search suggestions.
Expand All @@ -10,6 +12,7 @@ enum SearchSuggestionsParser {

// Navigate to contents array
guard let contents = data["contents"] as? [[String: Any]] else {
Self.logger.debug("SearchSuggestionsParser: No contents array found. Top keys: \(data.keys.sorted())")
return suggestions
}

Expand Down
2 changes: 2 additions & 0 deletions Core/Services/API/Parsers/SongMetadataParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Foundation

/// Parses song metadata from YouTube Music API responses.
enum SongMetadataParser {
private static let logger = DiagnosticsLogger.api

/// Contains parsed menu data including feedback tokens, library status, and like status.
struct MenuParseResult {
var feedbackTokens: FeedbackTokens?
Expand Down
7 changes: 2 additions & 5 deletions Core/Services/NetworkMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,8 @@ final class NetworkMonitor {
}
}

// swiftformat:disable modifierOrder
/// NWPathMonitor accessed from deinit, which is non-isolated in Swift 6.
/// Using nonisolated(unsafe) since monitor.cancel() is thread-safe.
nonisolated(unsafe) private let monitor: NWPathMonitor
// swiftformat:enable modifierOrder
/// NWPathMonitor is Sendable and immutable, so no isolation annotation needed.
private let monitor: NWPathMonitor
private let queue: DispatchQueue
private let logger = DiagnosticsLogger.network

Expand Down
4 changes: 2 additions & 2 deletions Core/Services/Notification/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ final class NotificationService {
private let settingsManager: SettingsManager
private let logger = DiagnosticsLogger.notification
// swiftformat:disable modifierOrder
/// Task accessed from deinit, which is non-isolated in Swift 6.
/// Using nonisolated(unsafe) since Task.cancel() is thread-safe.
/// Task for observing player changes, cancelled in deinit.
/// nonisolated(unsafe) required for deinit access; Swift 6.2 warning is expected.
nonisolated(unsafe) private var observationTask: Task<Void, Never>?
// swiftformat:enable modifierOrder
private var lastNotifiedTrackId: String?
Expand Down
8 changes: 6 additions & 2 deletions Core/Services/WebKit/WebKitManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ enum KeychainCookieStorage {
for (key, value) in properties {
stringProperties[key.rawValue] = value
}
// Note: Cookie properties dictionary contains types like String, Date, Number, Bool
// which all support NSSecureCoding. However, using requiringSecureCoding: false here
// because [String: Any] doesn't directly conform to NSSecureCoding.
// The unarchive side uses explicit class allowlists for security.
return try? NSKeyedArchiver.archivedData(
withRootObject: stringProperties,
requiringSecureCoding: false
Expand All @@ -54,8 +58,8 @@ enum KeychainCookieStorage {

guard !cookieData.isEmpty,
let data = try? NSKeyedArchiver.archivedData(
withRootObject: cookieData,
requiringSecureCoding: false
withRootObject: cookieData as NSArray,
requiringSecureCoding: true
)
else {
Self.logger.error("Failed to serialize cookies for Keychain")
Expand Down
3 changes: 2 additions & 1 deletion Core/Utilities/ImageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,8 @@ actor ImageCache {

/// Evicts oldest files until disk cache is under the size limit.
/// Uses LRU (Least Recently Used) eviction based on file modification dates.
private func evictDiskCacheIfNeeded() {
/// Marked async to document the I/O-bound nature and satisfy actor isolation.
private func evictDiskCacheIfNeeded() async {
let currentSize = self.diskCacheSize()
guard currentSize > Self.maxDiskCacheSize else { return }

Expand Down
12 changes: 9 additions & 3 deletions Core/ViewModels/ChartsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ final class ChartsViewModel {
/// The API client (exposed for navigation to detail views).
let client: any YTMusicClientProtocol
private let logger = DiagnosticsLogger.api

/// Task for background loading of additional sections.
private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:disable modifierOrder
/// Task for background loading, cancelled in deinit.
/// nonisolated(unsafe) required for deinit access; Swift 6.2 warning is expected.
nonisolated(unsafe) private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:enable modifierOrder

/// Number of background continuations loaded.
private var continuationsLoaded = 0
Expand All @@ -32,6 +34,10 @@ final class ChartsViewModel {
self.client = client
}

deinit {
self.backgroundLoadTask?.cancel()
}

/// Loads charts content with fast initial load.
func load() async {
guard self.loadingState != .loading else { return }
Expand Down
12 changes: 9 additions & 3 deletions Core/ViewModels/ExploreViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ final class ExploreViewModel {
/// The API client (exposed for navigation to detail views).
let client: any YTMusicClientProtocol
private let logger = DiagnosticsLogger.api

/// Task for background loading of additional sections.
private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:disable modifierOrder
/// Task for background loading, cancelled in deinit.
/// nonisolated(unsafe) required for deinit access; Swift 6.2 warning is expected.
nonisolated(unsafe) private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:enable modifierOrder

/// Number of background continuations loaded.
private var continuationsLoaded = 0
Expand All @@ -32,6 +34,10 @@ final class ExploreViewModel {
self.client = client
}

deinit {
self.backgroundLoadTask?.cancel()
}

/// Loads explore content with fast initial load.
func load() async {
guard self.loadingState != .loading else { return }
Expand Down
12 changes: 9 additions & 3 deletions Core/ViewModels/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ final class HomeViewModel {
/// The API client (exposed for navigation to detail views).
let client: any YTMusicClientProtocol
private let logger = DiagnosticsLogger.api

/// Task for background loading of additional sections.
private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:disable modifierOrder
/// Task for background loading, cancelled in deinit.
/// nonisolated(unsafe) required for deinit access; Swift 6.2 warning is expected.
nonisolated(unsafe) private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:enable modifierOrder

/// Number of background continuations loaded.
private var continuationsLoaded = 0
Expand All @@ -32,6 +34,10 @@ final class HomeViewModel {
self.client = client
}

deinit {
self.backgroundLoadTask?.cancel()
}

/// Loads home content with fast initial load.
func load() async {
guard self.loadingState != .loading else { return }
Expand Down
12 changes: 9 additions & 3 deletions Core/ViewModels/MoodsAndGenresViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ final class MoodsAndGenresViewModel {
/// The API client (exposed for navigation to detail views).
let client: any YTMusicClientProtocol
private let logger = DiagnosticsLogger.api

/// Task for background loading of additional sections.
private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:disable modifierOrder
/// Task for background loading, cancelled in deinit.
/// nonisolated(unsafe) required for deinit access; Swift 6.2 warning is expected.
nonisolated(unsafe) private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:enable modifierOrder

/// Number of background continuations loaded.
private var continuationsLoaded = 0
Expand All @@ -32,6 +34,10 @@ final class MoodsAndGenresViewModel {
self.client = client
}

deinit {
self.backgroundLoadTask?.cancel()
}

/// Loads moods and genres content with fast initial load.
func load() async {
guard self.loadingState != .loading else { return }
Expand Down
12 changes: 9 additions & 3 deletions Core/ViewModels/NewReleasesViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ final class NewReleasesViewModel {
/// The API client (exposed for navigation to detail views).
let client: any YTMusicClientProtocol
private let logger = DiagnosticsLogger.api

/// Task for background loading of additional sections.
private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:disable modifierOrder
/// Task for background loading, cancelled in deinit.
/// nonisolated(unsafe) required for deinit access; Swift 6.2 warning is expected.
nonisolated(unsafe) private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:enable modifierOrder

/// Number of background continuations loaded.
private var continuationsLoaded = 0
Expand All @@ -32,6 +34,10 @@ final class NewReleasesViewModel {
self.client = client
}

deinit {
self.backgroundLoadTask?.cancel()
}

/// Loads new releases content with fast initial load.
func load() async {
guard self.loadingState != .loading else { return }
Expand Down
12 changes: 9 additions & 3 deletions Core/ViewModels/PodcastsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ final class PodcastsViewModel {
/// The API client (exposed for navigation to detail views).
let client: any YTMusicClientProtocol
private let logger = DiagnosticsLogger.api

/// Task for background loading of additional sections.
private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:disable modifierOrder
/// Task for background loading, cancelled in deinit.
/// nonisolated(unsafe) required for deinit access; Swift 6.2 warning is expected.
nonisolated(unsafe) private var backgroundLoadTask: Task<Void, Never>?
// swiftformat:enable modifierOrder

/// Number of background continuations loaded.
private var continuationsLoaded = 0
Expand All @@ -31,6 +33,10 @@ final class PodcastsViewModel {
self.client = client
}

deinit {
self.backgroundLoadTask?.cancel()
}

/// Loads podcasts content with fast initial load.
func load() async {
guard self.loadingState != .loading else { return }
Expand Down
13 changes: 11 additions & 2 deletions Core/ViewModels/SearchViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,22 @@ final class SearchViewModel {

let client: any YTMusicClientProtocol
private let logger = DiagnosticsLogger.api
private var searchTask: Task<Void, Never>?
private var suggestionsTask: Task<Void, Never>?
// swiftformat:disable modifierOrder
/// Tasks for search operations, cancelled in deinit.
/// nonisolated(unsafe) required for deinit access; Swift 6.2 warning is expected.
nonisolated(unsafe) private var searchTask: Task<Void, Never>?
nonisolated(unsafe) private var suggestionsTask: Task<Void, Never>?
// swiftformat:enable modifierOrder

init(client: any YTMusicClientProtocol) {
self.client = client
}

deinit {
searchTask?.cancel()
suggestionsTask?.cancel()
}

/// Fetches search suggestions with debounce.
func fetchSuggestions() {
self.suggestionsTask?.cancel()
Expand Down
Loading
Loading