Add interface package. Start adding auth stuff for refreshing tokens.
This commit is contained in:
parent
5ff56d8396
commit
08cc752932
16
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
16
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,16 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="JSUnresolvedFunction" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="JSUnresolvedVariable" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="ignoredErrors">
|
||||
<list>
|
||||
<option value="E402" />
|
||||
</list>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
<inspection_tool class="SqlResolveInspection" enabled="false" level="ERROR" enabled_by_default="false" />
|
||||
</profile>
|
||||
</component>
|
84
.idea/workspace.xml
generated
84
.idea/workspace.xml
generated
@ -13,10 +13,17 @@
|
||||
<cargoProject FILE="$PROJECT_DIR$/navidrome/Cargo.toml" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="52900e09-9584-4b6c-95ff-fbd4ed5d8b2c" name="Changes" comment="Track media server songs list">
|
||||
<list default="true" id="52900e09-9584-4b6c-95ff-fbd4ed5d8b2c" name="Changes" comment="Move listenbrainz user to env var">
|
||||
<change afterPath="$PROJECT_DIR$/navidrome/src/client/auth.rs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/listenbrainz.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/listenbrainz.rs" 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$/migrations/20230816135241_initial.sql" beforeDir="false" afterPath="$PROJECT_DIR$/migrations/20230816135241_initial.sql" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/navidrome/src/client/builder.rs" beforeDir="false" afterPath="$PROJECT_DIR$/navidrome/src/client/builder.rs" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/navidrome/src/client/mod.rs" beforeDir="false" afterPath="$PROJECT_DIR$/navidrome/src/client/mod.rs" 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/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" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@ -42,39 +49,43 @@
|
||||
<component name="MarkdownSettingsMigration">
|
||||
<option name="stateVersion" value="1" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 2
|
||||
}</component>
|
||||
<component name="ProjectId" id="2TwLYTFaOXHKmGf9lC3ltOC00Kl" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.cidr.known.project.marker": "true",
|
||||
"WebServerToolWindowFactoryState": "false",
|
||||
"cf.first.check.clang-format": "false",
|
||||
"cidr.known.project.marker": "true",
|
||||
"last_opened_file_path": "/home/jude/Documents/navidrome-playlists/src/daemon",
|
||||
"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.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",
|
||||
"settings.editor.selected.configurable": "language.rust.cargo.check",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.cidr.known.project.marker": "true",
|
||||
"WebServerToolWindowFactoryState": "false",
|
||||
"cf.first.check.clang-format": "false",
|
||||
"cidr.known.project.marker": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"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.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",
|
||||
"settings.editor.selected.configurable": "language.rust.cargo.check",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
},
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"postgresql"
|
||||
"keyToStringList": {
|
||||
"DatabaseDriversLRU": [
|
||||
"postgresql"
|
||||
]
|
||||
}
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/src/daemon" />
|
||||
@ -128,7 +139,11 @@
|
||||
<workItem from="1692003763542" duration="5103000" />
|
||||
<workItem from="1692042976149" duration="10055000" />
|
||||
<workItem from="1694271920428" duration="9295000" />
|
||||
<workItem from="1694285959491" duration="2258000" />
|
||||
<workItem from="1694285959491" duration="2278000" />
|
||||
<workItem from="1694290396939" duration="1743000" />
|
||||
<workItem from="1694333344521" duration="1478000" />
|
||||
<workItem from="1694450589184" duration="2108000" />
|
||||
<workItem from="1694534880857" duration="6011000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Structure">
|
||||
<created>1692008860369</created>
|
||||
@ -145,7 +160,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1694286183476</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="3" />
|
||||
<task id="LOCAL-00003" summary="Move listenbrainz user to env var">
|
||||
<option name="closed" value="true" />
|
||||
<created>1694288928392</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1694288928392</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="4" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@ -154,7 +177,8 @@
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="Structure" />
|
||||
<MESSAGE value="Track media server songs list" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Track media server songs list" />
|
||||
<MESSAGE value="Move listenbrainz user to env var" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Move listenbrainz user to env var" />
|
||||
</component>
|
||||
<component name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
|
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -928,6 +928,7 @@ dependencies = [
|
||||
"serde",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1994,6 +1995,10 @@ name = "uuid"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
|
@ -6,12 +6,13 @@ authors = ["Jude Southworth (judesouthworth@pm.me)"]
|
||||
license = "AGPL-3.0 only"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.6.20"
|
||||
axum = { version = "0.6.20", features = ["json"] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
sqlx = { version = "0.7.1", features = ["runtime-tokio", "postgres", "uuid"] }
|
||||
reqwest = { version = "0.11.18", features = ["json"] }
|
||||
serde = { version = "1.0.183", features = ["derive"] }
|
||||
navidrome = { path = "navidrome" }
|
||||
uuid = { version = "1.4.1", features = ["v4", "serde"] }
|
||||
|
||||
[package.metadata.deb]
|
||||
depends = "$auto"
|
||||
|
24
app/.gitignore
vendored
Normal file
24
app/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
1
app/.prettierrc.toml
Normal file
1
app/.prettierrc.toml
Normal file
@ -0,0 +1 @@
|
||||
tabWidth = 4
|
13
app/index.html
Normal file
13
app/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<title>Playlist</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
4534
app/package-lock.json
generated
Normal file
4534
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
app/package.json
Normal file
23
app/package.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "example",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"preact": "^10.13.1",
|
||||
"wouter-preact": "^2.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.5.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-preact": "^1.3.0",
|
||||
"prettier": "^3.0.3",
|
||||
"sass": "^1.66.1",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.3.2"
|
||||
}
|
||||
}
|
24
app/src/app.scss
Normal file
24
app/src/app.scss
Normal file
@ -0,0 +1,24 @@
|
||||
:root {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
width: 100vw;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.playlist-list {
|
||||
|
||||
}
|
||||
|
||||
.playlist {
|
||||
font-size: 1.125rem;
|
||||
margin: 8px 0;
|
||||
padding: 4px;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: black;
|
||||
border-radius: 8px;
|
||||
}
|
75
app/src/index.tsx
Normal file
75
app/src/index.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { render } from "preact";
|
||||
import { Route } from "wouter-preact";
|
||||
import "./app.scss";
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<section class={"app-wrapper"}>
|
||||
<div class={"app"}>
|
||||
<h1>Playlists</h1>
|
||||
<Route path={"/playlists"} component={ActivePlaylists} />
|
||||
<Route path={"/playlists/:id"} component={ViewPlaylist} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
type TrackedPlaylist = {
|
||||
rule_id: number;
|
||||
playlist_id: string | null;
|
||||
playlist_name: string | null;
|
||||
playlist_size: number;
|
||||
tracking_user: string | null;
|
||||
tracking_type: string | null;
|
||||
reduce_duplication_on: string | null;
|
||||
};
|
||||
|
||||
const ActivePlaylists = () => {
|
||||
const playlists: TrackedPlaylist[] = [
|
||||
{
|
||||
rule_id: 1,
|
||||
playlist_id: "aaaaaaaa-bbbbbbbbbbbbbbbb-cccccccc",
|
||||
playlist_name: "Playlist 1",
|
||||
playlist_size: 25,
|
||||
tracking_user: "jellywx",
|
||||
tracking_type: "week",
|
||||
reduce_duplication_on: null,
|
||||
},
|
||||
{
|
||||
rule_id: 2,
|
||||
playlist_id: "aaaaaaaa-bbbbbbbbbbbbbbbb-cccccccc",
|
||||
playlist_name: "Playlist 2",
|
||||
playlist_size: 25,
|
||||
tracking_user: "jellywx",
|
||||
tracking_type: "week",
|
||||
reduce_duplication_on: null,
|
||||
},
|
||||
{
|
||||
rule_id: 3,
|
||||
playlist_id: "aaaaaaaa-bbbbbbbbbbbbbbbb-cccccccc",
|
||||
playlist_name: "Playlist 3",
|
||||
playlist_size: 25,
|
||||
tracking_user: "jellywx",
|
||||
tracking_type: "week",
|
||||
reduce_duplication_on: null,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div class={"playlist-list"}>
|
||||
{playlists.map((p) => (
|
||||
<div class={"playlist"}>{p.playlist_name}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ViewPlaylist = ({ params }) => {
|
||||
return (
|
||||
<>
|
||||
<div>Subpage {params.id}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render(<App />, document.getElementById("app"));
|
13
app/tsconfig.json
Normal file
13
app/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"noEmit": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
},
|
||||
"include": ["node_modules/vite/client.d.ts", "**/*"]
|
||||
}
|
7
app/vite.config.ts
Normal file
7
app/vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import preact from '@preact/preset-vite';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [preact()],
|
||||
});
|
@ -1,9 +1,9 @@
|
||||
CREATE TABLE "tracked_playlist" (
|
||||
rule_id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||
playlist_id UUID,
|
||||
playlist_name VARCHAR(250),
|
||||
playlist_size INT NOT NULL,
|
||||
tracking_user VARCHAR(250),
|
||||
tracking_type VARCHAR(14),
|
||||
reduce_duplication_on VARCHAR(250)
|
||||
rule_id UUID NOT NULL PRIMARY KEY, -- Internal ID
|
||||
playlist_id UUID, -- UUID of the playlist in navidrome
|
||||
playlist_name VARCHAR(250), -- Name of the playlist in navidrome
|
||||
playlist_size INT NOT NULL, -- Max number of tracks to populate
|
||||
tracking_user VARCHAR(250), -- User to track stats for
|
||||
tracking_type VARCHAR(14), -- Matches time period params from listenbrainz
|
||||
reduce_duplication_on VARCHAR(250) -- Allow e.g preferring songs from different albums or artists
|
||||
);
|
||||
|
6
navidrome/src/client/auth.rs
Normal file
6
navidrome/src/client/auth.rs
Normal file
@ -0,0 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Auth {
|
||||
fn login(&mut self) -> Result<(), reqwest::Error>;
|
||||
}
|
@ -5,6 +5,8 @@ use std::fmt::{Debug, Display, Formatter};
|
||||
pub struct NavidromeBuilder {
|
||||
base: Option<String>,
|
||||
token: Option<String>,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -26,6 +28,8 @@ impl NavidromeBuilder {
|
||||
return NavidromeBuilder {
|
||||
base: None,
|
||||
token: None,
|
||||
username: None,
|
||||
password: None,
|
||||
};
|
||||
}
|
||||
|
||||
@ -39,6 +43,16 @@ impl NavidromeBuilder {
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn username(&mut self, username: impl ToString) -> &mut NavidromeBuilder {
|
||||
self.username = Some(username.to_string());
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn password(&mut self, password: impl ToString) -> &mut NavidromeBuilder {
|
||||
self.password = Some(password.to_string());
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn build(&self) -> Result<Navidrome, NavidromeBuilderError> {
|
||||
let client = reqwest::ClientBuilder::new()
|
||||
.build()
|
||||
@ -50,10 +64,9 @@ impl NavidromeBuilder {
|
||||
.clone()
|
||||
.ok_or(NavidromeBuilderError::MissingParam)?,
|
||||
http: client,
|
||||
token: self
|
||||
.token
|
||||
.clone()
|
||||
.ok_or(NavidromeBuilderError::MissingParam)?,
|
||||
token: self.token.clone(),
|
||||
username: self.username.ok_or(NavidromeBuilderError::MissingParam)?,
|
||||
password: self.password.ok_or(NavidromeBuilderError::MissingParam)?,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,9 @@ pub type MediaResult<U> = Result<U, reqwest::Error>;
|
||||
pub struct Navidrome {
|
||||
base: String,
|
||||
http: reqwest::Client,
|
||||
token: String,
|
||||
token: Option<String>,
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl QualifyPath for Navidrome {
|
||||
|
@ -35,4 +35,4 @@ pub async fn update_playlists_daemon() -> Result<(), Box<dyn std::error::Error>>
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_playlist(playlist: TrackedPlaylist) {}
|
||||
async fn update_playlist(_playlist: TrackedPlaylist) {}
|
||||
|
64
src/main.rs
64
src/main.rs
@ -2,17 +2,16 @@ mod daemon;
|
||||
mod listenbrainz;
|
||||
mod models;
|
||||
|
||||
use navidrome::{
|
||||
client::{builder::NavidromeBuilder, library::Library},
|
||||
models::navidrome::NavidromeTrack,
|
||||
};
|
||||
use navidrome::{client::builder::NavidromeBuilder, models::navidrome::NavidromeTrack};
|
||||
|
||||
use crate::daemon::{update_playlists_daemon, update_tracks_daemon};
|
||||
use crate::listenbrainz::StatsRange;
|
||||
use crate::models::{PartialTrackedPlaylist, TrackedPlaylist};
|
||||
use axum::extract::State;
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use axum::routing::{get, put};
|
||||
use axum::{extract, Json, Router};
|
||||
use serde::Serialize;
|
||||
use sqlx::postgres::PgPool;
|
||||
use sqlx::Error;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::net::SocketAddr;
|
||||
@ -50,7 +49,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let app = Router::new().route("/", get(index)).with_state(database);
|
||||
let app = Router::new()
|
||||
.route("/playlists", get(all_playlists))
|
||||
.route("/playlists", put(create_playlist))
|
||||
.with_state(database)
|
||||
.with_state(state);
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
axum::Server::bind(&addr)
|
||||
@ -60,15 +63,38 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn index(State(pool): State<PgPool>) -> String {
|
||||
let response = listenbrainz::recordings(env::var("LISTENBRAINZ_USER")?, StatsRange::Week)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
response
|
||||
.recordings
|
||||
.iter()
|
||||
.map(|a| a.track_name.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
#[derive(Serialize)]
|
||||
struct JsonError {
|
||||
error: String,
|
||||
}
|
||||
|
||||
impl From<Error> for JsonError {
|
||||
fn from(value: Error) -> Self {
|
||||
Self {
|
||||
error: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
enum Response<T> {
|
||||
Success(T),
|
||||
Error(JsonError),
|
||||
}
|
||||
|
||||
async fn create_playlist(
|
||||
State(pool): State<PgPool>,
|
||||
extract::Json(partial): extract::Json<PartialTrackedPlaylist>,
|
||||
) -> Json<Response<TrackedPlaylist>> {
|
||||
match partial.record(&pool).await {
|
||||
Ok(playlist) => Json(Response::Success(playlist)),
|
||||
Err(e) => Json(Response::Error(e.into())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn all_playlists(State(pool): State<PgPool>) -> Json<Vec<TrackedPlaylist>> {
|
||||
match TrackedPlaylist::all(&pool).await {
|
||||
Ok(playlists) => Json(playlists),
|
||||
Err(_) => Json(vec![]),
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,71 @@
|
||||
use sqlx::types::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::error::Error;
|
||||
use sqlx::types::Uuid as UuidType;
|
||||
use sqlx::{Executor, Postgres};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct TrackedPlaylist {
|
||||
pub rule_id: i64,
|
||||
pub playlist_id: Option<Uuid>,
|
||||
#[derive(Deserialize)]
|
||||
pub struct PartialTrackedPlaylist {
|
||||
pub playlist_name: Option<String>,
|
||||
pub playlist_size: i32,
|
||||
pub tracking_user: Option<String>,
|
||||
pub tracking_type: Option<String>,
|
||||
pub reduce_duplication_on: Option<String>,
|
||||
}
|
||||
|
||||
impl PartialTrackedPlaylist {
|
||||
pub async fn record(
|
||||
&self,
|
||||
pool: impl Executor<'_, Database = Postgres> + Copy,
|
||||
) -> Result<TrackedPlaylist, Error> {
|
||||
let uuid = Uuid::new_v4();
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO "tracked_playlist"
|
||||
(rule_id, playlist_name, playlist_size, tracking_user, tracking_type, reduce_duplication_on)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)"#,
|
||||
uuid,
|
||||
self.playlist_name,
|
||||
self.playlist_size,
|
||||
self.tracking_user,
|
||||
self.tracking_type,
|
||||
self.reduce_duplication_on,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
TrackedPlaylist::rule(pool, uuid).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct TrackedPlaylist {
|
||||
pub rule_id: Uuid,
|
||||
pub playlist_id: Option<UuidType>,
|
||||
pub playlist_name: Option<String>,
|
||||
pub playlist_size: i32,
|
||||
pub tracking_user: Option<String>,
|
||||
pub tracking_type: Option<String>,
|
||||
pub reduce_duplication_on: Option<String>,
|
||||
}
|
||||
|
||||
impl TrackedPlaylist {
|
||||
pub async fn all(pool: impl Executor<'_, Database = Postgres>) -> Result<Vec<Self>, Error> {
|
||||
sqlx::query_as!(Self, r#"SELECT * FROM "tracked_playlist""#)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn rule(
|
||||
pool: impl Executor<'_, Database = Postgres>,
|
||||
uuid: Uuid,
|
||||
) -> Result<Self, Error> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
r#"SELECT * FROM "tracked_playlist" WHERE rule_id = $1"#,
|
||||
uuid
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user