Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bd8238a
Tidy Chart Axis Text Sizes
IDGBAN Oct 1, 2026
665aa35
Open the Spotify Export ZIP Without Extracting It
IDGBAN Oct 1, 2026
f683f68
Count Plays Once When Loaded Exports Overlap
IDGBAN Oct 1, 2026
cec10c3
Remember Filters Between Launches and Add Saved Filter Sets
IDGBAN Oct 1, 2026
5399b15
Clear a Single Filter From Its Chip
IDGBAN Oct 2, 2026
cb0fcda
Add One-Click Date Ranges
IDGBAN Oct 2, 2026
9013bba
Filter by Device, Country, Shuffle, Offline and Private Sessions
IDGBAN Oct 2, 2026
523f1fd
Note the Active Filters in Exported Reports
IDGBAN Oct 2, 2026
535c23f
Copy Tracks as Spotify Links and Act on Several Rows at Once
IDGBAN Oct 2, 2026
e6cee99
Open Tracks, Albums and Artists From Inside a Detail View
IDGBAN Oct 2, 2026
024602f
Add Detail Views for Podcasts and Audiobooks
IDGBAN Oct 2, 2026
0796bce
Jump to Any Artist, Track, Album or Podcast With Ctrl+K
IDGBAN Oct 2, 2026
d5e2f93
Open a Bar's Breakdown by Clicking It
IDGBAN Oct 2, 2026
08ac657
Show Forgotten Favorites on the Insights Tab
IDGBAN Oct 2, 2026
7e897c1
Show Your #1 Artist and Track Each Month
IDGBAN Oct 2, 2026
8e27faf
Mark the Milestones in a Listening History
IDGBAN Oct 2, 2026
e8535b6
Compare Artists Over Time on the Trends Tab
IDGBAN Oct 2, 2026
b286dd8
Add a Compare Tab for Two Periods Side by Side
IDGBAN Oct 2, 2026
5fe9ad9
Make a Shareable Year in Review Image
IDGBAN Oct 2, 2026
9638a86
Add AI Agent Tools
IDGBAN Oct 2, 2026
5642438
Keep the Open History When a Load Finds No Plays
IDGBAN Oct 2, 2026
6a3c857
Write Gregorian Dates Whatever the Windows Language
IDGBAN Oct 3, 2026
eb6a0a0
Say When a Copy Worked and Skip Links for Local Files
IDGBAN Oct 3, 2026
cb218a8
Log Background Errors and Save Settings Without Half-Writes
IDGBAN Oct 3, 2026
96b46ea
Define Podcast Names Once and Remove Unused Members
IDGBAN Oct 4, 2026
181bc8f
Draw a Title Bar That Follows the Theme
IDGBAN Oct 4, 2026
51819db
Brighten the Light Theme's Charts and Controls
IDGBAN Oct 4, 2026
80ef9c7
Add the Sortify Logo
IDGBAN Oct 4, 2026
aacd278
Move the Main Toolbar Into the Title Bar
IDGBAN Oct 4, 2026
31745bf
Bump Version to 4.0.0
IDGBAN Oct 4, 2026
c3b0177
Merge branch 'main' of https://github.com/IDGBAN/Sortify into update-…
IDGBAN Oct 4, 2026
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
96 changes: 96 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# AGENTS.md

This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.

## What this is

Sortify is a Windows desktop (WPF, .NET 8) app that reads a user's exported Spotify streaming
history (JSON) and turns it into browsable statistics, charts and insights. It ships as a single
portable self-contained `.exe`; there is no server component and no network calls — everything
runs and is stored locally (`%LOCALAPPDATA%\Sortify` holds the settings file, the parsed-history
cache and an error log).

## Commands

```bash
# Run the app
dotnet run --project Sortify/Sortify.csproj

# Build
dotnet build

# Run the full test suite
dotnet test

# Run a single test class or method (filter by fully-qualified name substring)
dotnet test --filter "FullyQualifiedName~AnalysisEngineTests"
dotnet test --filter "FullyQualifiedName~AnalysisEngineTests.SomeMethod"

# Publish a self-contained single-file build (64-bit or 32-bit)
dotnet publish Sortify/Sortify.csproj -c Release -r win-x64
dotnet publish Sortify/Sortify.csproj -c Release -r win-x86
```

