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
Normal file
16
.idea/inspectionProfiles/Project_Default.xml
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>
|
@ -13,10 +13,17 @@
|
|||||||
<cargoProject FILE="$PROJECT_DIR$/navidrome/Cargo.toml" />
|
<cargoProject FILE="$PROJECT_DIR$/navidrome/Cargo.toml" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<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$/.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/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>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@ -42,39 +49,43 @@
|
|||||||
<component name="MarkdownSettingsMigration">
|
<component name="MarkdownSettingsMigration">
|
||||||
<option name="stateVersion" value="1" />
|
<option name="stateVersion" value="1" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"associatedIndex": 2
|
||||||
|
}</component>
|
||||||
<component name="ProjectId" id="2TwLYTFaOXHKmGf9lC3ltOC00Kl" />
|
<component name="ProjectId" id="2TwLYTFaOXHKmGf9lC3ltOC00Kl" />
|
||||||
<component name="ProjectViewState">
|
<component name="ProjectViewState">
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
<component name="PropertiesComponent">{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.cidr.known.project.marker": "true",
|
"RunOnceActivity.cidr.known.project.marker": "true",
|
||||||
"WebServerToolWindowFactoryState": "false",
|
"WebServerToolWindowFactoryState": "false",
|
||||||
"cf.first.check.clang-format": "false",
|
"cf.first.check.clang-format": "false",
|
||||||
"cidr.known.project.marker": "true",
|
"cidr.known.project.marker": "true",
|
||||||
"last_opened_file_path": "/home/jude/Documents/navidrome-playlists/src/daemon",
|
"git-widget-placeholder": "master",
|
||||||
"node.js.detected.package.eslint": "true",
|
"last_opened_file_path": "/home/jude/Documents/navidrome-playlists",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"org.rust.cargo.project.model.PROJECT_DISCOVERY": "true",
|
"nodejs_package_manager_path": "npm",
|
||||||
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/mod.rs": "true",
|
"org.rust.cargo.project.model.PROJECT_DISCOVERY": "true",
|
||||||
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/playlists.rs": "true",
|
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/mod.rs": "true",
|
||||||
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/lib.rs": "true",
|
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/client/playlists.rs": "true",
|
||||||
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/models.rs": "true",
|
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/lib.rs": "true",
|
||||||
"settings.editor.selected.configurable": "language.rust.cargo.check",
|
"org.rust.disableDetachedFileInspection/home/jude/navidrome-playlists/navidrome/src/models.rs": "true",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"settings.editor.selected.configurable": "language.rust.cargo.check",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
},
|
},
|
||||||
"keyToStringList": {
|
"keyToStringList": {
|
||||||
"DatabaseDriversLRU": [
|
"DatabaseDriversLRU": [
|
||||||
"postgresql"
|
"postgresql"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}]]></component>
|
}</component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
<key name="CopyFile.RECENT_KEYS">
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
<recent name="$PROJECT_DIR$/src/daemon" />
|
<recent name="$PROJECT_DIR$/src/daemon" />
|
||||||
@ -128,7 +139,11 @@
|
|||||||
<workItem from="1692003763542" duration="5103000" />
|
<workItem from="1692003763542" duration="5103000" />
|
||||||
<workItem from="1692042976149" duration="10055000" />
|
<workItem from="1692042976149" duration="10055000" />
|
||||||
<workItem from="1694271920428" duration="9295000" />
|
<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>
|
||||||
<task id="LOCAL-00001" summary="Structure">
|
<task id="LOCAL-00001" summary="Structure">
|
||||||
<created>1692008860369</created>
|
<created>1692008860369</created>
|
||||||
@ -145,7 +160,15 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1694286183476</updated>
|
<updated>1694286183476</updated>
|
||||||
</task>
|
</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 />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@ -154,7 +177,8 @@
|
|||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<MESSAGE value="Structure" />
|
<MESSAGE value="Structure" />
|
||||||
<MESSAGE value="Track media server songs list" />
|
<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>
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
<component name="XSLT-Support.FileAssociations.UIState">
|
||||||
<expand />
|
<expand />
|
||||||
|
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -928,6 +928,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1994,6 +1995,10 @@ name = "uuid"
|
|||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
|
@ -6,12 +6,13 @@ authors = ["Jude Southworth (judesouthworth@pm.me)"]
|
|||||||
license = "AGPL-3.0 only"
|
license = "AGPL-3.0 only"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.6.20"
|
axum = { version = "0.6.20", features = ["json"] }
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
sqlx = { version = "0.7.1", features = ["runtime-tokio", "postgres", "uuid"] }
|
sqlx = { version = "0.7.1", features = ["runtime-tokio", "postgres", "uuid"] }
|
||||||
reqwest = { version = "0.11.18", features = ["json"] }
|
reqwest = { version = "0.11.18", features = ["json"] }
|
||||||
serde = { version = "1.0.183", features = ["derive"] }
|
serde = { version = "1.0.183", features = ["derive"] }
|
||||||
navidrome = { path = "navidrome" }
|
navidrome = { path = "navidrome" }
|
||||||
|
uuid = { version = "1.4.1", features = ["v4", "serde"] }
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
depends = "$auto"
|
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" (
|
CREATE TABLE "tracked_playlist" (
|
||||||
rule_id BIGSERIAL NOT NULL PRIMARY KEY,
|
rule_id UUID NOT NULL PRIMARY KEY, -- Internal ID
|
||||||
playlist_id UUID,
|
playlist_id UUID, -- UUID of the playlist in navidrome
|
||||||
playlist_name VARCHAR(250),
|
playlist_name VARCHAR(250), -- Name of the playlist in navidrome
|
||||||
playlist_size INT NOT NULL,
|
playlist_size INT NOT NULL, -- Max number of tracks to populate
|
||||||
tracking_user VARCHAR(250),
|
tracking_user VARCHAR(250), -- User to track stats for
|
||||||
tracking_type VARCHAR(14),
|
tracking_type VARCHAR(14), -- Matches time period params from listenbrainz
|
||||||
reduce_duplication_on VARCHAR(250)
|
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 {
|
pub struct NavidromeBuilder {
|
||||||
base: Option<String>,
|
base: Option<String>,
|
||||||
token: Option<String>,
|
token: Option<String>,
|
||||||
|
username: Option<String>,
|
||||||
|
password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -26,6 +28,8 @@ impl NavidromeBuilder {
|
|||||||
return NavidromeBuilder {
|
return NavidromeBuilder {
|
||||||
base: None,
|
base: None,
|
||||||
token: None,
|
token: None,
|
||||||
|
username: None,
|
||||||
|
password: None,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,6 +43,16 @@ impl NavidromeBuilder {
|
|||||||
return self;
|
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> {
|
pub fn build(&self) -> Result<Navidrome, NavidromeBuilderError> {
|
||||||
let client = reqwest::ClientBuilder::new()
|
let client = reqwest::ClientBuilder::new()
|
||||||
.build()
|
.build()
|
||||||
@ -50,10 +64,9 @@ impl NavidromeBuilder {
|
|||||||
.clone()
|
.clone()
|
||||||
.ok_or(NavidromeBuilderError::MissingParam)?,
|
.ok_or(NavidromeBuilderError::MissingParam)?,
|
||||||
http: client,
|
http: client,
|
||||||
token: self
|
token: self.token.clone(),
|
||||||
.token
|
username: self.username.ok_or(NavidromeBuilderError::MissingParam)?,
|
||||||
.clone()
|
password: self.password.ok_or(NavidromeBuilderError::MissingParam)?,
|
||||||
.ok_or(NavidromeBuilderError::MissingParam)?,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,9 @@ pub type MediaResult<U> = Result<U, reqwest::Error>;
|
|||||||
pub struct Navidrome {
|
pub struct Navidrome {
|
||||||
base: String,
|
base: String,
|
||||||
http: reqwest::Client,
|
http: reqwest::Client,
|
||||||
token: String,
|
token: Option<String>,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl QualifyPath for Navidrome {
|
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 listenbrainz;
|
||||||
mod models;
|
mod models;
|
||||||
|
|
||||||
use navidrome::{
|
use navidrome::{client::builder::NavidromeBuilder, models::navidrome::NavidromeTrack};
|
||||||
client::{builder::NavidromeBuilder, library::Library},
|
|
||||||
models::navidrome::NavidromeTrack,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::daemon::{update_playlists_daemon, update_tracks_daemon};
|
use crate::daemon::{update_playlists_daemon, update_tracks_daemon};
|
||||||
use crate::listenbrainz::StatsRange;
|
use crate::models::{PartialTrackedPlaylist, TrackedPlaylist};
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::routing::get;
|
use axum::routing::{get, put};
|
||||||
use axum::Router;
|
use axum::{extract, Json, Router};
|
||||||
|
use serde::Serialize;
|
||||||
use sqlx::postgres::PgPool;
|
use sqlx::postgres::PgPool;
|
||||||
|
use sqlx::Error;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
@ -50,7 +49,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.unwrap();
|
.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));
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||||
axum::Server::bind(&addr)
|
axum::Server::bind(&addr)
|
||||||
@ -60,15 +63,38 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn index(State(pool): State<PgPool>) -> String {
|
#[derive(Serialize)]
|
||||||
let response = listenbrainz::recordings(env::var("LISTENBRAINZ_USER")?, StatsRange::Week)
|
struct JsonError {
|
||||||
.await
|
error: String,
|
||||||
.unwrap();
|
}
|
||||||
|
|
||||||
response
|
impl From<Error> for JsonError {
|
||||||
.recordings
|
fn from(value: Error) -> Self {
|
||||||
.iter()
|
Self {
|
||||||
.map(|a| a.track_name.clone())
|
error: value.to_string(),
|
||||||
.collect::<Vec<String>>()
|
}
|
||||||
.join(", ")
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
#[derive(Deserialize)]
|
||||||
pub rule_id: i64,
|
pub struct PartialTrackedPlaylist {
|
||||||
pub playlist_id: Option<Uuid>,
|
|
||||||
pub playlist_name: Option<String>,
|
pub playlist_name: Option<String>,
|
||||||
pub playlist_size: i32,
|
pub playlist_size: i32,
|
||||||
pub tracking_user: Option<String>,
|
pub tracking_user: Option<String>,
|
||||||
pub tracking_type: Option<String>,
|
pub tracking_type: Option<String>,
|
||||||
pub reduce_duplication_on: 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