From 30cb6f58385b867c02f55aea5c1a9a454654b3d5 Mon Sep 17 00:00:00 2001 From: Hamzenis Kryeziu Date: Fri, 3 Jan 2025 21:15:45 +0100 Subject: [PATCH 1/3] Added gitignore --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) delete mode 100644 .DS_Store create mode 100644 .gitignore diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 212297b37d5db60ddad9c19dd5208ee1a244653a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ5B>J5S@Vtt&}DurLT}1m}ogcE&xdo6j^PgK<^$YXWiW6x*3Zx!1kBD#3k%tdA*(x4HQ%7}1!)O6y`6QI@{_p;yK&X?=H+l?eT#wo7- zOs-{*-;Io~e_-$Wwq0%e72=ww_4DiD@#W%kj;`N)>$AK2Q#>8xw~I9e1HnKr5Dff` z0i4;CW@;FHFc1s`10M`{txz)`6~;06<=&t3YQh!I5VcUCg{qer zYT=kq?w1{lp@oZj@uB|Y_u@tC(~&<_chPJZeJ~IV3>i4J;f&}1CH^v_MgA}(M!`TZ z@Xr{~NxN(pcqu Date: Fri, 3 Jan 2025 21:21:25 +0100 Subject: [PATCH 2/3] More websites support --- Sora/Utils/Modules/ModuleStruct.swift | 2 + Sora/Views/MediaViews/MediaExtraction.swift | 253 +++++++++++++++--- .../Views/SearchViews/SearchResultsView.swift | 10 + 3 files changed, 221 insertions(+), 44 deletions(-) diff --git a/Sora/Utils/Modules/ModuleStruct.swift b/Sora/Utils/Modules/ModuleStruct.swift index a5eba63..c8725cc 100644 --- a/Sora/Utils/Modules/ModuleStruct.swift +++ b/Sora/Utils/Modules/ModuleStruct.swift @@ -35,6 +35,7 @@ struct ModuleStruct: Codable { let title: String let image: Image let href: String + let searchable: Bool? struct Image: Codable, Hashable { let url: String @@ -57,6 +58,7 @@ struct ModuleStruct: Codable { struct Details: Codable, Hashable { let baseURL: String + let pageRedirects: Bool? let aliases: Aliases let synopsis: String let airdate: String diff --git a/Sora/Views/MediaViews/MediaExtraction.swift b/Sora/Views/MediaViews/MediaExtraction.swift index 6d43ec5..6153247 100644 --- a/Sora/Views/MediaViews/MediaExtraction.swift +++ b/Sora/Views/MediaViews/MediaExtraction.swift @@ -50,59 +50,95 @@ extension MediaView { } func fetchEpisodeStream(urlString: String) { - guard let url = URL(string: urlString.hasPrefix("https") ? urlString : "\(module.module[0].details.baseURL)\(urlString)") else { return } + guard var url = URL(string: urlString.hasPrefix("https") ? urlString : "\(module.module[0].details.baseURL)\(urlString)") else { return } Logger.shared.log("Pressed episode button") - URLSession.custom.dataTask(with: url) { data, _, error in - guard let data = data, error == nil else { return } - - let html = String(data: data, encoding: .utf8) ?? "" - let streamType = module.stream - let streamURLs = extractStreamURLs(from: html, streamType: streamType) - - if module.extractor == "dub-sub" { - Logger.shared.log("extracting for dub-sub") - let dubSubURLs = extractDubSubURLs(from: html) - let subURLs = dubSubURLs.filter { $0.type == "SUB" }.map { $0.url } - let dubURLs = dubSubURLs.filter { $0.type == "DUB" }.map { $0.url } + + let dispatchGroup = DispatchGroup() + + let pageRedirects = module.module[0].details.pageRedirects ?? false + + + if pageRedirects { + dispatchGroup.enter() // Start tracking the redirect task + URLSession.custom.dataTask(with: url) { data, response, error in + guard let data = data, error == nil else { + dispatchGroup.leave() // End tracking if there's an error + return + } - if !subURLs.isEmpty || !dubURLs.isEmpty { - DispatchQueue.main.async { - self.presentStreamSelection(subURLs: subURLs, dubURLs: dubURLs, fullURL: urlString) + let html = String(data: data, encoding: .utf8) ?? "" + let redirectedUrl = extractFromRedirectURL(from: html) + if let redirect = redirectedUrl, let newURL = URL(string: redirect) { + url = newURL + } + dispatchGroup.leave() // End tracking after successful execution + }.resume() + } + + dispatchGroup.notify(queue: .main) { // This block executes after all tasks + URLSession.custom.dataTask(with: url) { data, _, error in + guard let data = data, error == nil else { return } + + let html = String(data: data, encoding: .utf8) ?? "" + + + let streamType = module.stream + let streamURLs = extractStreamURLs(from: html, streamType: streamType) + + if module.extractor == "dub-sub" { + Logger.shared.log("extracting for dub-sub") + let dubSubURLs = extractDubSubURLs(from: html) + let subURLs = dubSubURLs.filter { $0.type == "SUB" }.map { $0.url } + let dubURLs = dubSubURLs.filter { $0.type == "DUB" }.map { $0.url } + + if !subURLs.isEmpty || !dubURLs.isEmpty { + DispatchQueue.main.async { + self.presentStreamSelection(subURLs: subURLs, dubURLs: dubURLs, fullURL: urlString) + } + } else { + DispatchQueue.main.async { + self.playStream(urlString: streamURLs.first, fullURL: urlString) + } } + } else if module.extractor == "pattern-mp4" || module.extractor == "pattern-HLS" { + Logger.shared.log("extracting for pattern-mp4/hls") + let patternURL = extractPatternURL(from: html) + guard let patternURL = patternURL else { return } + + URLSession.custom.dataTask(with: patternURL) { data, _, error in + guard let data = data, error == nil else { return } + + let patternHTML = String(data: data, encoding: .utf8) ?? "" + let mp4URLs = extractStreamURLs(from: patternHTML, streamType: streamType).map { $0.replacingOccurrences(of: "amp;", with: "") } + + DispatchQueue.main.async { + self.playStream(urlString: mp4URLs.first, fullURL: urlString) + } + }.resume() + } else if module.extractor == "pattern" { + Logger.shared.log("extracting for pattern") + let patternURL = extractPatternURL(from: html) + + DispatchQueue.main.async { + self.playStream(urlString: patternURL?.absoluteString, fullURL: urlString) + } + } else if module.extractor == "voe" { + Logger.shared.log("extracting for voe") + + let voeUrl = extractVoeStream(from: html) + + DispatchQueue.main.async { + self.playStream(urlString: voeUrl?.absoluteString, fullURL: urlString) + } + } else { DispatchQueue.main.async { self.playStream(urlString: streamURLs.first, fullURL: urlString) } } - } else if module.extractor == "pattern-mp4" || module.extractor == "pattern-HLS" { - Logger.shared.log("extracting for pattern-mp4/hls") - let patternURL = extractPatternURL(from: html) - guard let patternURL = patternURL else { return } - - URLSession.custom.dataTask(with: patternURL) { data, _, error in - guard let data = data, error == nil else { return } - - let patternHTML = String(data: data, encoding: .utf8) ?? "" - let mp4URLs = extractStreamURLs(from: patternHTML, streamType: streamType).map { $0.replacingOccurrences(of: "amp;", with: "") } - - DispatchQueue.main.async { - self.playStream(urlString: mp4URLs.first, fullURL: urlString) - } - }.resume() - } else if module.extractor == "pattern" { - Logger.shared.log("extracting for pattern") - let patternURL = extractPatternURL(from: html) - - DispatchQueue.main.async { - self.playStream(urlString: patternURL?.absoluteString, fullURL: urlString) - } - } else { - DispatchQueue.main.async { - self.playStream(urlString: streamURLs.first, fullURL: urlString) - } - } - }.resume() + }.resume() + } } func extractStreamURLs(from html: String, streamType: String) -> [String] { @@ -189,6 +225,24 @@ extension MediaView { } } + /// Grabs hls stream from voe sites + func extractVoeStream(from html: String) -> URL? { + + let hlsPattern = "'hls': '(.*?)'" + guard let regex = try? NSRegularExpression(pattern: hlsPattern, options: []) else { return nil } + let range = NSRange(html.startIndex..., in: html) + if let match = regex.firstMatch(in: html, options: [], range: range), + let matchRange = Range(match.range(at: 1), in: html) { + let base64Hls = String(html[matchRange]) + guard let data = Data(base64Encoded: base64Hls), + let decodedURLString = String(data: data, encoding: .utf8) + else { return nil } + return URL(string: decodedURLString) + } + return nil + } + + func presentStreamSelection(subURLs: [String], dubURLs: [String], fullURL: String) { let uniqueSubURLs = Array(Set(subURLs)) let uniqueDubURLs = Array(Set(dubURLs)) @@ -235,4 +289,115 @@ extension MediaView { } } } + + + /// Extracts the URL from a redirect page + /// Example: href="/redirect/1234567" -> https://baseUrl.com/redirect/1234567 + func extractFromRedirectURL(from html: String) -> String? { + + let pattern = #"href="\/redirect\/\d+""# + + do { + let regex = try NSRegularExpression(pattern: pattern, options: []) + let range = NSRange(html.startIndex.. String? { + let semaphore = DispatchSemaphore(value: 0) // To block the thread until the task completes + var redirectedURLString: String? + + var request = URLRequest(url: url) + request.httpMethod = "HEAD" // Use HEAD to get only headers + + let delegate = RedirectHandler() + let sessionConfig = URLSessionConfiguration.default + let session = URLSession(configuration: sessionConfig, delegate: delegate, delegateQueue: nil) + + session.dataTask(with: request) { _, response, error in + // Extract httpResponse as a standalone variable + guard let httpResponse = response as? HTTPURLResponse else { + Logger.shared.log("Invalid response for URL: \(url)") + semaphore.signal() + return + } + + // Process the httpResponse for redirection logic + if (httpResponse.statusCode == 301 || httpResponse.statusCode == 302), + let location = httpResponse.value(forHTTPHeaderField: "Location"), + let redirectedURL = URL(string: location) { + redirectedURLString = redirectedURL.absoluteString + Logger.shared.log("Redirected URL: \(redirectedURLString ?? "nil")") + } else { + if let error = error { + Logger.shared.log("Error fetching redirected URL: \(error.localizedDescription)") + } else { + Logger.shared.log("No redirection for URL: \(url)") + } + } + semaphore.signal() // Signal the semaphore to resume execution + }.resume() + + semaphore.wait() // Wait for the network task to complete + + if redirectedURLString?.contains("voe.sx") == true { + return voeUrlHandler(url: URL(string: redirectedURLString!)!) + } + else { + return redirectedURLString + } + + } + + /// Voe uses a custom handler to extract the video URL from the page + /// The site uses windows.location.href to redirect to the video page, usally another domain but with the same path + /// The replacement URL is hardcoded right now TODO: Make it dynamic + func voeUrlHandler(url: URL) -> String? { + + let urlString = url.absoluteString + + // Check if the URL is a voe.sx URL + guard urlString.contains("voe.sx") else { + Logger.shared.log("Not a voe.sx URL") + return nil + } + + // Extract the path from the URL and append it to the hardcoded replacement URL + // Example: https://voe.sx/e/1234567 -> /e/1234567 + let hardcodedURL = "https://sandratableother.com" + let finishedUrl = urlString.replacingOccurrences(of: "https://voe.sx", with: hardcodedURL) + + return finishedUrl + } + +} + +/// Custom handler to handle HTTP redirections and prevent them +class RedirectHandler: NSObject, URLSessionDelegate, URLSessionTaskDelegate { + func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping @Sendable (URLRequest?) -> Void + ) { + completionHandler(nil) + } } diff --git a/Sora/Views/SearchViews/SearchResultsView.swift b/Sora/Views/SearchViews/SearchResultsView.swift index b66fd33..0ce4eef 100644 --- a/Sora/Views/SearchViews/SearchResultsView.swift +++ b/Sora/Views/SearchViews/SearchResultsView.swift @@ -210,10 +210,20 @@ struct SearchResultsView: View { imageURL = imageURL.replacingOccurrences(of: " ", with: "%20") + // If imageURL is not available or is the same as the baseURL, use a default image + if imageURL.isEmpty || imageURL == module.module[0].details.baseURL + "/" { + imageURL = "https://s4.anilist.co/file/anilistcdn/character/large/default.jpg" + } + let result = ItemResult(name: title, imageUrl: imageURL, href: href) results.append(result) } + // Filter out non-searchable modules + if module.module[0].search.searchable == false { + results = results.filter { $0.name.lowercased().contains(searchText.lowercased()) } + } + DispatchQueue.main.async { self.searchResults = results } From c28960116808668270441c592befe56385714d85 Mon Sep 17 00:00:00 2001 From: cranci <100066266+cranci1@users.noreply.github.com> Date: Fri, 3 Jan 2025 21:34:59 +0100 Subject: [PATCH 3/3] Update Sora/Views/MediaViews/MediaExtraction.swift Co-authored-by: codefactor-io[bot] <47775046+codefactor-io[bot]@users.noreply.github.com> --- Sora/Views/MediaViews/MediaExtraction.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sora/Views/MediaViews/MediaExtraction.swift b/Sora/Views/MediaViews/MediaExtraction.swift index 6153247..accb81f 100644 --- a/Sora/Views/MediaViews/MediaExtraction.swift +++ b/Sora/Views/MediaViews/MediaExtraction.swift @@ -61,7 +61,7 @@ extension MediaView { if pageRedirects { dispatchGroup.enter() // Start tracking the redirect task - URLSession.custom.dataTask(with: url) { data, response, error in + URLSession.custom.dataTask(with: url) { data, _, error in guard let data = data, error == nil else { dispatchGroup.leave() // End tracking if there's an error return