The published exe lands in `Sortify/bin/Release/net8.0-windows/<rid>/publish/`. CI (`.github/workflows/ci.yml`)
runs `dotnet restore`, `dotnet format --verify-no-changes`, `dotnet build -c Debug`,
`dotnet test -c Debug` and a win-x64 Release publish on `windows-latest` for pushes to
`main`/`update-*` and all PRs.

Formatting/style rules (naming, brace placement, `var` usage, file-scoped namespaces) live in
`.editorconfig`. CI runs `dotnet format --verify-no-changes`; run `dotnet format` locally to fix
whatever it flags.

## Architecture

Two projects: `Sortify` (the WPF app) and `Sortify.Tests` (xUnit; `InternalsVisibleTo` is set so
tests can reach internal members). MVVM via CommunityToolkit.Mvvm (`[ObservableProperty]`,
`[RelayCommand]`); charts via LiveChartsCore.SkiaSharpView.WPF.

**Pipeline, in order** (driven by `ViewModels/MainViewModel.cs`, the single hub the views bind to):

1. **`Services/HistoryParser.cs`** — finds and parses the Spotify export JSON files (both the
modern extended-history format and the older legacy `StreamingHistory*.json` format) into
normalized `Models/PlayRecord.cs` objects.
2. **`Services/RecordCache.cs`** — a binary cache of parsed records, keyed by the exact set of
source file paths + sizes + write times (`%LOCALAPPDATA%\Sortify\records.cache`). Reopening an
unchanged export skips JSON parsing entirely; any change to the file set invalidates the key.
3. **`Services/FilterEngine.cs`** — applies the current `Models/FilterOptions` (date range, min
play duration, search text, excluded artists/tracks, time-of-day/day-of-week windows, whether
podcasts count) as a streaming filter over raw records.
4. **`Services/AnalysisEngine.cs`** — the aggregation core. Consumes filtered records and produces
one `AnalysisResult` (`Models/Stats.cs`): per-track/artist/album/year stats, skip/completion
rates, sessions, streaks, hour/day-of-week breakdowns, podcast stats, playback-context
breakdowns (device/country/shuffle/offline). Runs on a background thread via `AnalyzeAsync`.
Note it re-applies filtering itself with `ignoreMinDuration: true` so skip/reason-end stats can
still see the short plays a duration cutoff would otherwise drop.
5. **`Services/ChartBuilder.cs`** — turns an `AnalysisResult` into `Services/ChartData.cs` /
LiveCharts `ISeries[]` for each chart on screen. Charts bake colors into Skia paints at build
time, so a theme change rebuilds charts rather than repainting them (see
`ThemeService.Changed` in `MainViewModel`).
6. **`Services/DetailEngine.cs`** — builds the focused drill-down (`DetailResult`,
`Models/DetailStats.cs`) shown in `Views/DetailWindow.xaml` when a track/artist/album/year row
is double-clicked; reruns aggregation scoped to one entity under the current filters.
7. **`Services/ExportService.cs`** — writes the current `AnalysisResult` out as txt/Markdown/JSON/CSV.

