mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-05-22 06:46:46 +01:00
feat(tvos): Add TopShelf Extension (#1561)
This commit is contained in:
@@ -24,3 +24,10 @@ export type {
|
||||
VideoSource as MpvVideoSource,
|
||||
} from "./mpv-player";
|
||||
export { MpvPlayerView } from "./mpv-player";
|
||||
// Top Shelf cache (tvOS)
|
||||
export type {
|
||||
TopShelfCacheItem,
|
||||
TopShelfCachePayload,
|
||||
TopShelfCacheSection,
|
||||
} from "./top-shelf-cache";
|
||||
export { clearTopShelfCache, writeTopShelfCache } from "./top-shelf-cache";
|
||||
|
||||
6
modules/top-shelf-cache/expo-module.config.json
Normal file
6
modules/top-shelf-cache/expo-module.config.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"platforms": ["apple"],
|
||||
"apple": {
|
||||
"modules": ["TopShelfCacheModule"]
|
||||
}
|
||||
}
|
||||
1
modules/top-shelf-cache/index.ts
Normal file
1
modules/top-shelf-cache/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./src";
|
||||
23
modules/top-shelf-cache/ios/TopShelfCache.podspec
Normal file
23
modules/top-shelf-cache/ios/TopShelfCache.podspec
Normal file
@@ -0,0 +1,23 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'TopShelfCache'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'Shared Top Shelf cache writer for Streamyfin tvOS'
|
||||
s.description = 'Writes lightweight Top Shelf cache payloads to an App Group container for the tvOS extension.'
|
||||
s.author = 'Streamyfin'
|
||||
s.homepage = 'https://github.com/streamyfin/streamyfin'
|
||||
s.platforms = {
|
||||
:ios => '15.1',
|
||||
:tvos => '15.1'
|
||||
}
|
||||
s.source = { git: '' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_VERSION' => '5.9'
|
||||
}
|
||||
|
||||
s.source_files = "*.{h,m,mm,swift}"
|
||||
end
|
||||
112
modules/top-shelf-cache/ios/TopShelfCacheModule.swift
Normal file
112
modules/top-shelf-cache/ios/TopShelfCacheModule.swift
Normal 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
|
||||
}
|
||||
}
|
||||
21
modules/top-shelf-cache/src/TopShelfCache.types.ts
Normal file
21
modules/top-shelf-cache/src/TopShelfCache.types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export type TopShelfCacheModuleEvents = Record<string, never>;
|
||||
|
||||
export interface TopShelfCacheItem {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
imageUrl?: string;
|
||||
route: string;
|
||||
playRoute?: string;
|
||||
}
|
||||
|
||||
export interface TopShelfCacheSection {
|
||||
title: string;
|
||||
items: TopShelfCacheItem[];
|
||||
}
|
||||
|
||||
export interface TopShelfCachePayload {
|
||||
version: 1;
|
||||
updatedAt: string;
|
||||
sections: TopShelfCacheSection[];
|
||||
}
|
||||
37
modules/top-shelf-cache/src/TopShelfCacheModule.ts
Normal file
37
modules/top-shelf-cache/src/TopShelfCacheModule.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NativeModule, requireNativeModule } from "expo";
|
||||
import { Platform } from "react-native";
|
||||
import type { TopShelfCacheModuleEvents } from "./TopShelfCache.types";
|
||||
|
||||
declare class TopShelfCacheModuleType extends NativeModule<TopShelfCacheModuleEvents> {
|
||||
writeCache(json: string, apiKey?: string): boolean;
|
||||
clearCache(): boolean;
|
||||
}
|
||||
|
||||
let TopShelfCacheNativeModule: TopShelfCacheModuleType | null = null;
|
||||
|
||||
if (Platform.OS === "ios" && Platform.isTV) {
|
||||
try {
|
||||
TopShelfCacheNativeModule =
|
||||
requireNativeModule<TopShelfCacheModuleType>("TopShelfCache");
|
||||
} catch {
|
||||
TopShelfCacheNativeModule = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeTopShelfCache(json: string, apiKey?: string): boolean {
|
||||
if (!TopShelfCacheNativeModule) return false;
|
||||
|
||||
try {
|
||||
return TopShelfCacheNativeModule.writeCache(json, apiKey);
|
||||
} catch {
|
||||
try {
|
||||
return TopShelfCacheNativeModule.writeCache(json);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clearTopShelfCache(): boolean {
|
||||
return TopShelfCacheNativeModule?.clearCache() ?? false;
|
||||
}
|
||||
2
modules/top-shelf-cache/src/index.ts
Normal file
2
modules/top-shelf-cache/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./TopShelfCache.types";
|
||||
export { clearTopShelfCache, writeTopShelfCache } from "./TopShelfCacheModule";
|
||||
Reference in New Issue
Block a user