Add macOS local playback engine

This commit is contained in:
diyaa
2026-05-28 14:05:15 +02:00
parent 183d5db4a9
commit 799c07b068
14 changed files with 1427 additions and 29 deletions
@@ -0,0 +1,86 @@
import Foundation
import XCTest
import VelodyDomain
@testable import VelodyPlayback
@MainActor
final class PlaybackControllerTests: XCTestCase {
func testPlayPauseRestartsTrackAfterPlaybackFinishesAtQueueEnd() {
let engine = FakePlaybackEngine()
let sessionStore = InMemoryPlaybackSessionStore()
let controller = PlaybackController(
engine: engine,
sessionStore: sessionStore
)
let track = LibraryTrack(
id: "track-1",
title: "Finished Track",
artist: "Tester",
durationSeconds: 120,
localFilePath: "/tmp/finished-track.mp3"
)
controller.setCatalogTracks([track])
controller.play(trackID: track.id)
XCTAssertEqual(engine.loadTrackCallCount, 1)
XCTAssertEqual(engine.playCallCount, 1)
XCTAssertTrue(controller.nowPlayingState.isPlaying)
engine.simulatePlaybackFinished()
XCTAssertFalse(controller.nowPlayingState.isPlaying)
XCTAssertEqual(controller.nowPlayingState.currentTime, 120)
controller.playPause()
XCTAssertEqual(engine.loadTrackCallCount, 2)
XCTAssertEqual(engine.playCallCount, 2)
XCTAssertEqual(engine.lastLoadedStartTime, 0)
XCTAssertTrue(controller.nowPlayingState.isPlaying)
XCTAssertEqual(controller.nowPlayingState.currentTime, 0)
}
}
@MainActor
private final class FakePlaybackEngine: PlaybackEngine {
var onEvent: (@MainActor @Sendable (PlaybackEngineEvent) -> Void)?
var currentTime: Double = 0
var duration: Double = 120
var isPlaying = false
private(set) var loadTrackCallCount = 0
private(set) var playCallCount = 0
private(set) var lastLoadedStartTime: Double?
func loadTrack(at fileURL: URL, startTime: Double) throws {
loadTrackCallCount += 1
lastLoadedStartTime = startTime
currentTime = startTime
isPlaying = false
}
func play() throws {
playCallCount += 1
isPlaying = true
}
func pause() {
isPlaying = false
}
func stop() {
isPlaying = false
currentTime = 0
}
func seek(to time: Double) throws {
currentTime = min(max(time, 0), duration)
}
func simulatePlaybackFinished() {
isPlaying = false
currentTime = duration
onEvent?(.finishedPlaying)
}
}
@@ -0,0 +1,86 @@
import XCTest
@testable import VelodyPlayback
final class PlaybackQueueTests: XCTestCase {
func testQueueStartsWithoutCurrentTrackSelection() {
let queue = PlaybackQueue(trackIDs: ["a", "b", "c"])
XCTAssertEqual(queue.catalogTrackIDs, ["a", "b", "c"])
XCTAssertEqual(queue.queuedTrackIDs, ["a", "b", "c"])
XCTAssertNil(queue.currentTrackID)
XCTAssertEqual(queue.nextTrackID(), "a")
}
func testSelectingTrackAndAdvancingUsesQueueOrder() {
var queue = PlaybackQueue(trackIDs: ["a", "b", "c"])
queue.selectTrack("b")
XCTAssertEqual(queue.currentTrackID, "b")
XCTAssertEqual(queue.nextTrackID(), "c")
XCTAssertEqual(queue.previousTrackID(), "a")
XCTAssertEqual(queue.advanceToNextTrack(), "c")
XCTAssertEqual(queue.currentTrackID, "c")
}
func testShuffleCanUsePersistedQueueOrderWhileKeepingSameTrackSet() {
var queue = PlaybackQueue(trackIDs: ["a", "b", "c", "d"])
queue.selectTrack("c")
queue.setShuffleEnabled(true, queuedTrackIDs: ["c", "a", "d", "b"])
XCTAssertTrue(queue.isShuffleEnabled)
XCTAssertEqual(queue.currentTrackID, "c")
XCTAssertEqual(queue.queuedTrackIDs, ["c", "a", "d", "b"])
queue.setShuffleEnabled(false)
XCTAssertFalse(queue.isShuffleEnabled)
XCTAssertEqual(queue.queuedTrackIDs, ["a", "b", "c", "d"])
XCTAssertEqual(queue.currentTrackID, "c")
}
func testRepeatAllWrapsAroundQueueBoundaries() {
var queue = PlaybackQueue(trackIDs: ["a", "b", "c"])
queue.selectTrack("c")
queue.setRepeatMode(.all)
XCTAssertEqual(queue.nextTrackID(), "a")
XCTAssertEqual(queue.advanceToNextTrack(), "a")
XCTAssertEqual(queue.currentTrackID, "a")
XCTAssertEqual(queue.previousTrackID(), "c")
}
func testRepeatOneKeepsCurrentTrackForNextAndPrevious() {
var queue = PlaybackQueue(trackIDs: ["a", "b", "c"])
queue.selectTrack("b")
queue.setRepeatMode(.one)
XCTAssertEqual(queue.nextTrackID(), "b")
XCTAssertEqual(queue.previousTrackID(), "b")
XCTAssertEqual(queue.advanceToNextTrack(), "b")
XCTAssertEqual(queue.moveToPreviousTrack(), "b")
}
func testReplacingTrackIDsDropsRemovedTracksFromQueue() {
var queue = PlaybackQueue(
trackIDs: ["a", "b", "c"],
currentTrackID: "b",
queuedTrackIDs: ["b", "c", "a"],
isShuffleEnabled: true,
repeatMode: .all
)
queue.replaceTrackIDs(
["a", "c", "d"],
currentTrackID: queue.currentTrackID,
queuedTrackIDs: queue.queuedTrackIDs
)
XCTAssertEqual(queue.catalogTrackIDs, ["a", "c", "d"])
XCTAssertEqual(queue.queuedTrackIDs, ["c", "a", "d"])
XCTAssertNil(queue.currentTrackID)
XCTAssertEqual(queue.nextTrackID(), "c")
}
}