├── .github └── workflows │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── Makefile ├── Package.swift ├── README.md ├── Sources ├── StremioCore │ ├── Core.swift │ ├── StremioApi+CallbackType.swift │ ├── StremioApi.swift │ ├── google │ │ └── protobuf │ │ │ ├── empty.proto │ │ │ └── timestamp.proto │ ├── stremio │ └── swift-protobuf-config.json └── Wrapper │ ├── include │ └── wrapper.h │ └── somec.c ├── Support ├── cbindgen.toml ├── installDependencies.command └── protoc ├── build.rs └── src ├── commonMain └── rust │ ├── bridge.rs │ ├── bridge │ ├── action.rs │ ├── apple_model_field.rs │ ├── auth_request.rs │ ├── date.rs │ ├── env_error.rs │ ├── event.rs │ ├── events.rs │ ├── extra_value.rs │ ├── from_protobuf.rs │ ├── library_item.rs │ ├── link.rs │ ├── list.rs │ ├── loadable.rs │ ├── manifest.rs │ ├── meta_preview.rs │ ├── option.rs │ ├── pair.rs │ ├── poster_shape.rs │ ├── profile.rs │ ├── resource_loadable.rs │ ├── resource_path.rs │ ├── resource_request.rs │ ├── stream.rs │ ├── string.rs │ ├── subtitle.rs │ └── to_protobuf.rs │ ├── env.rs │ ├── env │ ├── env.rs │ ├── event.rs │ ├── fetch.rs │ └── storage.rs │ ├── lib.rs │ ├── model.rs │ ├── model │ ├── addons.rs │ ├── fields.rs │ ├── fields │ │ ├── addon_detail.rs │ │ ├── addons_with_filters.rs │ │ ├── catalogs_with_extra.rs │ │ ├── continue_watching_preview.rs │ │ ├── ctx.rs │ │ ├── discover.rs │ │ ├── library.rs │ │ ├── library_by_type.rs │ │ ├── link.rs │ │ ├── meta_details.rs │ │ ├── player.rs │ │ └── streaming_server.rs │ └── model.rs │ └── stremio_core_apple.rs └── main └── proto └── stremio └── core ├── models ├── addon_details.proto ├── addons_with_filters.proto ├── catalog_with_filters.proto ├── catalogs_with_extra.proto ├── continue_watching_preview.proto ├── ctx.proto ├── library_by_type.proto ├── library_with_filters.proto ├── link.proto ├── loadable.proto ├── meta_details.proto ├── player.proto └── streaming_server.proto ├── runtime ├── action.proto ├── action_catalog_with_filters.proto ├── action_catalogs_with_extra.proto ├── action_ctx.proto ├── action_library_by_type.proto ├── action_link.proto ├── action_load.proto ├── action_meta_details.proto ├── action_player.proto ├── action_streaming_server.proto ├── event.proto ├── field.proto └── runtime.proto └── types ├── addon.proto ├── api.proto ├── auth_request.proto ├── library.proto ├── manifest.proto ├── meta_item.proto ├── meta_item_preview.proto ├── profile.proto ├── stream.proto ├── subtitle.proto └── video.proto /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version" 8 | required: true 9 | 10 | jobs: 11 | Release: 12 | runs-on: macos-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Git config 19 | run: | 20 | git config user.name "github-actions[bot]" 21 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 22 | - name: Set default rust toolchain 23 | run: rustup default nightly 24 | - name: Install rustfmt 25 | run: rustup component add rustfmt 26 | - name: Install clippy 27 | run: rustup component add clippy 28 | - name: Add x86_64-apple-ios target 29 | run: rustup target add x86_64-apple-ios 30 | - name: Add aarch64-apple-ios target 31 | run: rustup target add aarch64-apple-ios 32 | - name: Add aarch64-apple-ios-sim target 33 | run: rustup target add aarch64-apple-ios-sim 34 | - name: Install rust-src 35 | run: rustup component add rust-src --toolchain nightly-aarch64-apple-darwin 36 | - name: Install cargo lipo 37 | run: cargo install cargo-lipo 38 | - name: Install cbindgen 39 | run: brew install cbindgen 40 | - name: Install protobuf 41 | run: brew install protobuf 42 | - name: Install swift-protobuf 43 | run: brew install swift-protobuf 44 | - name: Setup Xcode 45 | run: sudo xcode-select -switch /Applications/Xcode_15.4.app 46 | - name: Build stremio-core-swift 47 | run: make all 48 | - name: Archive StremioCore.xcframework 49 | run: cd .build && zip -r StremioCore.xcframework.zip ./StremioCore.xcframework 50 | - name: Compute sha checksum of StremioCore.xcframework.zip 51 | run: cd .build && shasum -a 256 StremioCore.xcframework.zip > StremioCore.xcframework.zip.sha256 52 | - name: Update Package.swift manifest 53 | run: make manifest url="https://github.com/Stremio/stremio-core-swift/releases/download/${{ github.event.inputs.version }}/StremioCore.xcframework.zip" sha256="$(cat .build/StremioCore.xcframework.zip.sha256 | awk '{print $1}')" 54 | - name: Push Package.swift manifest 55 | run: | 56 | git add Package.swift 57 | git commit -m "Release version ${{ github.event.inputs.version }}" 58 | git push 59 | - name: Push git tag 60 | run: | 61 | git tag "${{ github.event.inputs.version }}" 62 | git push --tags 63 | - name: Upload build artifact to GitHub release assets 64 | uses: svenstaro/upload-release-action@v2 65 | with: 66 | repo_token: ${{ secrets.GITHUB_TOKEN }} 67 | tag: ${{ github.event.inputs.version }} 68 | file: ./.build/StremioCore.xcframework.zip* 69 | file_glob: true 70 | overwrite: true 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /target 3 | /src/debug 4 | /src/release 5 | /**/rust/protobuf/* 6 | !/**/rust/protobuf/README.md 7 | /.idea/ 8 | /local.properties 9 | /bridge/stremio/ 10 | /bridge/wrapper.hpp 11 | .DS_Store 12 | /.build 13 | /Packages 14 | xcuserdata/ 15 | DerivedData/ 16 | .swiftpm 17 | .swiftpm/configuration/registries.json 18 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 19 | .netrc 20 | /Package.resolved 21 | /Sources/StremioCore/stremio/* 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stremio-core-swift" 3 | version = "1.2.62" 4 | authors = ["Smart Code OOD"] 5 | edition = "2021" 6 | resolver = "2" 7 | 8 | [lib] 9 | crate-type = ["staticlib"] 10 | path = "src/commonMain/rust/lib.rs" 11 | 12 | [profile.release] 13 | lto = true 14 | opt-level = 3 15 | 16 | [dependencies] 17 | stremio-core = { git = "https://github.com/Stremio/stremio-core", rev = "f74c212752443ef625a17d48ce4dc955f2300bc7", features = [ 18 | "derive", 19 | "analytics", 20 | "env-future-send", 21 | ] } 22 | 23 | stremio-watched-bitfield = { git = "https://github.com/Stremio/stremio-core", rev = "f74c212752443ef625a17d48ce4dc955f2300bc7" } 24 | serde = "1.0.*" 25 | serde_json = "1.0.*" 26 | futures = "0.3.*" 27 | http = "0.2.*" 28 | url = { version = "2.4", features = ["serde"] } 29 | chrono = "0.4.*" 30 | semver = { version = "1", features = ["serde"] } 31 | hex = "0.4.*" 32 | 33 | once_cell = "1" 34 | enclose = "1.1.*" 35 | boolinator = "2.4.*" 36 | Inflector = "0.11.*" 37 | getrandom = "0.2.*" 38 | cfg-if = "0.1.*" 39 | serde_path_to_error = "0.1.*" 40 | tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] } 41 | prost = "0.12" 42 | prost-types = "0.12" 43 | 44 | objc2-foundation = { version = "0.2.2", features = [ 45 | "NSString", 46 | "NSData", 47 | "NSUserDefaults", 48 | "NSURLRequest", 49 | "NSURLResponse", 50 | "NSURLSession", 51 | "NSURL", 52 | "NSError", 53 | "block2", 54 | ] } 55 | objc2 = "0.5.2" 56 | block2 = "0.5.1" 57 | itertools = "0.13.0" 58 | 59 | [build-dependencies] 60 | prost-build = "0.12" 61 | protox = "0.5" 62 | glob = "0.3.0" 63 | 64 | # A way to quickly test with local version of `core` crates 65 | # [patch.'https://github.com/Stremio/stremio-core'] 66 | # stremio-core = { path = "../stremio-core" } 67 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © 2019 SmartCode OOD 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIBRARY_NAME := libstremio_core_swift 2 | FRAMEWORK_NAME := StremioCore 3 | SWIFT_FILE := Package.swift 4 | .PHONY: all 5 | 6 | all: macos ios iossim tvossim tvos visionossim package 7 | 8 | macos: 9 | @cargo +nightly build -Z build-std --release --lib --target aarch64-apple-ios-macabi 10 | @cargo +nightly build -Z build-std --release --lib --target x86_64-apple-ios-macabi 11 | @$(RM) -rf target/universal/$(LIBRARY_NAME)-macabi.a 12 | @mkdir -p target/universal/ 13 | @lipo -create -output target/universal/$(LIBRARY_NAME)-macabi.a \ 14 | target/aarch64-apple-ios-macabi/release/$(LIBRARY_NAME).a \ 15 | target/x86_64-apple-ios-macabi/release/$(LIBRARY_NAME).a 16 | 17 | ios: 18 | @cargo build --release --lib --target aarch64-apple-ios 19 | @$(RM) -rf target/universal/$(LIBRARY_NAME)-ios.a 20 | @mkdir -p target/universal/ 21 | @cp target/aarch64-apple-ios/release/$(LIBRARY_NAME).a target/universal/$(LIBRARY_NAME)-ios.a 22 | 23 | iossim: 24 | @cargo build --release --lib --target aarch64-apple-ios-sim 25 | @cargo build --release --lib --target x86_64-apple-ios 26 | @$(RM) -rf target/universal/$(LIBRARY_NAME)-ios-sim.a 27 | @mkdir -p target/universal/ 28 | @lipo -create -output target/universal/$(LIBRARY_NAME)-ios-sim.a \ 29 | target/aarch64-apple-ios-sim/release/$(LIBRARY_NAME).a \ 30 | target/x86_64-apple-ios/release/$(LIBRARY_NAME).a 31 | 32 | tvossim: 33 | @cargo build -Z build-std --release --lib --target aarch64-apple-tvos-sim 34 | @cargo build -Z build-std --release --lib --target x86_64-apple-tvos 35 | @$(RM) -rf target/universal/$(LIBRARY_NAME)-tvos-sim.a 36 | @mkdir -p target/universal/ 37 | @lipo -create -output target/universal/$(LIBRARY_NAME)-tvos-sim.a \ 38 | target/aarch64-apple-tvos-sim/release/$(LIBRARY_NAME).a \ 39 | target/x86_64-apple-tvos/release/$(LIBRARY_NAME).a 40 | 41 | tvos: 42 | @cargo build -Z build-std --release --lib --target aarch64-apple-tvos 43 | @$(RM) -rf target/universal/$(LIBRARY_NAME)-tvos.a 44 | @mkdir -p target/universal/ 45 | @cp target/aarch64-apple-tvos/release/$(LIBRARY_NAME).a target/universal/$(LIBRARY_NAME)-tvos.a 46 | 47 | visionossim: 48 | @cargo +nightly build -Z build-std --release --lib --target aarch64-apple-visionos-sim 49 | @$(RM) -rf target/universal/$(LIBRARY_NAME)-visionos-sim.a 50 | @mkdir -p target/universal/ 51 | @cp target/aarch64-apple-visionos-sim/release/$(LIBRARY_NAME).a target/universal/$(LIBRARY_NAME)-visionos-sim.a 52 | 53 | framework: 54 | @mkdir -p .build/ 55 | @$(RM) -rf .build/$(FRAMEWORK_NAME).xcframework 56 | @xcodebuild -create-xcframework \ 57 | -library target/universal/$(LIBRARY_NAME)-ios.a \ 58 | -library target/universal/$(LIBRARY_NAME)-ios-sim.a \ 59 | -library target/universal/$(LIBRARY_NAME)-macabi.a \ 60 | -library target/universal/$(LIBRARY_NAME)-visionos-sim.a \ 61 | -library target/universal/$(LIBRARY_NAME)-tvos-sim.a \ 62 | -library target/universal/$(LIBRARY_NAME)-tvos.a \ 63 | -output .build/$(FRAMEWORK_NAME).xcframework 64 | 65 | package: framework 66 | @$(RM) -rf Sources/StremioCore/stremio 67 | @cbindgen --config Support/cbindgen.toml -o Sources/Wrapper/include/wrapper.h 68 | 69 | manifest: 70 | @echo "Updating URL and SHA256 in $(SWIFT_FILE)..." 71 | @sed -i '' 's|let url = .*|let url = "$(url)";|' $(SWIFT_FILE) 72 | @sed -i '' 's|let sha256 = .*|let sha256 = "$(sha256)";|' $(SWIFT_FILE) 73 | @echo "Done." 74 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | let sha256 = "8c5042a450587cfd54b8c83a48127e5ea5e58ee3d9f04d10276949e0880c6d72"; 4 | let url = "https://github.com/Stremio/stremio-core-swift/releases/download/1.2.62/StremioCore.xcframework.zip"; 5 | 6 | import PackageDescription 7 | 8 | let package = Package( 9 | name: "StremioCore", 10 | platforms: [ 11 | .macCatalyst(.v13), 12 | .iOS(.v12), 13 | .visionOS(.v1), 14 | .tvOS(.v12) 15 | ], 16 | products: [ 17 | .library( 18 | name: "StremioCore", 19 | targets: ["StremioCore", "XCFramework"]), 20 | ], 21 | dependencies: [ 22 | .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.0.0"), 23 | ], 24 | targets: [ 25 | .target(name: "StremioCore", 26 | dependencies: ["Wrapper", .product(name: "SwiftProtobuf", package: "swift-protobuf")], plugins: [ 27 | .plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf") 28 | ]), 29 | .target(name: "Wrapper", dependencies: []), 30 | //.binaryTarget(name: "XCFramework", path: ".build/StremioCore.xcframework") 31 | .binaryTarget(name: "XCFramework", url: url, checksum: sha256) 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stremio-core-swift 2 | 3 | `stremio-core-swift` is a wrapper for Apple operating systems, based on `stremio-core-kotlin`. Any changes that are not specific to Apple should be contributed to `stremio-core-kotlin`. 4 | 5 | ## Adding as an SPM Package 6 | 7 | To integrate `stremio-core-swift` into your project, simply add it as a Swift Package Manager (SPM) package. It will automatically handle everything for you: 8 | 9 | - Protobuf will be generated at Xcode compile time. 10 | - The compiled Rust binary will be fetched from the releases. 11 | 12 | ## Alternatively, complie Rust Binary on Your Mac 13 | 14 | To compile the Rust binary on your macOS machine, follow these steps: 15 | 16 | 1. **Install Apple Rust Dependencies:** 17 | 18 | Run the following command to install the necessary dependencies: 19 | 20 | ```zsh 21 | Support/installDependencies.command 22 | ``` 23 | 24 | 2. **Compile rust code:** 25 | ```zsh 26 | make all 27 | ``` 28 | 29 | 30 | 3. **Define the XCFramework in `Package.swift`:** 31 | 32 | Uncomment the local binary target in the Package.swift file: 33 | 34 | ```swift 35 | .binaryTarget(name: "XCFramework", path: ".build/StremioCore.xcframework") 36 | //.binaryTarget(name: "XCFramework", url: url, checksum: sha256) 37 | ``` 38 | 39 | 4. **Add the Local Package in Xcode:** 40 | 41 | Finally, add the package as a local package in Xcode. 42 | -------------------------------------------------------------------------------- /Sources/StremioCore/Core.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Core.swift 3 | // Stremio 4 | // 5 | // Created by Alvin on 17.01.24. 6 | // 7 | 8 | import Foundation 9 | import SwiftProtobuf 10 | import Wrapper 11 | import os.log 12 | 13 | #if targetEnvironment(macCatalyst) 14 | #else 15 | import UIKit 16 | #endif 17 | 18 | public class Core { 19 | //MARK: - callback 20 | private static let oslog = OSLog(subsystem: "com.stremio.core.Swift", category: "Wrapper") 21 | private static var fieldListener : [Stremio_Core_Runtime_Field : (Any) -> Void] = [:] 22 | private static var eventListener : [Int : (Stremio_Core_Runtime_Event) -> Void] = [:] 23 | public static var coreEventListener : ((Stremio_Core_Runtime_Event) -> Void)? 24 | 25 | ///Make sure to remove listener before function gets deallocated to avoid undefined behaviour. Handle UI tasks in Main thread 26 | public static func addEventListener(type: Stremio_Core_Runtime_Field, _ function: @escaping (Any) -> Void) { 27 | Core.fieldListener[type] = function 28 | } 29 | ///Make sure to remove listener before function gets deallocated to avoid undefined behaviour. Handle UI tasks in Main thread 30 | internal static func addEventListener(type: Int, _ function: @escaping (Stremio_Core_Runtime_Event) -> Void) { 31 | Core.eventListener[type] = function 32 | } 33 | 34 | public static func removeEventListener(type: Stremio_Core_Runtime_Field) { 35 | Core.fieldListener.removeValue(forKey: type) 36 | } 37 | 38 | internal static func removeEventListener(type: Int) { 39 | Core.eventListener.removeValue(forKey: type) 40 | } 41 | @objc internal static func onRuntimeEvent(_ eventData: NSData){ 42 | do { 43 | let event = try Stremio_Core_Runtime_RuntimeEvent(serializedData: eventData as Data) 44 | os_log(.debug, log: oslog, "%@", event.debugDescription) 45 | if case .coreEvent(_:) = event.event{ 46 | let function = { 47 | if case .error(_:) = event.coreEvent.type{ 48 | return Core.eventListener.first(where: {event.coreEvent.error.source.getMessageTag == $0.key})?.value 49 | } 50 | return Core.eventListener.first(where: {event.coreEvent.getMessageTag == $0.key})?.value 51 | }() 52 | 53 | coreEventListener?(event.coreEvent) 54 | if function == nil {return} 55 | function?(event.coreEvent) 56 | } 57 | else { 58 | for field in event.newState.fields{ 59 | if let function = Core.fieldListener[field] { 60 | function(field) 61 | } 62 | } 63 | } 64 | } 65 | catch{ 66 | os_log(.error, log: oslog, "Error onRuntimeEvent: %@", error.localizedDescription) 67 | } 68 | } 69 | 70 | @objc internal static func onRustPanic(_ errorString: NSString){ 71 | let oslog = OSLog(subsystem: "com.stremio.core.Rust", category: "Fatal") 72 | os_log(.fault, log: oslog, "Rust paniced: %{public}s", errorString) 73 | } 74 | 75 | //MARK: - rust calls 76 | public static func initialize() -> Stremio_Core_Runtime_EnvError? { 77 | initialize_rust() 78 | do { 79 | if let swiftData = initializeNative(getDeviceInfo()) as? NSData{ 80 | defer {defer {releaseObjectNative(swiftData)}} 81 | return try Stremio_Core_Runtime_EnvError(serializedData: swiftData as Data) 82 | } 83 | } catch { 84 | os_log(.error, log: oslog, "Error envError: %@", error.localizedDescription) 85 | } 86 | return nil 87 | } 88 | 89 | public static func getState(_ field: Stremio_Core_Runtime_Field) -> T? { 90 | do { 91 | if let swiftData = getStateNative(Int32(field.rawValue)) as? NSData{ 92 | defer {defer {releaseObjectNative(swiftData)}} 93 | return try T(serializedData: swiftData as Data) 94 | } 95 | } catch { 96 | os_log(.error, log: oslog, "Error getState: %@", error.localizedDescription) 97 | } 98 | return nil 99 | } 100 | 101 | public static func dispatch(action: Stremio_Core_Runtime_Action,field: Stremio_Core_Runtime_Field? = nil) { 102 | var runtimeAction = Stremio_Core_Runtime_RuntimeAction() 103 | runtimeAction.action = action 104 | 105 | if let field = field{ 106 | runtimeAction.field = field 107 | } 108 | do { 109 | let actionProtobuf = try NSData(data: runtimeAction.serializedData()) 110 | dispatchNative(actionProtobuf) 111 | } catch { 112 | os_log(.error, log: oslog, "Error dispatch: %@", error.localizedDescription) 113 | } 114 | } 115 | 116 | public static func decodeStreamData(streamData: String) -> Stremio_Core_Types_Stream? { 117 | do { 118 | if let swiftData = decodeStreamDataNative(streamData) as? NSData{ 119 | defer {releaseObjectNative(swiftData)} 120 | return try Stremio_Core_Types_Stream(serializedData: swiftData as Data) 121 | } 122 | } catch { 123 | os_log(.error, log: oslog, "Error decodeStreamData: %@", error.localizedDescription) 124 | } 125 | return nil 126 | } 127 | 128 | public static func getVersion() -> String { 129 | return getVersionNative() 130 | } 131 | } 132 | 133 | fileprivate func getDeviceInfo() -> String { 134 | #if targetEnvironment(macCatalyst) 135 | let service = IOServiceGetMatchingService(kIOMasterPortDefault, 136 | IOServiceMatching("IOPlatformExpertDevice")) 137 | var modelIdentifier: String 138 | if let modelData = IORegistryEntryCreateCFProperty(service, "model" as CFString, kCFAllocatorDefault, 0).takeRetainedValue() as? Data { 139 | modelIdentifier = String(data: modelData, encoding: .utf8)?.trimmingCharacters(in: .controlCharacters) ?? "UNKNOWN" 140 | } 141 | else {modelIdentifier = "UNKNOWN"} 142 | IOObjectRelease(service) 143 | 144 | let osType = "macOS" 145 | let osVersion = ProcessInfo.processInfo.operatingSystemVersion.description 146 | #else 147 | var systemInfo = utsname() 148 | uname(&systemInfo) 149 | let machineMirror = Mirror(reflecting: systemInfo.machine) 150 | let modelIdentifier = machineMirror.children.reduce("") { identifier, element in 151 | guard let value = element.value as? Int8, value != 0 else { return identifier } 152 | return identifier + String(UnicodeScalar(UInt8(value))) 153 | } 154 | 155 | let osType = UIDevice.current.systemName 156 | let osVersion = UIDevice.current.systemVersion 157 | #endif 158 | return "(\(modelIdentifier); \(osType) \(osVersion))" 159 | } 160 | 161 | //TODO: Find a way to get tag properly 162 | extension SwiftProtobuf.Message { 163 | var getMessageTag: Int { 164 | let def = try! SwiftProtobuf.Google_Protobuf_MessageOptions(serializedData: self.serializedData()) 165 | var messageText = def.textFormatString().components(separatedBy: "\n").first 166 | messageText = messageText?.replacingOccurrences(of: " {", with: "") 167 | return Int(messageText!) ?? 0 168 | } 169 | } 170 | 171 | #if targetEnvironment(macCatalyst) 172 | extension OperatingSystemVersion { 173 | var description: String { 174 | var patchVersion = "" 175 | if self.patchVersion != 0{ 176 | patchVersion = ".\(self.patchVersion)" 177 | } 178 | 179 | return "\(self.majorVersion).\(self.minorVersion)\(patchVersion)" 180 | } 181 | } 182 | #endif 183 | -------------------------------------------------------------------------------- /Sources/StremioCore/StremioApi+CallbackType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Alvin on 17.06.24. 6 | // 7 | 8 | import Foundation 9 | import SwiftProtobuf 10 | import os.log 11 | 12 | extension StremioApi { 13 | public class CallbackType { 14 | public static var error : Int = { 15 | var type = Stremio_Core_Runtime_Event() 16 | type.error.error = "" 17 | type.error.source = Stremio_Core_Runtime_Event() 18 | return type.getMessageTag 19 | }() 20 | 21 | public static var addonUninstalled : Int = { 22 | var type = Stremio_Core_Runtime_Event() 23 | type.addonUninstalled.transportURL = "" 24 | type.addonUninstalled.id = "" 25 | return type.getMessageTag 26 | }() 27 | 28 | public static var addonInstalled : Int = { 29 | var type = Stremio_Core_Runtime_Event() 30 | type.addonInstalled.transportURL = "" 31 | type.addonInstalled.id = "" 32 | return type.getMessageTag 33 | }() 34 | 35 | public static var addonUpgraded : Int = { 36 | var type = Stremio_Core_Runtime_Event() 37 | type.addonUpgraded.transportURL = "" 38 | type.addonUpgraded.id = "" 39 | return type.getMessageTag 40 | }() 41 | 42 | public static var settingsUpdated : Int = { 43 | var type = Stremio_Core_Runtime_Event() 44 | type.settingsUpdated.settings.interfaceLanguage = "" 45 | type.settingsUpdated.settings.streamingServerURL = "" 46 | type.settingsUpdated.settings.bingeWatching = false 47 | type.settingsUpdated.settings.playInBackground = false 48 | type.settingsUpdated.settings.hardwareDecoding = false 49 | type.settingsUpdated.settings.audioPassthrough = false 50 | type.settingsUpdated.settings.audioLanguage = "" 51 | type.settingsUpdated.settings.subtitlesLanguage = "" 52 | type.settingsUpdated.settings.subtitlesSize = 0 53 | type.settingsUpdated.settings.subtitlesFont = "" 54 | type.settingsUpdated.settings.subtitlesBold = false 55 | type.settingsUpdated.settings.subtitlesOffset = 0 56 | type.settingsUpdated.settings.subtitlesTextColor = "" 57 | type.settingsUpdated.settings.subtitlesBackgroundColor = "" 58 | type.settingsUpdated.settings.subtitlesOutlineColor = "" 59 | type.settingsUpdated.settings.subtitlesOpacity = 0 60 | type.settingsUpdated.settings.escExitFullscreen = false 61 | type.settingsUpdated.settings.seekTimeDuration = 0 62 | type.settingsUpdated.settings.seekShortTimeDuration = 0 63 | type.settingsUpdated.settings.pauseOnMinimize = false 64 | type.settingsUpdated.settings.secondaryAudioLanguage = "" 65 | type.settingsUpdated.settings.secondarySubtitlesLanguage = "" 66 | type.settingsUpdated.settings.playerType = "" 67 | type.settingsUpdated.settings.frameRateMatchingStrategy = .disabled 68 | type.settingsUpdated.settings.nextVideoNotificationDuration = 0 69 | type.settingsUpdated.settings.surroundSound = false 70 | return type.getMessageTag 71 | }() 72 | 73 | public static var userAuthenticated : Int = { 74 | var type = Stremio_Core_Runtime_Event() 75 | type.userAuthenticated.authRequest.loginWithToken.token = "" 76 | return type.getMessageTag 77 | }() 78 | 79 | } 80 | 81 | static internal func handleEvent(callbackType: Int, 82 | completionHandler: ((Result) -> Void)?) { 83 | if let completionHandler = completionHandler { 84 | Core.addEventListener(type: callbackType) { result in 85 | if case .error(_:) = result.type{ 86 | completionHandler(.failure(result.error)) 87 | } 88 | else if let success = result.type?.get() as T?{ 89 | completionHandler(.success(success)) 90 | } 91 | else { 92 | os_log(.fault, log: StremioApi.oslog, "Casting failed for type: %@, result: %@", String(describing: T.self), String(describing: result)) 93 | } 94 | Core.removeEventListener(type: callbackType) 95 | } 96 | } 97 | } 98 | } 99 | 100 | //useless code cos swiftProtobuf is shit 101 | extension Stremio_Core_Runtime_Event.Error : Error{} 102 | extension Stremio_Core_Runtime_Event.OneOf_Type { 103 | func get() -> T? { 104 | switch self { 105 | case .profilePushedToStorage(let value as T): return value 106 | case .libraryItemsPushedToStorage(let value as T): return value 107 | case .streamsPushedToStorage(let value as T): return value 108 | case .searchHistoryPushedToStorage(let value as T): return value 109 | case .notificationsPushedToStorage(let value as T): return value 110 | case .dismissedEventsPushedToStorage(let value as T): return value 111 | case .userPulledFromApi(let value as T): return value 112 | case .userPushedToApi(let value as T): return value 113 | case .addonsPulledFromApi(let value as T): return value 114 | case .addonsPushedToApi(let value as T): return value 115 | case .librarySyncWithApiPlanned(let value as T): return value 116 | case .libraryItemsPushedToApi(let value as T): return value 117 | case .libraryItemsPulledFromApi(let value as T): return value 118 | case .userAuthenticated(let value as T): return value 119 | case .userAddonsLocked(let value as T): return value 120 | case .userLibraryMissing(let value as T): return value 121 | case .userLoggedOut(let value as T): return value 122 | case .sessionDeleted(let value as T): return value 123 | case .traktAddonFetched(let value as T): return value 124 | case .traktLoggedOut(let value as T): return value 125 | case .addonInstalled(let value as T): return value 126 | case .addonUpgraded(let value as T): return value 127 | case .addonUninstalled(let value as T): return value 128 | case .settingsUpdated(let value as T): return value 129 | case .libraryItemAdded(let value as T): return value 130 | case .libraryItemRemoved(let value as T): return value 131 | case .libraryItemRewinded(let value as T): return value 132 | case .libraryItemNotificationsToggled(let value as T): return value 133 | case .libraryItemMarkedAsWatched(let value as T): return value 134 | case .notificationsDismissed(let value as T): return value 135 | case .playerPlaying(let value as T): return value 136 | case .playerStopped(let value as T): return value 137 | case .playerNextVideo(let value as T): return value 138 | case .playerEnded(let value as T): return value 139 | case .traktPlaying(let value as T): return value 140 | case .traktPaused(let value as T): return value 141 | case .magnetParsed(let value as T): return value 142 | case .torrentParsed(let value as T): return value 143 | case .playingOnDevice(let value as T): return value 144 | case .error(let value as T): return value 145 | default: return nil 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/StremioCore/google/protobuf/empty.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option go_package = "google.golang.org/protobuf/types/known/emptypb"; 36 | option java_package = "com.google.protobuf"; 37 | option java_outer_classname = "EmptyProto"; 38 | option java_multiple_files = true; 39 | option objc_class_prefix = "GPB"; 40 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 41 | option cc_enable_arenas = true; 42 | 43 | // A generic empty message that you can re-use to avoid defining duplicated 44 | // empty messages in your APIs. A typical example is to use it as the request 45 | // or the response type of an API method. For instance: 46 | // 47 | // service Foo { 48 | // rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty); 49 | // } 50 | // 51 | message Empty {} 52 | -------------------------------------------------------------------------------- /Sources/StremioCore/google/protobuf/timestamp.proto: -------------------------------------------------------------------------------- 1 | // Protocol Buffers - Google's data interchange format 2 | // Copyright 2008 Google Inc. All rights reserved. 3 | // https://developers.google.com/protocol-buffers/ 4 | // 5 | // Redistribution and use in source and binary forms, with or without 6 | // modification, are permitted provided that the following conditions are 7 | // met: 8 | // 9 | // * Redistributions of source code must retain the above copyright 10 | // notice, this list of conditions and the following disclaimer. 11 | // * Redistributions in binary form must reproduce the above 12 | // copyright notice, this list of conditions and the following disclaimer 13 | // in the documentation and/or other materials provided with the 14 | // distribution. 15 | // * Neither the name of Google Inc. nor the names of its 16 | // contributors may be used to endorse or promote products derived from 17 | // this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | syntax = "proto3"; 32 | 33 | package google.protobuf; 34 | 35 | option cc_enable_arenas = true; 36 | option go_package = "google.golang.org/protobuf/types/known/timestamppb"; 37 | option java_package = "com.google.protobuf"; 38 | option java_outer_classname = "TimestampProto"; 39 | option java_multiple_files = true; 40 | option objc_class_prefix = "GPB"; 41 | option csharp_namespace = "Google.Protobuf.WellKnownTypes"; 42 | 43 | // A Timestamp represents a point in time independent of any time zone or local 44 | // calendar, encoded as a count of seconds and fractions of seconds at 45 | // nanosecond resolution. The count is relative to an epoch at UTC midnight on 46 | // January 1, 1970, in the proleptic Gregorian calendar which extends the 47 | // Gregorian calendar backwards to year one. 48 | // 49 | // All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap 50 | // second table is needed for interpretation, using a [24-hour linear 51 | // smear](https://developers.google.com/time/smear). 52 | // 53 | // The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By 54 | // restricting to that range, we ensure that we can convert to and from [RFC 55 | // 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. 56 | // 57 | // # Examples 58 | // 59 | // Example 1: Compute Timestamp from POSIX `time()`. 60 | // 61 | // Timestamp timestamp; 62 | // timestamp.set_seconds(time(NULL)); 63 | // timestamp.set_nanos(0); 64 | // 65 | // Example 2: Compute Timestamp from POSIX `gettimeofday()`. 66 | // 67 | // struct timeval tv; 68 | // gettimeofday(&tv, NULL); 69 | // 70 | // Timestamp timestamp; 71 | // timestamp.set_seconds(tv.tv_sec); 72 | // timestamp.set_nanos(tv.tv_usec * 1000); 73 | // 74 | // Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. 75 | // 76 | // FILETIME ft; 77 | // GetSystemTimeAsFileTime(&ft); 78 | // UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; 79 | // 80 | // // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z 81 | // // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. 82 | // Timestamp timestamp; 83 | // timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); 84 | // timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); 85 | // 86 | // Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. 87 | // 88 | // long millis = System.currentTimeMillis(); 89 | // 90 | // Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) 91 | // .setNanos((int) ((millis % 1000) * 1000000)).build(); 92 | // 93 | // Example 5: Compute Timestamp from Java `Instant.now()`. 94 | // 95 | // Instant now = Instant.now(); 96 | // 97 | // Timestamp timestamp = 98 | // Timestamp.newBuilder().setSeconds(now.getEpochSecond()) 99 | // .setNanos(now.getNano()).build(); 100 | // 101 | // Example 6: Compute Timestamp from current time in Python. 102 | // 103 | // timestamp = Timestamp() 104 | // timestamp.GetCurrentTime() 105 | // 106 | // # JSON Mapping 107 | // 108 | // In JSON format, the Timestamp type is encoded as a string in the 109 | // [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the 110 | // format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" 111 | // where {year} is always expressed using four digits while {month}, {day}, 112 | // {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional 113 | // seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), 114 | // are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone 115 | // is required. A proto3 JSON serializer should always use UTC (as indicated by 116 | // "Z") when printing the Timestamp type and a proto3 JSON parser should be 117 | // able to accept both UTC and other timezones (as indicated by an offset). 118 | // 119 | // For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past 120 | // 01:30 UTC on January 15, 2017. 121 | // 122 | // In JavaScript, one can convert a Date object to this format using the 123 | // standard 124 | // [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) 125 | // method. In Python, a standard `datetime.datetime` object can be converted 126 | // to this format using 127 | // [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with 128 | // the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use 129 | // the Joda Time's [`ISODateTimeFormat.dateTime()`]( 130 | // http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime() 131 | // ) to obtain a formatter capable of generating timestamps in this format. 132 | // 133 | message Timestamp { 134 | // Represents seconds of UTC time since Unix epoch 135 | // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to 136 | // 9999-12-31T23:59:59Z inclusive. 137 | int64 seconds = 1; 138 | 139 | // Non-negative fractions of a second at nanosecond resolution. Negative 140 | // second values with fractions must still have non-negative nanos values 141 | // that count forward in time. Must be from 0 to 999,999,999 142 | // inclusive. 143 | int32 nanos = 2; 144 | } 145 | -------------------------------------------------------------------------------- /Sources/StremioCore/stremio: -------------------------------------------------------------------------------- 1 | ../../src/main/proto/stremio -------------------------------------------------------------------------------- /Sources/StremioCore/swift-protobuf-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocPath": "/$SRCROOT/Support/protoc", 3 | "invocations": [ 4 | { 5 | "protoFiles": [ 6 | "stremio/core/models/addon_details.proto", 7 | "stremio/core/models/addons_with_filters.proto", 8 | "stremio/core/models/catalog_with_filters.proto", 9 | "stremio/core/models/catalogs_with_extra.proto", 10 | "stremio/core/models/continue_watching_preview.proto", 11 | "stremio/core/models/ctx.proto", 12 | "stremio/core/models/library_by_type.proto", 13 | "stremio/core/models/library_with_filters.proto", 14 | "stremio/core/models/link.proto", 15 | "stremio/core/models/loadable.proto", 16 | "stremio/core/models/meta_details.proto", 17 | "stremio/core/models/player.proto", 18 | "stremio/core/models/streaming_server.proto", 19 | "stremio/core/runtime/action.proto", 20 | "stremio/core/runtime/action_catalog_with_filters.proto", 21 | "stremio/core/runtime/action_catalogs_with_extra.proto", 22 | "stremio/core/runtime/action_ctx.proto", 23 | "stremio/core/runtime/action_library_by_type.proto", 24 | "stremio/core/runtime/action_link.proto", 25 | "stremio/core/runtime/action_load.proto", 26 | "stremio/core/runtime/action_meta_details.proto", 27 | "stremio/core/runtime/action_player.proto", 28 | "stremio/core/runtime/action_streaming_server.proto", 29 | "stremio/core/runtime/event.proto", 30 | "stremio/core/runtime/field.proto", 31 | "stremio/core/runtime/runtime.proto", 32 | "stremio/core/types/addon.proto", 33 | "stremio/core/types/api.proto", 34 | "stremio/core/types/auth_request.proto", 35 | "stremio/core/types/library.proto", 36 | "stremio/core/types/manifest.proto", 37 | "stremio/core/types/meta_item.proto", 38 | "stremio/core/types/meta_item_preview.proto", 39 | "stremio/core/types/profile.proto", 40 | "stremio/core/types/stream.proto", 41 | "stremio/core/types/subtitle.proto", 42 | "stremio/core/types/video.proto" 43 | ], 44 | "visibility": "public" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /Sources/Wrapper/include/wrapper.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | void initialize_rust(void); 8 | 9 | NSObject *initializeNative(NSString *device_info); 10 | 11 | void dispatchNative(NSObject *action_protobuf); 12 | 13 | NSObject *getStateNative(int32_t field); 14 | 15 | NSObject *decodeStreamDataNative(NSString *field); 16 | 17 | void sendNextAnalyticsBatch(void); 18 | 19 | NSString *getVersionNative(void); 20 | 21 | void releaseObjectNative(NSObject *object); 22 | -------------------------------------------------------------------------------- /Sources/Wrapper/somec.c: -------------------------------------------------------------------------------- 1 | // intentionally empty 2 | -------------------------------------------------------------------------------- /Support/cbindgen.toml: -------------------------------------------------------------------------------- 1 | language = "C" 2 | sys_includes = ["Foundation/Foundation.h"] 3 | -------------------------------------------------------------------------------- /Support/installDependencies.command: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # make sure that you installed brew and rustup 3 | 4 | cargo install cargo-lipo 5 | rustup toolchain install nightly 6 | rustup target add aarch64-apple-ios 7 | rustup target add aarch64-apple-ios-sim 8 | rustup target add x86_64-apple-ios 9 | rustup component add rust-src --toolchain nightly-aarch64-apple-darwin 10 | brew install cbindgen 11 | -------------------------------------------------------------------------------- /Support/protoc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stremio/stremio-core-swift/900958d22cdc7423507e4cdde4a7a8ec6b89210c/Support/protoc -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use glob::glob; 2 | use prost_build::Config; 3 | use std::fs; 4 | 5 | fn main() { 6 | let proto_dir = "src/main/proto"; 7 | let proto_paths = glob(format!("{proto_dir}/**/*.proto").as_str()) 8 | .unwrap() 9 | .filter_map(Result::ok) 10 | .collect::>(); 11 | for path in &proto_paths { 12 | let display_path = path.display(); 13 | println!("cargo:rerun-if-changed={display_path}"); 14 | } 15 | let file_descriptors = 16 | protox::compile(proto_paths, [proto_dir]).expect("Expected file descriptors"); 17 | 18 | let out_dir = "src/commonMain/rust/protobuf"; 19 | 20 | fs::create_dir_all(out_dir).expect("Failed to create output directory"); 21 | 22 | Config::new() 23 | .compile_well_known_types() 24 | .out_dir(out_dir) 25 | .include_file("mod.rs") 26 | .compile_fds(file_descriptors) 27 | .expect("Expected successful protobuf codegen"); 28 | } -------------------------------------------------------------------------------- /src/commonMain/rust/bridge.rs: -------------------------------------------------------------------------------- 1 | //! [`ToProtobuf`] and [`FromProtobuf`] impls for various fields 2 | //! 3 | //! [`ToProtobuf`]: crate::bridge::ToProtobuf 4 | //! [`FromProtobuf`]: crate::bridge::FromProtobuf 5 | pub use to_protobuf::*; 6 | pub use from_protobuf::*; 7 | 8 | mod action; 9 | mod apple_model_field; 10 | mod auth_request; 11 | mod date; 12 | mod env_error; 13 | mod event; 14 | mod events; 15 | mod extra_value; 16 | mod library_item; 17 | mod link; 18 | mod list; 19 | mod loadable; 20 | mod manifest; 21 | mod meta_preview; 22 | mod option; 23 | mod pair; 24 | mod poster_shape; 25 | mod profile; 26 | mod resource_loadable; 27 | mod resource_path; 28 | mod resource_request; 29 | mod stream; 30 | mod string; 31 | mod subtitle; 32 | mod to_protobuf; 33 | mod from_protobuf; 34 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/apple_model_field.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::{FromProtobuf, ToProtobuf}; 2 | use crate::model::AppleModelField; 3 | use crate::protobuf::stremio::core::runtime::Field; 4 | 5 | impl FromProtobuf for Field { 6 | fn from_protobuf(&self) -> AppleModelField { 7 | match self { 8 | Field::Ctx => AppleModelField::Ctx, 9 | Field::AuthLink => AppleModelField::AuthLink, 10 | Field::ContinueWatchingPreview => AppleModelField::ContinueWatchingPreview, 11 | Field::Discover => AppleModelField::Discover, 12 | Field::Library => AppleModelField::Library, 13 | Field::LibraryByType => AppleModelField::LibraryByType, 14 | Field::Board => AppleModelField::Board, 15 | Field::Search => AppleModelField::Search, 16 | Field::MetaDetails => AppleModelField::MetaDetails, 17 | Field::Addons => AppleModelField::Addons, 18 | Field::AddonDetails => AppleModelField::AddonDetails, 19 | Field::StreamingServer => AppleModelField::StreamingServer, 20 | Field::Player => AppleModelField::Player, 21 | } 22 | } 23 | } 24 | 25 | impl ToProtobuf for AppleModelField { 26 | fn to_protobuf(&self, _args: &()) -> Field { 27 | match self { 28 | AppleModelField::Ctx => Field::Ctx, 29 | AppleModelField::AuthLink => Field::AuthLink, 30 | AppleModelField::ContinueWatchingPreview => Field::ContinueWatchingPreview, 31 | AppleModelField::Discover => Field::Discover, 32 | AppleModelField::Library => Field::Library, 33 | AppleModelField::LibraryByType => Field::LibraryByType, 34 | AppleModelField::Board => Field::Board, 35 | AppleModelField::Search => Field::Search, 36 | AppleModelField::MetaDetails => Field::MetaDetails, 37 | AppleModelField::Addons => Field::Addons, 38 | AppleModelField::AddonDetails => Field::AddonDetails, 39 | AppleModelField::StreamingServer => Field::StreamingServer, 40 | AppleModelField::Player => Field::Player, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/auth_request.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::types::api::AuthRequest; 2 | 3 | use crate::bridge::{FromProtobuf, ToProtobuf}; 4 | use crate::protobuf::stremio::core::types; 5 | 6 | impl FromProtobuf for types::AuthRequest { 7 | fn from_protobuf(&self) -> AuthRequest { 8 | match &self.r#type { 9 | Some(types::auth_request::Type::Login(login)) => AuthRequest::Login { 10 | email: login.email.to_owned(), 11 | password: login.password.to_owned(), 12 | facebook: login.facebook.to_owned(), 13 | }, 14 | Some(types::auth_request::Type::LoginWithToken(login_with_token)) => { 15 | AuthRequest::LoginWithToken { 16 | token: login_with_token.token.to_owned(), 17 | } 18 | } 19 | Some(types::auth_request::Type::Facebook(facebook)) => AuthRequest::Facebook { 20 | token: facebook.token.to_owned(), 21 | }, 22 | Some(types::auth_request::Type::Register(register)) => AuthRequest::Register { 23 | email: register.email.to_owned(), 24 | password: register.password.to_owned(), 25 | gdpr_consent: register.gdpr_consent.from_protobuf(), 26 | }, 27 | None => unimplemented!("AuthRequest must be present"), 28 | } 29 | } 30 | } 31 | 32 | impl ToProtobuf for AuthRequest { 33 | fn to_protobuf(&self, _args: &()) -> types::AuthRequest { 34 | let request = match self { 35 | AuthRequest::Login { 36 | email, 37 | password, 38 | facebook, 39 | } => types::auth_request::Type::Login(types::auth_request::Login { 40 | email: email.to_owned(), 41 | password: password.to_owned(), 42 | facebook: facebook.to_owned(), 43 | }), 44 | AuthRequest::LoginWithToken { token } => { 45 | types::auth_request::Type::LoginWithToken(types::auth_request::LoginWithToken { 46 | token: token.to_owned(), 47 | }) 48 | } 49 | AuthRequest::Facebook { token } => { 50 | types::auth_request::Type::Facebook(types::auth_request::Facebook { 51 | token: token.to_owned(), 52 | }) 53 | } 54 | AuthRequest::Register { 55 | email, 56 | password, 57 | gdpr_consent, 58 | } => types::auth_request::Type::Register(types::auth_request::Register { 59 | email: email.to_owned(), 60 | password: password.to_owned(), 61 | gdpr_consent: gdpr_consent.to_protobuf(&()), 62 | }), 63 | }; 64 | types::AuthRequest { 65 | r#type: Some(request), 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/date.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, TimeZone, Utc}; 2 | 3 | use crate::bridge::{FromProtobuf, ToProtobuf}; 4 | use crate::protobuf::google::protobuf::Timestamp; 5 | 6 | impl FromProtobuf> for Timestamp { 7 | fn from_protobuf(&self) -> DateTime { 8 | Utc.timestamp_opt(self.seconds, self.nanos as u32).unwrap() 9 | } 10 | } 11 | 12 | impl ToProtobuf for DateTime { 13 | fn to_protobuf(&self, _args: &()) -> Timestamp { 14 | Timestamp { 15 | seconds: self.timestamp(), 16 | nanos: self.timestamp_subsec_nanos() as i32, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/env_error.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::ToProtobuf; 2 | use crate::protobuf::stremio::core::runtime; 3 | use stremio_core::runtime::EnvError; 4 | 5 | impl ToProtobuf for EnvError { 6 | fn to_protobuf(&self, _args: &()) -> runtime::EnvError { 7 | runtime::EnvError { 8 | code: self.code() as i32, 9 | message: self.message(), 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/events.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::types::api::{GetModalResponse, GetNotificationResponse}; 2 | use stremio_core::types::events::Events; 3 | 4 | use crate::bridge::ToProtobuf; 5 | use crate::protobuf::stremio::core::models; 6 | 7 | impl ToProtobuf for Events { 8 | fn to_protobuf(&self, _args: &()) -> models::Events { 9 | models::Events { 10 | modal: self.modal.to_protobuf(&()), 11 | notification: self.notification.to_protobuf(&()), 12 | } 13 | } 14 | } 15 | 16 | impl ToProtobuf for GetModalResponse { 17 | fn to_protobuf(&self, _args: &()) -> models::EventModal { 18 | models::EventModal { 19 | id: self.id.to_owned(), 20 | title: self.title.to_owned(), 21 | message: self.message.to_owned(), 22 | image_url: self.image_url.to_protobuf(&()), 23 | addon: self 24 | .addon 25 | .as_ref() 26 | .map(|addon| models::event_modal::ModalAddon { 27 | manifest_url: addon.manifest_url.to_protobuf(&()), 28 | name: addon.name.to_owned(), 29 | }), 30 | external_url: self.external_url.to_protobuf(&()), 31 | } 32 | } 33 | } 34 | 35 | impl ToProtobuf for GetNotificationResponse { 36 | fn to_protobuf(&self, _args: &()) -> models::EventNotification { 37 | models::EventNotification { 38 | id: self.id.to_owned(), 39 | title: self.title.to_owned(), 40 | message: self.message.to_owned(), 41 | external_url: self.external_url.to_protobuf(&()), 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/extra_value.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::types::addon::ExtraValue; 2 | 3 | use crate::bridge::{FromProtobuf, ToProtobuf}; 4 | use crate::protobuf::stremio::core::types; 5 | 6 | impl FromProtobuf for types::ExtraValue { 7 | fn from_protobuf(&self) -> ExtraValue { 8 | ExtraValue { 9 | name: self.name.to_owned(), 10 | value: self.value.to_owned(), 11 | } 12 | } 13 | } 14 | 15 | impl ToProtobuf for ExtraValue { 16 | fn to_protobuf(&self, _args: &()) -> types::ExtraValue { 17 | types::ExtraValue { 18 | name: self.name.to_owned(), 19 | value: self.value.to_owned(), 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/from_protobuf.rs: -------------------------------------------------------------------------------- 1 | pub trait FromProtobuf { 2 | #[allow(clippy::wrong_self_convention)] 3 | fn from_protobuf(&self) -> T; 4 | } 5 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/library_item.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::deep_links::LibraryItemDeepLinks; 2 | use stremio_core::models::ctx::Ctx; 3 | use stremio_core::types::library::LibraryItem; 4 | 5 | use crate::bridge::ToProtobuf; 6 | use crate::protobuf::stremio::core::types; 7 | 8 | impl ToProtobuf)> for LibraryItem { 9 | fn to_protobuf( 10 | &self, 11 | (ctx, maybe_notifications): &(&Ctx, Option), 12 | ) -> types::LibraryItem { 13 | let notifications = maybe_notifications 14 | .or_else(|| { 15 | ctx.notifications 16 | .items 17 | .get(&self.id) 18 | .map(|notifs| notifs.len()) 19 | }) 20 | .unwrap_or_default(); 21 | let settings = &ctx.profile.settings; 22 | let streaming_server_url = &settings.streaming_server_url; 23 | let deep_links = 24 | LibraryItemDeepLinks::from((self, None, Some(streaming_server_url), settings)); 25 | types::LibraryItem { 26 | id: self.id.to_string(), 27 | r#type: self.r#type.to_string(), 28 | name: self.name.to_string(), 29 | poster: self.poster.to_protobuf(&()), 30 | poster_shape: self.poster_shape.to_protobuf(&()) as i32, 31 | state: types::LibraryItemState { 32 | time_offset: self.state.time_offset, 33 | duration: self.state.duration, 34 | video_id: self.state.video_id.clone(), 35 | no_notif: self.state.no_notif, 36 | }, 37 | behavior_hints: self.behavior_hints.to_protobuf(&()), 38 | deep_links: types::MetaItemDeepLinks { 39 | meta_details_videos: deep_links.meta_details_videos, 40 | meta_details_streams: deep_links.meta_details_streams, 41 | player: deep_links.player, 42 | }, 43 | progress: self.progress(), 44 | watched: self.state.times_watched > 0, 45 | notifications: notifications as u64, 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/link.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::types::resource::Link; 2 | 3 | use crate::bridge::ToProtobuf; 4 | use crate::protobuf::stremio::core::types; 5 | 6 | impl ToProtobuf for Link { 7 | fn to_protobuf(&self, _args: &()) -> types::LinkPreview { 8 | types::LinkPreview { 9 | name: self.name.to_string(), 10 | category: self.category.to_string(), 11 | } 12 | } 13 | } 14 | 15 | impl ToProtobuf for Link { 16 | fn to_protobuf(&self, _args: &()) -> types::Link { 17 | types::Link { 18 | name: self.name.to_string(), 19 | category: self.category.to_string(), 20 | url: self.url.to_string(), 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/list.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::{FromProtobuf, ToProtobuf}; 2 | 3 | impl, U> FromProtobuf> for Vec { 4 | fn from_protobuf(&self) -> Vec { 5 | self.iter().map(|item| item.from_protobuf()).collect() 6 | } 7 | } 8 | 9 | impl, U, A> ToProtobuf, A> for Vec { 10 | fn to_protobuf(&self, args: &A) -> Vec { 11 | self.iter().map(|item| item.to_protobuf(args)).collect() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/meta_preview.rs: -------------------------------------------------------------------------------- 1 | use chrono::Duration; 2 | use stremio_core::deep_links::MetaItemDeepLinks; 3 | use stremio_core::models::ctx::Ctx; 4 | use stremio_core::runtime::Env; 5 | use stremio_core::types::addon::ResourceRequest; 6 | use stremio_core::types::resource::{MetaItemBehaviorHints, MetaItemPreview, PosterShape}; 7 | 8 | use crate::bridge::{FromProtobuf, ToProtobuf}; 9 | use crate::env::AppleEnv; 10 | use crate::protobuf::stremio::core::types; 11 | 12 | impl FromProtobuf for types::MetaItemBehaviorHints { 13 | fn from_protobuf(&self) -> MetaItemBehaviorHints { 14 | MetaItemBehaviorHints { 15 | default_video_id: self.default_video_id.to_owned(), 16 | featured_video_id: self.featured_video_id.to_owned(), 17 | has_scheduled_videos: self.has_scheduled_videos, 18 | other: Default::default(), 19 | } 20 | } 21 | } 22 | 23 | impl FromProtobuf for types::MetaItemPreview { 24 | fn from_protobuf(&self) -> MetaItemPreview { 25 | MetaItemPreview { 26 | id: self.id.to_owned(), 27 | r#type: self.r#type.to_owned(), 28 | name: self.name.to_owned(), 29 | poster_shape: types::PosterShape::try_from(self.poster_shape) 30 | .ok() 31 | .from_protobuf() 32 | .unwrap_or(PosterShape::Poster), 33 | poster: self.poster.from_protobuf(), 34 | background: self.background.from_protobuf(), 35 | logo: self.logo.from_protobuf(), 36 | description: self.description.to_owned(), 37 | release_info: self.release_info.to_owned(), 38 | runtime: self.runtime.to_owned(), 39 | released: self.released.from_protobuf(), 40 | links: Default::default(), 41 | trailer_streams: Default::default(), 42 | behavior_hints: self.behavior_hints.from_protobuf(), 43 | } 44 | } 45 | } 46 | 47 | impl ToProtobuf for MetaItemBehaviorHints { 48 | fn to_protobuf(&self, _args: &()) -> types::MetaItemBehaviorHints { 49 | types::MetaItemBehaviorHints { 50 | default_video_id: self.default_video_id.clone(), 51 | featured_video_id: self.featured_video_id.clone(), 52 | has_scheduled_videos: self.has_scheduled_videos, 53 | } 54 | } 55 | } 56 | 57 | impl ToProtobuf for MetaItemDeepLinks { 58 | fn to_protobuf(&self, _args: &()) -> types::MetaItemDeepLinks { 59 | types::MetaItemDeepLinks { 60 | meta_details_videos: self.meta_details_videos.clone(), 61 | meta_details_streams: self.meta_details_streams.clone(), 62 | player: self.player.clone(), 63 | } 64 | } 65 | } 66 | 67 | impl ToProtobuf for MetaItemPreview { 68 | fn to_protobuf( 69 | &self, 70 | (ctx, meta_request): &(&Ctx, &ResourceRequest), 71 | ) -> types::MetaItemPreview { 72 | types::MetaItemPreview { 73 | id: self.id.to_string(), 74 | r#type: self.r#type.to_string(), 75 | name: self.name.to_string(), 76 | poster_shape: self.poster_shape.to_protobuf(&()) as i32, 77 | poster: self.poster.to_protobuf(&()), 78 | background: self.background.to_protobuf(&()), 79 | logo: self.logo.to_protobuf(&()), 80 | description: self.description.clone(), 81 | release_info: self.release_info.clone(), 82 | runtime: self.runtime.clone(), 83 | released: self.released.to_protobuf(&()), 84 | links: self.links.to_protobuf(&()), 85 | behavior_hints: self.behavior_hints.to_protobuf(&()), 86 | deep_links: MetaItemDeepLinks::from((self, *meta_request)).to_protobuf(&()), 87 | in_library: ctx 88 | .library 89 | .items 90 | .get(&self.id) 91 | .map(|library_item| !library_item.removed) 92 | .unwrap_or_default(), 93 | in_cinema: self 94 | .released 95 | .filter(|_released| self.r#type == "movie") 96 | .map(|released| released + Duration::days(30) > AppleEnv::now()) 97 | .unwrap_or_default(), 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/option.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::{FromProtobuf, ToProtobuf}; 2 | 3 | impl, U> FromProtobuf> for Option { 4 | fn from_protobuf(&self) -> Option { 5 | self.as_ref().map(|item| item.from_protobuf()) 6 | } 7 | } 8 | 9 | impl, U, A> ToProtobuf, A> for Option { 10 | fn to_protobuf(&self, args: &A) -> Option { 11 | self.as_ref().map(|item| item.to_protobuf(args)) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/pair.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::ToProtobuf; 2 | use crate::protobuf::stremio::core::runtime; 3 | 4 | impl ToProtobuf for (Vec, Vec) { 5 | fn to_protobuf(&self, _args: &()) -> runtime::PlanPair { 6 | runtime::PlanPair { 7 | first: self.0.clone(), 8 | second: self.1.clone(), 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/poster_shape.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::types::resource::PosterShape; 2 | 3 | use crate::bridge::{FromProtobuf, ToProtobuf}; 4 | use crate::protobuf::stremio::core::types; 5 | 6 | impl FromProtobuf for types::PosterShape { 7 | fn from_protobuf(&self) -> PosterShape { 8 | match self { 9 | types::PosterShape::Poster => PosterShape::Poster, 10 | types::PosterShape::Landscape => PosterShape::Landscape, 11 | types::PosterShape::Square => PosterShape::Square, 12 | } 13 | } 14 | } 15 | 16 | impl ToProtobuf for PosterShape { 17 | fn to_protobuf(&self, _args: &()) -> types::PosterShape { 18 | match self { 19 | PosterShape::Poster => types::PosterShape::Poster, 20 | PosterShape::Landscape => types::PosterShape::Landscape, 21 | PosterShape::Square => types::PosterShape::Square, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/profile.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use stremio_core::types::api::{LinkAuthKey, LinkCodeResponse}; 4 | use stremio_core::types::profile::{ 5 | Auth, FrameRateMatchingStrategy, GDPRConsent, Profile, Settings, User, 6 | }; 7 | 8 | use crate::bridge::{FromProtobuf, ToProtobuf}; 9 | use crate::protobuf::stremio::core::types; 10 | 11 | impl FromProtobuf for types::profile::FrameRateMatchingStrategy { 12 | fn from_protobuf(&self) -> FrameRateMatchingStrategy { 13 | match self { 14 | types::profile::FrameRateMatchingStrategy::Disabled => { 15 | FrameRateMatchingStrategy::Disabled 16 | } 17 | types::profile::FrameRateMatchingStrategy::FrameRateOnly => { 18 | FrameRateMatchingStrategy::FrameRateOnly 19 | } 20 | types::profile::FrameRateMatchingStrategy::FrameRateAndResolution => { 21 | FrameRateMatchingStrategy::FrameRateAndResolution 22 | } 23 | } 24 | } 25 | } 26 | 27 | impl ToProtobuf for FrameRateMatchingStrategy { 28 | fn to_protobuf(&self, _args: &()) -> types::profile::FrameRateMatchingStrategy { 29 | match self { 30 | FrameRateMatchingStrategy::Disabled => { 31 | types::profile::FrameRateMatchingStrategy::Disabled 32 | } 33 | FrameRateMatchingStrategy::FrameRateOnly => { 34 | types::profile::FrameRateMatchingStrategy::FrameRateOnly 35 | } 36 | FrameRateMatchingStrategy::FrameRateAndResolution => { 37 | types::profile::FrameRateMatchingStrategy::FrameRateAndResolution 38 | } 39 | } 40 | } 41 | } 42 | 43 | impl FromProtobuf for types::GdprConsent { 44 | fn from_protobuf(&self) -> GDPRConsent { 45 | GDPRConsent { 46 | tos: self.tos, 47 | privacy: self.privacy, 48 | marketing: self.marketing, 49 | from: self.from.clone(), 50 | } 51 | } 52 | } 53 | 54 | impl FromProtobuf for types::profile::Settings { 55 | fn from_protobuf(&self) -> Settings { 56 | Settings { 57 | interface_language: self.interface_language.to_string(), 58 | streaming_server_url: self.streaming_server_url.from_protobuf(), 59 | player_type: self.player_type.clone(), 60 | binge_watching: self.binge_watching, 61 | play_in_background: self.play_in_background, 62 | hardware_decoding: self.hardware_decoding, 63 | frame_rate_matching_strategy: types::profile::FrameRateMatchingStrategy::try_from( 64 | self.frame_rate_matching_strategy, 65 | ) 66 | .ok() 67 | .from_protobuf() 68 | .unwrap_or(FrameRateMatchingStrategy::Disabled), 69 | next_video_notification_duration: u32::try_from(cmp::max( 70 | self.next_video_notification_duration, 71 | 0, 72 | )) 73 | .unwrap_or(u32::MAX), 74 | audio_passthrough: self.audio_passthrough, 75 | audio_language: Some(self.audio_language.to_string()), 76 | secondary_audio_language: self.secondary_audio_language.clone(), 77 | subtitles_language: Some(self.subtitles_language.to_string()), 78 | secondary_subtitles_language: self.secondary_subtitles_language.clone(), 79 | subtitles_size: u8::try_from(cmp::max(self.subtitles_size, 0)).unwrap_or(u8::MAX), 80 | subtitles_font: self.subtitles_font.to_string(), 81 | subtitles_bold: self.subtitles_bold, 82 | subtitles_offset: u8::try_from(cmp::max(self.subtitles_offset, 0)).unwrap_or(u8::MAX), 83 | subtitles_text_color: self.subtitles_text_color.to_string(), 84 | subtitles_background_color: self.subtitles_background_color.to_string(), 85 | subtitles_outline_color: self.subtitles_outline_color.to_string(), 86 | subtitles_opacity: u8::try_from(cmp::max(self.subtitles_opacity, 0)).unwrap_or(u8::MAX), 87 | esc_exit_fullscreen: self.esc_exit_fullscreen, 88 | seek_time_duration: u32::try_from(cmp::max(self.seek_time_duration, 0)) 89 | .unwrap_or(u32::MAX), 90 | seek_short_time_duration: u32::try_from(cmp::max(self.seek_time_duration, 0)) 91 | .unwrap_or(u32::MAX), 92 | pause_on_minimize: self.pause_on_minimize, 93 | surround_sound: self.surround_sound, 94 | streaming_server_warning_dismissed: None, 95 | } 96 | } 97 | } 98 | 99 | impl ToProtobuf for LinkAuthKey { 100 | fn to_protobuf(&self, _args: &()) -> types::LinkAuthKey { 101 | types::LinkAuthKey { 102 | auth_key: self.auth_key.to_string(), 103 | } 104 | } 105 | } 106 | 107 | impl ToProtobuf for LinkCodeResponse { 108 | fn to_protobuf(&self, _args: &()) -> types::LinkCodeResponse { 109 | types::LinkCodeResponse { 110 | code: self.code.to_string(), 111 | link: self.link.to_string(), 112 | qrcode: self.qrcode.to_string(), 113 | } 114 | } 115 | } 116 | 117 | impl ToProtobuf for GDPRConsent { 118 | fn to_protobuf(&self, _args: &()) -> types::GdprConsent { 119 | types::GdprConsent { 120 | tos: self.tos, 121 | privacy: self.privacy, 122 | marketing: self.marketing, 123 | from: self.from.clone(), 124 | } 125 | } 126 | } 127 | 128 | impl ToProtobuf for User { 129 | fn to_protobuf(&self, _args: &()) -> types::User { 130 | types::User { 131 | id: self.id.to_string(), 132 | email: self.email.to_string(), 133 | fb_id: self.fb_id.clone(), 134 | avatar: self.avatar.clone(), 135 | gdpr_consent: self.gdpr_consent.to_protobuf(&()), 136 | date_registered: self.date_registered.to_protobuf(&()), 137 | last_modified: self.last_modified.to_protobuf(&()), 138 | premium_expire: self.premium_expire.to_protobuf(&()), 139 | } 140 | } 141 | } 142 | 143 | impl ToProtobuf for Auth { 144 | fn to_protobuf(&self, _args: &()) -> types::Auth { 145 | types::Auth { 146 | key: self.key.0.to_string(), 147 | user: self.user.to_protobuf(&()), 148 | } 149 | } 150 | } 151 | 152 | impl ToProtobuf for Settings { 153 | fn to_protobuf(&self, _args: &()) -> types::profile::Settings { 154 | types::profile::Settings { 155 | interface_language: self.interface_language.to_string(), 156 | streaming_server_url: self.streaming_server_url.to_string(), 157 | binge_watching: self.binge_watching, 158 | play_in_background: self.play_in_background, 159 | hardware_decoding: self.hardware_decoding, 160 | audio_passthrough: self.audio_passthrough, 161 | audio_language: self.audio_language.clone().unwrap_or_default(), 162 | subtitles_language: self.subtitles_language.clone().unwrap_or_default(), 163 | subtitles_size: self.subtitles_size as i32, 164 | subtitles_font: self.subtitles_font.to_string(), 165 | subtitles_bold: self.subtitles_bold, 166 | subtitles_offset: self.subtitles_offset as i32, 167 | subtitles_text_color: self.subtitles_text_color.to_string(), 168 | subtitles_background_color: self.subtitles_background_color.to_string(), 169 | subtitles_outline_color: self.subtitles_outline_color.to_string(), 170 | subtitles_opacity: self.subtitles_opacity as i32, 171 | esc_exit_fullscreen: self.esc_exit_fullscreen, 172 | seek_time_duration: self.seek_time_duration as i64, 173 | seek_short_time_duration: self.seek_short_time_duration as i64, 174 | pause_on_minimize: self.pause_on_minimize, 175 | secondary_audio_language: self.secondary_audio_language.clone(), 176 | secondary_subtitles_language: self.secondary_subtitles_language.clone(), 177 | player_type: self.player_type.clone(), 178 | frame_rate_matching_strategy: self.frame_rate_matching_strategy.to_protobuf(&()) as i32, 179 | next_video_notification_duration: self.next_video_notification_duration as i64, 180 | surround_sound: self.surround_sound, 181 | } 182 | } 183 | } 184 | 185 | impl ToProtobuf for Profile { 186 | fn to_protobuf(&self, _args: &()) -> types::Profile { 187 | types::Profile { 188 | auth: self.auth.to_protobuf(&()), 189 | settings: self.settings.to_protobuf(&()), 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/resource_loadable.rs: -------------------------------------------------------------------------------- 1 | use inflector::Inflector; 2 | use stremio_core::deep_links::DiscoverDeepLinks; 3 | use stremio_core::models::common::{DescriptorLoadable, ResourceLoadable}; 4 | use stremio_core::models::ctx::Ctx; 5 | use stremio_core::types::addon::{DescriptorPreview, ResourceRequest}; 6 | use stremio_core::types::library::LibraryItem; 7 | use stremio_core::types::resource::{MetaItem, MetaItemPreview, Stream, Subtitles}; 8 | use stremio_watched_bitfield::WatchedBitField; 9 | use url::Url; 10 | 11 | use crate::bridge::ToProtobuf; 12 | use crate::protobuf::stremio::core::models; 13 | 14 | impl ToProtobuf for ResourceLoadable> { 15 | fn to_protobuf(&self, ctx: &Ctx) -> models::LoadablePage { 16 | let title = ctx 17 | .profile 18 | .addons 19 | .iter() 20 | .find(|addon| addon.transport_url == self.request.base) 21 | .and_then(|addon| { 22 | addon 23 | .manifest 24 | .catalogs 25 | .iter() 26 | .find(|manifest_catalog| { 27 | manifest_catalog.id == self.request.path.id 28 | && manifest_catalog.r#type == self.request.path.r#type 29 | }) 30 | .map(|manifest_catalog| (addon, manifest_catalog)) 31 | }) 32 | .map(|(addon, manifest_catalog)| { 33 | format!( 34 | "{} - {}", 35 | &manifest_catalog 36 | .name 37 | .as_ref() 38 | .unwrap_or(&addon.manifest.name) 39 | .to_title_case(), 40 | &manifest_catalog.r#type.to_title_case(), 41 | ) 42 | }) 43 | .unwrap_or_default(); 44 | let deep_links = DiscoverDeepLinks::from(&self.request).to_protobuf(&()); 45 | models::LoadablePage { 46 | title, 47 | request: self.request.to_protobuf(&()), 48 | content: self.content.to_protobuf(&(ctx, &self.request)), 49 | deep_links, 50 | } 51 | } 52 | } 53 | 54 | impl ToProtobuf, Option<&WatchedBitField>)> 55 | for &ResourceLoadable 56 | { 57 | fn to_protobuf( 58 | &self, 59 | (ctx, library_item, watched): &(&Ctx, Option<&LibraryItem>, Option<&WatchedBitField>), 60 | ) -> models::LoadableMetaItem { 61 | let addon_name = get_addon_name(ctx, &self.request.base); 62 | models::LoadableMetaItem { 63 | title: addon_name.to_string(), 64 | request: self.request.to_protobuf(&()), 65 | content: self.content.to_protobuf(&( 66 | *library_item, 67 | *watched, 68 | Some(&addon_name), 69 | &self.request, 70 | )), 71 | } 72 | } 73 | } 74 | 75 | impl ToProtobuf)> 76 | for ResourceLoadable> 77 | { 78 | fn to_protobuf( 79 | &self, 80 | (ctx, meta_request): &(&Ctx, Option<&ResourceRequest>), 81 | ) -> models::LoadableStreams { 82 | let addon_name = get_addon_name(ctx, &self.request.base); 83 | models::LoadableStreams { 84 | title: addon_name.to_owned(), 85 | request: self.request.to_protobuf(&()), 86 | content: self 87 | .content 88 | .to_protobuf(&(ctx, &addon_name, &self.request, *meta_request)), 89 | } 90 | } 91 | } 92 | 93 | impl ToProtobuf)> 94 | for ResourceLoadable> 95 | { 96 | fn to_protobuf( 97 | &self, 98 | (ctx, meta_request): &(&Ctx, Option<&ResourceRequest>), 99 | ) -> models::LoadableStream { 100 | let addon_name = get_addon_name(ctx, &self.request.base); 101 | models::LoadableStream { 102 | request: self.request.to_protobuf(&()), 103 | content: self 104 | .content 105 | .to_protobuf(&(ctx, &addon_name, &self.request, *meta_request)), 106 | } 107 | } 108 | } 109 | 110 | impl ToProtobuf for ResourceLoadable> { 111 | fn to_protobuf(&self, ctx: &Ctx) -> models::LoadableSubtitles { 112 | let addon_name = get_addon_name(ctx, &self.request.base); 113 | models::LoadableSubtitles { 114 | title: addon_name.to_owned(), 115 | request: self.request.to_protobuf(&()), 116 | content: self.content.to_protobuf(&(Some(&addon_name))), 117 | } 118 | } 119 | } 120 | 121 | impl ToProtobuf for &ResourceLoadable> { 122 | fn to_protobuf(&self, ctx: &Ctx) -> models::LoadableAddonCatalog { 123 | models::LoadableAddonCatalog { 124 | request: self.request.to_protobuf(&()), 125 | content: self.content.to_protobuf(ctx), 126 | } 127 | } 128 | } 129 | 130 | impl ToProtobuf for DescriptorLoadable { 131 | fn to_protobuf(&self, ctx: &Ctx) -> models::LoadableDescriptor { 132 | models::LoadableDescriptor { 133 | transport_url: self.transport_url.to_string(), 134 | content: Some(self.content.to_protobuf(ctx)), 135 | } 136 | } 137 | } 138 | 139 | fn get_addon_name(ctx: &Ctx, addon_url: &Url) -> String { 140 | ctx.profile 141 | .addons 142 | .iter() 143 | .find(|addon| &addon.transport_url == addon_url) 144 | .map(|addon| &addon.manifest.name) 145 | .cloned() 146 | .unwrap_or_default() 147 | } 148 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/resource_path.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::types::addon::ResourcePath; 2 | 3 | use crate::bridge::{FromProtobuf, ToProtobuf}; 4 | use crate::protobuf::stremio::core::types; 5 | 6 | impl FromProtobuf for types::ResourcePath { 7 | fn from_protobuf(&self) -> ResourcePath { 8 | ResourcePath { 9 | resource: self.resource.to_owned(), 10 | r#type: self.r#type.to_owned(), 11 | id: self.id.to_owned(), 12 | extra: self.extra.from_protobuf(), 13 | } 14 | } 15 | } 16 | 17 | impl ToProtobuf for ResourcePath { 18 | fn to_protobuf(&self, _args: &()) -> types::ResourcePath { 19 | types::ResourcePath { 20 | resource: self.resource.to_owned(), 21 | r#type: self.r#type.to_owned(), 22 | id: self.id.to_owned(), 23 | extra: self.extra.to_protobuf(&()), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/resource_request.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::models::installed_addons_with_filters::InstalledAddonsRequest; 2 | use stremio_core::types::addon::ResourceRequest; 3 | 4 | use crate::bridge::{FromProtobuf, ToProtobuf}; 5 | use crate::protobuf::stremio::core::types; 6 | 7 | impl FromProtobuf for types::ResourceRequest { 8 | fn from_protobuf(&self) -> ResourceRequest { 9 | ResourceRequest { 10 | base: self.base.from_protobuf(), 11 | path: self.path.from_protobuf(), 12 | } 13 | } 14 | } 15 | 16 | impl FromProtobuf for types::ResourceRequest { 17 | fn from_protobuf(&self) -> InstalledAddonsRequest { 18 | InstalledAddonsRequest { 19 | r#type: Some(self.path.r#type.to_string()).filter(|s| !s.is_empty()), 20 | } 21 | } 22 | } 23 | 24 | impl ToProtobuf for ResourceRequest { 25 | fn to_protobuf(&self, _args: &()) -> types::ResourceRequest { 26 | types::ResourceRequest { 27 | base: self.base.to_string(), 28 | path: self.path.to_protobuf(&()), 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/stream.rs: -------------------------------------------------------------------------------- 1 | use hex::FromHex; 2 | use stremio_core::deep_links::StreamDeepLinks; 3 | use stremio_core::models::ctx::Ctx; 4 | use stremio_core::types::addon::ResourceRequest; 5 | use stremio_core::types::resource::{ 6 | Stream, StreamBehaviorHints, StreamProxyHeaders, StreamSource, 7 | }; 8 | 9 | use crate::bridge::{FromProtobuf, ToProtobuf}; 10 | use crate::protobuf::stremio::core::types; 11 | 12 | impl FromProtobuf for types::stream::Source { 13 | fn from_protobuf(&self) -> StreamSource { 14 | match self { 15 | types::stream::Source::Url(source) => StreamSource::Url { 16 | url: source.url.from_protobuf(), 17 | }, 18 | types::stream::Source::YouTube(source) => StreamSource::YouTube { 19 | yt_id: source.yt_id.to_owned(), 20 | }, 21 | types::stream::Source::Torrent(source) => StreamSource::Torrent { 22 | info_hash: <[u8; 20]>::from_hex(source.info_hash.as_str()) 23 | .expect("Stream.info_hash parse failed"), 24 | file_idx: source.file_idx.map(|idx| idx as u16), 25 | announce: source.announce.clone(), 26 | file_must_include: source.file_must_include.to_owned(), 27 | }, 28 | types::stream::Source::External(source) => StreamSource::External { 29 | external_url: source.external_url.from_protobuf(), 30 | android_tv_url: source.android_tv_url.from_protobuf(), 31 | tizen_url: None, 32 | webos_url: None, 33 | }, 34 | types::stream::Source::PlayerFrame(source) => StreamSource::PlayerFrame { 35 | player_frame_url: source.player_frame_url.from_protobuf(), 36 | }, 37 | types::stream::Source::Rar(source) => StreamSource::Rar { 38 | rar_urls: source.rar_urls.from_protobuf(), 39 | file_idx: source.file_idx.map(|idx| idx as u16), 40 | file_must_include: source.file_must_include.to_owned(), 41 | }, 42 | types::stream::Source::Zip(source) => StreamSource::Zip { 43 | zip_urls: source.zip_urls.from_protobuf(), 44 | file_idx: source.file_idx.map(|idx| idx as u16), 45 | file_must_include: source.file_must_include.to_owned(), 46 | }, 47 | } 48 | } 49 | } 50 | 51 | impl FromProtobuf for types::StreamProxyHeaders { 52 | fn from_protobuf(&self) -> StreamProxyHeaders { 53 | StreamProxyHeaders { 54 | request: self.request.to_owned(), 55 | response: self.response.to_owned(), 56 | } 57 | } 58 | } 59 | 60 | impl FromProtobuf for types::Stream { 61 | fn from_protobuf(&self) -> Stream { 62 | Stream { 63 | source: self.source.from_protobuf().unwrap(), 64 | name: self.name.to_owned(), 65 | description: self.description.to_owned(), 66 | thumbnail: self.thumbnail.to_owned(), 67 | subtitles: self.subtitles.from_protobuf(), 68 | behavior_hints: StreamBehaviorHints { 69 | not_web_ready: self.behavior_hints.not_web_ready, 70 | binge_group: self.behavior_hints.binge_group.to_owned(), 71 | country_whitelist: Some(self.behavior_hints.country_whitelist.to_owned()), 72 | proxy_headers: self.behavior_hints.proxy_headers.from_protobuf(), 73 | filename: self.behavior_hints.filename.to_owned(), 74 | video_hash: self.behavior_hints.video_hash.to_owned(), 75 | video_size: self.behavior_hints.video_size, 76 | other: Default::default(), 77 | }, 78 | } 79 | } 80 | } 81 | 82 | impl ToProtobuf for StreamSource { 83 | fn to_protobuf(&self, _args: &()) -> types::stream::Source { 84 | match self { 85 | StreamSource::Url { url } => types::stream::Source::Url(types::stream::Url { 86 | url: url.to_string(), 87 | }), 88 | StreamSource::YouTube { yt_id } => { 89 | types::stream::Source::YouTube(types::stream::YouTube { 90 | yt_id: yt_id.to_string(), 91 | }) 92 | } 93 | StreamSource::Torrent { 94 | info_hash, 95 | file_idx, 96 | announce, 97 | file_must_include, 98 | } => types::stream::Source::Torrent(types::stream::Torrent { 99 | info_hash: hex::encode(info_hash), 100 | file_idx: file_idx.map(|idx| idx as i32), 101 | announce: announce.clone(), 102 | file_must_include: file_must_include.to_owned(), 103 | }), 104 | StreamSource::External { 105 | external_url, 106 | android_tv_url, 107 | .. 108 | } => types::stream::Source::External(types::stream::External { 109 | external_url: external_url.to_protobuf(&()), 110 | android_tv_url: android_tv_url.to_protobuf(&()), 111 | }), 112 | StreamSource::PlayerFrame { player_frame_url } => { 113 | types::stream::Source::PlayerFrame(types::stream::PlayerFrame { 114 | player_frame_url: player_frame_url.to_string(), 115 | }) 116 | } 117 | StreamSource::Rar { 118 | rar_urls, 119 | file_idx, 120 | file_must_include, 121 | } => types::stream::Source::Rar(types::stream::Rar { 122 | rar_urls: rar_urls.to_protobuf(&()), 123 | file_idx: file_idx.map(|idx| idx as i32), 124 | file_must_include: file_must_include.to_owned(), 125 | }), 126 | StreamSource::Zip { 127 | zip_urls, 128 | file_idx, 129 | file_must_include, 130 | } => types::stream::Source::Zip(types::stream::Zip { 131 | zip_urls: zip_urls.to_protobuf(&()), 132 | file_idx: file_idx.map(|idx| idx as i32), 133 | file_must_include: file_must_include.to_owned(), 134 | }), 135 | } 136 | } 137 | } 138 | 139 | impl ToProtobuf for StreamProxyHeaders { 140 | fn to_protobuf(&self, _args: &()) -> types::StreamProxyHeaders { 141 | types::StreamProxyHeaders { 142 | request: self.request.to_owned(), 143 | response: self.response.to_owned(), 144 | } 145 | } 146 | } 147 | 148 | impl 149 | ToProtobuf< 150 | types::Stream, 151 | ( 152 | Option<&Ctx>, 153 | Option<&String>, 154 | Option<&ResourceRequest>, 155 | Option<&ResourceRequest>, 156 | ), 157 | > for Stream 158 | { 159 | fn to_protobuf( 160 | &self, 161 | (ctx, addon_name, stream_request, meta_request): &( 162 | Option<&Ctx>, 163 | Option<&String>, 164 | Option<&ResourceRequest>, 165 | Option<&ResourceRequest>, 166 | ), 167 | ) -> types::Stream { 168 | // in calls that have None for ctx this would panic if we don't set it to default. 169 | let settings = ctx 170 | .map(|ctx| ctx.profile.settings.to_owned()) 171 | .unwrap_or_default(); 172 | 173 | let deep_links = match (stream_request, meta_request) { 174 | (Some(stream_request), Some(meta_request)) => StreamDeepLinks::from(( 175 | self, 176 | *stream_request, 177 | *meta_request, 178 | &ctx.map(|ctx| ctx.profile.settings.streaming_server_url.clone()), 179 | &settings, 180 | )), 181 | _ => StreamDeepLinks::from(( 182 | self, 183 | &ctx.map(|ctx| ctx.profile.settings.streaming_server_url.clone()), 184 | &settings, 185 | )), 186 | }; 187 | 188 | types::Stream { 189 | name: self.name.to_owned().or_else(|| addon_name.cloned()), 190 | description: self.description.clone(), 191 | thumbnail: self.thumbnail.clone(), 192 | subtitles: self.subtitles.to_protobuf(addon_name), 193 | behavior_hints: types::StreamBehaviorHints { 194 | not_web_ready: self.behavior_hints.not_web_ready, 195 | binge_group: self.behavior_hints.binge_group.to_owned(), 196 | country_whitelist: self 197 | .behavior_hints 198 | .country_whitelist 199 | .to_owned() 200 | .unwrap_or_default(), 201 | proxy_headers: self.behavior_hints.proxy_headers.to_protobuf(&()), 202 | filename: self.behavior_hints.filename.to_owned(), 203 | video_hash: self.behavior_hints.video_hash.to_owned(), 204 | video_size: self.behavior_hints.video_size, 205 | }, 206 | deep_links: types::StreamDeepLinks { 207 | player: deep_links.player, 208 | external_player: types::stream_deep_links::ExternalPlayerLink { 209 | download: deep_links.external_player.download, 210 | streaming: deep_links.external_player.streaming, 211 | }, 212 | }, 213 | source: Some(self.source.to_protobuf(&())), 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/string.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::{FromProtobuf, ToProtobuf}; 2 | use url::Url; 3 | 4 | impl FromProtobuf for String { 5 | fn from_protobuf(&self) -> Url { 6 | Url::parse(self).expect("url parse failed") 7 | } 8 | } 9 | 10 | impl ToProtobuf for Url { 11 | fn to_protobuf(&self, _args: &()) -> String { 12 | self.to_string() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/subtitle.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::types::resource::Subtitles; 2 | 3 | use crate::bridge::{FromProtobuf, ToProtobuf}; 4 | use crate::protobuf::stremio::core::types; 5 | 6 | impl FromProtobuf for types::Subtitle { 7 | fn from_protobuf(&self) -> Subtitles { 8 | Subtitles { 9 | lang: self.lang.to_string(), 10 | url: self.url.from_protobuf(), 11 | } 12 | } 13 | } 14 | 15 | impl ToProtobuf> for Subtitles { 16 | fn to_protobuf(&self, addon_name: &Option<&String>) -> types::Subtitle { 17 | types::Subtitle { 18 | lang: self.lang.to_string(), 19 | url: self.url.to_string(), 20 | name: addon_name.cloned(), 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commonMain/rust/bridge/to_protobuf.rs: -------------------------------------------------------------------------------- 1 | pub trait ToProtobuf { 2 | fn to_protobuf(&self, args: &A) -> T; 3 | } 4 | -------------------------------------------------------------------------------- /src/commonMain/rust/env.rs: -------------------------------------------------------------------------------- 1 | pub use env::*; 2 | pub use event::*; 3 | pub use fetch::*; 4 | pub use storage::*; 5 | 6 | mod env; 7 | mod event; 8 | mod fetch; 9 | mod storage; 10 | -------------------------------------------------------------------------------- /src/commonMain/rust/env/env.rs: -------------------------------------------------------------------------------- 1 | use std::sync::RwLock; 2 | 3 | use chrono::{DateTime, Utc}; 4 | use futures::{Future, TryFutureExt}; 5 | use http::Request; 6 | use once_cell::sync::Lazy; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use stremio_core::{ 10 | analytics::Analytics, 11 | models::{ctx::Ctx, streaming_server::StreamingServer}, 12 | runtime::{Env, EnvError, EnvFuture, EnvFutureExt, TryEnvFuture}, 13 | }; 14 | 15 | use crate::{ 16 | env::{fetch, AppleEvent, Storage}, 17 | model::AppleModel, 18 | }; 19 | 20 | const INSTALLATION_ID_STORAGE_KEY: &str = "installation_id"; 21 | #[cfg(debug_assertions)] 22 | const LOG_TAG: &str = "AppleEnv"; 23 | 24 | static CONCURRENT_RUNTIME: Lazy> = Lazy::new(|| { 25 | RwLock::new( 26 | tokio::runtime::Builder::new_multi_thread() 27 | .thread_name("CONCURRENT_RUNTIME_THREAD") 28 | .worker_threads(5) 29 | .enable_all() 30 | .build() 31 | .expect("CONCURRENT_RUNTIME create failed"), 32 | ) 33 | }); 34 | static SEQUENTIAL_RUNTIME: Lazy> = Lazy::new(|| { 35 | RwLock::new( 36 | tokio::runtime::Builder::new_multi_thread() 37 | .worker_threads(1) 38 | .thread_name("SEQUENTIAL_RUNTIME_THREAD") 39 | .enable_all() 40 | .build() 41 | .expect("SEQUENTIAL_RUNTIME create failed"), 42 | ) 43 | }); 44 | static STORAGE: Lazy>> = Lazy::new(|| Default::default()); 45 | static ANALYTICS: Lazy> = Lazy::new(|| Default::default()); 46 | static INSTALLATION_ID: Lazy>> = Lazy::new(|| Default::default()); 47 | static VISIT_ID: Lazy = Lazy::new(|| hex::encode(AppleEnv::random_buffer(10))); 48 | 49 | #[derive(Serialize)] 50 | #[serde(rename_all = "camelCase")] 51 | struct AnalyticsContext { 52 | app_type: String, 53 | app_version: String, 54 | server_version: Option, 55 | shell_version: Option, 56 | system_language: Option, 57 | app_language: String, 58 | #[serde(rename = "installationID")] 59 | installation_id: String, 60 | #[serde(rename = "visitID")] 61 | visit_id: String, 62 | #[serde(rename = "url")] 63 | path: String, 64 | } 65 | 66 | pub enum AppleEnv {} 67 | 68 | impl AppleEnv { 69 | pub fn init() -> TryEnvFuture<()> { 70 | *STORAGE.write().expect("STORAGE write failed") = 71 | Some(Storage::new().expect("Create Storage failed")); 72 | AppleEnv::migrate_storage_schema() 73 | .and_then(|_| async { 74 | let installation_id = get_installation_id().await?; 75 | *INSTALLATION_ID 76 | .write() 77 | .expect("INSTALLATION_ID write failed") = Some(installation_id); 78 | Ok(()) 79 | }) 80 | .boxed_env() 81 | } 82 | pub fn exec_sync(future: F) -> F::Output { 83 | SEQUENTIAL_RUNTIME 84 | .read() 85 | .expect("SEQUENTIAL_RUNTIME read failed") 86 | .block_on(future) 87 | } 88 | //TODO: Analyits disabled and iOS implimentation needed. Also this is not offical project 89 | pub fn emit_to_analytics(_event: &AppleEvent, _model: &AppleModel, _path: &str) { 90 | println!("Analytis triggered"); 91 | // let (name, data) = match event { 92 | // AppleEvent::CoreEvent(Event::PlayerPlaying { load_time, context }) => ( 93 | // "playerPlaying".to_owned(), 94 | // json!({ 95 | // "loadTime": load_time, 96 | // "player": context 97 | // }), 98 | // ), 99 | // AppleEvent::CoreEvent(Event::PlayerStopped { context }) => { 100 | // ("playerStopped".to_owned(), json!({ "player": context })) 101 | // } 102 | // AppleEvent::CoreEvent(Event::PlayerEnded { 103 | // context, 104 | // is_binge_enabled, 105 | // is_playing_next_video, 106 | // }) => ( 107 | // "playerEnded".to_owned(), 108 | // json!({ 109 | // "player": context, 110 | // "isBingeEnabled": is_binge_enabled, 111 | // "isPlayingNextVideo": is_playing_next_video 112 | // }), 113 | // ), 114 | // AppleEvent::CoreEvent(Event::TraktPlaying { context }) => { 115 | // ("traktPlaying".to_owned(), json!({ "player": context })) 116 | // } 117 | // AppleEvent::CoreEvent(Event::TraktPaused { context }) => { 118 | // ("traktPaused".to_owned(), json!({ "player": context })) 119 | // } 120 | // _ => return, 121 | // }; 122 | // ANALYTICS.emit(name, data, &model.ctx, &model.streaming_server, path); 123 | } 124 | pub fn send_next_analytics_batch() -> impl Future { 125 | ANALYTICS.send_next_batch() 126 | } 127 | pub fn random_buffer(len: usize) -> Vec { 128 | let mut buffer = vec![0u8; len]; 129 | getrandom::getrandom(buffer.as_mut_slice()).expect("getrandom failed"); 130 | buffer 131 | } 132 | } 133 | 134 | impl Env for AppleEnv { 135 | fn fetch Deserialize<'de> + Send + 'static>( 136 | request: Request, 137 | ) -> TryEnvFuture { 138 | fetch(request) 139 | } 140 | fn get_storage Deserialize<'de> + Send + 'static>( 141 | key: &str, 142 | ) -> TryEnvFuture> { 143 | let storage = STORAGE.read().expect("STORAGE read failed"); 144 | let storage = storage.as_ref().expect("STORAGE not initialized"); 145 | storage.get::(key) 146 | } 147 | fn set_storage(key: &str, value: Option<&T>) -> TryEnvFuture<()> { 148 | let storage = STORAGE.read().expect("STORAGE read failed"); 149 | let storage = storage.as_ref().expect("STORAGE not initialized"); 150 | storage.set::(key, value) 151 | } 152 | 153 | fn exec_concurrent + Send + 'static>(future: F) { 154 | CONCURRENT_RUNTIME 155 | .read() 156 | .expect("CONCURRENT_RUNTIME read failed") 157 | .spawn(future); 158 | } 159 | fn exec_sequential + Send + 'static>(future: F) { 160 | SEQUENTIAL_RUNTIME 161 | .read() 162 | .expect("SEQUENTIAL_RUNTIME read failed") 163 | .spawn(future); 164 | } 165 | fn now() -> DateTime { 166 | Utc::now() 167 | } 168 | fn flush_analytics() -> EnvFuture<'static, ()> { 169 | ANALYTICS.flush().boxed_env() 170 | } 171 | fn analytics_context( 172 | ctx: &Ctx, 173 | streaming_server: &StreamingServer, 174 | path: &str, 175 | ) -> serde_json::Value { 176 | serde_json::to_value(AnalyticsContext { 177 | app_type: "apple".to_owned(), 178 | app_version: "TODO".to_owned(), 179 | server_version: streaming_server 180 | .settings 181 | .as_ref() 182 | .ready() 183 | .map(|settings| settings.server_version.to_owned()), 184 | shell_version: None, 185 | system_language: Some("TODO".to_owned()), 186 | app_language: ctx.profile.settings.interface_language.to_owned(), 187 | installation_id: INSTALLATION_ID 188 | .read() 189 | .expect("installation id read failed") 190 | .as_ref() 191 | .expect("installation id not available") 192 | .to_owned(), 193 | visit_id: VISIT_ID.to_owned(), 194 | path: path.to_owned(), 195 | }) 196 | .unwrap() 197 | } 198 | #[cfg(debug_assertions)] 199 | fn log(message: String) { 200 | use std::ffi::CString; 201 | let tag = CString::new(LOG_TAG).unwrap(); 202 | let message = CString::new(message).unwrap(); 203 | 204 | let tag_str = tag.to_str().expect("Failed to convert tag to &str"); 205 | let message_str = message.to_str().expect("Failed to convert message to &str"); 206 | 207 | //TODO: impliment native logging mechanism 208 | println!("{}: {}", tag_str, message_str); 209 | } 210 | } 211 | 212 | async fn get_installation_id() -> Result { 213 | let installation_id = AppleEnv::get_storage::(INSTALLATION_ID_STORAGE_KEY).await?; 214 | let installation_id = 215 | installation_id.unwrap_or_else(|| hex::encode(AppleEnv::random_buffer(10))); 216 | AppleEnv::set_storage(INSTALLATION_ID_STORAGE_KEY, Some(&installation_id)).await?; 217 | Ok(installation_id) 218 | } 219 | -------------------------------------------------------------------------------- /src/commonMain/rust/env/event.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::runtime::msg::Event; 2 | 3 | pub enum AppleEvent { 4 | CoreEvent(Event), 5 | } 6 | -------------------------------------------------------------------------------- /src/commonMain/rust/env/fetch.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use block2::RcBlock; 4 | use futures::future; 5 | use http::{Method, Request}; 6 | use objc2::{rc::Retained, ClassType}; 7 | use objc2_foundation::{ 8 | ns_string, NSData, NSError, NSHTTPURLResponse, NSMutableURLRequest, NSObjectProtocol, NSString, 9 | NSURLResponse, NSURLSession, NSURL, 10 | }; 11 | use once_cell::sync::OnceCell; 12 | use serde::{Deserialize, Serialize}; 13 | use serde_json::Deserializer; 14 | 15 | use stremio_core::runtime::{EnvError, EnvFutureExt, TryEnvFuture}; 16 | 17 | use crate::stremio_core_apple::DEVICE_NAME; 18 | 19 | /// User Agent used to make all calls to fetch. 20 | /// 21 | /// This need to be initialized once on the first call to [`fetch`] 22 | /// due to the device name being provided by the application on 23 | /// initialization. 24 | pub(crate) static USER_AGENT: OnceCell> = OnceCell::new(); 25 | 26 | pub fn fetch Deserialize<'de> + Send + 'static>( 27 | request: Request, 28 | ) -> TryEnvFuture { 29 | // Initialize the OnceCell on the first call. 30 | // we expect the Device name to be initialized first by passing it from the application 31 | let user_agent = USER_AGENT.get_or_init(|| { 32 | let package_version = env!("CARGO_PKG_VERSION"); 33 | let device_name = DEVICE_NAME.get().cloned().unwrap_or("Unknown".to_string()); 34 | NSString::from_str(format!("Stremio-Apple/{package_version} {device_name}").as_str()) 35 | }); 36 | 37 | let (parts, body) = request.into_parts(); 38 | 39 | let mut request = unsafe { 40 | let url = 41 | NSURL::initWithString(NSURL::alloc(), &NSString::from_str(&parts.uri.to_string())); 42 | NSMutableURLRequest::initWithURL(NSMutableURLRequest::alloc(), &url.unwrap()) 43 | }; 44 | unsafe { request.setValue_forHTTPHeaderField(Some(user_agent), ns_string!("User-Agent")) }; 45 | 46 | match serde_json::to_string(&body) { 47 | Ok(body) if body != "null" && parts.method != Method::GET => { 48 | let nsbody = NSData::with_bytes(body.as_bytes()); 49 | unsafe { 50 | request.setHTTPMethod(ns_string!("POST")); 51 | request.setHTTPBody(Some(&nsbody)); 52 | } 53 | nsbody 54 | } 55 | Ok(_) => unsafe { NSData::data() }, 56 | Err(error) => return future::err(EnvError::Serde(error.to_string())).boxed_env(), 57 | }; 58 | 59 | let (sender, mut receiver) = tokio::sync::mpsc::channel::, EnvError>>(1); 60 | let sender = Arc::new(sender); 61 | 62 | let completion_handler = RcBlock::new( 63 | enclose::enclose!((sender) move |data: *mut NSData, response: *mut NSURLResponse, error: *mut NSError| { 64 | let result = if !error.is_null() { 65 | let err = unsafe { &*error }.retain(); 66 | 67 | Err(EnvError::Fetch(err.localizedDescription().to_string())) 68 | } else { 69 | let data: Option<&[u8]> = { 70 | if !data.is_null() { 71 | Some(unsafe { &*data }.bytes()) 72 | } else{ 73 | None 74 | } 75 | }; 76 | 77 | let response: Option> = { 78 | if !response.is_null() { 79 | let response = unsafe { &*response }; 80 | if response.isKindOfClass(NSHTTPURLResponse::class()){ 81 | Some(unsafe { &*(response as *const NSURLResponse as *const NSHTTPURLResponse) }.retain()) 82 | }else{ 83 | None 84 | } 85 | } else{ 86 | None 87 | } 88 | }; 89 | 90 | match (data, response) { 91 | (data, Some(response)) => { 92 | let resp_code = unsafe { response.statusCode() }; 93 | if !(resp_code >= 200 && resp_code < 300 ) { 94 | Err(EnvError::Fetch(format!( 95 | "Unexpected HTTP status code {}", 96 | resp_code, 97 | ))) 98 | } else { 99 | data.map(|buf| buf.to_vec()).ok_or(EnvError::Fetch("Response data is missing".into())) 100 | } 101 | }, 102 | _ => Err(EnvError::Fetch("Failed to fetch any response from the request".into())) 103 | } 104 | }; 105 | 106 | 107 | // TODO: this is very tricky, somehow we need to pass the information 108 | // back to the receiver, but guard against null pointers, e.g. 109 | // we have data but not an Error 110 | 111 | if let Err(err) = futures::executor::block_on(sender.send(result)) { 112 | eprintln!("Failed to send values: {err}"); 113 | } 114 | }), 115 | ); 116 | 117 | unsafe { 118 | let nsurlsession = NSURLSession::sharedSession(); 119 | let task = NSURLSession::dataTaskWithRequest_completionHandler( 120 | &nsurlsession, 121 | &request.as_super(), 122 | &completion_handler, 123 | ); 124 | task.resume(); 125 | } 126 | 127 | async move { 128 | let receive_result = receiver.recv().await.ok_or(EnvError::Other( 129 | "Channel closed, should never happen".to_string(), 130 | ))?; 131 | 132 | receive_result.and_then(|data| { 133 | let mut deserializer = Deserializer::from_slice(&data); 134 | let result = serde_path_to_error::deserialize::<_, OUT>(&mut deserializer); 135 | 136 | result.map_err(|error| EnvError::Serde(error.to_string())) 137 | }) 138 | } 139 | .boxed_env() 140 | } 141 | -------------------------------------------------------------------------------- /src/commonMain/rust/env/storage.rs: -------------------------------------------------------------------------------- 1 | use futures::future; 2 | use objc2::runtime::AnyObject; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use objc2_foundation::{NSString, NSUserDefaults}; 6 | use stremio_core::runtime::{EnvError, EnvFutureExt, TryEnvFuture}; 7 | 8 | pub struct Storage {} 9 | //TODO: This implimentation probably have race condition. Proper implimentation needed 10 | impl Storage { 11 | pub fn new() -> Result { 12 | Ok(Self {}) 13 | } 14 | 15 | pub fn get Deserialize<'de> + Send + 'static>( 16 | &self, 17 | key: &str, 18 | ) -> TryEnvFuture> { 19 | let key = key.to_owned(); 20 | Box::pin(future::lazy(move |_| { 21 | let nskey = &NSString::from_str(&key); 22 | 23 | let user_defaults = unsafe { &NSUserDefaults::standardUserDefaults() }; 24 | let optional_value = unsafe { &NSUserDefaults::stringForKey(user_defaults, nskey) }; 25 | 26 | Ok(match optional_value { 27 | Some(value) => { 28 | let deserialized_value: T = 29 | serde_json::from_str(&value.to_string()).map_err(EnvError::from)?; // Adjust error handling as needed 30 | Some(deserialized_value) 31 | } 32 | None => None, 33 | }) 34 | })) 35 | .boxed_env() 36 | } 37 | pub fn set(&self, key: &str, value: Option<&T>) -> TryEnvFuture<()> { 38 | if let Some(value) = value { 39 | let nskey = &NSString::from_str(&key); 40 | let user_defaults = unsafe { &NSUserDefaults::standardUserDefaults() }; 41 | 42 | // Convert the value to a JSON string 43 | let nsvalue = match serde_json::to_string(value) { 44 | Ok(value) => NSString::from_str(&value), 45 | Err(error) => return future::err(EnvError::Serde(error.to_string())).boxed_env(), 46 | }; 47 | let nsvalue_object: Option<&AnyObject> = Some(&nsvalue); 48 | unsafe { NSUserDefaults::setObject_forKey(&user_defaults, nsvalue_object, nskey) }; 49 | } 50 | future::lazy(move |_| { 51 | Ok(()) // Return the serialized value 52 | }) 53 | .boxed_env() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/commonMain/rust/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_inception)] 2 | pub mod bridge; 3 | pub mod env; 4 | pub mod model; 5 | #[allow(clippy::all)] 6 | /// Auto-generated module from protobuf files to Rust structus 7 | pub mod protobuf; 8 | pub mod stremio_core_apple; 9 | -------------------------------------------------------------------------------- /src/commonMain/rust/model.rs: -------------------------------------------------------------------------------- 1 | pub use addons::*; 2 | pub use model::*; 3 | 4 | mod addons; 5 | mod fields; 6 | mod model; 7 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/addons.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::models::catalog_with_filters::CatalogWithFilters; 2 | use stremio_core::models::ctx::Ctx; 3 | use stremio_core::models::installed_addons_with_filters::InstalledAddonsWithFilters; 4 | use stremio_core::runtime::msg::{Action, ActionLoad, Msg}; 5 | use stremio_core::runtime::{Effects, Env, UpdateWithCtx}; 6 | use stremio_core::types::addon::DescriptorPreview; 7 | use stremio_core::types::profile::Profile; 8 | 9 | #[derive(Default, Clone)] 10 | pub struct AddonsWithFilters { 11 | pub remote_addons: CatalogWithFilters, 12 | pub installed_addons: InstalledAddonsWithFilters, 13 | } 14 | 15 | impl AddonsWithFilters { 16 | pub fn new(profile: &Profile) -> (Self, Effects) { 17 | let (remote_addons, remote_addons_effects) = 18 | CatalogWithFilters::::new(profile); 19 | let (installed_addons, installed_addons_effects) = InstalledAddonsWithFilters::new(profile); 20 | let effects = remote_addons_effects.join(installed_addons_effects); 21 | ( 22 | Self { 23 | remote_addons, 24 | installed_addons, 25 | }, 26 | effects, 27 | ) 28 | } 29 | } 30 | 31 | impl UpdateWithCtx for AddonsWithFilters { 32 | fn update(&mut self, msg: &Msg, ctx: &Ctx) -> Effects { 33 | match msg { 34 | Msg::Action(Action::Load(ActionLoad::InstalledAddonsWithFilters(_selected))) => { 35 | let unload_remote_effects = UpdateWithCtx::::update( 36 | &mut self.remote_addons, 37 | &Msg::Action(Action::Unload), 38 | ctx, 39 | ); 40 | let installed_addons_effects = 41 | UpdateWithCtx::::update(&mut self.installed_addons, msg, ctx); 42 | unload_remote_effects.join(installed_addons_effects) 43 | } 44 | Msg::Action(Action::Load(ActionLoad::CatalogWithFilters(_selected))) => { 45 | let unload_installed_effects = UpdateWithCtx::::update( 46 | &mut self.installed_addons, 47 | &Msg::Action(Action::Unload), 48 | ctx, 49 | ); 50 | let remote_effects = UpdateWithCtx::::update(&mut self.remote_addons, msg, ctx); 51 | unload_installed_effects.join(remote_effects) 52 | } 53 | _ => { 54 | let remote_effects = UpdateWithCtx::::update(&mut self.remote_addons, msg, ctx); 55 | let installed_effects = 56 | UpdateWithCtx::::update(&mut self.installed_addons, msg, ctx); 57 | remote_effects.join(installed_effects) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/fields.rs: -------------------------------------------------------------------------------- 1 | 2 | //! [`ToProtobuf`] and [`FromProtobuf`] impls for various fields 3 | //! 4 | //! [`ToProtobuf`]: crate::bridge::ToProtobuf 5 | //! [`FromProtobuf`]: crate::bridge::FromProtobuf 6 | 7 | mod addon_detail; 8 | mod addons_with_filters; 9 | mod catalogs_with_extra; 10 | mod continue_watching_preview; 11 | mod ctx; 12 | mod discover; 13 | mod library; 14 | mod library_by_type; 15 | mod link; 16 | mod meta_details; 17 | mod player; 18 | mod streaming_server; 19 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/fields/addon_detail.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::{FromProtobuf, ToProtobuf}; 2 | use crate::protobuf::stremio::core::models; 3 | use stremio_core::models::addon_details::{AddonDetails, Selected}; 4 | use stremio_core::models::ctx::Ctx; 5 | 6 | impl FromProtobuf for models::addon_details::Selected { 7 | fn from_protobuf(&self) -> Selected { 8 | let mut url = self.transport_url.from_protobuf(); 9 | if url.scheme() == "stremio" { 10 | let replaced_url = url.as_str().replacen("stremio://", "https://", 1); 11 | url = replaced_url.parse().expect("Should be able to parse URL"); 12 | } 13 | Selected { transport_url: url } 14 | } 15 | } 16 | 17 | impl ToProtobuf for Selected { 18 | fn to_protobuf(&self, _args: &()) -> models::addon_details::Selected { 19 | models::addon_details::Selected { 20 | transport_url: self.transport_url.to_string(), 21 | } 22 | } 23 | } 24 | 25 | impl ToProtobuf for AddonDetails { 26 | fn to_protobuf(&self, ctx: &Ctx) -> models::AddonDetails { 27 | models::AddonDetails { 28 | selected: self.selected.to_protobuf(&()), 29 | local_addon: self.local_addon.to_protobuf(ctx), 30 | remote_addon: self.remote_addon.to_protobuf(ctx), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/fields/addons_with_filters.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::{FromProtobuf, ToProtobuf}; 2 | use crate::model::AddonsWithFilters; 3 | use crate::protobuf::stremio::core::models::LoadableAddonCatalog; 4 | use crate::protobuf::stremio::core::{models, types}; 5 | use stremio_core::models::ctx::Ctx; 6 | use stremio_core::models::installed_addons_with_filters::InstalledAddonsRequest; 7 | use stremio_core::models::{catalog_with_filters, installed_addons_with_filters}; 8 | 9 | impl FromProtobuf for models::addons_with_filters::Selected { 10 | fn from_protobuf(&self) -> catalog_with_filters::Selected { 11 | catalog_with_filters::Selected { 12 | request: self.request.from_protobuf(), 13 | } 14 | } 15 | } 16 | 17 | impl FromProtobuf 18 | for models::addons_with_filters::Selected 19 | { 20 | fn from_protobuf(&self) -> installed_addons_with_filters::Selected { 21 | installed_addons_with_filters::Selected { 22 | request: self.request.from_protobuf(), 23 | } 24 | } 25 | } 26 | 27 | impl ToProtobuf for InstalledAddonsRequest { 28 | fn to_protobuf(&self, _args: &()) -> types::ResourceRequest { 29 | types::ResourceRequest { 30 | base: "".to_string(), 31 | path: types::ResourcePath { 32 | resource: "".to_string(), 33 | r#type: self.r#type.clone().unwrap_or_default(), 34 | id: "".to_string(), 35 | extra: vec![], 36 | }, 37 | } 38 | } 39 | } 40 | 41 | impl ToProtobuf 42 | for installed_addons_with_filters::Selected 43 | { 44 | fn to_protobuf(&self, _args: &()) -> models::addons_with_filters::Selected { 45 | models::addons_with_filters::Selected { 46 | request: self.request.to_protobuf(&()), 47 | } 48 | } 49 | } 50 | 51 | impl ToProtobuf for AddonsWithFilters { 52 | fn to_protobuf(&self, ctx: &Ctx) -> models::AddonsWithFilters { 53 | models::AddonsWithFilters { 54 | selected: self 55 | .remote_addons 56 | .selected 57 | .to_owned() 58 | .map(|selected| models::addons_with_filters::Selected { 59 | request: selected.request.to_protobuf(&()), 60 | }) 61 | .or_else(|| self.installed_addons.selected.to_protobuf(&())), 62 | selectable: models::addons_with_filters::Selectable { 63 | types: match &self.remote_addons.selected.is_some() { 64 | true => self 65 | .remote_addons 66 | .selectable 67 | .types 68 | .iter() 69 | .map( 70 | |selectable_type| models::addons_with_filters::SelectableType { 71 | r#type: selectable_type.r#type.to_string(), 72 | selected: selectable_type.selected, 73 | request: selectable_type.request.to_protobuf(&()), 74 | }, 75 | ) 76 | .collect(), 77 | false => self 78 | .installed_addons 79 | .selectable 80 | .types 81 | .iter() 82 | .map( 83 | |selectable_type| models::addons_with_filters::SelectableType { 84 | r#type: selectable_type.r#type.clone().unwrap_or_default(), 85 | selected: selectable_type.selected, 86 | request: selectable_type.request.to_protobuf(&()), 87 | }, 88 | ) 89 | .collect(), 90 | }, 91 | catalogs: self 92 | .remote_addons 93 | .selectable 94 | .catalogs 95 | .iter() 96 | .map(|catalog| models::addons_with_filters::SelectableCatalog { 97 | name: catalog.catalog.to_owned(), 98 | selected: catalog.selected, 99 | request: catalog.request.to_protobuf(&()), 100 | }) 101 | .chain([models::addons_with_filters::SelectableCatalog { 102 | name: "Installed".to_string(), 103 | selected: self.remote_addons.selected.is_none(), 104 | request: InstalledAddonsRequest { r#type: None }.to_protobuf(&()), 105 | }]) 106 | .collect(), 107 | }, 108 | catalog: match &self.remote_addons.selected { 109 | Some(_) => self 110 | .remote_addons 111 | .catalog 112 | .first() 113 | .map(|page| page.to_protobuf(ctx)), 114 | None => { 115 | self.installed_addons 116 | .selected 117 | .as_ref() 118 | .map(|selected| LoadableAddonCatalog { 119 | request: selected.request.to_protobuf(&()), 120 | content: Some(models::loadable_addon_catalog::Content::Ready( 121 | models::Addons { 122 | items: self.installed_addons.catalog.to_protobuf(ctx), 123 | }, 124 | )), 125 | }) 126 | } 127 | }, 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/fields/catalogs_with_extra.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::deep_links::DiscoverDeepLinks; 2 | use stremio_core::models::catalog_with_filters::Catalog; 3 | use stremio_core::models::catalogs_with_extra::{CatalogsWithExtra, Selected}; 4 | use stremio_core::models::ctx::Ctx; 5 | use stremio_core::types::resource::MetaItemPreview; 6 | 7 | use crate::bridge::{FromProtobuf, ToProtobuf}; 8 | use crate::protobuf::stremio::core::models; 9 | 10 | impl FromProtobuf for models::catalogs_with_extra::Selected { 11 | fn from_protobuf(&self) -> Selected { 12 | Selected { 13 | r#type: self.r#type.clone(), 14 | extra: self.extra.from_protobuf(), 15 | } 16 | } 17 | } 18 | 19 | impl ToProtobuf for Selected { 20 | fn to_protobuf(&self, _args: &()) -> models::catalogs_with_extra::Selected { 21 | models::catalogs_with_extra::Selected { 22 | r#type: self.r#type.clone(), 23 | extra: self.extra.to_protobuf(&()), 24 | } 25 | } 26 | } 27 | 28 | impl ToProtobuf for Catalog { 29 | fn to_protobuf(&self, ctx: &Ctx) -> models::Catalog { 30 | models::Catalog { 31 | pages: self.iter().map(|page| page.to_protobuf(ctx)).collect(), 32 | } 33 | } 34 | } 35 | 36 | impl ToProtobuf for CatalogsWithExtra { 37 | fn to_protobuf(&self, ctx: &Ctx) -> models::CatalogsWithExtra { 38 | models::CatalogsWithExtra { 39 | selected: self.selected.to_protobuf(&()), 40 | catalogs: self.catalogs.to_protobuf(ctx), 41 | } 42 | } 43 | } 44 | 45 | impl ToProtobuf for DiscoverDeepLinks { 46 | fn to_protobuf(&self, _args: &()) -> models::DiscoverDeepLinks { 47 | models::DiscoverDeepLinks { 48 | discover: self.discover.clone(), 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/fields/continue_watching_preview.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::models::continue_watching_preview::{ContinueWatchingPreview, Item}; 2 | use stremio_core::models::ctx::Ctx; 3 | 4 | use crate::bridge::ToProtobuf; 5 | use crate::protobuf::stremio::core::{models, types}; 6 | 7 | impl ToProtobuf for Item { 8 | fn to_protobuf(&self, ctx: &Ctx) -> types::LibraryItem { 9 | self.library_item 10 | .to_protobuf(&(ctx, Some(self.notifications))) 11 | } 12 | } 13 | 14 | impl ToProtobuf for ContinueWatchingPreview { 15 | fn to_protobuf(&self, ctx: &Ctx) -> models::ContinueWatchingPreview { 16 | models::ContinueWatchingPreview { 17 | library_items: self.items.to_protobuf(ctx), 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/fields/ctx.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::ToProtobuf; 2 | use crate::protobuf::stremio::core::models; 3 | use stremio_core::models::ctx::Ctx; 4 | 5 | impl ToProtobuf for Ctx { 6 | fn to_protobuf(&self, _args: &()) -> models::Ctx { 7 | models::Ctx { 8 | profile: self.profile.to_protobuf(&()), 9 | events: self.events.to_protobuf(&()), 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/fields/discover.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::models::catalog_with_filters::{ 2 | CatalogWithFilters, Selectable, SelectableCatalog, SelectableExtra, SelectableExtraOption, 3 | SelectablePage, SelectableType, Selected, 4 | }; 5 | use stremio_core::models::ctx::Ctx; 6 | use stremio_core::types::resource::MetaItemPreview; 7 | 8 | use crate::bridge::{FromProtobuf, ToProtobuf}; 9 | use crate::protobuf::stremio::core::models; 10 | 11 | impl FromProtobuf for models::catalog_with_filters::Selected { 12 | fn from_protobuf(&self) -> Selected { 13 | Selected { 14 | request: self.request.from_protobuf(), 15 | } 16 | } 17 | } 18 | 19 | impl ToProtobuf for Selected { 20 | fn to_protobuf(&self, _args: &()) -> models::catalog_with_filters::Selected { 21 | models::catalog_with_filters::Selected { 22 | request: self.request.to_protobuf(&()), 23 | } 24 | } 25 | } 26 | 27 | impl ToProtobuf for SelectableType { 28 | fn to_protobuf(&self, _args: &()) -> models::catalog_with_filters::SelectableType { 29 | models::catalog_with_filters::SelectableType { 30 | r#type: self.r#type.to_string(), 31 | selected: self.selected, 32 | request: self.request.to_protobuf(&()), 33 | } 34 | } 35 | } 36 | 37 | impl ToProtobuf for SelectableCatalog { 38 | fn to_protobuf(&self, _args: &()) -> models::catalog_with_filters::SelectableCatalog { 39 | models::catalog_with_filters::SelectableCatalog { 40 | name: self.catalog.to_string(), 41 | selected: self.selected, 42 | request: self.request.to_protobuf(&()), 43 | } 44 | } 45 | } 46 | 47 | impl ToProtobuf for SelectableExtraOption { 48 | fn to_protobuf(&self, _args: &()) -> models::catalog_with_filters::SelectableExtraOption { 49 | models::catalog_with_filters::SelectableExtraOption { 50 | value: self.value.clone(), 51 | selected: self.selected, 52 | request: self.request.to_protobuf(&()), 53 | } 54 | } 55 | } 56 | 57 | impl ToProtobuf for SelectableExtra { 58 | fn to_protobuf(&self, _args: &()) -> models::catalog_with_filters::SelectableExtra { 59 | models::catalog_with_filters::SelectableExtra { 60 | name: self.name.to_string(), 61 | is_required: self.is_required, 62 | options: self.options.to_protobuf(&()), 63 | } 64 | } 65 | } 66 | 67 | impl ToProtobuf for SelectablePage { 68 | fn to_protobuf(&self, _args: &()) -> models::catalog_with_filters::SelectablePage { 69 | models::catalog_with_filters::SelectablePage { 70 | request: self.request.to_protobuf(&()), 71 | } 72 | } 73 | } 74 | 75 | impl ToProtobuf for Selectable { 76 | fn to_protobuf(&self, _args: &()) -> models::catalog_with_filters::Selectable { 77 | models::catalog_with_filters::Selectable { 78 | types: self.types.to_protobuf(&()), 79 | catalogs: self.catalogs.to_protobuf(&()), 80 | extra: self.extra.to_protobuf(&()), 81 | next_page: self.next_page.to_protobuf(&()), 82 | } 83 | } 84 | } 85 | 86 | impl ToProtobuf for CatalogWithFilters { 87 | fn to_protobuf(&self, ctx: &Ctx) -> models::CatalogWithFilters { 88 | models::CatalogWithFilters { 89 | selected: self.selected.to_protobuf(&()), 90 | selectable: self.selectable.to_protobuf(&()), 91 | catalog: self.catalog.to_protobuf(ctx), 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/fields/library.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::num::NonZeroUsize; 3 | use stremio_core::models::ctx::Ctx; 4 | 5 | use stremio_core::models::library_with_filters::{ 6 | LibraryRequest, LibraryRequestPage, LibraryWithFilters, Selectable, SelectablePage, 7 | SelectableSort, SelectableType, Selected, Sort, 8 | }; 9 | 10 | use crate::bridge::{FromProtobuf, ToProtobuf}; 11 | use crate::protobuf::stremio::core::models; 12 | 13 | impl FromProtobuf for models::library_with_filters::Sort { 14 | fn from_protobuf(&self) -> Sort { 15 | match self { 16 | models::library_with_filters::Sort::LastWatched => Sort::LastWatched, 17 | models::library_with_filters::Sort::NameReverse => Sort::NameReverse, 18 | models::library_with_filters::Sort::Name => Sort::Name, 19 | models::library_with_filters::Sort::TimesWatched => Sort::TimesWatched, 20 | models::library_with_filters::Sort::Watched => Sort::Watched, 21 | models::library_with_filters::Sort::NotWatched => Sort::NotWatched, 22 | } 23 | } 24 | } 25 | 26 | impl FromProtobuf for models::library_with_filters::LibraryRequest { 27 | fn from_protobuf(&self) -> LibraryRequest { 28 | let page = usize::try_from(cmp::max(self.page, 1)).unwrap_or(usize::MAX); 29 | let page = LibraryRequestPage(NonZeroUsize::new(page).unwrap()); 30 | LibraryRequest { 31 | r#type: self.r#type.to_owned(), 32 | sort: models::library_with_filters::Sort::try_from(self.sort) 33 | .ok() 34 | .from_protobuf() 35 | .unwrap_or(Sort::LastWatched), 36 | page, 37 | } 38 | } 39 | } 40 | 41 | impl FromProtobuf for models::library_with_filters::Selected { 42 | fn from_protobuf(&self) -> Selected { 43 | Selected { 44 | request: self.request.from_protobuf(), 45 | } 46 | } 47 | } 48 | 49 | impl ToProtobuf for Sort { 50 | fn to_protobuf(&self, _args: &()) -> models::library_with_filters::Sort { 51 | match self { 52 | Sort::LastWatched => models::library_with_filters::Sort::LastWatched, 53 | Sort::Name => models::library_with_filters::Sort::Name, 54 | Sort::NameReverse => models::library_with_filters::Sort::NameReverse, 55 | Sort::TimesWatched => models::library_with_filters::Sort::TimesWatched, 56 | Sort::Watched => models::library_with_filters::Sort::Watched, 57 | Sort::NotWatched => models::library_with_filters::Sort::NotWatched, 58 | } 59 | } 60 | } 61 | 62 | impl ToProtobuf for LibraryRequest { 63 | fn to_protobuf(&self, _args: &()) -> models::library_with_filters::LibraryRequest { 64 | models::library_with_filters::LibraryRequest { 65 | r#type: self.r#type.clone(), 66 | sort: self.sort.to_protobuf(&()) as i32, 67 | page: i64::try_from(self.page.0.get()).unwrap_or(i64::MAX), 68 | } 69 | } 70 | } 71 | 72 | impl ToProtobuf for Selected { 73 | fn to_protobuf(&self, _args: &()) -> models::library_with_filters::Selected { 74 | models::library_with_filters::Selected { 75 | request: self.request.to_protobuf(&()), 76 | } 77 | } 78 | } 79 | 80 | impl ToProtobuf for SelectableType { 81 | fn to_protobuf(&self, _args: &()) -> models::library_with_filters::SelectableType { 82 | models::library_with_filters::SelectableType { 83 | r#type: self.r#type.clone(), 84 | selected: self.selected, 85 | request: self.request.to_protobuf(&()), 86 | } 87 | } 88 | } 89 | 90 | impl ToProtobuf for SelectableSort { 91 | fn to_protobuf(&self, _args: &()) -> models::library_with_filters::SelectableSort { 92 | models::library_with_filters::SelectableSort { 93 | sort: self.sort.to_protobuf(&()) as i32, 94 | selected: self.selected, 95 | request: self.request.to_protobuf(&()), 96 | } 97 | } 98 | } 99 | 100 | impl ToProtobuf for SelectablePage { 101 | fn to_protobuf(&self, _args: &()) -> models::library_with_filters::SelectablePage { 102 | models::library_with_filters::SelectablePage { 103 | request: self.request.to_protobuf(&()), 104 | } 105 | } 106 | } 107 | 108 | impl ToProtobuf for Selectable { 109 | fn to_protobuf(&self, _args: &()) -> models::library_with_filters::Selectable { 110 | models::library_with_filters::Selectable { 111 | types: self.types.to_protobuf(&()), 112 | sorts: self.sorts.to_protobuf(&()), 113 | next_page: self.next_page.to_protobuf(&()), 114 | } 115 | } 116 | } 117 | 118 | impl ToProtobuf for LibraryWithFilters { 119 | fn to_protobuf(&self, ctx: &Ctx) -> models::LibraryWithFilters { 120 | models::LibraryWithFilters { 121 | selected: self.selected.to_protobuf(&()), 122 | selectable: self.selectable.to_protobuf(&()), 123 | catalog: self.catalog.to_protobuf(&(ctx, None)), 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/fields/library_by_type.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::models::ctx::Ctx; 2 | use stremio_core::models::library_by_type::{ 3 | Catalog, LibraryByType, Selectable, SelectableSort, Selected, 4 | }; 5 | use stremio_core::models::library_with_filters::Sort; 6 | 7 | use crate::bridge::{FromProtobuf, ToProtobuf}; 8 | use crate::protobuf::stremio::core::models; 9 | 10 | impl FromProtobuf for models::library_by_type::Selected { 11 | fn from_protobuf(&self) -> Selected { 12 | Selected { 13 | sort: models::library_with_filters::Sort::try_from(self.sort) 14 | .ok() 15 | .from_protobuf() 16 | .unwrap_or(Sort::LastWatched), 17 | } 18 | } 19 | } 20 | 21 | impl ToProtobuf for Selected { 22 | fn to_protobuf(&self, _args: &()) -> models::library_by_type::Selected { 23 | models::library_by_type::Selected { 24 | sort: self.sort.to_protobuf(&()) as i32, 25 | } 26 | } 27 | } 28 | 29 | impl ToProtobuf for SelectableSort { 30 | fn to_protobuf(&self, _args: &()) -> models::library_by_type::SelectableSort { 31 | models::library_by_type::SelectableSort { 32 | sort: self.sort.to_protobuf(&()) as i32, 33 | selected: self.selected, 34 | } 35 | } 36 | } 37 | 38 | impl ToProtobuf for Selectable { 39 | fn to_protobuf(&self, _args: &()) -> models::library_by_type::Selectable { 40 | models::library_by_type::Selectable { 41 | sorts: self.sorts.to_protobuf(&()), 42 | } 43 | } 44 | } 45 | 46 | impl ToProtobuf for Catalog { 47 | fn to_protobuf(&self, ctx: &Ctx) -> models::LibraryCatalog { 48 | let items = self 49 | .iter() 50 | .flatten() 51 | .map(|item| item.to_protobuf(&(ctx, None))) 52 | .collect::>(); 53 | let r#type = items.first().map(|item| item.r#type.to_owned()); 54 | models::LibraryCatalog { r#type, items } 55 | } 56 | } 57 | 58 | impl ToProtobuf for LibraryByType { 59 | fn to_protobuf(&self, ctx: &Ctx) -> models::LibraryByType { 60 | models::LibraryByType { 61 | selected: self.selected.to_protobuf(&()), 62 | selectable: self.selectable.to_protobuf(&()), 63 | catalogs: self.catalogs.to_protobuf(ctx), 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/fields/link.rs: -------------------------------------------------------------------------------- 1 | use stremio_core::models::link::Link; 2 | use stremio_core::types::api::LinkAuthKey; 3 | 4 | use crate::bridge::ToProtobuf; 5 | use crate::protobuf::stremio::core::models; 6 | 7 | impl ToProtobuf for Link { 8 | fn to_protobuf(&self, _args: &()) -> models::AuthLink { 9 | models::AuthLink { 10 | code: self.code.to_protobuf(&()), 11 | data: self.data.to_protobuf(&()), 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/commonMain/rust/model/fields/meta_details.rs: -------------------------------------------------------------------------------- 1 | use boolinator::Boolinator; 2 | use stremio_core::deep_links::MetaItemDeepLinks; 3 | use stremio_core::models::ctx::Ctx; 4 | use stremio_core::models::meta_details::{MetaDetails, Selected}; 5 | use stremio_core::runtime::Env; 6 | use stremio_core::types::addon::ResourceRequest; 7 | use stremio_core::types::library::LibraryItem; 8 | use stremio_core::types::resource::{MetaItem, SeriesInfo, Video}; 9 | use stremio_watched_bitfield::WatchedBitField; 10 | 11 | use crate::bridge::{FromProtobuf, ToProtobuf}; 12 | use crate::env::AppleEnv; 13 | use crate::protobuf::stremio::core::{models, types}; 14 | 15 | impl FromProtobuf for models::meta_details::Selected { 16 | fn from_protobuf(&self) -> Selected { 17 | Selected { 18 | meta_path: self.meta_path.from_protobuf(), 19 | stream_path: self.stream_path.from_protobuf(), 20 | guess_stream: self.guess_stream_path, 21 | } 22 | } 23 | } 24 | 25 | impl ToProtobuf for Selected { 26 | fn to_protobuf(&self, _args: &()) -> models::meta_details::Selected { 27 | models::meta_details::Selected { 28 | meta_path: self.meta_path.to_protobuf(&()), 29 | stream_path: self.stream_path.to_protobuf(&()), 30 | guess_stream_path: self.guess_stream, 31 | } 32 | } 33 | } 34 | 35 | impl ToProtobuf for SeriesInfo { 36 | fn to_protobuf(&self, _args: &()) -> types::video::SeriesInfo { 37 | types::video::SeriesInfo { 38 | season: self.season as i64, 39 | episode: self.episode as i64, 40 | } 41 | } 42 | } 43 | 44 | impl FromProtobuf for types::video::SeriesInfo { 45 | fn from_protobuf(&self) -> SeriesInfo { 46 | SeriesInfo { 47 | season: self.season.unsigned_abs() as u32, 48 | episode: self.episode.unsigned_abs() as u32, 49 | } 50 | } 51 | } 52 | 53 | impl 54 | ToProtobuf< 55 | types::Video, 56 | ( 57 | Option<&LibraryItem>, 58 | Option<&WatchedBitField>, 59 | Option<&String>, 60 | ), 61 | > for Video 62 | { 63 | fn to_protobuf( 64 | &self, 65 | (library_item, watched, addon_name): &( 66 | Option<&LibraryItem>, 67 | Option<&WatchedBitField>, 68 | Option<&String>, 69 | ), 70 | ) -> types::Video { 71 | types::Video { 72 | id: self.id.to_string(), 73 | title: self.title.to_string(), 74 | released: self.released.to_protobuf(&()), 75 | overview: self.overview.clone(), 76 | thumbnail: self.thumbnail.clone(), 77 | streams: self.streams.to_protobuf(&(None, *addon_name, None, None)), 78 | series_info: self.series_info.to_protobuf(&()), 79 | upcoming: self 80 | .released 81 | .map(|released| released > AppleEnv::now()) 82 | .unwrap_or_default(), 83 | watched: watched 84 | .map(|watched| watched.get_video(&self.id)) 85 | .unwrap_or_default(), 86 | current_video: library_item 87 | .and_then(|library_item| library_item.state.video_id.to_owned()) 88 | .map(|current_video_id| current_video_id == self.id) 89 | .unwrap_or_default(), 90 | } 91 | } 92 | } 93 | 94 | impl FromProtobuf