More websites support

This commit is contained in:
Hamzenis Kryeziu 2025-01-03 21:21:25 +01:00
parent 30cb6f5838
commit f10442fec4
3 changed files with 221 additions and 44 deletions

View file

@ -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

View file

@ -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..<html.endIndex, in: html)
if let match = regex.firstMatch(in: html, options: [], range: range),
let matchRange = Range(match.range, in: html) {
var urlString = String(html[matchRange])
urlString = urlString.replacingOccurrences(of: "href=\"", with: "")
urlString = urlString.replacingOccurrences(of: "\"", with: "")
// Ensure the baseURL ends with "/" before appending the path
let baseURL = module.module[0].details.baseURL
let redirectUrl = baseURL + urlString
let finalUrl = fetchRedirectedURLFromHeader(url: URL(string: redirectUrl)!)
return finalUrl
}
} catch {
print("Invalid regex: \(error)")
Logger.shared.log("Invalid regex: \(error)")
}
return nil
}
/// Fetches the redirected URL from the header of a given URL
/// Header Parameter: Location
func fetchRedirectedURLFromHeader(url: URL) -> 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)
}
}

View file

@ -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
}