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.
@@ -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
+ ![Screenshot of script and phone](https://i.imgur.com/7NhSQFM.png)
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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """SonicRatings: provider ratings synced to Navidrome/Subsonic."""
2
+ __version__ = "1.4.0"
@@ -0,0 +1,5 @@
1
+ """Run SonicRatings with python -m sonicratings."""
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())