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
60 changes: 60 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: πŸ› Bug Report
description: Create a report to help us reproduce and fix a bug in Cygnus Player.
title: "[BUG]: "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thank you for reporting a bug! Please ensure you are testing against local MP3 files and M3U/M3U8 playlists.
- type: input
id: version
attributes:
label: Cygnus Player Version
description: What version of Cygnus Player are you using? (Check Settings screen or version.properties)
placeholder: "e.g., 1.0.2"
validations:
required: true
- type: input
id: environment
attributes:
label: Device & Android Version
description: What device and Android version are you using?
placeholder: "e.g., Pixel 8 Pro running Android 16 (API 36) / Android Emulator API 36"
validations:
required: true
- type: textarea
id: description
attributes:
label: Description & Reproduction Steps
description: Explain the issue clearly and list the exact steps to reproduce it.
placeholder: |
1. Open Cygnus Player
2. Load M3U playlist "rock_classics.m3u"
3. Tap Play/Pause toggle
4. See error...
validations:
required: true
- type: textarea
id: expected-actual
attributes:
label: Expected vs Actual Behavior
description: Describe what you expected to happen versus what actually occurred.
validations:
required: true
- type: textarea
id: playlist-context
attributes:
label: Playlist & Audio Context
description: Detail relevant audio format, M3U path structure (relative vs absolute), ReplayGain tags, or SAF root folder permissions.
placeholder: "e.g., Relative path M3U playlist with 15,000 MP3 files stored in Scoped Storage root Music folder."
validations:
required: false
- type: textarea
id: logs
attributes:
label: Logcat / Stack Trace / Screenshots
description: Paste any relevant logcat output, stack traces, or upload screenshots.
render: shell
validations:
required: false
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Contribution Guidelines
url: https://github.com/FesterHead/cygnus-player/blob/develop/CONTRIBUTING.md
about: Review project scope, architectural rules, and technical standards before opening a pull request.
- name: Storage & SAF Setup
url: https://github.com/FesterHead/cygnus-player/blob/develop/README.md#-%EF%B8%8F-storage--scoped-storage-compliance
about: Guidelines for setting up Scoped Storage access and MediaStore relative path resolution.
41 changes: 41 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: πŸš€ Feature Request
description: Propose an idea or enhancement for Cygnus Player.
title: "[FEATURE]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thank you for proposing a feature! Before submitting, please review [CONTRIBUTING.md](../CONTRIBUTING.md) to ensure your request aligns with Cygnus Player's minimalist design philosophy.
- type: checkboxes
id: scope-alignment
attributes:
label: Alignment with Project Scope
options:
- label: I confirm this feature is strictly for **local MP3 playback** and **M3U/M3U8 playlists**.
required: true
- label: I confirm this proposal maintains a **minimalist UI** (no seek bars, no previous/next skip controls).
required: true
- label: I confirm this proposal maintains a low memory footprint ($O(1)$ relative to queue size).
required: true
- type: textarea
id: problem-statement
attributes:
label: Problem Statement / Use Case
description: What problem does this feature solve or what workflow does it enhance?
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: Proposed Solution
description: Describe how you envision this feature working.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternative Solutions Considered
description: Have you considered any alternative solutions or workarounds?
validations:
required: false
21 changes: 21 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# πŸ“ Description

Please include a summary of the change, the rationale behind it, and any relevant motivation or context.

Fixes / Closes #(issue number)

## 🌌 Scope & Architecture Checklist

Please confirm that your Pull Request adheres to Cygnus Player's core constraints:

- [ ] **MP3 Playback Only**: Changes are strictly for local MP3 audio files and M3U/M3U8 playlists.
- [ ] **Minimalist UX**: Maintains single Play/Pause control; no seek bars or skip/navigation buttons added.
- [ ] **Linear Memory Footprint ($O(1)$)**: Low memory footprint relative to queue size maintained using primitive arrays (`LongArray`/`IntArray`) and Room sliding cursors.
- [ ] **Gapless & Audio Focus**: Media3 gapless playback and Android Audio Focus handling preserved.

## πŸ§ͺ Testing Checklist

