From e255191880fbbb4d7ae2e63d487dd2f849644c09 Mon Sep 17 00:00:00 2001 From: NeilMuss <37507669+NeilMuss@users.noreply.github.com> Date: Fri, 26 Dec 2025 23:13:09 -0500 Subject: [PATCH] Add Swift package scaffolding and CI --- .github/workflows/ci.yml | 24 +++++++ Makefile | 16 +++++ Package.swift | 66 +++++++++++++++++++ README.md | 26 +++++++- Sources/App/main.swift | 12 ++++ Sources/CorePlayback/MediaPlayer.swift | 46 +++++++++++++ Sources/Queue/TrackQueue.swift | 25 +++++++ Sources/UI/ConsoleUI.swift | 24 +++++++ .../CorePlaybackTests/MediaPlayerTests.swift | 30 +++++++++ Tests/QueueTests/TrackQueueTests.swift | 17 +++++ Tests/UITests/ConsoleUITests.swift | 11 ++++ 11 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 Makefile create mode 100644 Package.swift create mode 100644 Sources/App/main.swift create mode 100644 Sources/CorePlayback/MediaPlayer.swift create mode 100644 Sources/Queue/TrackQueue.swift create mode 100644 Sources/UI/ConsoleUI.swift create mode 100644 Tests/CorePlaybackTests/MediaPlayerTests.swift create mode 100644 Tests/QueueTests/TrackQueueTests.swift create mode 100644 Tests/UITests/ConsoleUITests.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1ddd162 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + pull_request: + push: + branches: [ main ] + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Swift + uses: swift-actions/setup-swift@v1 + with: + swift-version: '6.0' + + - name: Build + run: swift build --build-path .build + + - name: Test + run: swift test --build-path .build diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b63648d --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ + +.PHONY: build test run clean + +SWIFT=swift + +build: + $(SWIFT) build --build-path .build + +test: + $(SWIFT) test --build-path .build + +run: + $(SWIFT) run --build-path .build App + +clean: + rm -rf .build diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..85a16c9 --- /dev/null +++ b/Package.swift @@ -0,0 +1,66 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "OpenPlayt", + platforms: [ + .macOS(.v13), + .iOS(.v17) + ], + products: [ + .executable( + name: "App", + targets: ["App"] + ), + .library( + name: "CorePlayback", + targets: ["CorePlayback"] + ), + .library( + name: "Queue", + targets: ["Queue"] + ), + .library( + name: "UI", + targets: ["UI"] + ) + ], + targets: [ + .executableTarget( + name: "App", + dependencies: [ + "CorePlayback", + "Queue", + "UI" + ] + ), + .target( + name: "CorePlayback" + ), + .target( + name: "Queue", + dependencies: [ + "CorePlayback" + ] + ), + .target( + name: "UI", + dependencies: [ + "CorePlayback", + "Queue" + ] + ), + .testTarget( + name: "CorePlaybackTests", + dependencies: ["CorePlayback"] + ), + .testTarget( + name: "QueueTests", + dependencies: ["Queue", "CorePlayback"] + ), + .testTarget( + name: "UITests", + dependencies: ["UI", "CorePlayback", "Queue"] + ) + ] +) diff --git a/README.md b/README.md index 5f709d9..633dcf1 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,31 @@ Generic Web Player ├── style.css # Layout and typography ├── player.js # Playback + album logic ├── logo.js # Playt logo animation -└── README.md` +├── README.md +└── Package.swift # Swift Package Manager entry point for native prototypes +``` + +### Swift package layout + +``` +├── Package.swift +├── Sources +│ ├── App # Executable entry point wiring the modules together +│ ├── CorePlayback # Playback state and track model +│ ├── Queue # Simple queue for tracks +│ └── UI # Console-based façade over playback + queue +└── Tests + ├── CorePlaybackTests + ├── QueueTests + └── UITests +``` + +Run the native prototype with SwiftPM (or the provided `Makefile` shortcuts): + +```bash +make build # Compile all targets +make test # Run unit tests +make run # Execute the App target ``` --- diff --git a/Sources/App/main.swift b/Sources/App/main.swift new file mode 100644 index 0000000..5c640cc --- /dev/null +++ b/Sources/App/main.swift @@ -0,0 +1,12 @@ +import CorePlayback +import Queue +import UI + +let tracks = [ + Track(id: "1", title: "First Light", artist: "OpenPlayt"), + Track(id: "2", title: "Second Wave", artist: "OpenPlayt"), +] + +let queue = TrackQueue(items: tracks) +let ui = ConsoleUI(player: MediaPlayer(), queue: queue) +ui.startDemoSession() diff --git a/Sources/CorePlayback/MediaPlayer.swift b/Sources/CorePlayback/MediaPlayer.swift new file mode 100644 index 0000000..cf5487d --- /dev/null +++ b/Sources/CorePlayback/MediaPlayer.swift @@ -0,0 +1,46 @@ +public struct Track { + public let id: String + public let title: String + public let artist: String + + public init(id: String, title: String, artist: String) { + self.id = id + self.title = title + self.artist = artist + } +} + +public final class MediaPlayer { + public enum State { + case stopped + case playing(Track) + case paused(Track) + } + + public private(set) var state: State = .stopped + + public init() {} + + @discardableResult + public func play(_ track: Track) -> State { + state = .playing(track) + return state + } + + @discardableResult + public func pause() -> State { + switch state { + case .playing(let track): + state = .paused(track) + default: + break + } + return state + } + + @discardableResult + public func stop() -> State { + state = .stopped + return state + } +} diff --git a/Sources/Queue/TrackQueue.swift b/Sources/Queue/TrackQueue.swift new file mode 100644 index 0000000..606effe --- /dev/null +++ b/Sources/Queue/TrackQueue.swift @@ -0,0 +1,25 @@ +import CorePlayback + +public final class TrackQueue { + private var items: [Track] + + public init(items: [Track] = []) { + self.items = items + } + + public var isEmpty: Bool { items.isEmpty } + + public func enqueue(_ track: Track) { + items.append(track) + } + + @discardableResult + public func dequeue() -> Track? { + guard !items.isEmpty else { return nil } + return items.removeFirst() + } + + public func peek() -> Track? { + items.first + } +} diff --git a/Sources/UI/ConsoleUI.swift b/Sources/UI/ConsoleUI.swift new file mode 100644 index 0000000..51e00ce --- /dev/null +++ b/Sources/UI/ConsoleUI.swift @@ -0,0 +1,24 @@ +import CorePlayback +import Queue + +public struct ConsoleUI { + private let player: MediaPlayer + private let queue: TrackQueue + + public init(player: MediaPlayer = MediaPlayer(), queue: TrackQueue = TrackQueue()) { + self.player = player + self.queue = queue + } + + public func startDemoSession() { + print("OpenPlayt Console UI") + if queue.isEmpty { + print("Queue is empty. Add tracks to start playback.") + return + } + + guard let nowPlaying = queue.peek() else { return } + _ = player.play(nowPlaying) + print("Now playing: \(nowPlaying.title) by \(nowPlaying.artist)") + } +} diff --git a/Tests/CorePlaybackTests/MediaPlayerTests.swift b/Tests/CorePlaybackTests/MediaPlayerTests.swift new file mode 100644 index 0000000..ae0d512 --- /dev/null +++ b/Tests/CorePlaybackTests/MediaPlayerTests.swift @@ -0,0 +1,30 @@ +import XCTest +@testable import CorePlayback + +final class MediaPlayerTests: XCTestCase { + func testPlayAndPauseUpdatesState() { + let track = Track(id: "1", title: "Test", artist: "Artist") + let player = MediaPlayer() + + player.play(track) + if case .playing(let nowPlaying) = player.state { + XCTAssertEqual(nowPlaying.id, track.id) + } else { + XCTFail("Player did not enter playing state") + } + + player.pause() + if case .paused(let paused) = player.state { + XCTAssertEqual(paused.title, track.title) + } else { + XCTFail("Player did not pause") + } + + player.stop() + if case .stopped = player.state { + XCTAssertTrue(true) + } else { + XCTFail("Player did not stop") + } + } +} diff --git a/Tests/QueueTests/TrackQueueTests.swift b/Tests/QueueTests/TrackQueueTests.swift new file mode 100644 index 0000000..01a50e1 --- /dev/null +++ b/Tests/QueueTests/TrackQueueTests.swift @@ -0,0 +1,17 @@ +import XCTest +import Queue +import CorePlayback + +final class TrackQueueTests: XCTestCase { + func testEnqueueAndDequeue() { + let queue = TrackQueue() + XCTAssertTrue(queue.isEmpty) + + queue.enqueue(Track(id: "1", title: "Hello", artist: "Test")) + XCTAssertFalse(queue.isEmpty) + + let next = queue.dequeue() + XCTAssertEqual(next?.title, "Hello") + XCTAssertTrue(queue.isEmpty) + } +} diff --git a/Tests/UITests/ConsoleUITests.swift b/Tests/UITests/ConsoleUITests.swift new file mode 100644 index 0000000..76045b9 --- /dev/null +++ b/Tests/UITests/ConsoleUITests.swift @@ -0,0 +1,11 @@ +import XCTest +@testable import UI +import CorePlayback +import Queue + +final class ConsoleUITests: XCTestCase { + func testDemoSessionDoesNotCrashWhenQueueEmpty() { + let ui = ConsoleUI(player: MediaPlayer(), queue: TrackQueue()) + XCTAssertNoThrow(ui.startDemoSession()) + } +}