Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
66 changes: 66 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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"]
)
]
)
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

---
Expand Down
12 changes: 12 additions & 0 deletions Sources/App/main.swift
Original file line number Diff line number Diff line change
@@ -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()
46 changes: 46 additions & 0 deletions Sources/CorePlayback/MediaPlayer.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
25 changes: 25 additions & 0 deletions Sources/Queue/TrackQueue.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
24 changes: 24 additions & 0 deletions Sources/UI/ConsoleUI.swift
Original file line number Diff line number Diff line change
@@ -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)")
}
}
30 changes: 30 additions & 0 deletions Tests/CorePlaybackTests/MediaPlayerTests.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
17 changes: 17 additions & 0 deletions Tests/QueueTests/TrackQueueTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
11 changes: 11 additions & 0 deletions Tests/UITests/ConsoleUITests.swift
Original file line number Diff line number Diff line change
@@ -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())
}
}
Loading