Supporting services: `Services/AppSettings.cs` (JSON-persisted preferences — recent folders, theme,
session-gap minutes, chart animation toggle — failures degrade to defaults rather than surfacing
errors), `Services/ThemeService.cs` (swaps the palette `ResourceDictionary` at a fixed slot in
`App.xaml`'s merged dictionaries so every DynamicResource-bound style repaints; raises `Changed`),
`Services/ImageExporter.cs` (copy/save a chart as PNG, wired to chart right-click), `Views/Converters.cs`.

**UI structure**: `Views/MainWindow.xaml(.cs)` is the main shell (tabs: Overview, Tracks, Artists,
Albums, Years, Podcasts, Trends, Insights) bound to `MainViewModel`. `Views/FilterPanel.xaml(.cs)`
+ `ViewModels/FilterViewModel.cs` is the filter sidebar, which raises `FiltersChanged` on any
change; `MainViewModel` debounces that (300ms `DispatcherTimer`) before re-running analysis.
`Views/DetailWindow.xaml(.cs)` is the per-row drill-down. `Views/SettingsWindow.xaml(.cs)` covers
theme/animation/session-gap/cache-clearing preferences. Grid item sources (`Tracks`, `Artists`,
etc.) are swapped wholesale after each analysis pass rather than mutated via
`ObservableCollection`, since incremental updates to tens of thousands of rows would raise a
`CollectionChanged` per row and freeze the UI. Large horizontal bar charts (tracks/artists/albums)
page in via `LoadMore*` methods as the user scrolls rather than rendering every bar up front.

Tests (`Sortify.Tests/`) that touch WPF types (windows, converters, dependency properties) run
through `WpfTestHost.cs`, which spins up a single STA thread with one live `Application` and merges
the theme/converter resources by hand (bypassing `App.xaml`'s `StartupUri` so no real window
opens). WPF only allows one `Application` per process, so this host is shared across all UI tests
rather than created per-test.
107 changes: 107 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

Sortify is a Windows desktop (WPF, .NET 8) app that reads a user's exported Spotify streaming
history (JSON) and turns it into browsable statistics, charts and insights. It ships as a single
portable self-contained `.exe`; there is no server component and no network calls — everything
runs and is stored locally (`%LOCALAPPDATA%\Sortify` holds the settings file, the parsed-history
cache and an error log).

## Commands

```bash
# Run the app
dotnet run --project Sortify/Sortify.csproj

# Build
dotnet build

# Run the full test suite
dotnet test

# Run a single test class or method (filter by fully-qualified name substring)
dotnet test --filter "FullyQualifiedName~AnalysisEngineTests"
dotnet test --filter "FullyQualifiedName~AnalysisEngineTests.SomeMethod"

# Publish a self-contained single-file build (64-bit or 32-bit)
dotnet publish Sortify/Sortify.csproj -c Release -r win-x64
dotnet publish Sortify/Sortify.csproj -c Release -r win-x86
```

The published exe lands in `Sortify/bin/Release/net8.0-windows/<rid>/publish/`. CI (`.github/workflows/ci.yml`)
runs `dotnet restore`, `dotnet format --verify-no-changes`, `dotnet build -c Debug`,
`dotnet test -c Debug` and a win-x64 Release publish on `windows-latest` for pushes to
`main`/`update-*` and all PRs.

Formatting/style rules (naming, brace placement, `var` usage, file-scoped namespaces) live in
`.editorconfig`. CI runs `dotnet format --verify-no-changes`; run `dotnet format` locally to fix
whatever it flags.

## Architecture

Two projects: `Sortify` (the WPF app) and `Sortify.Tests` (xUnit; `InternalsVisibleTo` is set so
tests can reach internal members). MVVM via CommunityToolkit.Mvvm (`[ObservableProperty]`,
`[RelayCommand]`); charts via LiveChartsCore.SkiaSharpView.WPF.

**Pipeline, in order** (driven by `ViewModels/MainViewModel.cs`, the single hub the views bind to):

1. **`Services/HistoryParser.cs`** — finds and parses the Spotify export JSON files (both the
modern extended-history format and the older legacy `StreamingHistory*.json` format) into
normalized `Models/PlayRecord.cs` objects.
2. **`Services/RecordCache.cs`** — a binary cache of parsed records, keyed by the exact set of
source file paths + sizes + write times (`%LOCALAPPDATA%\Sortify\records.cache`). Reopening an
unchanged export skips JSON parsing entirely; any change to the file set invalidates the key.
3. **`Services/FilterEngine.cs`** — applies the current `Models/FilterOptions` (date range, min
play duration, search text, excluded artists/tracks, time-of-day/day-of-week windows, whether
podcasts count) as a streaming filter over raw records.
4. **`Services/AnalysisEngine.cs`** — the aggregation core. Consumes filtered records and produces
one `AnalysisResult` (`Models/Stats.cs`): per-track/artist/album/year stats, skip/completion
rates, sessions, streaks, hour/day-of-week breakdowns, podcast stats, playback-context
breakdowns (device/country/shuffle/offline). Runs on a background thread via `AnalyzeAsync`.
Note it re-applies filtering itself with `ignoreMinDuration: true` so skip/reason-end stats can
still see the short plays a duration cutoff would otherwise drop.
5. **`Services/ChartBuilder.cs`** — turns an `AnalysisResult` into `Services/ChartData.cs` /
LiveCharts `ISeries[]` for each chart on screen. Charts bake colors into Skia paints at build
time, so a theme change rebuilds charts rather than repainting them (see
`ThemeService.Changed` in `MainViewModel`).
6. **`Services/DetailEngine.cs`** — builds the focused drill-down (`DetailResult`,
`Models/DetailStats.cs`) shown in `Views/DetailWindow.xaml` when a track/artist/album/year row
is double-clicked; reruns aggregation scoped to one entity under the current filters.
7. **`Services/ExportService.cs`** — writes the current `AnalysisResult` out as txt/Markdown/JSON/CSV.

Supporting services: `Services/AppSettings.cs` (JSON-persisted preferences — recent folders, theme,
session-gap minutes, chart animation toggle — failures degrade to defaults rather than surfacing
errors), `Services/ThemeService.cs` (swaps the palette `ResourceDictionary` at a fixed slot in
`App.xaml`'s merged dictionaries so every DynamicResource-bound style repaints; raises `Changed`),
`Services/ImageExporter.cs` (copy/save a chart as PNG, wired to chart right-click), `Views/Converters.cs`.

**UI structure**: `Views/MainWindow.xaml(.cs)` is the main shell (tabs: Overview, Tracks, Artists,
Albums, Years, Podcasts, Trends, Insights) bound to `MainViewModel`. `Views/FilterPanel.xaml(.cs)`
+ `ViewModels/FilterViewModel.cs` is the filter sidebar, which raises `FiltersChanged` on any
change; `MainViewModel` debounces that (300ms `DispatcherTimer`) before re-running analysis.
`Views/DetailWindow.xaml(.cs)` is the per-row drill-down. `Views/SettingsWindow.xaml(.cs)` covers
theme/animation/session-gap/cache-clearing preferences. Every window derives from
`Views/ThemedWindow.cs`, which swaps the native title bar for a palette-coloured one via `WindowChrome`
(template and caption-button styles at the end of `Themes/Controls.xaml`); a new window should derive
from it too, or it gets the white Windows title bar. MainWindow's toolbar lives in that bar, via
`ThemedWindow.TitleBarContent`: its buttons take clicks, while the gaps and anything marked
`IsHitTestVisible="False"` (the logo, the name, the progress bar) still drag the window. Grid item
sources (`Tracks`, `Artists`, etc.) are swapped wholesale after each analysis pass rather than
mutated via `ObservableCollection`, since incremental updates to tens of thousands of rows would
raise a `CollectionChanged` per row and freeze the UI. Large horizontal bar charts
(tracks/artists/albums) page in via `LoadMore*` methods as the user scrolls rather than rendering
every bar up front.

The logo's source is `Sortify/Assets/sortify-logo.svg`. `Themes/Logo.xaml` is the same drawing as a
WPF `DrawingImage` (key `SortifyLogo`) that the title bar, toolbar, Settings and the year-in-review
card draw, and `Assets/Sortify.ico` is rendered from it at 16-256 px for the exe, taskbar and Alt+Tab.
A change to the logo means updating all three.

Tests (`Sortify.Tests/`) that touch WPF types (windows, converters, dependency properties) run
through `WpfTestHost.cs`, which spins up a single STA thread with one live `Application` and merges
the theme/converter resources by hand (bypassing `App.xaml`'s `StartupUri` so no real window
opens). WPF only allows one `Application` per process, so this host is shared across all UI tests
rather than created per-test.
Loading
Loading