diff --git a/CCCApi/Sources/CCCApi/ApiService.swift b/CCCApi/Sources/CCCApi/MediaCCCApiClient.swift similarity index 95% rename from CCCApi/Sources/CCCApi/ApiService.swift rename to CCCApi/Sources/CCCApi/MediaCCCApiClient.swift index 72c4a54..244dc25 100644 --- a/CCCApi/Sources/CCCApi/ApiService.swift +++ b/CCCApi/Sources/CCCApi/MediaCCCApiClient.swift @@ -1,5 +1,5 @@ // -// ApiService.swift +// MediaCCCApiClient.swift // CCCApi // // Created by Mathijs Bernson on 29/07/2022. @@ -7,17 +7,13 @@ import Foundation -@MainActor -@Observable -public class ApiService { +public final class MediaCCCApiClient { private let session: URLSession private let baseURL = URL(string: "https://api.media.ccc.de/public")! private let decoder = JSONDecoder() - public static let shared = ApiService() - - public init() { - session = URLSession(configuration: .default) + public init(urlSession: URLSession = .shared) { + session = urlSession decoder.dateDecodingStrategy = .formatted(CustomISO8601DateFormatter()) } diff --git a/HackerTube.xcodeproj/project.pbxproj b/HackerTube.xcodeproj/project.pbxproj index 03a1f93..d129691 100644 --- a/HackerTube.xcodeproj/project.pbxproj +++ b/HackerTube.xcodeproj/project.pbxproj @@ -402,7 +402,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 18.6; }; name = Debug; }; @@ -425,7 +424,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 18.6; }; name = Release; }; @@ -436,7 +434,6 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; PRODUCT_BUNDLE_IDENTIFIER = nl.bernson.CCCTubeTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; @@ -444,7 +441,6 @@ SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2,3"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HackerTube.app/HackerTube"; - TVOS_DEPLOYMENT_TARGET = 18.6; }; name = Debug; }; @@ -455,7 +451,6 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; PRODUCT_BUNDLE_IDENTIFIER = nl.bernson.CCCTubeTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator"; @@ -463,7 +458,6 @@ SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2,3"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HackerTube.app/HackerTube"; - TVOS_DEPLOYMENT_TARGET = 18.6; }; name = Release; }; @@ -522,6 +516,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_UIRequiredDeviceCapabilities = arm64; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.5.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -530,8 +525,12 @@ SDKROOT = appletvos; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; + TVOS_DEPLOYMENT_TARGET = 18.6; + XROS_DEPLOYMENT_TARGET = 26.0; }; name = Debug; }; @@ -584,16 +583,21 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_UIRequiredDeviceCapabilities = arm64; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.5.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = appletvos; STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_OPTIMIZATION_LEVEL = "-O"; - SWIFT_VERSION = 6.0; + SWIFT_VERSION = 5.0; + TVOS_DEPLOYMENT_TARGET = 18.6; VALIDATE_PRODUCT = YES; + XROS_DEPLOYMENT_TARGET = 26.0; }; name = Release; }; @@ -623,7 +627,6 @@ INFOPLIST_KEY_UIRequiredDeviceCapabilities = arm64; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -637,8 +640,6 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; TARGETED_DEVICE_FAMILY = "1,2,3,6,7"; - TVOS_DEPLOYMENT_TARGET = 18.6; - XROS_DEPLOYMENT_TARGET = 26.0; }; name = Debug; }; @@ -668,7 +669,6 @@ INFOPLIST_KEY_UIRequiredDeviceCapabilities = arm64; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UIUserInterfaceStyle = Automatic; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -682,8 +682,6 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; TARGETED_DEVICE_FAMILY = "1,2,3,6,7"; - TVOS_DEPLOYMENT_TARGET = 18.6; - XROS_DEPLOYMENT_TARGET = 26.0; }; name = Release; }; @@ -696,7 +694,6 @@ DEAD_CODE_STRIPPING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; PRODUCT_BUNDLE_IDENTIFIER = nl.bernson.CCCTubeUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -706,7 +703,6 @@ SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2,3"; TEST_TARGET_NAME = HackerTube; - TVOS_DEPLOYMENT_TARGET = 18.6; }; name = Debug; }; @@ -719,7 +715,6 @@ DEAD_CODE_STRIPPING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.6; PRODUCT_BUNDLE_IDENTIFIER = nl.bernson.CCCTubeUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -728,7 +723,6 @@ SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2,3"; TEST_TARGET_NAME = HackerTube; - TVOS_DEPLOYMENT_TARGET = 18.6; }; name = Release; }; diff --git a/HackerTube/Domain/ApiService.swift b/HackerTube/Domain/ApiService.swift new file mode 100644 index 0000000..45ff76a --- /dev/null +++ b/HackerTube/Domain/ApiService.swift @@ -0,0 +1,56 @@ +// +// ApiService.swift +// HackerTube +// +// Created by Mathijs Bernson on 13/01/2026. +// + +import CCCApi +import Foundation + +@Observable +class ApiService { + private let client: MediaCCCApiClient + + init() { + client = .init() + } + + // MARK: Conferences + + func conferences() async throws -> [Conference] { + try await client.conferences() + } + + func conference(acronym: String) async throws -> Conference { + try await client.conference(acronym: acronym) + } + + // MARK: Talks + + func talk(id: String) async throws -> Talk { + try await client.talk(id: id) + } + +// func talks() async throws -> [Talk] { +// try await client.talks() +// } + + func recentTalks() async throws -> [Talk] { + try await client.recentTalks() + } + + func popularTalks(in year: Int) async throws -> [Talk] { + try await client.popularTalks(in: year) + } + + func searchTalks(query: String) async throws -> [Talk] { + try await client.searchTalks(query: query) + } + + // MARK: Recordings + + func recordings(for talk: Talk) async throws -> [Recording] { + try await client.recordings(for: talk) + } +} diff --git a/HackerTube/Features/Talk/TalkPlayerViewModel.swift b/HackerTube/Features/Talk/TalkPlayerViewModel.swift index 6858ba4..e6e8356 100644 --- a/HackerTube/Features/Talk/TalkPlayerViewModel.swift +++ b/HackerTube/Features/Talk/TalkPlayerViewModel.swift @@ -11,8 +11,7 @@ import Foundation import os.log @Observable -@MainActor -final class TalkPlayerViewModel { +class TalkPlayerViewModel { var player = AVPlayer() var currentRecording: Recording? diff --git a/HackerTube/Features/Talk/TalkViewModel.swift b/HackerTube/Features/Talk/TalkViewModel.swift index 2ef666a..e2fff71 100644 --- a/HackerTube/Features/Talk/TalkViewModel.swift +++ b/HackerTube/Features/Talk/TalkViewModel.swift @@ -16,17 +16,22 @@ enum CopyrightState: Equatable { } @Observable -@MainActor -final class TalkViewModel { +class TalkViewModel { var currentTalk: Talk? var recordings: [Recording] = [] var preferredRecording: Recording? var copyright: CopyrightState = .loading - private let mediaAnalyzer = MediaAnalyzer() + private let client: MediaCCCApiClient + private let mediaAnalyzer: MediaAnalyzer + + init() { + client = .init() + mediaAnalyzer = .init() + } func loadRecordings(for talk: Talk) async throws { - let recordings = try await ApiService.shared.recordings(for: talk) + let recordings = try await client.recordings(for: talk) currentTalk = talk self.recordings = recordings let hdRecording = recordings.first(where: { $0.isHighQuality && $0.isVideo }) diff --git a/HackerTube/HackerTubeApp.swift b/HackerTube/HackerTubeApp.swift index 7ef7206..d5f8379 100644 --- a/HackerTube/HackerTubeApp.swift +++ b/HackerTube/HackerTubeApp.swift @@ -16,7 +16,7 @@ struct HackerTubeApp: App { var body: some Scene { WindowGroup { ContentView() - .environment(ApiService.shared) + .environment(ApiService()) .onAppear { do { try AVAudioSession.sharedInstance().setCategory(.playback) diff --git a/HackerTubeUITests/HackerTubeUIScreenshotTests.swift b/HackerTubeUITests/HackerTubeUIScreenshotTests.swift index f961b42..22688c7 100644 --- a/HackerTubeUITests/HackerTubeUIScreenshotTests.swift +++ b/HackerTubeUITests/HackerTubeUIScreenshotTests.swift @@ -7,8 +7,7 @@ import XCTest -@MainActor -final class HackerTubeUIScreenshotTests: XCTestCase { +class HackerTubeUIScreenshotTests: XCTestCase { var app: XCUIApplication! override func setUpWithError() throws { diff --git a/TopShelf/ContentProvider.swift b/TopShelf/ContentProvider.swift index e50a77f..141dd0e 100644 --- a/TopShelf/ContentProvider.swift +++ b/TopShelf/ContentProvider.swift @@ -13,7 +13,7 @@ class ContentProvider: TVTopShelfContentProvider { override func loadTopShelfContent() async -> TVTopShelfContent? { do { - let api = await ApiService.shared + let api = MediaCCCApiClient() async let recentTalks = api.recentTalks() let currentYear = Calendar.current.component(.year, from: .now) async let popularTalks = api.popularTalks(in: currentYear)