Renaming to scrbblr
This commit is contained in:
157
AGENTS.md
Normal file
157
AGENTS.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# AGENTS.md — Coding Agent Guidelines
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**scrbblr** is a Rust CLI tool that scrobbles music from MPRIS-compatible
|
||||||
|
players and MPD on Linux. It stores data in SQLite, generates terminal/JSON/HTML
|
||||||
|
reports, and enriches album metadata from MusicBrainz/Cover Art Archive.
|
||||||
|
|
||||||
|
Single binary, no async, flat module structure. Rust 2024 edition.
|
||||||
|
|
||||||
|
## Build / Test / Lint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build # debug build
|
||||||
|
cargo build --release # release build
|
||||||
|
cargo test # run all tests (currently 63)
|
||||||
|
cargo test <name> # single test, e.g. cargo test test_top_artists
|
||||||
|
cargo test --lib db::tests # all tests in one module
|
||||||
|
cargo test -- --nocapture # show println/eprintln output
|
||||||
|
cargo fmt # format (default rustfmt, no config file)
|
||||||
|
cargo clippy # lint (no clippy.toml)
|
||||||
|
```
|
||||||
|
|
||||||
|
Always run `cargo fmt`, `cargo test`, and `cargo build` before committing.
|
||||||
|
There is no CI pipeline — local verification is the only gate.
|
||||||
|
|
||||||
|
## Module Structure
|
||||||
|
|
||||||
|
All source lives in `src/`. Flat layout, no nested modules.
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|--------------|-------------------------------------------------------------|
|
||||||
|
| `main.rs` | CLI (clap derive), subcommand dispatch, process management |
|
||||||
|
| `db.rs` | SQLite schema, all queries, data structs |
|
||||||
|
| `watcher.rs` | playerctl event parsing, ScrobbleTracker state machine |
|
||||||
|
| `report.rs` | Terminal tables, JSON output, HTML report + CSS generation |
|
||||||
|
| `enrich.rs` | MusicBrainz/CAA API, cover downloads, genre extraction |
|
||||||
|
|
||||||
|
Database access goes through `db.rs` public functions only — no raw SQL elsewhere.
|
||||||
|
|
||||||
|
## Language & Spelling
|
||||||
|
|
||||||
|
Use **British English** everywhere: comments, docs, README, variable names.
|
||||||
|
|
||||||
|
- `normalise` not `normalize`
|
||||||
|
- `behaviour` not `behavior`
|
||||||
|
- `licence` not `license`
|
||||||
|
- `colour` not `color` (in prose; CSS property names stay American per spec)
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
|
||||||
|
Default `rustfmt` (no `rustfmt.toml`). 4-space indent, trailing commas in
|
||||||
|
multi-line expressions. Run `cargo fmt` before every commit.
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
|
||||||
|
- Functions: `snake_case` — `open_memory_db`, `parse_metadata_line`
|
||||||
|
- Types/Structs: `PascalCase` — `ScrobbleTracker`, `TopArtist`
|
||||||
|
- Enums: `PascalCase` type + variants — `Event::Metadata`, `PlayerStatus::Playing`
|
||||||
|
- Constants: `SCREAMING_SNAKE_CASE` — `DEFAULT_PLAYER`, `RATE_LIMIT_DELAY`
|
||||||
|
- Tests: `test_` prefix — `test_top_artists`, `test_schema_creation`
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
|
||||||
|
No strict grouping enforced. General pattern: `crate::` imports, external crates,
|
||||||
|
then `std`. Let `cargo fmt` handle ordering within groups.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use crate::db;
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Section Separators
|
||||||
|
|
||||||
|
Use box-drawing comment blocks between major sections:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Section Name
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
```
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Module-level `//!` doc comments at top of every `.rs` file.
|
||||||
|
- `///` doc comments on all public functions and important private functions.
|
||||||
|
- Inline `//` comments explaining "why" not just "what".
|
||||||
|
- Comments should be thorough — this codebase favours extensive documentation.
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
**Layered approach — no panics in production code paths:**
|
||||||
|
|
||||||
|
1. **`Result<T>` + `?`** in library functions (all `db.rs` queries, `gather_report`, etc.)
|
||||||
|
2. **`eprintln!` + `std::process::exit(1)`** for fatal CLI errors in `main.rs`
|
||||||
|
3. **`.expect("descriptive message")`** only for truly unrecoverable cases
|
||||||
|
(missing `HOME`, failed dir creation)
|
||||||
|
4. **`.unwrap()` only in test code** — never in production paths
|
||||||
|
5. **`match` on `Result`** with `eprintln!` for non-fatal errors (failed cache writes, etc.)
|
||||||
|
|
||||||
|
All user-facing progress/error output goes to **stderr** via `eprintln!`.
|
||||||
|
Only report content (tables, JSON, HTML) goes to stdout.
|
||||||
|
|
||||||
|
Logging prefixes: `[scrobbled]`, `[error]`, `[warn]`, or unprefixed for progress.
|
||||||
|
|
||||||
|
## Rust 2024 Edition Features
|
||||||
|
|
||||||
|
The codebase uses edition 2024 features freely:
|
||||||
|
|
||||||
|
- Let chains: `if let Some(x) = foo && condition { ... }`
|
||||||
|
- `is_none_or()` method
|
||||||
|
- Other recently stabilised APIs
|
||||||
|
|
||||||
|
## Test Patterns
|
||||||
|
|
||||||
|
Every source file has `#[cfg(test)] mod tests` at the bottom.
|
||||||
|
|
||||||
|
### Key test helpers
|
||||||
|
|
||||||
|
- `db::open_memory_db()` — in-memory SQLite with schema. Used by all modules.
|
||||||
|
- `db::tests::seed_db(conn)` — populates 5 test scrobbles across two artists/days.
|
||||||
|
- `watcher::TestableTracker` — simulated clock (`advance_time(secs)`) + scrobble
|
||||||
|
collection for unit-testing the state machine without real time.
|
||||||
|
|
||||||
|
### Test conventions
|
||||||
|
|
||||||
|
- Each test creates its own `open_memory_db()` — no shared state.
|
||||||
|
- `.unwrap()` is fine in tests (panic = test failure).
|
||||||
|
- Use `.into()` for string literals in test data: `artist: "Deftones".into()`.
|
||||||
|
- Descriptive names: `test_top_genres_merges_hyphen_and_space_variants`.
|
||||||
|
|
||||||
|
## Key Architectural Notes
|
||||||
|
|
||||||
|
- **No async** — uses `reqwest::blocking` and `std::thread`.
|
||||||
|
- **No logging crate** — direct `eprintln!` calls throughout.
|
||||||
|
- **No template engine** — HTML built via custom `HtmlWriter` helper in `report.rs`.
|
||||||
|
- **No unsafe code** except one `libc::ioctl` for terminal width (`#[cfg(unix)]`).
|
||||||
|
- **XDG paths** — DB at `$XDG_DATA_HOME/scrbblr/scrobbles.db`, covers in
|
||||||
|
`covers/` subdirectory. State marker for publish in `$XDG_STATE_HOME`.
|
||||||
|
- MusicBrainz rate limiting: 1-second delay between API calls (`RATE_LIMIT_DELAY`).
|
||||||
|
- HTML report is fully self-contained (relative `covers/` paths, no external JS).
|
||||||
|
- Font loaded from Google Fonts CDN with monospace fallback chain for offline use.
|
||||||
|
|
||||||
|
## Files Outside src/
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|-------------------------------|--------------------------------------------|
|
||||||
|
| `install.sh` | Interactive installer (binary + service) |
|
||||||
|
| `uninstall.sh` | Interactive uninstaller |
|
||||||
|
| `scrbblr-publish.sh` | Incremental report publish (rsync) |
|
||||||
|
| `contrib/examples/publish.conf.example` | Config template for publish |
|
||||||
|
| `contrib/systemd/user/` | systemd user service unit |
|
||||||
|
| `FUTURE.md` | Parking lot for future feature ideas |
|
||||||
30
Cargo.lock
generated
30
Cargo.lock
generated
@@ -1273,21 +1273,6 @@ dependencies = [
|
|||||||
"pxfm",
|
"pxfm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mpris-scrobbler"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"clap",
|
|
||||||
"ctrlc",
|
|
||||||
"image",
|
|
||||||
"libc",
|
|
||||||
"reqwest",
|
|
||||||
"rusqlite",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "native-tls"
|
name = "native-tls"
|
||||||
version = "0.2.18"
|
version = "0.2.18"
|
||||||
@@ -1848,6 +1833,21 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scrbblr"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"clap",
|
||||||
|
"ctrlc",
|
||||||
|
"image",
|
||||||
|
"libc",
|
||||||
|
"reqwest",
|
||||||
|
"rusqlite",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "3.7.0"
|
version = "3.7.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mpris-scrobbler"
|
name = "scrbblr"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
|||||||
94
README.md
94
README.md
@@ -1,4 +1,4 @@
|
|||||||
# mpris-scrobbler
|
# scrbblr
|
||||||
|
|
||||||
A local music scrobbler for MPRIS-compatible players, built in Rust. It tracks what you listen to via `playerctl`, stores scrobble data in a local SQLite database, and generates listening reports.
|
A local music scrobbler for MPRIS-compatible players, built in Rust. It tracks what you listen to via `playerctl`, stores scrobble data in a local SQLite database, and generates listening reports.
|
||||||
|
|
||||||
@@ -42,13 +42,13 @@ Build and install the binary into `~/.local/bin`:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build --release
|
cargo build --release
|
||||||
install -Dm755 target/release/mpris-scrobbler ~/.local/bin/mpris-scrobbler
|
install -Dm755 target/release/scrbblr ~/.local/bin/scrbblr
|
||||||
```
|
```
|
||||||
|
|
||||||
Make sure `~/.local/bin` is in your `PATH`:
|
Make sure `~/.local/bin` is in your `PATH`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
command -v mpris-scrobbler
|
command -v scrbblr
|
||||||
```
|
```
|
||||||
|
|
||||||
If that prints nothing, add this to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.):
|
If that prints nothing, add this to your shell profile (`~/.bashrc`, `~/.zshrc`, etc.):
|
||||||
@@ -60,7 +60,7 @@ export PATH="$HOME/.local/bin:$PATH"
|
|||||||
Then open a new shell and verify:
|
Then open a new shell and verify:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mpris-scrobbler --help
|
scrbblr --help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Autostart (recommended)
|
## Autostart (recommended)
|
||||||
@@ -73,7 +73,7 @@ Copy the provided unit file from the repo:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.config/systemd/user
|
mkdir -p ~/.config/systemd/user
|
||||||
cp contrib/systemd/user/mpris-scrobbler.service ~/.config/systemd/user/
|
cp contrib/systemd/user/scrbblr.service ~/.config/systemd/user/
|
||||||
```
|
```
|
||||||
|
|
||||||
If needed, edit the player in the service (`--player com.blitzfc.qbz`).
|
If needed, edit the player in the service (`--player com.blitzfc.qbz`).
|
||||||
@@ -82,14 +82,14 @@ If needed, edit the player in the service (`--player com.blitzfc.qbz`).
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user enable --now mpris-scrobbler.service
|
systemctl --user enable --now scrbblr.service
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3) Verify
|
### 3) Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl --user status mpris-scrobbler.service
|
systemctl --user status scrbblr.service
|
||||||
journalctl --user -u mpris-scrobbler.service -f
|
journalctl --user -u scrbblr.service -f
|
||||||
```
|
```
|
||||||
|
|
||||||
### Optional: keep running without active login
|
### Optional: keep running without active login
|
||||||
@@ -112,7 +112,7 @@ Recommended workflow:
|
|||||||
- Generate HTML reports manually whenever you want:
|
- Generate HTML reports manually whenever you want:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mpris-scrobbler report --html --output ~/music-report
|
scrbblr report --html --output ~/music-report
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates a self-contained directory:
|
This creates a self-contained directory:
|
||||||
@@ -134,7 +134,7 @@ When `--html` is used, the tool automatically runs enrichment first (for missing
|
|||||||
cargo build --release
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
The binary will be at `target/release/mpris-scrobbler`.
|
The binary will be at `target/release/scrbblr`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -144,16 +144,16 @@ Start the watcher to begin recording what you listen to:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Default player (com.blitzfc.qbz)
|
# Default player (com.blitzfc.qbz)
|
||||||
mpris-scrobbler watch
|
scrbblr watch
|
||||||
|
|
||||||
# Specify a different player
|
# Specify a different player
|
||||||
mpris-scrobbler watch --player spotify
|
scrbblr watch --player spotify
|
||||||
```
|
```
|
||||||
|
|
||||||
The watcher runs in the foreground and logs scrobbles to stderr:
|
The watcher runs in the foreground and logs scrobbles to stderr:
|
||||||
|
|
||||||
```
|
```
|
||||||
Database: /home/user/.local/share/mpris-scrobbler/scrobbles.db
|
Database: /home/user/.local/share/scrbblr/scrobbles.db
|
||||||
Watching player: com.blitzfc.qbz
|
Watching player: com.blitzfc.qbz
|
||||||
[scrobbled] ††† (Crosses) - This Is a Trick (186s)
|
[scrobbled] ††† (Crosses) - This Is a Trick (186s)
|
||||||
[scrobbled] ††† (Crosses) - Telepathy (200s)
|
[scrobbled] ††† (Crosses) - Telepathy (200s)
|
||||||
@@ -179,25 +179,25 @@ The HTML head loads JetBrains Mono from Google Fonts and falls back to local mon
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# All-time summary with top artists, albums, genres, tracks
|
# All-time summary with top artists, albums, genres, tracks
|
||||||
mpris-scrobbler report
|
scrbblr report
|
||||||
|
|
||||||
# Filter by period
|
# Filter by period
|
||||||
mpris-scrobbler report --period today
|
scrbblr report --period today
|
||||||
mpris-scrobbler report --period week
|
scrbblr report --period week
|
||||||
mpris-scrobbler report --period month
|
scrbblr report --period month
|
||||||
mpris-scrobbler report --period year
|
scrbblr report --period year
|
||||||
|
|
||||||
# JSON output
|
# JSON output
|
||||||
mpris-scrobbler report --json
|
scrbblr report --json
|
||||||
|
|
||||||
# HTML output to stdout (no covers)
|
# HTML output to stdout (no covers)
|
||||||
mpris-scrobbler report --html
|
scrbblr report --html
|
||||||
|
|
||||||
# HTML output to directory (with covers)
|
# HTML output to directory (with covers)
|
||||||
mpris-scrobbler report --html --output ~/music-report
|
scrbblr report --html --output ~/music-report
|
||||||
|
|
||||||
# Change the number of entries in top-N lists (default: 20)
|
# Change the number of entries in top-N lists (default: 20)
|
||||||
mpris-scrobbler report --limit 20
|
scrbblr report --limit 20
|
||||||
```
|
```
|
||||||
|
|
||||||
Example terminal output:
|
Example terminal output:
|
||||||
@@ -227,11 +227,11 @@ Top Artists
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
```
|
```
|
||||||
mpris-scrobbler watch [OPTIONS]
|
scrbblr watch [OPTIONS]
|
||||||
--player <NAME> Player name for playerctl [default: com.blitzfc.qbz]
|
--player <NAME> Player name for playerctl [default: com.blitzfc.qbz]
|
||||||
--db-path <PATH> Path to the SQLite database
|
--db-path <PATH> Path to the SQLite database
|
||||||
|
|
||||||
mpris-scrobbler report [OPTIONS]
|
scrbblr report [OPTIONS]
|
||||||
--period <PERIOD> today, week, month, year, all [default: all]
|
--period <PERIOD> today, week, month, year, all [default: all]
|
||||||
--json Output as JSON
|
--json Output as JSON
|
||||||
--html Output as standalone HTML
|
--html Output as standalone HTML
|
||||||
@@ -240,11 +240,11 @@ mpris-scrobbler report [OPTIONS]
|
|||||||
--all-time-limit <N> Override all-time top-N limit [default: 2.5x --limit]
|
--all-time-limit <N> Override all-time top-N limit [default: 2.5x --limit]
|
||||||
--db-path <PATH> Path to the SQLite database
|
--db-path <PATH> Path to the SQLite database
|
||||||
|
|
||||||
mpris-scrobbler enrich [OPTIONS]
|
scrbblr enrich [OPTIONS]
|
||||||
--force Re-fetch metadata for all albums
|
--force Re-fetch metadata for all albums
|
||||||
--db-path <PATH> Path to the SQLite database
|
--db-path <PATH> Path to the SQLite database
|
||||||
|
|
||||||
mpris-scrobbler last-scrobble [OPTIONS]
|
scrbblr last-scrobble [OPTIONS]
|
||||||
--db-path <PATH> Path to the SQLite database
|
--db-path <PATH> Path to the SQLite database
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ mpris-scrobbler last-scrobble [OPTIONS]
|
|||||||
Enrichment is automatic for `report --html`, but you can still run it manually if you want to prefetch metadata:
|
Enrichment is automatic for `report --html`, but you can still run it manually if you want to prefetch metadata:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mpris-scrobbler enrich
|
scrbblr enrich
|
||||||
```
|
```
|
||||||
|
|
||||||
When MusicBrainz matching is tricky, enrichment now retries with normalised
|
When MusicBrainz matching is tricky, enrichment now retries with normalised
|
||||||
@@ -275,7 +275,7 @@ re-tried after 7 days, not on every report run.
|
|||||||
Use force mode when you want immediate backfill/refresh for everything:
|
Use force mode when you want immediate backfill/refresh for everything:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mpris-scrobbler enrich --force
|
scrbblr enrich --force
|
||||||
```
|
```
|
||||||
|
|
||||||
Genre normalisation notes:
|
Genre normalisation notes:
|
||||||
@@ -289,7 +289,7 @@ Genre normalisation notes:
|
|||||||
|
|
||||||
Downloaded covers are stored in:
|
Downloaded covers are stored in:
|
||||||
|
|
||||||
`~/.local/share/mpris-scrobbler/covers/`
|
`~/.local/share/scrbblr/covers/`
|
||||||
|
|
||||||
#### Manually pinning an album (`pin-album`)
|
#### Manually pinning an album (`pin-album`)
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ When that happens you can pin the correct MusicBrainz release manually:
|
|||||||
`[N/M] Artist - Album` line) and run:
|
`[N/M] Artist - Album` line) and run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mpris-scrobbler pin-album \
|
scrbblr pin-album \
|
||||||
--artist "Coro della Radiotelevisione Svizzera" \
|
--artist "Coro della Radiotelevisione Svizzera" \
|
||||||
--album "Vivaldi: Gloria; Nisi Dominus; Nulla in mundo pax" \
|
--album "Vivaldi: Gloria; Nisi Dominus; Nulla in mundo pax" \
|
||||||
--mbid "f2ff907a-0355-451b-9c68-f0b7c09bb145"
|
--mbid "f2ff907a-0355-451b-9c68-f0b7c09bb145"
|
||||||
@@ -325,7 +325,7 @@ If the Cover Art Archive has no image for the release (the command prints
|
|||||||
"No cover art available"), supply one with `--cover-url`:
|
"No cover art available"), supply one with `--cover-url`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mpris-scrobbler pin-album \
|
scrbblr pin-album \
|
||||||
--artist "Coro della Radiotelevisione Svizzera" \
|
--artist "Coro della Radiotelevisione Svizzera" \
|
||||||
--album "Vivaldi: Gloria; Nisi Dominus; Nulla in mundo pax" \
|
--album "Vivaldi: Gloria; Nisi Dominus; Nulla in mundo pax" \
|
||||||
--mbid "f2ff907a-0355-451b-9c68-f0b7c09bb145" \
|
--mbid "f2ff907a-0355-451b-9c68-f0b7c09bb145" \
|
||||||
@@ -341,17 +341,17 @@ Bandcamp, etc.
|
|||||||
If you publish the report to a remote host, use the included helper script:
|
If you publish the report to a remote host, use the included helper script:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./mpris-scrobbler-publish.sh
|
./scrbblr-publish.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
It runs `report --html` and `rsync` only when a newer scrobble exists.
|
It runs `report --html` and `rsync` only when a newer scrobble exists.
|
||||||
The script tracks the last published scrobble timestamp in:
|
The script tracks the last published scrobble timestamp in:
|
||||||
|
|
||||||
`$XDG_STATE_HOME/mpris-scrobbler/last-published-scrobble.txt`
|
`$XDG_STATE_HOME/scrbblr/last-published-scrobble.txt`
|
||||||
|
|
||||||
Set defaults in a config file so you can run the script without passing flags:
|
Set defaults in a config file so you can run the script without passing flags:
|
||||||
|
|
||||||
`~/.config/mpris-scrobbler/publish.conf`
|
`~/.config/scrbblr/publish.conf`
|
||||||
|
|
||||||
If you use `./install.sh`, an example config is installed there automatically
|
If you use `./install.sh`, an example config is installed there automatically
|
||||||
when the file does not already exist.
|
when the file does not already exist.
|
||||||
@@ -366,31 +366,31 @@ DB_PATH=""
|
|||||||
|
|
||||||
Legacy fallback is also supported:
|
Legacy fallback is also supported:
|
||||||
|
|
||||||
`~/.mpris-scrobbler-publish.conf`
|
`~/.scrbblr-publish.conf`
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./mpris-scrobbler-publish.sh --output ~/music-report --remote user@host:/var/www/music-report
|
./scrbblr-publish.sh --output ~/music-report --remote user@host:/var/www/music-report
|
||||||
./mpris-scrobbler-publish.sh --db-path /custom/path/scrobbles.db
|
./scrbblr-publish.sh --db-path /custom/path/scrobbles.db
|
||||||
|
|
||||||
# Keep running and check every 5 minutes (default interval):
|
# Keep running and check every 5 minutes (default interval):
|
||||||
./mpris-scrobbler-publish.sh --watch
|
./scrbblr-publish.sh --watch
|
||||||
|
|
||||||
# Custom interval (seconds):
|
# Custom interval (seconds):
|
||||||
./mpris-scrobbler-publish.sh --watch --interval 600
|
./scrbblr-publish.sh --watch --interval 600
|
||||||
|
|
||||||
# Force regeneration even when no new scrobbles exist:
|
# Force regeneration even when no new scrobbles exist:
|
||||||
./mpris-scrobbler-publish.sh --force
|
./scrbblr-publish.sh --force
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer also places this helper in `~/.local/bin` as:
|
The installer also places this helper in `~/.local/bin` as:
|
||||||
|
|
||||||
`mpris-scrobbler-publish`
|
`scrbblr-publish`
|
||||||
|
|
||||||
## Data storage
|
## Data storage
|
||||||
|
|
||||||
Scrobbles are stored in SQLite at `~/.local/share/mpris-scrobbler/scrobbles.db` (respects `$XDG_DATA_HOME`).
|
Scrobbles are stored in SQLite at `~/.local/share/scrbblr/scrobbles.db` (respects `$XDG_DATA_HOME`).
|
||||||
|
|
||||||
Each scrobble records:
|
Each scrobble records:
|
||||||
|
|
||||||
@@ -422,11 +422,11 @@ Check the player name:
|
|||||||
playerctl -l
|
playerctl -l
|
||||||
```
|
```
|
||||||
|
|
||||||
If needed, edit `~/.config/systemd/user/mpris-scrobbler.service` and change `--player ...`, then reload/restart:
|
If needed, edit `~/.config/systemd/user/scrbblr.service` and change `--player ...`, then reload/restart:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user restart mpris-scrobbler.service
|
systemctl --user restart scrbblr.service
|
||||||
```
|
```
|
||||||
|
|
||||||
### Player is in another account/session
|
### Player is in another account/session
|
||||||
@@ -436,14 +436,14 @@ MPRIS is session-scoped. The service must run in the same account/session as the
|
|||||||
### Check logs
|
### Check logs
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
journalctl --user -u mpris-scrobbler.service -n 200
|
journalctl --user -u scrbblr.service -n 200
|
||||||
journalctl --user -u mpris-scrobbler.service -f
|
journalctl --user -u scrbblr.service -f
|
||||||
```
|
```
|
||||||
|
|
||||||
### Verify database is being written
|
### Verify database is being written
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mpris-scrobbler report --period today
|
scrbblr report --period today
|
||||||
```
|
```
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# mpris-scrobbler publish helper configuration
|
# scrbblr publish helper configuration
|
||||||
#
|
#
|
||||||
# Copy to:
|
# Copy to:
|
||||||
# ~/.config/mpris-scrobbler/publish.conf
|
# ~/.config/scrbblr/publish.conf
|
||||||
#
|
#
|
||||||
# Or let install.sh install it automatically if missing.
|
# Or let install.sh install it automatically if missing.
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=MPRIS Scrobbler watcher
|
Description=MPRIS Scrobbler watcher
|
||||||
Documentation=https://github.com/arturmeski/mpris_scrobbler
|
Documentation=https://github.com/arturmeski/scrbblr
|
||||||
After=graphical-session.target
|
After=graphical-session.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=%h/.local/bin/mpris-scrobbler watch --player com.blitzfc.qbz
|
ExecStart=%h/.local/bin/scrbblr watch --player com.blitzfc.qbz
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|
||||||
16
install.sh
16
install.sh
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Interactive installer for mpris-scrobbler.
|
# Interactive installer for scrbblr.
|
||||||
#
|
#
|
||||||
# Steps:
|
# Steps:
|
||||||
# 1. Build release binary
|
# 1. Build release binary
|
||||||
@@ -14,16 +14,16 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
BIN_NAME="mpris-scrobbler"
|
BIN_NAME="scrbblr"
|
||||||
PUBLISH_SCRIPT_NAME="mpris-scrobbler-publish.sh"
|
PUBLISH_SCRIPT_NAME="scrbblr-publish.sh"
|
||||||
PUBLISH_INSTALLED_NAME="mpris-scrobbler-publish"
|
PUBLISH_INSTALLED_NAME="scrbblr-publish"
|
||||||
PUBLISH_CONFIG_EXAMPLE_SRC="$SCRIPT_DIR/contrib/examples/publish.conf.example"
|
PUBLISH_CONFIG_EXAMPLE_SRC="$SCRIPT_DIR/contrib/examples/publish.conf.example"
|
||||||
PUBLISH_CONFIG_DIR="$HOME/.config/mpris-scrobbler"
|
PUBLISH_CONFIG_DIR="$HOME/.config/scrbblr"
|
||||||
PUBLISH_CONFIG_FILE="$PUBLISH_CONFIG_DIR/publish.conf"
|
PUBLISH_CONFIG_FILE="$PUBLISH_CONFIG_DIR/publish.conf"
|
||||||
INSTALL_DIR="$HOME/.local/bin"
|
INSTALL_DIR="$HOME/.local/bin"
|
||||||
SERVICE_SRC="$SCRIPT_DIR/contrib/systemd/user/mpris-scrobbler.service"
|
SERVICE_SRC="$SCRIPT_DIR/contrib/systemd/user/scrbblr.service"
|
||||||
SERVICE_DIR="$HOME/.config/systemd/user"
|
SERVICE_DIR="$HOME/.config/systemd/user"
|
||||||
SERVICE_NAME="mpris-scrobbler.service"
|
SERVICE_NAME="scrbblr.service"
|
||||||
|
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
@@ -183,4 +183,4 @@ info " Stop service: systemctl --user stop $SERVICE_NAME"
|
|||||||
info " Restart: systemctl --user restart $SERVICE_NAME"
|
info " Restart: systemctl --user restart $SERVICE_NAME"
|
||||||
info " Generate report: $BIN_NAME report --html --output ~/music-report"
|
info " Generate report: $BIN_NAME report --html --output ~/music-report"
|
||||||
info " Publish report: $PUBLISH_INSTALLED_NAME"
|
info " Publish report: $PUBLISH_INSTALLED_NAME"
|
||||||
info " Configure defaults in: ~/.config/mpris-scrobbler/publish.conf"
|
info " Configure defaults in: ~/.config/scrbblr/publish.conf"
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ set -euo pipefail
|
|||||||
# Defaults can be overridden by config file and then by flags:
|
# Defaults can be overridden by config file and then by flags:
|
||||||
#
|
#
|
||||||
# Config file precedence:
|
# Config file precedence:
|
||||||
# 1) $XDG_CONFIG_HOME/mpris-scrobbler/publish.conf
|
# 1) $XDG_CONFIG_HOME/scrbblr/publish.conf
|
||||||
# 2) ~/.config/mpris-scrobbler/publish.conf
|
# 2) ~/.config/scrbblr/publish.conf
|
||||||
# 3) ~/.mpris-scrobbler-publish.conf (legacy fallback)
|
# 3) ~/.scrbblr-publish.conf (legacy fallback)
|
||||||
#
|
#
|
||||||
# Supported config variables:
|
# Supported config variables:
|
||||||
# OUTPUT_DIR
|
# OUTPUT_DIR
|
||||||
@@ -30,8 +30,8 @@ INTERVAL=300
|
|||||||
FORCE=0
|
FORCE=0
|
||||||
|
|
||||||
XDG_CONFIG_BASE="${XDG_CONFIG_HOME:-${HOME}/.config}"
|
XDG_CONFIG_BASE="${XDG_CONFIG_HOME:-${HOME}/.config}"
|
||||||
PRIMARY_CONFIG="${XDG_CONFIG_BASE}/mpris-scrobbler/publish.conf"
|
PRIMARY_CONFIG="${XDG_CONFIG_BASE}/scrbblr/publish.conf"
|
||||||
LEGACY_CONFIG="${HOME}/.mpris-scrobbler-publish.conf"
|
LEGACY_CONFIG="${HOME}/.scrbblr-publish.conf"
|
||||||
|
|
||||||
if [[ -f "${PRIMARY_CONFIG}" ]]; then
|
if [[ -f "${PRIMARY_CONFIG}" ]]; then
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
@@ -79,8 +79,8 @@ if [[ ! "${INTERVAL}" =~ ^[1-9][0-9]*$ ]]; then
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v mpris-scrobbler >/dev/null 2>&1; then
|
if ! command -v scrbblr >/dev/null 2>&1; then
|
||||||
printf 'mpris-scrobbler not found in PATH\n' >&2
|
printf 'scrbblr not found in PATH\n' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ if ! command -v rsync >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
STATE_HOME="${XDG_STATE_HOME:-${HOME}/.local/state}"
|
STATE_HOME="${XDG_STATE_HOME:-${HOME}/.local/state}"
|
||||||
STATE_DIR="${STATE_HOME}/mpris-scrobbler"
|
STATE_DIR="${STATE_HOME}/scrbblr"
|
||||||
MARKER_FILE="${STATE_DIR}/last-published-scrobble.txt"
|
MARKER_FILE="${STATE_DIR}/last-published-scrobble.txt"
|
||||||
mkdir -p "${STATE_DIR}"
|
mkdir -p "${STATE_DIR}"
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ do_publish() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local latest_scrobble
|
local latest_scrobble
|
||||||
latest_scrobble="$(mpris-scrobbler last-scrobble "${last_args[@]}")"
|
latest_scrobble="$(scrbblr last-scrobble "${last_args[@]}")"
|
||||||
if [[ -z "${latest_scrobble}" ]]; then
|
if [[ -z "${latest_scrobble}" ]]; then
|
||||||
printf 'No scrobbles yet. Nothing to publish.\n'
|
printf 'No scrobbles yet. Nothing to publish.\n'
|
||||||
return 0
|
return 0
|
||||||
@@ -127,7 +127,7 @@ do_publish() {
|
|||||||
if [[ -n "${DB_PATH}" ]]; then
|
if [[ -n "${DB_PATH}" ]]; then
|
||||||
report_args+=(--db-path "${DB_PATH}")
|
report_args+=(--db-path "${DB_PATH}")
|
||||||
fi
|
fi
|
||||||
mpris-scrobbler "${report_args[@]}"
|
scrbblr "${report_args[@]}"
|
||||||
|
|
||||||
printf 'Publishing via rsync to %s...\n' "${REMOTE_TARGET}"
|
printf 'Publishing via rsync to %s...\n' "${REMOTE_TARGET}"
|
||||||
rsync -Pavz "${OUTPUT_DIR}" "${REMOTE_TARGET}"
|
rsync -Pavz "${OUTPUT_DIR}" "${REMOTE_TARGET}"
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
//! Database module for the MPRIS scrobbler.
|
//! Database module for scrbblr.
|
||||||
//!
|
//!
|
||||||
//! This module handles all SQLite interactions: schema creation, inserting
|
//! This module handles all SQLite interactions: schema creation, inserting
|
||||||
//! scrobble records, and querying data for reports. The database is stored
|
//! scrobble records, and querying data for reports. The database is stored
|
||||||
//! as a single SQLite file (by default at `~/.local/share/mpris-scrobbler/scrobbles.db`).
|
//! as a single SQLite file (by default at `~/.local/share/scrbblr/scrobbles.db`).
|
||||||
//!
|
//!
|
||||||
//! ## Tables
|
//! ## Tables
|
||||||
//!
|
//!
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
//! ## User-Agent
|
//! ## User-Agent
|
||||||
//!
|
//!
|
||||||
//! MusicBrainz requires a descriptive User-Agent header. We use:
|
//! MusicBrainz requires a descriptive User-Agent header. We use:
|
||||||
//! `mpris-scrobbler/0.1.0 (https://github.com/arturmeski/mpris_scrobbler)`
|
//! `scrbblr/0.1.0 (https://github.com/arturmeski/scrbblr)`
|
||||||
|
|
||||||
use crate::db::{self, AlbumCacheEntry};
|
use crate::db::{self, AlbumCacheEntry};
|
||||||
use image::codecs::jpeg::JpegEncoder;
|
use image::codecs::jpeg::JpegEncoder;
|
||||||
@@ -57,7 +57,7 @@ use std::time::Duration;
|
|||||||
/// User-Agent string sent with all HTTP requests to MusicBrainz.
|
/// User-Agent string sent with all HTTP requests to MusicBrainz.
|
||||||
/// Required by MusicBrainz API policy:
|
/// Required by MusicBrainz API policy:
|
||||||
/// https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting
|
/// https://musicbrainz.org/doc/MusicBrainz_API/Rate_Limiting
|
||||||
const USER_AGENT: &str = "mpris-scrobbler/0.1.0 (https://github.com/arturmeski/mpris_scrobbler)";
|
const USER_AGENT: &str = "scrbblr/0.1.0 (https://github.com/arturmeski/scrbblr)";
|
||||||
|
|
||||||
/// Build the shared HTTP client used for all MusicBrainz and Cover Art Archive requests.
|
/// Build the shared HTTP client used for all MusicBrainz and Cover Art Archive requests.
|
||||||
fn build_client() -> Client {
|
fn build_client() -> Client {
|
||||||
@@ -188,14 +188,14 @@ struct MbTag {
|
|||||||
|
|
||||||
/// Get the directory where cover art images are stored, creating it if needed.
|
/// Get the directory where cover art images are stored, creating it if needed.
|
||||||
///
|
///
|
||||||
/// Default: `~/.local/share/mpris-scrobbler/covers/`
|
/// Default: `~/.local/share/scrbblr/covers/`
|
||||||
/// (respects `$XDG_DATA_HOME`)
|
/// (respects `$XDG_DATA_HOME`)
|
||||||
pub fn covers_dir() -> PathBuf {
|
pub fn covers_dir() -> PathBuf {
|
||||||
let data_dir = std::env::var("XDG_DATA_HOME").unwrap_or_else(|_| {
|
let data_dir = std::env::var("XDG_DATA_HOME").unwrap_or_else(|_| {
|
||||||
let home = std::env::var("HOME").expect("HOME not set");
|
let home = std::env::var("HOME").expect("HOME not set");
|
||||||
format!("{}/.local/share", home)
|
format!("{}/.local/share", home)
|
||||||
});
|
});
|
||||||
let dir = PathBuf::from(format!("{}/mpris-scrobbler/covers", data_dir));
|
let dir = PathBuf::from(format!("{}/scrbblr/covers", data_dir));
|
||||||
fs::create_dir_all(&dir).expect("Failed to create covers directory");
|
fs::create_dir_all(&dir).expect("Failed to create covers directory");
|
||||||
dir
|
dir
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/main.rs
12
src/main.rs
@@ -1,4 +1,4 @@
|
|||||||
//! MPRIS + MPD Scrobbler — a local music scrobbler for Linux.
|
//! scrbblr — a local music scrobbler for MPRIS and MPD on Linux.
|
||||||
//!
|
//!
|
||||||
//! This is the CLI entry point. It provides five subcommands:
|
//! This is the CLI entry point. It provides five subcommands:
|
||||||
//!
|
//!
|
||||||
@@ -69,7 +69,7 @@ const DEFAULT_PLAYER: &str = "com.blitzfc.qbz";
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "mpris-scrobbler", about = "MPRIS scrobbler using playerctl")]
|
#[command(name = "scrbblr", about = "Local music scrobbler for MPRIS and MPD")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Commands,
|
command: Commands,
|
||||||
@@ -110,7 +110,7 @@ enum Commands {
|
|||||||
mpd_port: u16,
|
mpd_port: u16,
|
||||||
|
|
||||||
/// Path to the SQLite database file. If not specified, defaults to
|
/// Path to the SQLite database file. If not specified, defaults to
|
||||||
/// ~/.local/share/mpris-scrobbler/scrobbles.db (respects $XDG_DATA_HOME).
|
/// ~/.local/share/scrbblr/scrobbles.db (respects $XDG_DATA_HOME).
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
db_path: Option<String>,
|
db_path: Option<String>,
|
||||||
},
|
},
|
||||||
@@ -228,8 +228,8 @@ enum Commands {
|
|||||||
|
|
||||||
/// Determine the default database path, following the XDG Base Directory spec.
|
/// Determine the default database path, following the XDG Base Directory spec.
|
||||||
///
|
///
|
||||||
/// Path: $XDG_DATA_HOME/mpris-scrobbler/scrobbles.db
|
/// Path: $XDG_DATA_HOME/scrbblr/scrobbles.db
|
||||||
/// Falls back to: ~/.local/share/mpris-scrobbler/scrobbles.db
|
/// Falls back to: ~/.local/share/scrbblr/scrobbles.db
|
||||||
///
|
///
|
||||||
/// Creates the parent directory if it doesn't exist.
|
/// Creates the parent directory if it doesn't exist.
|
||||||
fn default_db_path() -> String {
|
fn default_db_path() -> String {
|
||||||
@@ -237,7 +237,7 @@ fn default_db_path() -> String {
|
|||||||
let home = std::env::var("HOME").expect("HOME not set");
|
let home = std::env::var("HOME").expect("HOME not set");
|
||||||
format!("{}/.local/share", home)
|
format!("{}/.local/share", home)
|
||||||
});
|
});
|
||||||
let dir = format!("{}/mpris-scrobbler", data_dir);
|
let dir = format!("{}/scrbblr", data_dir);
|
||||||
std::fs::create_dir_all(&dir).expect("Failed to create data directory");
|
std::fs::create_dir_all(&dir).expect("Failed to create data directory");
|
||||||
format!("{}/scrobbles.db", dir)
|
format!("{}/scrobbles.db", dir)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -784,7 +784,7 @@ pub fn render_html_report(conn: &Connection, limit: i64, all_time_limit: i64) ->
|
|||||||
h.open("<div class=\"hero\">");
|
h.open("<div class=\"hero\">");
|
||||||
h.line("<h1>Listening Report</h1>");
|
h.line("<h1>Listening Report</h1>");
|
||||||
h.linef(format_args!(
|
h.linef(format_args!(
|
||||||
"<div class=\"sub\">Generated by mpris-scrobbler on {}</div>",
|
"<div class=\"sub\">Generated by scrbblr on {}</div>",
|
||||||
html_escape(&generated_at)
|
html_escape(&generated_at)
|
||||||
));
|
));
|
||||||
h.close("</div>");
|
h.close("</div>");
|
||||||
@@ -1477,7 +1477,7 @@ fn pin_album_cmd(artist: &str, album: &str, mbid: Option<&str>) -> String {
|
|||||||
let a = artist.replace('"', "\\\"");
|
let a = artist.replace('"', "\\\"");
|
||||||
let b = album.replace('"', "\\\"");
|
let b = album.replace('"', "\\\"");
|
||||||
let m = mbid.unwrap_or("MBID_HERE");
|
let m = mbid.unwrap_or("MBID_HERE");
|
||||||
format!("mpris-scrobbler pin-album --artist \"{a}\" --album \"{b}\" --mbid \"{m}\"")
|
format!("scrbblr pin-album --artist \"{a}\" --album \"{b}\" --mbid \"{m}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn html_escape(s: &str) -> String {
|
fn html_escape(s: &str) -> String {
|
||||||
|
|||||||
10
uninstall.sh
10
uninstall.sh
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Interactive uninstaller for mpris-scrobbler.
|
# Interactive uninstaller for scrbblr.
|
||||||
#
|
#
|
||||||
# Steps:
|
# Steps:
|
||||||
# 1. Stop and disable the systemd user service
|
# 1. Stop and disable the systemd user service
|
||||||
@@ -12,13 +12,13 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
BIN_NAME="mpris-scrobbler"
|
BIN_NAME="scrbblr"
|
||||||
PUBLISH_BIN_NAME="mpris-scrobbler-publish"
|
PUBLISH_BIN_NAME="scrbblr-publish"
|
||||||
INSTALL_DIR="$HOME/.local/bin"
|
INSTALL_DIR="$HOME/.local/bin"
|
||||||
SERVICE_DIR="$HOME/.config/systemd/user"
|
SERVICE_DIR="$HOME/.config/systemd/user"
|
||||||
SERVICE_NAME="mpris-scrobbler.service"
|
SERVICE_NAME="scrbblr.service"
|
||||||
|
|
||||||
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/mpris-scrobbler"
|
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/scrbblr"
|
||||||
|
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
|
|||||||
Reference in New Issue
Block a user