Compare commits

..

4 Commits

Author SHA1 Message Date
jude
558c79d0cb Fix type error in listenbrainz response 2025-09-19 19:00:22 +01:00
jude
bca4cd4eb1 Reduce error proneness 2024-08-16 21:27:49 +01:00
jude
eb30bc11d9 Fall back to just matching song title if album can't be matched
Fixes most cases where a song couldn't be found on the media server
2024-05-25 14:27:48 +01:00
jude
b0f9764fcf Add intruction for tracked playlist rules 2024-04-20 20:12:27 +01:00
6 changed files with 102 additions and 69 deletions

93
.idea/workspace.xml generated
View File

@@ -9,18 +9,18 @@
</configurations>
</component>
<component name="CargoProjects">
<cargoProject FILE="$PROJECT_DIR$/Cargo.toml" />
<cargoProject FILE="$PROJECT_DIR$/Cargo.toml">
<package file="$PROJECT_DIR$">
<feature name="default" enabled="true" />
</package>
</cargoProject>
</component>
<component name="ChangeListManager">
<list default="true" id="52900e09-9584-4b6c-95ff-fbd4ed5d8b2c" name="Changes" comment="Add interface package. Start adding auth stuff for refreshing tokens.">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Cargo.lock" beforeDir="false" afterPath="$PROJECT_DIR$/Cargo.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Cargo.toml" beforeDir="false" afterPath="$PROJECT_DIR$/Cargo.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/etc/playlistd.toml" beforeDir="false" afterPath="$PROJECT_DIR$/etc/playlistd/playlistd.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/daemon/create_playlists.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/daemon/create_playlists.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/main.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/models.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/models.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/systemd/playlistd.service" beforeDir="false" afterPath="$PROJECT_DIR$/systemd/playlistd.service" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/daemon/update_playlists.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/daemon/update_playlists.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/listenbrainz.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/listenbrainz.rs" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -30,6 +30,7 @@
<component name="ClangdSettings">
<option name="formatViaClangd" value="false" />
</component>
<component name="ExecutionTargetManager" SELECTED_TARGET="RsBuildProfile:dev" />
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
@@ -57,42 +58,46 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Cargo.Run.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;RunOnceActivity.rust.reset.selective.auto.import&quot;: &quot;true&quot;,
&quot;WebServerToolWindowFactoryState&quot;: &quot;false&quot;,
&quot;cf.first.check.clang-format&quot;: &quot;false&quot;,
&quot;cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/jude/Documents/navidrome-playlists&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;org.rust.cargo.project.model.PROJECT_DISCOVERY&quot;: &quot;true&quot;,
&quot;org.rust.cargo.project.model.impl.CargoExternalSystemProjectAware.subscribe.first.balloon&quot;: &quot;&quot;,
&quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/mod.rs&quot;: &quot;true&quot;,
&quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/playlists.rs&quot;: &quot;true&quot;,
&quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/lib.rs&quot;: &quot;true&quot;,
&quot;org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/models.rs&quot;: &quot;true&quot;,
&quot;org.rust.first.attach.projects&quot;: &quot;true&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;language.rust.cargo.check&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Cargo.Run.executor": "Run",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.cidr.known.project.marker": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.rust.reset.selective.auto.import": "true",
"WebServerToolWindowFactoryState": "false",
"cf.first.check.clang-format": "false",
"cidr.known.project.marker": "true",
"git-widget-placeholder": "master",
"junie.onboarding.icon.badge.shown": "true",
"last_opened_file_path": "/home/jude/Documents/navidrome-playlists",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"org.rust.cargo.project.model.PROJECT_DISCOVERY": "true",
"org.rust.cargo.project.model.impl.CargoExternalSystemProjectAware.subscribe.first.balloon": "",
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/mod.rs": "true",
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/playlists.rs": "true",
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/lib.rs": "true",
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/models.rs": "true",
"org.rust.first.attach.projects": "true",
"settings.editor.selected.configurable": "language.rust.cargo.check",
"to.speed.mode.migration.done": "true",
"vue.rearranger.settings.migration": "true"
},
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;postgresql&quot;
"keyToStringList": {
"DatabaseDriversLRU": [
"postgresql"
],
&quot;com.intellij.ide.scratch.LRUPopupBuilder$1/SQL Dialect&quot;: [
&quot;PostgreSQL&quot;
"com.intellij.ide.scratch.LRUPopupBuilder$1/SQL Dialect": [
"PostgreSQL"
]
}
}</component>
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/src/daemon" />
@@ -165,7 +170,15 @@
<workItem from="1713385358427" duration="2705000" />
<workItem from="1713465993328" duration="1750000" />
<workItem from="1713469457285" duration="6588000" />
<workItem from="1713609379678" duration="6626000" />
<workItem from="1713609379678" duration="7216000" />
<workItem from="1713639881551" duration="577000" />
<workItem from="1716043707662" duration="542000" />
<workItem from="1716046070333" duration="1484000" />
<workItem from="1716640392358" duration="367000" />
<workItem from="1716642009543" duration="1546000" />
<workItem from="1716650511971" duration="14000" />
<workItem from="1723834879628" duration="4208000" />
<workItem from="1758303664979" duration="1155000" />
</task>
<task id="LOCAL-00001" summary="Structure">
<created>1692008860369</created>