- [ ] I have executed `./gradlew test` locally and all unit tests pass.
- [ ] New classes, interfaces, methods, and functions include full KDoc documentation (`@param`, `@return`).
- [ ] No raw absolute file paths or unhandled exceptions introduced.
- [ ] `CHANGELOG.md` updated under `## [Unreleased]` (if applicable).
10 changes: 9 additions & 1 deletion .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ jobs:
build:
name: Build & Test
runs-on: ubuntu-latest
env:
HAS_KEYSTORE: ${{ secrets.KEYSTORE_BASE64 != '' }}

steps:
- name: Checkout code
Expand All @@ -32,20 +34,26 @@ jobs:
- name: Run Unit Tests
run: ./gradlew test

- name: Build Debug APK
run: ./gradlew assembleDebug

- name: Decode Keystore
if: env.HAS_KEYSTORE == 'true'
env:
ENCODED_KEYSTORE: ${{ secrets.KEYSTORE_BASE64 }}
run: echo $ENCODED_KEYSTORE | base64 --decode > cygnus-release.keystore

- name: Build Release APK
if: env.HAS_KEYSTORE == 'true'
env:
KEYSTORE_FILE_PATH: ../cygnus-release.keystore
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: ./gradlew assembleRelease

- name: Upload APK Artifact
- name: Upload Release APK Artifact
if: env.HAS_KEYSTORE == 'true'
uses: actions/upload-artifact@v4
with:
name: cygnus-player-release
Expand Down
12 changes: 12 additions & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"MD013": false,
"MD024": {
"siblings_only": true
},
"MD025": false,
"MD033": {
"allowed_elements": [
"img"
]
}
}
14 changes: 13 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
"java.configuration.updateBuildConfiguration": "automatic",
"markdownlint.config": {
"MD013": false,
"MD024": {
"siblings_only": true
},
"MD025": false,
"MD033": {
"allowed_elements": [
"img"
]
}
}
}
48 changes: 24 additions & 24 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ You are an expert Android Engineer specializing in low-overhead audio architectu
## 1. Commit and Push Protocol

- **No Automated Git Operations:** The AI Agent is strictly prohibited from executing `git commit` or `git push`.
- **Incremental Changes:** Work will be broken down into small, logical increments. The Agent will pause after each increment to allow the user to review, commit, and push via the Android Studio UI.
- **Incremental Changes:** Work will be broken down into small, logical increments. The Agent will pause after each increment to allow the user to review, commit, and push via their preferred IDE interface (such as Antigravity IDE or Android Studio).
- **Branching Strategy:** All feature development and AI-assisted coding must target the `develop` branch. Pull requests from `develop` to `main` are used to trigger signed Release builds via GitHub Actions.
- **Versioning Strategy:** The `version.properties` file in the project root is the source of truth for the app's version. To avoid `BuildConfig` instability in experimental environments, a manual `VersionInfo.kt` object must also be maintained in sync with this file. Before any code is merged into `main` for a release, the AI Agent must verify and update `VERSION_NAME` and `VERSION_CODE` in both `version.properties` and `app/src/main/java/com/festerhead/cygnusplayer/VersionInfo.kt`. The GitHub Action will use `version.properties` to automatically publish a public Release.
- **Static Analysis:** The Agent must run `analyze_file` (IDE Inspections) on every modified file to identify and resolve unused code, deprecations, and style issues before task completion.
- **Documentation Standards:** Every new class, interface, method, and function must include proper KDoc documentation. KDoc must clearly explain the purpose, parameters (`@param`), and return values (`@return`). For Room entities, explain the role of each column.
- **AI Commit Messages:** The user will utilize the Android Studio AI-generated commit message feature.
- **AI Commit Messages:** The user will utilize their preferred IDE's AI-generated commit message feature (such as Antigravity IDE or Android Studio).

## 2. Automated Testing Strategy

Expand All @@ -26,7 +26,7 @@ You are an expert Android Engineer specializing in low-overhead audio architectu

- **Package Namespace:** `com.festerhead.cygnusplayer`

- **Target Platform:** Android 17 (API Level 37)
- **Target Platform:** Android 16 (API Level 36)

- **Language:** Always generate concise Kotlin code using modern language features (coroutines, flows). Do not write Java boilerplate.

