sonicratings 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sonicratings-1.4.0/LICENSE +21 -0
- sonicratings-1.4.0/PKG-INFO +34 -0
- sonicratings-1.4.0/README.md +492 -0
- sonicratings-1.4.0/pyproject.toml +18 -0
- sonicratings-1.4.0/setup.cfg +4 -0
- sonicratings-1.4.0/sonicratings/__init__.py +2 -0
- sonicratings-1.4.0/sonicratings/__main__.py +5 -0
- sonicratings-1.4.0/sonicratings/automation.py +312 -0
- sonicratings-1.4.0/sonicratings/cli.py +1363 -0
- sonicratings-1.4.0/sonicratings/discogs.py +203 -0
- sonicratings-1.4.0/sonicratings/lastfm_matching.py +56 -0
- sonicratings-1.4.0/sonicratings/score.py +24 -0
- sonicratings-1.4.0/sonicratings/tidal.py +122 -0
- sonicratings-1.4.0/sonicratings.egg-info/PKG-INFO +34 -0
- sonicratings-1.4.0/sonicratings.egg-info/SOURCES.txt +24 -0
- sonicratings-1.4.0/sonicratings.egg-info/dependency_links.txt +1 -0
- sonicratings-1.4.0/sonicratings.egg-info/entry_points.txt +3 -0
- sonicratings-1.4.0/sonicratings.egg-info/requires.txt +5 -0
- sonicratings-1.4.0/sonicratings.egg-info/top_level.txt +1 -0
- sonicratings-1.4.0/tests/test_automation.py +162 -0
- sonicratings-1.4.0/tests/test_bug_regressions.py +111 -0
- sonicratings-1.4.0/tests/test_discogs.py +240 -0
- sonicratings-1.4.0/tests/test_lastfm.py +125 -0
- sonicratings-1.4.0/tests/test_package_cli.py +158 -0
- sonicratings-1.4.0/tests/test_scoring.py +23 -0
- sonicratings-1.4.0/tests/test_tidal.py +94 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Kevin Restaino
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sonicratings
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Sync provider ratings to Navidrome and Subsonic libraries
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2024 Kevin Restaino
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: requests>=2.31
|
|
30
|
+
Requires-Dist: python-dotenv>=1.0
|
|
31
|
+
Requires-Dist: colorama>=0.4.6
|
|
32
|
+
Requires-Dist: tqdm>=4.66.1
|
|
33
|
+
Requires-Dist: mutagen>=1.47
|
|
34
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# SonicRatings
|
|
2
|
+
|
|
3
|
+
Bring track popularity and community ratings into your Navidrome or Subsonic-compatible music library. SonicRatings supports Spotify, TIDAL, Last.fm, MusicBrainz, and Discogs as rating sources.
|
|
4
|
+
|
|
5
|
+
SonicRatings works especially well alongside [navidrome-smart-playlists](https://github.com/SickProdigy/navidrome-smart-playlists). SonicRatings supplies the track ratings, while its Navidrome `.NSP` smart playlist definitions can use those ratings to build dynamic playlists of highly rated music by genre or decade. Run SonicRatings first, then refresh the playlists with a Navidrome library scan.
|
|
6
|
+
|
|
7
|
+
Originally called **SPTNR** (Spotify Popularity to Navidrome Ratings), this script was developed to repurpose the star ratings in Navidrome. As a Navidrome user who utilizes the 'favorite' feature instead of star ratings, I wanted to give new life and utility to the unused rating system. By mapping provider scores to library ratings, SonicRatings provides a quick way to identify popular tracks. This becomes particularly useful when frequently adding new albums and artists, especially those you're not familiar with.
|
|
8
|
+
|
|
9
|
+
The installed command and Python package are both `sonicratings`; the Compose service is also `sonicratings`. The Docker examples below build and use the local image `sonicratings:local`.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
1. [Features](#features)
|
|
16
|
+
2. [Requirements](#requirements)
|
|
17
|
+
3. [Quick Start](#quick-start)
|
|
18
|
+
4. [Using Docker Compose](#using-docker-compose)
|
|
19
|
+
5. [Running Natively or Building Locally](#running-natively-or-building-locally)
|
|
20
|
+
6. [Usage](#usage)
|
|
21
|
+
7. [Examples](#examples)
|
|
22
|
+
8. [Resuming Interrupted Sessions](#resuming-interrupted-sessions)
|
|
23
|
+
9. [Managing Docker Containers](#managing-docker-containers)
|
|
24
|
+
10. [Mapping Provider Scores to Library Ratings](#mapping-provider-scores-to-library-ratings)
|
|
25
|
+
11. [Estimated Processing Times](#estimated-processing-times)
|
|
26
|
+
12. [Importance of Accurate Metadata for Track Lookup](#importance-of-accurate-metadata-for-track-lookup)
|
|
27
|
+
13. [Logs](#logs)
|
|
28
|
+
14. [Package and Automation](#package-and-automation)
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Spotify Integration**: Connects to Spotify's API to fetch popularity data for tracks.
|
|
33
|
+
- **TIDAL Support**: Uses TIDAL catalog popularity with the same star conversion as Spotify. Requires TIDAL developer app credentials, not Spotify credentials.
|
|
34
|
+
- **Last.fm Support**: Can use Last.fm artist top-track position, listeners, and playcounts as an alternate popularity source. (A useful alternative when you want broader catalog coverage.)
|
|
35
|
+
- **MusicBrainz Support**: Can use MusicBrainz community ratings as a rating source. (Currently not a lot of ratings available, but worth having as an option for the future.)
|
|
36
|
+
- **Discogs Support**: Preview release ratings and optionally apply them to matching tracks. These are inherited album/edition ratings, with vote counts and release IDs shown in the logs.
|
|
37
|
+
- **Subsonic/Navidrome Integration**: Updates personal track ratings through the Subsonic API based on provider scores.
|
|
38
|
+
- **Flexible Processing**: Process specific artists, albums, or a range of artists or albums.
|
|
39
|
+
- **Unrated-Only Mode**: Skip tracks that already have a rating and only update unrated songs.
|
|
40
|
+
- **Preview Mode**: Run the script in preview mode to see changes without making any actual updates.
|
|
41
|
+
- **Logging**: Detailed logging of the process, both in the console and to a file.
|
|
42
|
+
- **Docker Support**: Run the script in a Docker container for consistent environments and ease of use.
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- Python 3.10 or newer, or Docker
|
|
47
|
+
- Access to a Navidrome or compatible Subsonic server
|
|
48
|
+
- Spotify client ID and client secret for the default Spotify provider ([Developer dashboard](https://developer.spotify.com/dashboard/create))
|
|
49
|
+
- Optional: a Last.fm API key if you want to use Last.fm as the popularity provider
|
|
50
|
+
- Optional: TIDAL developer app credentials if you want to use TIDAL
|
|
51
|
+
- MusicBrainz community rating lookups do not require provider credentials
|
|
52
|
+
- Optional: a Discogs personal access token (`DISCOGS_TOKEN`) if anonymous access is insufficient
|
|
53
|
+
|
|
54
|
+
**Compatibility Note**: SonicRatings is designed for Navidrome and uses the Subsonic API. It should work with any Subsonic/Navidrome server supporting the required browsing, personal-rating, and authentication APIs. Other server implementations have not been tested live. If you successfully use it with another server, please open an issue so I can document compatibility and assist others.
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
Build SonicRatings from this checkout, then replace the environment variable values below with your own:
|
|
59
|
+
|
|
60
|
+
```console
|
|
61
|
+
docker build -t sonicratings:local .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```console
|
|
65
|
+
docker run --rm -t \
|
|
66
|
+
-v sonicratings-logs:/usr/src/app/logs \
|
|
67
|
+
-e NAV_BASE_URL=your_navidrome_server_url \
|
|
68
|
+
-e NAV_USER=your_navidrome_username \
|
|
69
|
+
-e NAV_PASS=your_navidrome_password \
|
|
70
|
+
-e SPOTIFY_CLIENT_ID=your_spotify_client_id \
|
|
71
|
+
-e SPOTIFY_CLIENT_SECRET=your_spotify_client_secret \
|
|
72
|
+
sonicratings:local
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For Last.fm, add `-e LASTFM_API_KEY=your_lastfm_api_key` and `--provider lastfm`. MusicBrainz uses `--provider musicbrainz` without provider credentials. Only the selected provider needs credentials; TIDAL setup is described below.
|
|
76
|
+
|
|
77
|
+
The `sonicratings-logs` Docker volume keeps logs and update locks between runs. Compose below uses a local `./logs` directory instead.
|
|
78
|
+
|
|
79
|
+
**Note**: The `-t` flag is used to allocate a pseudo-terminal which assists in displaying colored and bold text in the terminal output, which this script uses.
|
|
80
|
+
|
|
81
|
+
### Using Docker Compose
|
|
82
|
+
|
|
83
|
+
1. **Create `docker-compose.yml` File**: Set up your `docker-compose.yml` file with the following content, replacing the environment variables with your own details:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
services:
|
|
87
|
+
sonicratings:
|
|
88
|
+
container_name: sonicratings
|
|
89
|
+
build: .
|
|
90
|
+
image: sonicratings:local
|
|
91
|
+
environment:
|
|
92
|
+
- NAV_BASE_URL=your_navidrome_server_url
|
|
93
|
+
- NAV_USER=your_navidrome_username
|
|
94
|
+
- NAV_PASS=your_navidrome_password
|
|
95
|
+
- SPOTIFY_CLIENT_ID=your_spotify_client_id
|
|
96
|
+
- SPOTIFY_CLIENT_SECRET=your_spotify_client_secret
|
|
97
|
+
- LASTFM_API_KEY=${LASTFM_API_KEY:-}
|
|
98
|
+
volumes:
|
|
99
|
+
- ./logs:/usr/src/app/logs
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
For Last.fm, set `LASTFM_API_KEY` in `.env` and run with `--provider lastfm`. For TIDAL, add its credentials to the service and run with `--provider tidal`. The [Compose example](docker-compose.yml.example) includes all provider settings.
|
|
103
|
+
|
|
104
|
+
2. **Run the Script**: Execute the Docker Compose command to run the script:
|
|
105
|
+
|
|
106
|
+
```console
|
|
107
|
+
docker compose run --rm sonicratings
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Running Natively or Building Locally
|
|
111
|
+
|
|
112
|
+
For those who prefer running the script natively using Python or building the Docker image locally, the following steps apply:
|
|
113
|
+
|
|
114
|
+
### Running Natively (Without Docker)
|
|
115
|
+
|
|
116
|
+
Requires Python 3.10 or newer. From the repository, create an environment and install the package.
|
|
117
|
+
|
|
118
|
+
Linux:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python3 -m venv .venv
|
|
122
|
+
source .venv/bin/activate
|
|
123
|
+
python -m pip install -e .
|
|
124
|
+
cp .env.example .env
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Windows PowerShell:
|
|
128
|
+
|
|
129
|
+
```powershell
|
|
130
|
+
python -m venv .venv
|
|
131
|
+
.\.venv\Scripts\Activate.ps1
|
|
132
|
+
python -m pip install -e .
|
|
133
|
+
Copy-Item .env.example .env
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Keep your existing `.env` when upgrading. Configure your server and selected provider using [.env.example](.env.example). Editable installation makes the command use local source changes immediately. Activate the environment again in each new terminal.
|
|
137
|
+
|
|
138
|
+
Preview a backfill of unrated songs:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
sonicratings --provider lastfm --unrated-only --preview
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Apply the same backfill:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
sonicratings --provider lastfm --unrated-only
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`sonicratings --help` lists all options. From the checkout you can also use `python -m sonicratings` with the same options. Run from the repository to reuse your existing `.env` and `logs/song_update_lock.json`; elsewhere, select them explicitly with `--env-file PATH` and `--log-dir PATH`.
|
|
151
|
+
|
|
152
|
+
### Building and Running with Docker Locally
|
|
153
|
+
|
|
154
|
+
1. **Clone the Repository**: Clone the full repository to your local machine so the local Docker build includes the matching and provider modules.
|
|
155
|
+
|
|
156
|
+
2. **Configure Docker Compose**: Copy the example and edit your `docker-compose.yml`:
|
|
157
|
+
|
|
158
|
+
```console
|
|
159
|
+
cp docker-compose.yml.example docker-compose.yml
|
|
160
|
+
# Edit the docker-compose.yml file
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
3. **Check the Docker Image Source**: The example uses `build: .` and the local image tag `sonicratings:local`.
|
|
164
|
+
|
|
165
|
+
4. **Build and Run**: Build the Docker image and run the script:
|
|
166
|
+
|
|
167
|
+
```console
|
|
168
|
+
docker compose build
|
|
169
|
+
docker compose run --rm sonicratings [options]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Usage
|
|
173
|
+
|
|
174
|
+
The script supports various options for flexible usage. Below are examples of how to run the script with different options, using the installed command, Docker Compose, and Docker Run. Replace `[options]` with any of the specified options based on your needs.
|
|
175
|
+
|
|
176
|
+
### Options
|
|
177
|
+
|
|
178
|
+
- `-p, --preview`: Look up and display ratings without writing ratings or update locks. Existing locks still apply; add `--lock-duration 0` to preview every selected track.
|
|
179
|
+
- `-a, --artist ARTIST_ID`: Process a specific artist. Multiple artists can be specified.
|
|
180
|
+
- `-b, --album ALBUM_ID`: Process a specific album. Multiple albums can be specified.
|
|
181
|
+
- `-s, --start START_INDEX`: Start processing from the artist at the specified index (0-based).
|
|
182
|
+
- `-l, --limit LIMIT`: Limit the processing to a specific number of artists from the start index.
|
|
183
|
+
- `-d, --lock-duration DURATION`: Number of days to lock song updates. Defaults to 7; 0 disables the update lock, while `--unrated-only` still skips already-rated songs.
|
|
184
|
+
- `-j, --lock-jitter HOURS`: Spread new lock expiries across a window of this many hours around the base duration. Defaults to 24 (up to 12 hours earlier or later).
|
|
185
|
+
- `--navidrome-retries RETRIES`: Number of attempts for Navidrome API requests. Defaults to 3.
|
|
186
|
+
- `--navidrome-timeout SECONDS`: Base timeout for Navidrome API requests. Defaults to 10 seconds and increases on each retry.
|
|
187
|
+
- `--provider spotify|lastfm|musicbrainz|tidal|discogs`: Choose the source used to derive the rating. Spotify is the default.
|
|
188
|
+
- `--discogs-inherit-album-rating`: Explicitly allow Discogs release ratings to be written to matching tracks. Required when applying Discogs ratings; preview works without it.
|
|
189
|
+
- `--discogs-min-votes VOTES`: Minimum votes for a Discogs release rating. Defaults to 5; must be positive.
|
|
190
|
+
- `--discogs-release ALBUM_ID=RELEASE_ID`: Select a specific Discogs edition for a Navidrome album. Repeat for multiple albums. Artist, album, and track matching checks still apply.
|
|
191
|
+
- `--unrated-only`: Only update songs that do not already have a Navidrome rating.
|
|
192
|
+
- `--env-file PATH`: Load configuration from this file; defaults to `.env` in the current directory. Existing environment variables take precedence.
|
|
193
|
+
- `--log-dir PATH`: Store logs and update locks here; defaults to `logs/`. Reuse the same directory to retain existing update history.
|
|
194
|
+
- `-v, --version`: Show the SonicRatings package version.
|
|
195
|
+
|
|
196
|
+
### Command Formats
|
|
197
|
+
|
|
198
|
+
1. **Running Natively (Installed Command)**:
|
|
199
|
+
|
|
200
|
+
```console
|
|
201
|
+
sonicratings [options]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
2. **Using Docker Compose**:
|
|
205
|
+
|
|
206
|
+
```console
|
|
207
|
+
docker compose run --rm sonicratings [options]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
3. **Using Docker Run**:
|
|
211
|
+
```console
|
|
212
|
+
docker run --rm -t [environment variables] sonicratings:local [options]
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Examples
|
|
216
|
+
|
|
217
|
+
- **Preview Mode**:
|
|
218
|
+
Run the script in preview mode to see changes without making any actual updates.
|
|
219
|
+
|
|
220
|
+
- Python: `sonicratings -p`
|
|
221
|
+
- Docker Compose: `docker compose run --rm sonicratings -p`
|
|
222
|
+
- Docker Run: `docker run --rm -t [env vars] sonicratings:local -p`
|
|
223
|
+
|
|
224
|
+
- **Process Specific Artist**:
|
|
225
|
+
Process only one artist by specifying their ID.
|
|
226
|
+
|
|
227
|
+
- Python: `sonicratings -a artist_id`
|
|
228
|
+
- Docker Compose: `docker compose run --rm sonicratings -a artist_id`
|
|
229
|
+
- Docker Run: `docker run --rm -t [env vars] sonicratings:local -a artist_id`
|
|
230
|
+
|
|
231
|
+
- **Process Specific Albums**:
|
|
232
|
+
Process multiple specific albums by specifying their IDs.
|
|
233
|
+
|
|
234
|
+
- Python: `sonicratings -b album_id1 -b album_id2`
|
|
235
|
+
- Docker Compose: `docker compose run --rm sonicratings -b album_id1 -b album_id2`
|
|
236
|
+
- Docker Run: `docker run --rm -t [env vars] sonicratings:local -b album_id1 -b album_id2`
|
|
237
|
+
|
|
238
|
+
- **Process Range of Artists**:
|
|
239
|
+
Process artists starting from a certain index with a limit.
|
|
240
|
+
- Python: `sonicratings -s 10 -l 5`
|
|
241
|
+
- Docker Compose: `docker compose run --rm sonicratings -s 10 -l 5`
|
|
242
|
+
- Docker Run: `docker run --rm -t [env vars] sonicratings:local -s 10 -l 5`
|
|
243
|
+
|
|
244
|
+
- **Only Update Unrated Songs**:
|
|
245
|
+
Skip tracks that already have a rating in Navidrome.
|
|
246
|
+
- Python: `sonicratings --unrated-only`
|
|
247
|
+
- Docker Compose: `docker compose run --rm sonicratings --unrated-only`
|
|
248
|
+
- Docker Run: `docker run --rm -t [env vars] sonicratings:local --unrated-only`
|
|
249
|
+
|
|
250
|
+
- **Tolerate Slow Navidrome Scans**:
|
|
251
|
+
Retry Navidrome API calls and increase the timeout for each retry.
|
|
252
|
+
- Python: `sonicratings --navidrome-retries 3 --navidrome-timeout 10`
|
|
253
|
+
- Docker Compose: `docker compose run --rm sonicratings --navidrome-retries 3 --navidrome-timeout 10`
|
|
254
|
+
- Docker Run: `docker run --rm -t [env vars] sonicratings:local --navidrome-retries 3 --navidrome-timeout 10`
|
|
255
|
+
|
|
256
|
+
- **Use Last.fm as the Provider**:
|
|
257
|
+
Use Last.fm artist top-track position, listener, and playcount data instead of Spotify popularity.
|
|
258
|
+
- Python: `LASTFM_API_KEY=... sonicratings --provider lastfm`
|
|
259
|
+
- Docker Compose: `docker compose run --rm -e LASTFM_API_KEY=... sonicratings --provider lastfm`
|
|
260
|
+
- Docker Run: `docker run --rm -t -e LASTFM_API_KEY=... [env vars] sonicratings:local --provider lastfm`
|
|
261
|
+
|
|
262
|
+
Last.fm does not provide a Spotify-style popularity number, so the script derives one from three signals:
|
|
263
|
+
|
|
264
|
+
- **Artist top-track position**: where the track appears in Last.fm's `artist.getTopTracks` results for that artist. This is the largest part of the score.
|
|
265
|
+
- **Listener reach**: the track's unique Last.fm listener count, scaled logarithmically.
|
|
266
|
+
- **Replay engagement**: plays per listener, used as a small capped bonus.
|
|
267
|
+
|
|
268
|
+
This keeps ordinary catalog tracks mostly in the 2-3 star range while letting occasional artist standouts reach 4-5 stars.
|
|
269
|
+
|
|
270
|
+
Last.fm lookups try deduplicated spelling variants for Unicode apostrophes/dashes,
|
|
271
|
+
invisible characters, punctuation, and spaced `+` separators. Empty catalog entries
|
|
272
|
+
do not prevent trying another spelling. Remix, live, instrumental, and other version
|
|
273
|
+
descriptors are retained, and returned titles/artists are checked before scoring.
|
|
274
|
+
Compilation lookups use each song's artist rather than `Various Artists`. Artist
|
|
275
|
+
top-track matching uses the same punctuation normalization as track lookup.
|
|
276
|
+
If `track.getInfo` misses a track, a matching entry in that artist's top-track
|
|
277
|
+
catalog can supply listener/play counts; the same title/version checks apply.
|
|
278
|
+
|
|
279
|
+
- **Use MusicBrainz as the Provider**:
|
|
280
|
+
Use MusicBrainz ratings to derive the same 0-5 Navidrome rating.
|
|
281
|
+
- Python: `sonicratings --provider musicbrainz`
|
|
282
|
+
- Docker Compose: `docker compose run --rm sonicratings --provider musicbrainz`
|
|
283
|
+
- Docker Run: `docker run --rm -t [env vars] sonicratings:local --provider musicbrainz`
|
|
284
|
+
|
|
285
|
+
### Use TIDAL as the Provider
|
|
286
|
+
|
|
287
|
+
Create an app in the [TIDAL developer portal](https://developer.tidal.com/) and set `TIDAL_CLIENT_ID` and `TIDAL_CLIENT_SECRET` in `.env` or your Compose environment. Set `TIDAL_COUNTRY_CODE` to your two-letter catalog country (default: `US`). Catalog lookup uses the client credentials flow; your personal TIDAL login and Spotify credentials are not required.
|
|
288
|
+
|
|
289
|
+
Preview before applying:
|
|
290
|
+
|
|
291
|
+
```console
|
|
292
|
+
sonicratings --provider tidal --unrated-only --preview
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
For the local Docker build:
|
|
296
|
+
|
|
297
|
+
```console
|
|
298
|
+
docker compose build sonicratings
|
|
299
|
+
docker compose run --rm sonicratings --provider tidal --unrated-only --preview
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Remove `--preview` to apply ratings. Searches require matching artist and title (including version), preferring matching albums. Missing matches or missing/invalid popularity preserve existing ratings. Scores mapping to zero do not write a rating or an update lock, so unrated tracks remain eligible on later runs, subject to any existing lock. API failures stop the run rather than clearing ratings.
|
|
303
|
+
|
|
304
|
+
### Use Discogs as the Provider
|
|
305
|
+
|
|
306
|
+
Discogs rates releases rather than individual songs. Automatic matching uses the designated main release of a uniquely matched master; it is an edition's rating, not an average across editions. If no master matches, a unique matching release can be used. Ambiguous matches, missing ratings, and releases below the minimum vote count preserve existing track ratings.
|
|
307
|
+
|
|
308
|
+
Preview first, including tracks with active locks:
|
|
309
|
+
|
|
310
|
+
```console
|
|
311
|
+
sonicratings --provider discogs --preview --lock-duration 0 -b album_id
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
To apply the release's rating to matching songs, explicitly enable inheritance:
|
|
315
|
+
|
|
316
|
+
```console
|
|
317
|
+
sonicratings --provider discogs --discogs-inherit-album-rating --unrated-only
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Track titles and performers must match the release's tracklist; live/remix/version words are retained. Compilation matching uses the album artist for the release and each song's performer for its track. Logs identify the Discogs release, selection method, vote count, and inherited rating. Native artist ratings and separate album-rating writes are not included.
|
|
321
|
+
|
|
322
|
+
Use `--discogs-release album_id=2879` to choose a particular edition. The [Discogs release ID](https://www.discogs.com/release/2879) is the number in its release URL, not a master ID. Ratings need at least five votes by default (`--discogs-min-votes` changes this); averages are rounded to the nearest star, with halves rounded up.
|
|
323
|
+
|
|
324
|
+
Requests are paced conservatively and album lookups are cached for the run. Anonymous access was tested successfully; if access is denied, set `DISCOGS_TOKEN` to a personal access token from [Discogs developer settings](https://www.discogs.com/settings/developers). For Compose, pass it as `DISCOGS_TOKEN=${DISCOGS_TOKEN:-}` in the service environment, as the example file does. API failures stop the run without writing a rating for the failed lookup. The reusable client is available from `sonicratings.discogs`.
|
|
325
|
+
|
|
326
|
+
For Docker, rebuild the image and use the same options with `docker compose run --rm sonicratings`.
|
|
327
|
+
|
|
328
|
+
## Resuming Interrupted Sessions
|
|
329
|
+
|
|
330
|
+
In cases where your session gets interrupted - for instance, if your machine goes to sleep, you encounter rate limits from Spotify, or for any other reason that causes the script to not complete - you have the option to resume from where you left off.
|
|
331
|
+
|
|
332
|
+
To determine the point of interruption, check the log file. The log entry will contain details of the artist it failed on, along with the index in a format similar to: `Artist: ARTIST_NAME (ARTIST_NAVIDROME_ID)[INDEX]`. Here, the index is enclosed in brackets.
|
|
333
|
+
|
|
334
|
+
Restart with the same provider and processing options, adding `-s INDEX` from the log. This starts again at the interrupted artist; successful updates within that artist are skipped while their locks remain active. For runs selected with `--artist` or `--album`, rerun those IDs instead.
|
|
335
|
+
|
|
336
|
+
Example command to continue from a specific point:
|
|
337
|
+
|
|
338
|
+
- Python: `sonicratings -s INDEX`
|
|
339
|
+
- Docker Compose: `docker compose run --rm sonicratings -s INDEX`
|
|
340
|
+
- Docker Run: `docker run --rm -t [env vars] sonicratings:local -s INDEX`
|
|
341
|
+
|
|
342
|
+
_Note: Replace `[env vars]` with the required environment variable arguments and `INDEX` with the specific index number from your log file._
|
|
343
|
+
|
|
344
|
+
## Managing Docker Containers
|
|
345
|
+
|
|
346
|
+
Use `docker compose run --rm sonicratings [options]` or `docker run --rm ...` for one-off runs. `--rm` removes the container after it exits; mount `/usr/src/app/logs` to preserve logs and update locks between runs, as the Compose example does.
|
|
347
|
+
|
|
348
|
+
## Mapping Provider Scores to Library Ratings
|
|
349
|
+
|
|
350
|
+
SonicRatings translates provider scores into the library's 5-star rating system. Each provider gets its score a little differently:
|
|
351
|
+
|
|
352
|
+
- **Spotify**: Uses Spotify's native track `popularity` value, which is already a 0 to 100 score.
|
|
353
|
+
- **TIDAL**: Multiplies the documented 0.0 to 1.0 track popularity by 100 and reuses the existing conversion without rounding.
|
|
354
|
+
- **Last.fm**: Derives a 0 to 100 score from three signals:
|
|
355
|
+
- artist top-track position from Last.fm's `artist.getTopTracks` results
|
|
356
|
+
- listener reach from the track's unique listener count
|
|
357
|
+
- replay engagement from plays per listener
|
|
358
|
+
- **MusicBrainz**: Reads the community recording rating from MusicBrainz's recording lookup endpoint. MusicBrainz ratings are already on a 0 to 5 scale, so they are rounded directly to Navidrome's 0 to 5 rating range instead of using the 0 to 100 mapping below.
|
|
359
|
+
- **Discogs**: Uses a release's 1–5 community average, rounded directly to stars after checking the vote count. Applying it to tracks requires `--discogs-inherit-album-rating`; it represents the release's rating rather than each song's individual popularity.
|
|
360
|
+
|
|
361
|
+
For Spotify and TIDAL, the existing conversion uses these exact ranges, without rounding:
|
|
362
|
+
|
|
363
|
+
- **0 to below 16.66**: 0 stars (Not popular; no rating write)
|
|
364
|
+
- **16.66 to below 33.33**: 1 star (Low popularity)
|
|
365
|
+
- **33.33 to below 50**: 2 stars (Moderately popular)
|
|
366
|
+
- **50 to below 66.66**: 3 stars (Popular)
|
|
367
|
+
- **66.66 to below 83.33**: 4 stars (Very popular)
|
|
368
|
+
- **83.33 to 100**: 5 stars (Extremely popular)
|
|
369
|
+
|
|
370
|
+
Last.fm keeps its existing weighted score (50% artist position, 40% listener reach,
|
|
371
|
+
10% replay bonus), rounded to an integer, without normalizing it upward:
|
|
372
|
+
|
|
373
|
+
- **5 stars**: score at least 80, artist top-track position 1-3, and at least 100,000 listeners.
|
|
374
|
+
- **4 stars**: score at least 65, unless the track qualifies for 5 stars.
|
|
375
|
+
- **0-3 stars**: the same lower ranges listed above; scores from 50 to below 65 receive 3 stars.
|
|
376
|
+
|
|
377
|
+
These conservative Last.fm cutoffs were calibrated against 519 tracks from 80 random
|
|
378
|
+
library albums: 24 tracks qualified for 4-5 stars, compared with 21 under the previous
|
|
379
|
+
conversion. They are library-based choices, not universal research-established star
|
|
380
|
+
thresholds. Missing artist rank cannot qualify a track for 5 stars. Existing matching,
|
|
381
|
+
update locks, and `--unrated-only` behavior still apply; already-rated songs are skipped
|
|
382
|
+
when that flag is used.
|
|
383
|
+
|
|
384
|
+
For example, TIDAL popularity `0.20` becomes 20 and receives 1 star; `0.56` becomes approximately 56 and receives 3 stars. These are popularity scores, not personal ratings or catalog percentiles. See the [TIDAL API schema](https://tidal-music.github.io/tidal-api-reference/tidal-api-oas.json).
|
|
385
|
+
|
|
386
|
+
## Estimated Processing Times
|
|
387
|
+
|
|
388
|
+
Rough times to preview a whole library, including already-rated tracks, with no tracks skipped:
|
|
389
|
+
|
|
390
|
+
| Library Size (Tracks) | Last.fm Preview | MusicBrainz Preview |
|
|
391
|
+
| ------------------------------- | ------- | ----------- |
|
|
392
|
+
| 1,000 | ~10 minutes | ~50 minutes |
|
|
393
|
+
| 5,000 | ~45 minutes | ~4 hours |
|
|
394
|
+
| 10,000 | ~1.5 hours | ~8 hours |
|
|
395
|
+
| 50,000 | ~7 hours | ~2 days |
|
|
396
|
+
| 100,000 | ~14 hours | ~3.5 days |
|
|
397
|
+
|
|
398
|
+
Actual updates also write ratings and save locks, so they take longer. Metadata, network speed, retries, and large lock files affect runtime; skipping rated or locked tracks speeds repeat runs. Representative timings are not yet available for Spotify, TIDAL, or Discogs.
|
|
399
|
+
|
|
400
|
+
*Timing basis: 237 tracks across 48 random albums, tested October 13, 2026 with SonicRatings v1.3.0 (Windows/Python 3.14), using `--preview --lock-duration 0` without `--unrated-only`. Rounded projections; revisit after performance changes.*
|
|
401
|
+
|
|
402
|
+
## Importance of Accurate Metadata for Track Lookup
|
|
403
|
+
|
|
404
|
+
The effectiveness of this script heavily relies on the accuracy of the artist, album, and track titles in your music library. For the supported providers to successfully recognize and match songs, these metadata details need to be precise.
|
|
405
|
+
|
|
406
|
+
I personally recommend using **MusicBrainz** to tag your music library. MusicBrainz is a comprehensive music database that provides reliable and standardized music metadata, which significantly enhances the accuracy of track matching with any of the supported providers.
|
|
407
|
+
|
|
408
|
+
However, it's important to acknowledge that even with a perfectly tagged MusicBrainz library, discrepancies can still occur between Spotify and MusicBrainz data. This may result in the script missing some songs during the matching process.
|
|
409
|
+
|
|
410
|
+
The following historical Spotify sample came from my MusicBrainz-tagged library. It describes that run, rather than expected matching accuracy for the current library or other providers:
|
|
411
|
+
|
|
412
|
+
- **Total Tracks**: 6,481
|
|
413
|
+
- **Tracks Matched**: 6,390
|
|
414
|
+
- **Tracks Not Found**: 91
|
|
415
|
+
- **Match Percentage**: 98.6%
|
|
416
|
+
|
|
417
|
+
## Logs
|
|
418
|
+
|
|
419
|
+
Unmatched tracks retain their existing server ratings and are not marked as
|
|
420
|
+
successfully updated, so later runs can retry them. Successful updates save a
|
|
421
|
+
fixed expiry in `logs/song_update_lock.json`; older timestamp-only entries remain
|
|
422
|
+
supported. `--lock-duration 0` bypasses update locks; `--unrated-only` still skips already-rated songs.
|
|
423
|
+
|
|
424
|
+
Logs are stored in the `logs` directory, and each script execution creates a new log file marked with a timestamp. Since these logs are retained indefinitely, you should manually delete old logs if they are no longer needed.
|
|
425
|
+
|
|
426
|
+
### Log Format
|
|
427
|
+
|
|
428
|
+
The script logs its actions in a straightforward format, using `p:49 -> r:2` to summarize operations:
|
|
429
|
+
|
|
430
|
+
- `p:49` indicates the provider score, where `49` is the specific score.
|
|
431
|
+
- `->` symbolizes the mapping performed by the script.
|
|
432
|
+
- `r:2` shows the calculated Navidrome rating; preview mode displays it without applying it.
|
|
433
|
+
- MusicBrainz uses `s:` for its 0-5 community rating instead of `p:`.
|
|
434
|
+
- Discogs uses `discogs-release:` for the inherited release average and logs its release ID and vote count.
|
|
435
|
+
- The final `Found` count records successful rating writes, so `Found` and `Match` remain zero in preview mode even when lookups match. Tracks skipped by update locks are excluded from the final `Tracks` count.
|
|
436
|
+
|
|
437
|
+
### Terminal Output Colors
|
|
438
|
+
|
|
439
|
+
In the terminal, certain lines are color-coded for quick identification:
|
|
440
|
+
|
|
441
|
+
- **Red**: Denotes tracks not matched with the selected provider's data. In the logs, these are shown with `??` for popularity scores.
|
|
442
|
+
- **Green**: Indicates successful matches and processing of tracks.
|
|
443
|
+
|
|
444
|
+
These colors are exclusive to the terminal output for visual clarity and are not included in the log files to facilitate easier file parsing.
|
|
445
|
+
|
|
446
|
+
## Package and Automation
|
|
447
|
+
|
|
448
|
+
All implementation lives in the [sonicratings](sonicratings/) package. Root script wrappers and the separately versioned `sonicratings-engine` package have been replaced by one installable application.
|
|
449
|
+
|
|
450
|
+
| Module | Purpose |
|
|
451
|
+
| --- | --- |
|
|
452
|
+
| [cli.py](sonicratings/cli.py) | Full library sync, artist/album selection, provider scoring, previews, and update locks. |
|
|
453
|
+
| [lastfm_matching.py](sonicratings/lastfm_matching.py) | Last.fm spelling variants and artist/title/version matching. |
|
|
454
|
+
| [tidal.py](sonicratings/tidal.py) | TIDAL authentication, catalog matching, and popularity lookup. |
|
|
455
|
+
| [discogs.py](sonicratings/discogs.py) | Discogs release matching, vote checks, and album ratings. |
|
|
456
|
+
| [automation.py](sonicratings/automation.py) | Durable Lidarr import queue, server matching, retry worker, and generated-rating provenance. |
|
|
457
|
+
| [score.py](sonicratings/score.py) | Optional `sonicratings-score` calculator using the same Last.fm scoring as the sync. |
|
|
458
|
+
|
|
459
|
+
For automation, install a published SonicRatings release and call `sonicratings` with explicit configuration and runtime paths. The same command runs manually and inside Docker. An imported album can be selected using its **Subsonic/Navidrome album ID**:
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
sonicratings --provider musicbrainz --album NAVIDROME_ALBUM_ID --unrated-only --preview \
|
|
463
|
+
--env-file /path/to/sonicratings.env --log-dir /path/to/sonicratings-logs
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
A Lidarr album ID is a different identifier. Arr-scripts integration will need to wait for the server import and resolve the matching Subsonic/Navidrome album before invoking this command. The queue adapter accepts Lidarr album IDs, snapshots embedded identities locally, and waits for matching tracks on the server:
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
sonicratings enqueue --state-dir logs/import-queue --album-id LIDARR_ALBUM_ID
|
|
470
|
+
sonicratings work --state-dir logs/import-queue --provider lastfm --preview
|
|
471
|
+
sonicratings work --state-dir logs/import-queue --provider lastfm
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Configure `LIDARR_URL` and `LIDARR_API_KEY` for enqueue, plus `SUBSONIC_URL`, `SUBSONIC_USERNAME`, and `SUBSONIC_PASSWORD` (or the existing `NAV_BASE_URL`, `NAV_USER`, and `NAV_PASS`). Queue commands load `.env`; `--env-file PATH` selects another file. `work` and `library` preserve existing personal ratings. Queue state, logs, locks, and `generated-ratings.json` are scoped by server URL and username beneath `--state-dir`.
|
|
475
|
+
|
|
476
|
+
For a whole-library backfill through the same adapter:
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
sonicratings library --state-dir logs/import-queue --provider musicbrainz --preview
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Remove `--preview` to apply. Preview retains queued work and does not write ratings, provenance, or update locks. Failed/missing matches stay queued with retry delays up to one hour; duplicate callbacks retain pending retry state. Discogs still requires `--discogs-inherit-album-rating` for writes. `sonicratings work --help` lists adapter options.
|
|
483
|
+
|
|
484
|
+
Arr-scripts installs this package independently of musicbrainz-helper and calls these commands from its background service. It supplies provenance to the contribution worker so MusicBrainz-derived stars are not submitted back as fresh votes. See the arr-scripts ratings guide for configuration and release pins.
|
|
485
|
+
|
|
486
|
+
Successful writes and pending write intents retain their source in `generated-ratings.json`. A caller can provide that file to a contribution tool without either package importing the other.
|
|
487
|
+
|
|
488
|
+
SonicRatings writes ratings to Navidrome/Subsonic. musicbrainz-helper independently contributes ratings and genres to MusicBrainz. Neither tool imports or installs the other.
|
|
489
|
+
|
|
490
|
+
Package and application version are **1.4.0**. `VERSION`, package metadata, and `sonicratings.__version__` agree. The old `engine-v0.1.0` release is superseded for future installation. Existing source-checkout users should reinstall with `python -m pip install -e .` and replace `python sonicratings.py` commands with `sonicratings` or `python -m sonicratings`.
|
|
491
|
+
|
|
492
|
+
Run checks with `python -B -m unittest discover -s tests`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sonicratings"
|
|
7
|
+
version = "1.4.0"
|
|
8
|
+
description = "Sync provider ratings to Navidrome and Subsonic libraries"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = ["requests>=2.31", "python-dotenv>=1.0", "colorama>=0.4.6", "tqdm>=4.66.1", "mutagen>=1.47"]
|
|
11
|
+
license = {file = "LICENSE"}
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
sonicratings = "sonicratings.cli:main"
|
|
15
|
+
sonicratings-score = "sonicratings.score:main"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
include = ["sonicratings*"]
|