2
Cargo.lock generated
View File

@@ -1456,7 +1456,7 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "playlistd"
version = "0.2.2"
version = "0.2.4"
dependencies = [
"chrono",
"config",

View File

@@ -1,6 +1,6 @@
[package]
name = "playlistd"
version = "0.2.2"
version = "0.2.5"
edition = "2021"
authors = ["Jude Southworth (judesouthworth@pm.me)"]
license = "AGPL-3.0 only"

View File

@@ -20,19 +20,28 @@ For deploying, these can be configured in `/etc/playlistd.toml`.
### Container build (Ubuntu 20.04)
`podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=postgres://jude@127.0.0.1/navidrome-playlists" playlistd cargo deb`
`podman run --rm --network=host -v "$PWD":/mnt -w /mnt -e "DATABASE_URL=postgres://jude@127.0.0.1/playlistd" playlistd cargo deb`
## How to use
Each of these will work best when your music library is well organised with Musicbrainz metadata.
### Tracked playlist
A tracked playlist is a single Subsonic playlist that tracks data from Listenbrainz. For example,
a playlist that contains your top 15 songs from the past week.
These will work best when your music library is well organised with Musicbrainz metadata.
You can create a tracked playlist by inserting a row into the `tracked_playlist` table.
### Static playlist
### Tracked playlist rule
TODO, not yet implemented
A tracked playlist rule is a time-based instruction to create a new tracked playlist. For example,
a playlist every year that contains your top songs.
You can create a tracked playlist rule by inserting a row into the `create_tracked_playlist` table.
For a yearly top songs playlist, run the following:
```sql
INSERT INTO create_tracked_playlist (id, name_template, playlist_size, tracking_user, tracking_type)
VALUES (gen_random_uuid(), '%Y - Top 100', 100, 'jellywx', 'this_year');
```

View File

@@ -2,7 +2,7 @@ use crate::listenbrainz::StatsRange;
use crate::models::TrackedPlaylist;
use crate::subsonic::Subsonic;
use crate::{listenbrainz, CONFIG};
use log::error;
use log::{error, warn};
use sqlx::postgres::PgPool;
use std::time::Duration;
use thiserror::Error;
@@ -44,8 +44,9 @@ pub async fn update_playlists_daemon(
match records_res {
Ok(records) => {
for record in records {
// todo handle me
update_playlist(media_client.clone(), record).await.unwrap();
if let Err(e) = update_playlist(media_client.clone(), record).await {
warn!("Could not update playlist: {:?}", e);
}
}
}
@@ -74,11 +75,16 @@ async fn update_playlist(
let mut tracks = vec![];
for recording in recordings {
if recording.artists.clone().map_or(true, |o| o.is_empty()) {
continue;
}
let search_results = media_client
.as_ref()
.song_search(format!(
"{} {}",
recording.track_name, recording.artists[0].artist_credit_name
recording.track_name,
recording.artists.unwrap_or(vec![])[0].artist_credit_name
))
.await?;
@@ -86,7 +92,10 @@ async fn update_playlist(
.iter()
.find(|s| {
alpha_compare(&s.title, &recording.track_name)
&& alpha_compare(&s.album, &recording.release_name)
&& match &recording.release_name {
Some(release_name) => alpha_compare(&s.album, release_name),
None => true,
}
})
.cloned();
@@ -95,10 +104,22 @@ async fn update_playlist(
tracks.push(track.id);
}
None => {
error!(
"Couldn't find matching track for {} - {}",
recording.track_name, recording.artist_name
)
let filtered = search_results
.iter()
.find(|s| alpha_compare(&s.title, &recording.track_name))
.cloned();
match filtered {
Some(track) => {
tracks.push(track.id);
}
None => {
error!(
"Couldn't find matching track for {} - {}",
recording.track_name, recording.artist_name
)
}
}
}
}
}

View File

@@ -71,18 +71,10 @@ struct RecordingsResponse {
#[derive(Deserialize)]
pub struct RecordingsPayload {
count: u64,
from_ts: u64,
to_ts: u64,
last_updated: u64,
total_recording_count: u64,
user_id: String,
offset: u64,
range: String,
pub recordings: Vec<RecordingsEntry>,
}
#[derive(Deserialize)]
#[derive(Deserialize, Clone)]
pub struct Artist {
pub artist_credit_name: String,
}
@@ -90,11 +82,9 @@ pub struct Artist {
#[derive(Deserialize)]
pub struct RecordingsEntry {
pub track_name: String,
pub release_name: String,
pub release_name: Option<String>,
pub artist_name: String,
pub artists: Vec<Artist>,
pub recording_mbid: String,
pub release_mbid: String,
pub artists: Option<Vec<Artist>>,
}
pub async fn recordings(