mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
More websites support
This commit is contained in:
parent
30cb6f5838
commit
f10442fec4
3 changed files with 221 additions and 44 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue