feat(tvos): Add TopShelf Extension (#1561)

This commit is contained in:
Steve Byatt
2026-05-21 07:47:45 +01:00
committed by GitHub
parent 4bef386b82
commit 121ff0eea0
19 changed files with 832 additions and 1 deletions

View File

@@ -0,0 +1,112 @@
import ExpoModulesCore
import Foundation
import Security
#if canImport(TVServices)
import TVServices
#endif
public class TopShelfCacheModule: Module {
private let appGroupInfoPlistKey = "StreamyfinAppGroupIdentifier"
private let keychainAccessGroupInfoPlistKey = "StreamyfinKeychainAccessGroupIdentifier"
private let cacheKey = "TopShelfCache"
private let apiKeyService = "StreamyfinTopShelf"
private let apiKeyAccount = "JellyfinApiKey"
private var appGroupIdentifier: String? {
if let appGroupIdentifier = Bundle.main.object(
forInfoDictionaryKey: appGroupInfoPlistKey
) as? String {
return appGroupIdentifier
}
guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
return nil
}
return "group.\(bundleIdentifier)"
}
private var keychainAccessGroupIdentifier: String? {
Bundle.main.object(forInfoDictionaryKey: keychainAccessGroupInfoPlistKey) as? String
}
public func definition() -> ModuleDefinition {
Name("TopShelfCache")
Function("writeCache") { (json: String, apiKey: String?) -> Bool in
guard
let appGroupIdentifier = appGroupIdentifier,
let defaults = UserDefaults(suiteName: appGroupIdentifier)
else {
return false
}
defaults.set(json, forKey: cacheKey)
defaults.set(Date().timeIntervalSince1970, forKey: "\(cacheKey)UpdatedAt")
defaults.synchronize()
let didSaveAPIKey = saveAPIKey(apiKey)
#if canImport(TVServices)
TVTopShelfContentProvider.topShelfContentDidChange()
#endif
return didSaveAPIKey
}
Function("clearCache") { () -> Bool in
guard
let appGroupIdentifier = appGroupIdentifier,
let defaults = UserDefaults(suiteName: appGroupIdentifier)
else {
return false
}
defaults.removeObject(forKey: cacheKey)
defaults.removeObject(forKey: "\(cacheKey)UpdatedAt")
defaults.synchronize()
let didDeleteAPIKey = deleteAPIKey()
#if canImport(TVServices)
TVTopShelfContentProvider.topShelfContentDidChange()
#endif
return didDeleteAPIKey
}
}
private func baseAPIKeyQuery() -> [String: Any] {
var query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: apiKeyService,
kSecAttrAccount as String: apiKeyAccount
]
if let keychainAccessGroupIdentifier {
query[kSecAttrAccessGroup as String] = keychainAccessGroupIdentifier
}
return query
}
private func saveAPIKey(_ apiKey: String?) -> Bool {
guard deleteAPIKey() else {
return false
}
guard
let apiKey,
!apiKey.isEmpty,
let data = apiKey.data(using: .utf8)
else {
return true
}
var query = baseAPIKeyQuery()
query[kSecValueData as String] = data
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
private func deleteAPIKey() -> Bool {
let status = SecItemDelete(baseAPIKeyQuery() as CFDictionary)
return status == errSecSuccess || status == errSecItemNotFound
}
}