These sources will be converted to be more flexible with JavaScript in the future. The source catalog is populated by adding a source list in settings then installing a source from the catalog. Sources can be enabled or disabled when using Ferrite. Signed-off-by: kingbri <bdashore3@gmail.com>
165 lines
5.2 KiB
Swift
165 lines
5.2 KiB
Swift
//
|
|
// ScrapingViewModel.swift
|
|
// Ferrite
|
|
//
|
|
// Created by Brian Dashore on 7/4/22.
|
|
//
|
|
|
|
import Base32
|
|
import SwiftSoup
|
|
import SwiftUI
|
|
|
|
public struct SearchResult: Hashable, Codable {
|
|
let title: String
|
|
let source: String
|
|
let size: String
|
|
let magnetLink: String
|
|
let magnetHash: String?
|
|
}
|
|
|
|
class ScrapingViewModel: ObservableObject {
|
|
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
|
|
|
// Link the toast view model for single-directional communication
|
|
var toastModel: ToastViewModel?
|
|
|
|
@Published var searchResults: [SearchResult] = []
|
|
@Published var debridHashes: [String] = []
|
|
@Published var searchText: String = ""
|
|
@Published var selectedSearchResult: SearchResult?
|
|
|
|
@MainActor
|
|
public func scanSources(sources: FetchedResults<TorrentSource>) async {
|
|
if sources.isEmpty {
|
|
print("Sources empty")
|
|
}
|
|
|
|
var tempResults: [SearchResult] = []
|
|
|
|
for source in sources {
|
|
if source.enabled {
|
|
guard let html = await fetchWebsiteHtml(source: source) else {
|
|
continue
|
|
}
|
|
|
|
let sourceResults = await scrapeWebsite(source: source, html: html)
|
|
tempResults += sourceResults
|
|
}
|
|
}
|
|
|
|
searchResults = tempResults
|
|
}
|
|
|
|
// Fetches the HTML body for the source website
|
|
@MainActor
|
|
public func fetchWebsiteHtml(source: TorrentSource) async -> String? {
|
|
guard let encodedQuery = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
|
|
toastModel?.toastDescription = "Could not process search query, invalid characters present."
|
|
print("Could not process search query, invalid characters present")
|
|
|
|
return nil
|
|
}
|
|
|
|
guard let url = URL(string: source.url + encodedQuery) else {
|
|
toastModel?.toastDescription = "Source doesn't contain a valid URL, contact the source dev!"
|
|
print("Source doesn't contain a valid URL, contact the source dev!")
|
|
|
|
return nil
|
|
}
|
|
|
|
do {
|
|
let (data, _) = try await URLSession.shared.data(from: url)
|
|
let html = String(data: data, encoding: .ascii)
|
|
return html
|
|
} catch {
|
|
toastModel?.toastDescription = "Error in fetching HTML \(error)"
|
|
print("Error in fetching HTML \(error)")
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Returns results to UI
|
|
// Results must have a link and title, but other parameters aren't required
|
|
@MainActor
|
|
public func scrapeWebsite(source: TorrentSource, html: String) async -> [SearchResult] {
|
|
var tempResults: [SearchResult] = []
|
|
var hashes: [String] = []
|
|
|
|
do {
|
|
let document = try SwiftSoup.parse(html)
|
|
|
|
let rows = try document.select(source.rowQuery)
|
|
|
|
for row in rows {
|
|
guard let link = try row.select(source.linkQuery).first() else {
|
|
continue
|
|
}
|
|
|
|
let href = try link.attr("href")
|
|
|
|
if !href.starts(with: "magnet:") {
|
|
continue
|
|
}
|
|
|
|
let magnetHash = fetchMagnetHash(magnetLink: href)
|
|
|
|
var title: String?
|
|
|
|
// Some sources may use last-child, but SwiftSoup doesn't support it
|
|
if let titleQuery = source.titleQuery {
|
|
if titleQuery.contains("last-child") {
|
|
let newTitleQuery = titleQuery.replacingOccurrences(of: ":last-child", with: "")
|
|
title = try row.select(newTitleQuery).last()?.text()
|
|
} else {
|
|
title = try row.select(titleQuery).first()?.text()
|
|
}
|
|
}
|
|
|
|
let size = try row.select(source.sizeQuery ?? "").first()
|
|
let sizeText = try size?.text()
|
|
|
|
let result = SearchResult(
|
|
title: title ?? "No title",
|
|
source: source.name ?? "N/A",
|
|
size: sizeText ?? "?B",
|
|
magnetLink: href,
|
|
magnetHash: magnetHash
|
|
)
|
|
|
|
// Change to bulk request to speed up UI
|
|
if let hash = magnetHash {
|
|
hashes.append(hash)
|
|
}
|
|
|
|
tempResults.append(result)
|
|
}
|
|
|
|
return tempResults
|
|
} catch {
|
|
toastModel?.toastDescription = "Error while scraping: \(error)"
|
|
print("Error while scraping: \(error)")
|
|
|
|
return []
|
|
}
|
|
}
|
|
|
|
// Fetches and possibly converts the magnet hash value to sha1
|
|
public func fetchMagnetHash(magnetLink: String) -> String? {
|
|
guard let firstSplit = magnetLink.split(separator: ":")[safe: 3] else {
|
|
return nil
|
|
}
|
|
|
|
guard let magnetHash = firstSplit.split(separator: "&")[safe: 0] else {
|
|
return nil
|
|
}
|
|
|
|
// Is this a Base32hex hash?
|
|
if magnetHash.count == 32 {
|
|
let decryptedMagnetHash = base32DecodeToData(String(magnetHash))
|
|
return decryptedMagnetHash?.hexEncodedString()
|
|
} else {
|
|
return String(magnetHash)
|
|
}
|
|
}
|
|
}
|