Expand All @@ -41,11 +41,11 @@ You are an expert Android Engineer specializing in low-overhead audio architectu
- **Accessibility & Theme:** Force a Dark Mode UI using the Monokai Pro (Filter Spectrum) color palette. Prioritize high-contrast Purple (#AB9DF2), Blue/Cyan (#78DCE8), and Orange (#FC9867) to ensure readability for red-green color-blind users. Avoid relying on Red/Green for critical state information.

- **UI/UX Philosophy:** The player is strictly minimalist. No "Next" or "Previous" buttons. No "Seek" bar (unless for visual progress only). The UI should focus on the current track and its position in the sequence (e.g., `2112/47533`).
- **Controls:** A single, high-contrast Play/Pause toggle that dynamically changes icon based on playback state.
- **Controls:** A single, high-contrast Play/Pause toggle that dynamically changes icon based on playback state.
- **Shuffle Cycling:** Changing the Shuffle Mode on the main player screen is not supported. Playlists are "minted" with an immutable Shuffle Mode upon loading; to change the mode, the playlist must be deleted from history and re-added.
- **Smart ReplayGain:** ReplayGain is automatic and context-aware. Use `ALBUM_GAIN` for `SEQUENTIAL` and `RANDOM_FOLDER_SEQUENTIAL` modes. Use `TRACK_GAIN` for `TRACK_RANDOM`.
- **Text Handling:**
- **Main App:** Use `Modifier.basicMarquee()` for song titles and album names to ensure long strings are fully readable.
- **Text Handling:**
- **Main App:** Use `Modifier.basicMarquee()` for song titles and album names to ensure long strings are fully readable.
- **Widget:** Use clean truncation (Ellipsis) for text to maintain a static, glanceable layout.

- **Changelog Maintenance:** Maintain `CHANGELOG.md` according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Ensure the version exactly matches `VERSION_NAME` in `version.properties`. Mint the numbered release heading before opening a pull request to `main`. When minting a new release, you MUST explicitly ask the user whether to increment the MAJOR, MINOR, or PATCH version number before finalizing the file changes.
Expand All @@ -56,9 +56,9 @@ You are an expert Android Engineer specializing in low-overhead audio architectu

## 1. M3U Sequence Mapping (Crucial)

- Traditional media queues struggle with duplicate entries of the same file path.
- Traditional media queues struggle with duplicate entries of the same file path.

- To handle repeated tracks cleanly, never map paths directly to the playlist queue.
- To handle repeated tracks cleanly, never map paths directly to the playlist queue.

- Always map database records and `MediaItem` tags to a unique, sequential ID (`sequence\_id` or `queue\_id`) so the playlist loop engine treats consecutive duplicate files as separate, unique nodes in the playback sequence.

Expand All @@ -84,7 +84,7 @@ You are an expert Android Engineer specializing in low-overhead audio architectu

## 4. Scrobbling Integration

- Do not write internal code for Last.fm or external tracking metrics.
- Do not write internal code for Last.fm or external tracking metrics.

- Simply ensure the `MediaSession` metadata is updated instantly and accurately on every track transition. External apps like Pano Scrobbler will capture these standard broadcasted system events natively.

Expand All @@ -97,13 +97,13 @@ You are an expert Android Engineer specializing in low-overhead audio architectu
- **Manageable Playlist History:** The file selection screen must maintain a list of recently opened M3U files for quick switching, ordered by most recently opened. Users must be able to remove individual playlists from this history to keep the selection interface clean.

- **Relative M3U Path Resolution:**
- The app must resolve playlist track paths relative to the directory where the `.m3u` / `.m3u8` file is located.
- Absolute file paths (e.g., paths containing Windows drive letters or starting with `/`) are explicitly unsupported to ensure library portability.
- The parser must ignore `#EXTINF` metadata tags, relying exclusively on the physical audio files as the source of truth for Artist, Album, and Title.
- **Initial M3U Parsing:** The initial parser only extracts and mints relative file paths and `sequence_ids` into the database.
- **Async Metadata Extraction:** Metadata extraction (reading physical ID3 tags/ReplayGain) is handled asynchronously in the background by a separate worker to ensure UI and playback responsiveness.
- **Metadata Fallback:** If tags are missing from the physical file after background processing, the extractor must return `"<not found>"` for those fields to maintain UI consistency.
- **Minting Workflow:** When a new playlist is added, the user must explicitly choose its initial `ShuffleMode`. This "Minted Identity" is immutable once selected; to change the mode, the playlist must be deleted from history and re-added. This ensures deterministic behavior for specialized collections.
- The app must resolve playlist track paths relative to the directory where the `.m3u` / `.m3u8` file is located.
- Absolute file paths (e.g., paths containing Windows drive letters or starting with `/`) are explicitly unsupported to ensure library portability.
- The parser must ignore `#EXTINF` metadata tags, relying exclusively on the physical audio files as the source of truth for Artist, Album, and Title.
- **Initial M3U Parsing:** The initial parser only extracts and mints relative file paths and `sequence_ids` into the database.
- **Async Metadata Extraction:** Metadata extraction (reading physical ID3 tags/ReplayGain) is handled asynchronously in the background by a separate worker to ensure UI and playback responsiveness.
- **Metadata Fallback:** If tags are missing from the physical file after background processing, the extractor must return `"<not found>"` for those fields to maintain UI consistency.
- **Minting Workflow:** When a new playlist is added, the user must explicitly choose its initial `ShuffleMode`. This "Minted Identity" is immutable once selected; to change the mode, the playlist must be deleted from history and re-added. This ensures deterministic behavior for specialized collections.

- If a relative file path appears multiple times sequentially or non-sequentially, the parser must cleanly instantiate a new database queue entry with a distinct `sequence\_id` for every occurrence.

Expand All @@ -114,17 +114,17 @@ You are an expert Android Engineer specializing in low-overhead audio architectu
- **Immutable Identity:** A playlist's shuffle strategy is locked at the moment of "minting" (load time). The UI allows for manual "Reshuffle" actions within the same mode, but changing the mode requires a fresh load.

- **Forward-Only Principle:** Manual reshuffles or mode transitions (if implemented) must never "wrap around" to previously played tracks in the current album or folder context. Any tracks appearing before the current playback anchor in the folder sequence are **permanently discarded** from the new queue to keep the experience forward-moving.
- **Structural Truncation:** Any tracks appearing before the current playback anchor in the folder sequence are permanently discarded from the new queue array.
- **Persistence:** This truncated `LongArray` must be persisted back to the `PlaylistStateEntity` in Room immediately.
- **UI Synchronization:** Updating the database with the new, truncated array ensures the UI counter (e.g., `1/91`) remains perfectly synchronized with the active playback sequence.
- **Structural Truncation:** Any tracks appearing before the current playback anchor in the folder sequence are permanently discarded from the new queue array.
- **Persistence:** This truncated `LongArray` must be persisted back to the `PlaylistStateEntity` in Room immediately.
- **UI Synchronization:** Updating the database with the new, truncated array ensures the UI counter (e.g., `1/91`) remains perfectly synchronized with the active playback sequence.

- **TRACK_RANDOM:** Shuffle mapping must be calculated using unique `sequence_id` bounds. UI should display current shuffled position (e.g., `5/100`).

- **RANDOM_FOLDER_SEQUENTIAL:**
- Shuffle the list of directories.
- Play all tracks in a directory sequentially.
- **History Buffer:** Maintain a history of at least 24 folders. Do not re-select a folder from this history.
- **Constraint:** If the total folder count in the M3U is less than 24, the history buffer size is 0.
- Shuffle the list of directories.
- Play all tracks in a directory sequentially.
- **History Buffer:** Maintain a history of at least 24 folders. Do not re-select a folder from this history.
- **Constraint:** If the total folder count in the M3U is less than 24, the history buffer size is 0.

## 7. Persistence & State

Expand All @@ -146,7 +146,7 @@ You are an expert Android Engineer specializing in low-overhead audio architectu

- Never instantiate heavy domain models or UI state representations for the entire queue or library at once.

- All shuffle mappings, track selections, and queue indices must be processed using low-overhead primitive data types (`IntArray` or `LongArray`).
- All shuffle mappings, track selections, and queue indices must be processed using low-overhead primitive data types (`IntArray` or `LongArray`).

- **Sequence ID Streaming:** The `QueueDao` must provide a `getAllQueueIds()` query to stream unique sequential IDs directly into primitive arrays for the `ShuffleEngine`.

Expand Down
Loading
Loading