mirror of
https://github.com/narrrl/proton-stream.git
synced 2026-11-03 16:41:26 +01:00
A BGProcessingTask runs the same crawl_for_news no sooner than every six hours, when iOS chooses. A processing task rather than an app refresh, since a crawl of a large share does not fit in thirty seconds.
83 lines
3.3 KiB
Swift
83 lines
3.3 KiB
Swift
import BackgroundTasks
|
|
import Foundation
|
|
import UserNotifications
|
|
|
|
/// Look at the shares while the app is away, and say when a show gained
|
|
/// episodes — what `NewEpisodesWorker` does on Android.
|
|
///
|
|
/// A processing task rather than an app refresh: a crawl of a large share
|
|
/// takes minutes, and an app refresh gets about thirty seconds. iOS runs it
|
|
/// when it sees fit, mostly overnight on a charger, so it is a best effort
|
|
/// where Android's is a schedule.
|
|
enum NewEpisodes {
|
|
static let identifier = "io.narl.protonstream.new-episodes"
|
|
private static let interval: TimeInterval = 6 * 60 * 60
|
|
|
|
/// At launch, before the app finishes launching: iOS refuses a handler
|
|
/// registered later.
|
|
static func register() {
|
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: identifier, using: nil) { task in
|
|
guard let task = task as? BGProcessingTask else { return }
|
|
run(task)
|
|
}
|
|
}
|
|
|
|
/// Keep the next look scheduled, or cancel it when the viewer turned it off.
|
|
static func schedule() {
|
|
guard SettingsStore().newEpisodeNotifications else {
|
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: identifier)
|
|
return
|
|
}
|
|
let request = BGProcessingTaskRequest(identifier: identifier)
|
|
request.requiresNetworkConnectivity = true
|
|
request.earliestBeginDate = Date(timeIntervalSinceNow: interval)
|
|
try? BGTaskScheduler.shared.submit(request)
|
|
}
|
|
|
|
/// Ask once, when the viewer has the setting on; iOS remembers the answer.
|
|
static func requestPermission() {
|
|
guard SettingsStore().newEpisodeNotifications else { return }
|
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { _, _ in }
|
|
}
|
|
|
|
private static func run(_ task: BGProcessingTask) {
|
|
schedule()
|
|
let work = Task {
|
|
let news = (try? await NativeRuntime.engine().crawlForNews()) ?? []
|
|
await post(news)
|
|
task.setTaskCompleted(success: !Task.isCancelled)
|
|
}
|
|
task.expirationHandler = {
|
|
work.cancel()
|
|
Task { try? await NativeRuntime.engine().stopCrawl() }
|
|
}
|
|
}
|
|
|
|
private static func post(_ news: [NewEpisodesRecord]) async {
|
|
let center = UNUserNotificationCenter.current()
|
|
guard await center.notificationSettings().authorizationStatus == .authorized else { return }
|
|
for item in news {
|
|
let content = UNMutableNotificationContent()
|
|
content.title = item.title
|
|
content.body = describe(item)
|
|
content.threadIdentifier = "new-episodes"
|
|
// Keyed by the show, so a second look's news replaces the first.
|
|
let request = UNNotificationRequest(identifier: "new-episodes-\(item.titleKey)", content: content, trigger: nil)
|
|
try? await center.add(request)
|
|
}
|
|
}
|
|
|
|
/// What one show's notification says: "S01E04 and 2 more are new".
|
|
static func describe(_ news: NewEpisodesRecord) -> String {
|
|
let more = Int(news.count) - 1
|
|
guard let first = news.first else {
|
|
return news.count == 1 ? "1 new episode" : "\(news.count) new episodes"
|
|
}
|
|
switch more {
|
|
case 0: return "\(first) is new"
|
|
case 1: return "\(first) and 1 more are new"
|
|
default: return "\(first) and \(more) more are new"
|
|
}
|
|
}
|
|
}
|