- Everything you'd expect from an offline music player!
- Map your music folders and browse your library in an organized view.
- Create playlists and manage the play queue interactively.
- Browse music using folder view when needed.
- Pin anything (almost!) to the sidebar for quick access to your favorite music.
- Navigate easily: right-click a track to go to its album, artist, year, etc.
- Native macOS integration with menubar and dock playback controls, plus dark mode support.
- Search quickly through large libraries containing thousands of songs.
💡 Tip: Petrichor relies heavily on tracks having good metadata for all its features to work well.
- Smart playlists with user-configurable conditional filters
- AirPlay 2 casting support
- Miniplayer and full-screen modes
- Automatic in-app updates
- Online album & artist information fetching
- ... and much more!
- Go to Releases and download the latest
.dmg
. - Open the
.dmg
and drag the app icon into the Applications folder. - In Applications, right-click Petrichor > Open.
P.S. I plan publish it on Homebrew soon.
I have a large collection of music files that I’ve gathered over the years, and I missed having a good offline music player on macOS. I used Swinsian (great app, by the way!), but it hasn't been updated in years. I also missed features commonly found in streaming apps; so I built Petrichor to scratch that itch and learn Swift and macOS app development along the way!
- Built entirely with Swift and SwiftUI for the best macOS integration.
- Once folders are added, the app scans them and populates a SQLite database using GRDB.
- Petrichor does not alter your music files, it only reads from the directories you add.
- Tracks searching is powered by SQLite FTS5 with fall-back to in-memory search.
- Playback is powered by AVFoundation.
View Database Schema
erDiagram
folders {
INTEGER id PK "AUTO_INCREMENT"
TEXT name "NOT NULL"
TEXT path "NOT NULL UNIQUE"
INTEGER track_count "NOT NULL DEFAULT 0"
DATETIME date_added "NOT NULL"
DATETIME date_updated "NOT NULL"
BLOB bookmark_data "Security-scoped bookmark"
}
artists {
INTEGER id PK "AUTO_INCREMENT"
TEXT name "NOT NULL"
TEXT normalized_name "NOT NULL UNIQUE"
TEXT sort_name
BLOB artwork_data
TEXT bio
TEXT bio_source
DATETIME bio_updated_at
TEXT image_url
TEXT image_source
DATETIME image_updated_at
TEXT discogs_id
TEXT musicbrainz_id
TEXT spotify_id
TEXT apple_music_id
TEXT country
INTEGER formed_year
INTEGER disbanded_year
TEXT genres "JSON array"
TEXT websites "JSON array"
TEXT members "JSON array"
INTEGER total_tracks "NOT NULL DEFAULT 0 CHECK >= 0"
INTEGER total_albums "NOT NULL DEFAULT 0 CHECK >= 0"
DATETIME created_at "NOT NULL"
DATETIME updated_at "NOT NULL"
}
albums {
INTEGER id PK "AUTO_INCREMENT"
TEXT title "NOT NULL"
TEXT normalized_title "NOT NULL"
TEXT sort_title
BLOB artwork_data
TEXT release_date
INTEGER release_year "CHECK 1900-2100"
TEXT album_type
INTEGER total_tracks "CHECK >= 0"
INTEGER total_discs "CHECK >= 0"
TEXT description
TEXT review
TEXT review_source
TEXT cover_art_url
TEXT thumbnail_url
TEXT discogs_id
TEXT musicbrainz_id
TEXT spotify_id
TEXT apple_music_id
TEXT label
TEXT catalog_number
TEXT barcode
TEXT genres "JSON array"
DATETIME created_at "NOT NULL"
DATETIME updated_at "NOT NULL"
}
album_artists {
INTEGER album_id FK "NOT NULL"
INTEGER artist_id FK "NOT NULL"
TEXT role "NOT NULL DEFAULT 'primary'"
INTEGER position "NOT NULL DEFAULT 0"
}
genres {
INTEGER id PK "AUTO_INCREMENT"
TEXT name "NOT NULL UNIQUE"
}
tracks {
INTEGER id PK "AUTO_INCREMENT"
INTEGER folder_id FK "NOT NULL"
INTEGER album_id FK
TEXT path "NOT NULL UNIQUE"
TEXT filename "NOT NULL"
TEXT title
TEXT artist
TEXT album
TEXT composer
TEXT genre
TEXT year
REAL duration "CHECK >= 0"
TEXT format
INTEGER file_size
DATETIME date_added "NOT NULL"
DATETIME date_modified
BLOB track_artwork_data
BOOLEAN is_favorite "NOT NULL DEFAULT false"
INTEGER play_count "NOT NULL DEFAULT 0"
DATETIME last_played_date
BOOLEAN is_duplicate "NOT NULL DEFAULT false"
INTEGER primary_track_id FK
TEXT duplicate_group_id
TEXT album_artist
INTEGER track_number "CHECK > 0"
INTEGER total_tracks
INTEGER disc_number "CHECK > 0"
INTEGER total_discs
INTEGER rating "CHECK 0-5"
BOOLEAN compilation "DEFAULT false"
TEXT release_date
TEXT original_release_date
INTEGER bpm
TEXT media_type "Music/Audiobook/Podcast"
INTEGER bitrate "CHECK > 0"
INTEGER sample_rate
INTEGER channels "1=mono, 2=stereo"
TEXT codec
INTEGER bit_depth
TEXT sort_title
TEXT sort_artist
TEXT sort_album
TEXT sort_album_artist
TEXT extended_metadata "JSON"
}
playlists {
TEXT id PK "UUID"
TEXT name "NOT NULL"
TEXT type "NOT NULL (regular/smart)"
BOOLEAN is_user_editable "NOT NULL"
BOOLEAN is_content_editable "NOT NULL"
DATETIME date_created "NOT NULL"
DATETIME date_modified "NOT NULL"
BLOB cover_artwork_data
TEXT smart_criteria "JSON"
INTEGER sort_order "NOT NULL DEFAULT 0"
}
playlist_tracks {
TEXT playlist_id FK "NOT NULL"
INTEGER track_id FK "NOT NULL"
INTEGER position "NOT NULL"
DATETIME date_added "NOT NULL"
}
track_artists {
INTEGER track_id FK "NOT NULL"
INTEGER artist_id FK "NOT NULL"
TEXT role "NOT NULL DEFAULT 'artist'"
INTEGER position "NOT NULL DEFAULT 0"
}
track_genres {
INTEGER track_id FK "NOT NULL"
INTEGER genre_id FK "NOT NULL"
}
pinned_items {
INTEGER id PK "AUTO_INCREMENT"
TEXT item_type "NOT NULL (library/playlist)"
TEXT filter_type "For library items"
TEXT filter_value "Artist/album name"
TEXT entity_id "UUID for entities"
INTEGER artist_id "Database ID"
INTEGER album_id "Database ID"
TEXT playlist_id "For playlist items"
TEXT display_name "NOT NULL"
TEXT subtitle "For albums"
TEXT icon_name "NOT NULL"
INTEGER sort_order "NOT NULL DEFAULT 0"
DATETIME date_added "NOT NULL"
}
tracks_fts {
INTEGER track_id "NOT INDEXED"
TEXT title
TEXT artist
TEXT album
TEXT album_artist
TEXT composer
TEXT genre
TEXT year
}
folders ||--o{ tracks : contains
albums ||--o{ album_artists : "has artists"
artists ||--o{ album_artists : "appears on"
albums ||--o{ tracks : contains
artists ||--o{ track_artists : "appears in"
tracks ||--o{ track_artists : "has artists"
tracks ||--o| tracks : "duplicate of"
genres ||--o{ track_genres : "categorizes"
tracks ||--o{ track_genres : "has genres"
playlists ||--o{ playlist_tracks : contains
tracks ||--o{ playlist_tracks : "appears in"
tracks ||--|| tracks_fts : "searchable in"
- Make sure you’re running macOS 14 or later.
- Install Xcode.
- To build the
.dmg
installer using thebuild-installer.sh
script, install: