WAY better logging

This commit is contained in:
Seiike 2025-01-22 01:32:23 +01:00
parent 17df9c4c0b
commit 65ee766579
7 changed files with 177 additions and 91 deletions

View file

@ -32,6 +32,7 @@
13EA2BD72D32D97400C1EBD7 /* MusicProgressSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */; };
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */; };
13EA2BDC2D32D9FF00C1EBD7 /* MiruDataStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13EA2BDB2D32D9FF00C1EBD7 /* MiruDataStruct.swift */; };
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */; };
1EE1DA962D3553C2002AEF73 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 1EE1DA952D3553C2002AEF73 /* Localizable.xcstrings */; };
/* End PBXBuildFile section */
@ -62,6 +63,7 @@
13EA2BD42D32D97400C1EBD7 /* MusicProgressSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MusicProgressSlider.swift; sourceTree = "<group>"; };
13EA2BD82D32D98400C1EBD7 /* NormalPlayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NormalPlayer.swift; sourceTree = "<group>"; };
13EA2BDB2D32D9FF00C1EBD7 /* MiruDataStruct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiruDataStruct.swift; sourceTree = "<group>"; };
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewLoggerFilter.swift; sourceTree = "<group>"; };
1EE1DA952D3553C2002AEF73 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -141,6 +143,7 @@
133D7C832D2BE2630075467E /* SettingsSubViews */ = {
isa = PBXGroup;
children = (
1E9FF1D22D403E42008AC100 /* SettingsViewLoggerFilter.swift */,
1399FAD32D3AB38C00E97C31 /* SettingsViewLogger.swift */,
133D7C842D2BE2630075467E /* SettingsViewModule.swift */,
);
@ -340,6 +343,7 @@
1399FAD62D3AB3DB00E97C31 /* Logger.swift in Sources */,
133D7C902D2BE2640075467E /* SettingsView.swift in Sources */,
13EA2BD72D32D97400C1EBD7 /* MusicProgressSlider.swift in Sources */,
1E9FF1D32D403E49008AC100 /* SettingsViewLoggerFilter.swift in Sources */,
13EA2BD92D32D98400C1EBD7 /* NormalPlayer.swift in Sources */,
133D7C932D2BE2640075467E /* Modules.swift in Sources */,
133D7C702D2BE2500075467E /* ContentView.swift in Sources */,

View file

@ -17,7 +17,7 @@ class JSController: ObservableObject {
private func setupContext() {
let logFunction: @convention(block) (String) -> Void = { message in
Logger.shared.log("JavaScript log: \(message)", level: .info)
Logger.shared.log("JavaScript log: \(message)", type: "Debug")
}
context.setObject(logFunction, forKeyedSubscript: "log" as NSString)
@ -35,18 +35,18 @@ class JSController: ObservableObject {
// Add other JavaScript functions like fetchNative
let fetchNativeFunction: @convention(block) (String, JSValue, JSValue) -> Void = { urlString, resolve, reject in
guard let url = URL(string: urlString) else {
Logger.shared.log("Invalid URL", level: .error)
Logger.shared.log("Invalid URL", type: "Error")
reject.call(withArguments: ["Invalid URL"])
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
Logger.shared.log("Network error in fetchNative: \(error.localizedDescription)", level: .error)
Logger.shared.log("Network error in fetchNative: \(error.localizedDescription)", type: "Error")
reject.call(withArguments: [error.localizedDescription])
return
}
guard let data = data, let text = String(data: data, encoding: .utf8) else {
Logger.shared.log("Failed to decode response data", level: .error)
Logger.shared.log("Failed to decode response data", type: "Error")
reject.call(withArguments: ["Failed to decode response data"])
return
}
@ -84,18 +84,18 @@ class JSController: ObservableObject {
guard let self = self else { return }
if let error = error {
Logger.shared.log("Network error: \(error)")
Logger.shared.log("Network error: \(error)", type: "Error")
DispatchQueue.main.async { completion([]) }
return
}
guard let data = data, let html = String(data: data, encoding: .utf8) else {
Logger.shared.log("Failed to decode HTML")
Logger.shared.log("Failed to decode HTML", type: "Error")
DispatchQueue.main.async { completion([]) }
return
}
Logger.shared.log(html)
Logger.shared.log(html, type: "Debug")
if let parseFunction = self.context.objectForKeyedSubscript("searchResults"),
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
let resultItems = results.map { item in
@ -109,7 +109,7 @@ class JSController: ObservableObject {
completion(resultItems)
}
} else {
Logger.shared.log("Failed to parse results")
Logger.shared.log("Failed to parse results", type: "Error")
DispatchQueue.main.async { completion([]) }
}
}.resume()
@ -125,13 +125,13 @@ class JSController: ObservableObject {
guard let self = self else { return }
if let error = error {
Logger.shared.log("Network error: \(error)")
Logger.shared.log("Network error: \(error)", type: "Error")
DispatchQueue.main.async { completion([], []) }
return
}
guard let data = data, let html = String(data: data, encoding: .utf8) else {
Logger.shared.log("Failed to decode HTML")
Logger.shared.log("Failed to decode HTML", type: "Error")
DispatchQueue.main.async { completion([], []) }
return
}
@ -139,7 +139,7 @@ class JSController: ObservableObject {
var resultItems: [MediaItem] = []
var episodeLinks: [EpisodeLink] = []
Logger.shared.log(html)
Logger.shared.log(html, type: "Debug")
if let parseFunction = self.context.objectForKeyedSubscript("extractDetails"),
let results = parseFunction.call(withArguments: [html]).toArray() as? [[String: String]] {
resultItems = results.map { item in
@ -150,7 +150,7 @@ class JSController: ObservableObject {
)
}
} else {
Logger.shared.log("Failed to parse results")
Logger.shared.log("Failed to parse results", type: "Error")
}
if let fetchEpisodesFunction = self.context.objectForKeyedSubscript("extractEpisodes"),
@ -178,26 +178,26 @@ class JSController: ObservableObject {
guard let self = self else { return }
if let error = error {
Logger.shared.log("Network error: \(error)")
Logger.shared.log("Network error: \(error)", type: "Error")
DispatchQueue.main.async { completion(nil) }
return
}
guard let data = data, let html = String(data: data, encoding: .utf8) else {
Logger.shared.log("Failed to decode HTML")
Logger.shared.log("Failed to decode HTML", type: "Error")
DispatchQueue.main.async { completion(nil) }
return
}
Logger.shared.log(html)
Logger.shared.log(html, type: "Debug")
if let parseFunction = self.context.objectForKeyedSubscript("extractStreamUrl"),
let streamUrl = parseFunction.call(withArguments: [html]).toString() {
Logger.shared.log("Staring stream from: \(streamUrl)", level: .info)
Logger.shared.log("Staring stream from: \(streamUrl)", type: "Stream")
DispatchQueue.main.async {
completion(streamUrl)
}
} else {
Logger.shared.log("Failed to extract stream URL")
Logger.shared.log("Failed to extract stream URL", type: "Error")
DispatchQueue.main.async { completion(nil) }
}
}.resume()
@ -205,27 +205,27 @@ class JSController: ObservableObject {
func fetchJsSearchResults(keyword: String, module: ScrapingModule, completion: @escaping ([SearchItem]) -> Void) {
if let exception = context.exception {
Logger.shared.log("JavaScript exception: \(exception)")
Logger.shared.log("JavaScript exception: \(exception)", type: "Error")
completion([])
return
}
guard let searchResultsFunction = context.objectForKeyedSubscript("searchResults") else {
Logger.shared.log("No JavaScript function searchResults found")
Logger.shared.log("No JavaScript function searchResults found", type: "Error")
completion([])
return
}
let promiseValue = searchResultsFunction.call(withArguments: [keyword])
guard let promise = promiseValue else {
Logger.shared.log("searchResults did not return a Promise")
Logger.shared.log("searchResults did not return a Promise", type: "Error")
completion([])
return
}
let thenBlock: @convention(block) (JSValue) -> Void = { result in
Logger.shared.log(result.toString())
Logger.shared.log(result.toString(), type: "Debug")
if let jsonString = result.toString(),
let data = jsonString.data(using: .utf8) {
do {
@ -242,13 +242,13 @@ class JSController: ObservableObject {
}
} else {
Logger.shared.log("Failed to parse JSON", level: .error)
Logger.shared.log("Failed to parse JSON", type: "Error")
DispatchQueue.main.async {
completion([])
}
}
} catch {
Logger.shared.log("JSON parsing error: \(error)", level: .error)
Logger.shared.log("JSON parsing error: \(error)", type: "Error")
DispatchQueue.main.async {
completion([])
}
@ -262,7 +262,7 @@ class JSController: ObservableObject {
}
let catchBlock: @convention(block) (JSValue) -> Void = { error in
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))")
Logger.shared.log("Promise rejected: \(String(describing: error.toString()))", type: "Error")
DispatchQueue.main.async {
completion([])
}
@ -282,19 +282,19 @@ class JSController: ObservableObject {
}
if let exception = context.exception {
Logger.shared.log("JavaScript exception: \(exception)")
Logger.shared.log("JavaScript exception: \(exception)", type: "Error")
completion([], [])
return
}
guard let extractDetailsFunction = context.objectForKeyedSubscript("extractDetails") else {
Logger.shared.log("No JavaScript function extractDetails found")
Logger.shared.log("No JavaScript function extractDetails found", type: "Error")
completion([], [])
return
}
guard let extractEpisodesFunction = context.objectForKeyedSubscript("extractEpisodes") else {
Logger.shared.log("No JavaScript function extractEpisodes found")
Logger.shared.log("No JavaScript function extractEpisodes found", type: "Error")
completion([], [])
return
}
@ -304,14 +304,14 @@ class JSController: ObservableObject {
let promiseValueDetails = extractDetailsFunction.call(withArguments: [url.absoluteString])
guard let promiseDetails = promiseValueDetails else {
Logger.shared.log("extractDetails did not return a Promise")
Logger.shared.log("extractDetails did not return a Promise", type: "Error")
completion([], [])
return
}
let thenBlockDetails: @convention(block) (JSValue) -> Void = { result in
Logger.shared.log(result.toString())
Logger.shared.log(result.toString(), type: "Debug")
if let jsonOfDetails = result.toString(),
let dataDetails = jsonOfDetails.data(using: .utf8) {
do {
@ -323,19 +323,19 @@ class JSController: ObservableObject {
return MediaItem(description: description, aliases: aliases, airdate: airdate)
}
} else {
Logger.shared.log("Failed to parse JSON of extractDetails")
Logger.shared.log("Failed to parse JSON of extractDetails", type: "Error")
DispatchQueue.main.async {
completion([], [])
}
}
} catch {
Logger.shared.log("JSON parsing error of extract details: \(error)")
Logger.shared.log("JSON parsing error of extract details: \(error)", type: "Error")
DispatchQueue.main.async {
completion([], [])
}
}
} else {
Logger.shared.log("Result is not a string of extractDetails")
Logger.shared.log("Result is not a string of extractDetails", type: "Error")
DispatchQueue.main.async {
completion([], [])
}
@ -343,7 +343,7 @@ class JSController: ObservableObject {
}
let catchBlockDetails: @convention(block) (JSValue) -> Void = { error in
Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))")
Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))", type: "Error")
DispatchQueue.main.async {
completion([], [])
}
@ -358,14 +358,14 @@ class JSController: ObservableObject {
let promiseValueEpisodes = extractEpisodesFunction.call(withArguments: [url.absoluteString])
guard let promiseEpisodes = promiseValueEpisodes else {
Logger.shared.log("extractEpisodes did not return a Promise")
Logger.shared.log("extractEpisodes did not return a Promise", type: "Error")
completion([], [])
return
}
let thenBlockEpisodes: @convention(block) (JSValue) -> Void = { result in
Logger.shared.log(result.toString())
Logger.shared.log(result.toString(), type: "Debug")
if let jsonOfEpisodes = result.toString(),
let dataEpisodes = jsonOfEpisodes.data(using: .utf8) {
do {
@ -381,19 +381,19 @@ class JSController: ObservableObject {
}
} else {
Logger.shared.log("Failed to parse JSON of extractEpisodes")
Logger.shared.log("Failed to parse JSON of extractEpisodes", type: "Error")
DispatchQueue.main.async {
completion([], [])
}
}
} catch {
Logger.shared.log("JSON parsing error of extractEpisodes: \(error)")
Logger.shared.log("JSON parsing error of extractEpisodes: \(error)", type: "Error")
DispatchQueue.main.async {
completion([], [])
}
}
} else {
Logger.shared.log("Result is not a string of extractEpisodes")
Logger.shared.log("Result is not a string of extractEpisodes", type: "Error")
DispatchQueue.main.async {
completion([], [])
}
@ -401,7 +401,7 @@ class JSController: ObservableObject {
}
let catchBlockEpisodes: @convention(block) (JSValue) -> Void = { error in
Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))")
Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))", type: "Error")
DispatchQueue.main.async {
completion([], [])
}

View file

@ -10,31 +10,34 @@ import Foundation
class Logger {
static let shared = Logger()
enum LogLevel: String {
case info = "INFO"
case warning = "WARNING"
case error = "ERROR"
struct LogEntry {
let message: String
let type: String
let timestamp: Date
}
private var logs: [(level: LogLevel, message: String, timestamp: Date)] = []
private var logs: [LogEntry] = []
private let logFileURL: URL
private let logFilterViewModel = LogFilterViewModel.shared // Use shared instance
private init() {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
logFileURL = documentDirectory.appendingPathComponent("logs.txt")
loadLogs()
}
func log(_ message: String, level: LogLevel = .info) {
let entry = (level: level, message: message, timestamp: Date())
func log(_ message: String, type: String = "General") {
// Check if the log type is enabled
guard logFilterViewModel.isFilterEnabled(for: type) else { return }
let entry = LogEntry(message: message, type: type, timestamp: Date())
logs.append(entry)
saveLogToFile(entry)
}
func getLogs() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return logs.map { "[\(dateFormatter.string(from: $0.timestamp))] [\($0.level.rawValue)] \($0.message)" }
dateFormatter.dateFormat = "dd-MM-yyyy HH:mm:ss"
return logs.map { "[\(dateFormatter.string(from: $0.timestamp))] [\($0.type)] \($0.message)" }
.joined(separator: "\n----------------------------------------------------------\n")
}
@ -43,32 +46,11 @@ class Logger {
try? FileManager.default.removeItem(at: logFileURL)
}
private func loadLogs() {
guard let data = try? Data(contentsOf: logFileURL),
let content = String(data: data, encoding: .utf8) else { return }
private func saveLogToFile(_ log: LogEntry) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.dateFormat = "dd-MM-yyyy HH:mm:ss"
content.components(separatedBy: "\n---\n").forEach { line in
let components = line.components(separatedBy: "] [")
guard components.count == 3,
let timestampString = components.first?.dropFirst().trimmingCharacters(in: .whitespaces),
let timestamp = dateFormatter.date(from: timestampString),
let message = components.last?.dropLast() else { return }
let levelRaw = components[1].trimmingCharacters(in: .whitespaces)
guard let level = LogLevel(rawValue: levelRaw) else { return }
logs.append((level: level, message: String(message), timestamp: timestamp))
}
}
private func saveLogToFile(_ log: (level: LogLevel, message: String, timestamp: Date)) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let logString = "[\(dateFormatter.string(from: log.timestamp))] [\(log.level.rawValue)] \(log.message)\n---\n"
let logString = "[\(dateFormatter.string(from: log.timestamp))] [\(log.type)] \(log.message)\n---\n"
if let data = logString.data(using: .utf8) {
if FileManager.default.fileExists(atPath: logFileURL.path) {

View file

@ -42,7 +42,7 @@ class LibraryManager: ObservableObject {
do {
bookmarks = try JSONDecoder().decode([LibraryItem].self, from: data)
} catch {
Logger.shared.log("Failed to decode bookmarks: \(error.localizedDescription)")
Logger.shared.log("Failed to decode bookmarks: \(error.localizedDescription)", type: "Error")
}
}
@ -51,7 +51,7 @@ class LibraryManager: ObservableObject {
let encoded = try JSONEncoder().encode(bookmarks)
UserDefaults.standard.set(encoded, forKey: bookmarksKey)
} catch {
Logger.shared.log("Failed to encode bookmarks: \(error.localizedDescription)")
Logger.shared.log("Failed to encode bookmarks: \(error.localizedDescription)", type: "Error")
}
}

View file

@ -266,7 +266,7 @@ struct MediaInfoView: View {
private func openSafariViewController(with urlString: String) {
guard let url = URL(string: urlString) else {
Logger.shared.log("Unable to open the webpage")
Logger.shared.log("Unable to open the webpage", type: "Error")
return
}
let safariViewController = SFSafariViewController(url: url)

View file

@ -9,7 +9,8 @@ import SwiftUI
struct SettingsViewLogger: View {
@State private var logs: String = ""
@StateObject private var filterViewModel = LogFilterViewModel.shared // Use shared instance
var body: some View {
VStack {
ScrollView {
@ -27,22 +28,27 @@ struct SettingsViewLogger: View {
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Menu {
Button(action: {
UIPasteboard.general.string = logs
}) {
Label("Copy to Clipboard", systemImage: "doc.on.doc")
HStack {
Menu {
Button(action: {
UIPasteboard.general.string = logs
}) {
Label("Copy to Clipboard", systemImage: "doc.on.doc")
}
Button(role: .destructive, action: {
Logger.shared.clearLogs()
logs = Logger.shared.getLogs()
}) {
Label("Clear Logs", systemImage: "trash")
}
} label: {
Image(systemName: "ellipsis.circle")
.resizable()
.frame(width: 20, height: 20)
}
Button(role: .destructive, action: {
Logger.shared.clearLogs()
logs = Logger.shared.getLogs()
}) {
Label("Clear Logs", systemImage: "trash")
NavigationLink(destination: SettingsViewLoggerFilter(viewModel: filterViewModel)) {
Image(systemName: "slider.horizontal.3")
}
} label: {
Image(systemName: "ellipsis.circle")
.resizable()
.frame(width: 20, height: 20)
}
}
}

View file

@ -0,0 +1,94 @@
//
// SettingsViewLoggerFilter.swift
// Sora
//
// Created by seiike on 21/01/2025.
//
import SwiftUI
struct LogFilter: Identifiable, Hashable {
let id = UUID()
let type: String
var isEnabled: Bool
let description: String
}
class LogFilterViewModel: ObservableObject {
static let shared = LogFilterViewModel() // Singleton instance
@Published var filters: [LogFilter] = [] {
didSet {
saveFiltersToUserDefaults()
}
}
private let userDefaultsKey = "LogFilterStates"
private let hardcodedFilters: [(type: String, description: String, defaultState: Bool)] = [
("Global", "Logs for general events and activities.", true), // Turned on by default
("Stream", "Logs for streaming and video playback.", true), // Turned on by default
("Error", "Logs for errors and critical issues.", true), // Turned on by default
("Debug", "Logs for debugging and troubleshooting.", false) // Turned off by default
]
private init() {
loadFilters()
}
func loadFilters() {
if let savedStates = UserDefaults.standard.dictionary(forKey: userDefaultsKey) as? [String: Bool] {
filters = hardcodedFilters.map {
LogFilter(
type: $0.type,
isEnabled: savedStates[$0.type] ?? $0.defaultState, // Use saved state if available, otherwise default
description: $0.description
)
}
} else {
filters = hardcodedFilters.map {
LogFilter(type: $0.type, isEnabled: $0.defaultState, description: $0.description)
}
}
}
func toggleFilter(for type: String) {
if let index = filters.firstIndex(where: { $0.type == type }) {
filters[index].isEnabled.toggle()
}
}
func isFilterEnabled(for type: String) -> Bool {
return filters.first(where: { $0.type == type })?.isEnabled ?? true
}
private func saveFiltersToUserDefaults() {
let states = filters.reduce(into: [String: Bool]()) { result, filter in
result[filter.type] = filter.isEnabled
}
UserDefaults.standard.set(states, forKey: userDefaultsKey)
}
}
struct SettingsViewLoggerFilter: View {
@ObservedObject var viewModel = LogFilterViewModel.shared
var body: some View {
List {
ForEach($viewModel.filters) { $filter in
VStack(alignment: .leading, spacing: 5) {
Toggle(filter.type, isOn: $filter.isEnabled)
.font(.headline)
Text(filter.description)
.font(.subheadline)
.foregroundColor(.secondary)
.padding(.leading, 5) // Indent description slightly
}
.padding(.vertical, 5)
}
}
.navigationTitle("Log Filters")
}
}