diff --git a/packages/webamp/css/subsonic-container.css b/packages/webamp/css/subsonic-container.css
new file mode 100644
index 0000000000..f79319c75a
--- /dev/null
+++ b/packages/webamp/css/subsonic-container.css
@@ -0,0 +1,8 @@
+#subsonic-container form {
+ width: auto;
+ background: #fff;
+ position: absolute;
+ display: grid;
+ grid-template-columns: 10em 10em;
+ z-index: 1;
+}
diff --git a/packages/webamp/demo/css/page.css b/packages/webamp/demo/css/page.css
index e6d1ab5a52..ea655acc8b 100644
--- a/packages/webamp/demo/css/page.css
+++ b/packages/webamp/demo/css/page.css
@@ -165,7 +165,7 @@ body {
.desktop-icon-title {
color: white;
- font-family: "MS Sans Serif", "Segoe UI", sans-serif;
+ font-family: "Microsoft Sans Serif", "Tahoma", "Segoe UI", sans-serif;
font-size: 11px;
-webkit-font-smoothing: none;
margin-top: 5px;
diff --git a/packages/webamp/demo/images/icons/subsonic-32x32.png b/packages/webamp/demo/images/icons/subsonic-32x32.png
new file mode 100644
index 0000000000..e3d4f27efe
Binary files /dev/null and b/packages/webamp/demo/images/icons/subsonic-32x32.png differ
diff --git a/packages/webamp/demo/images/icons/winamp-playlist-32x32.png b/packages/webamp/demo/images/icons/winamp-playlist-32x32.png
new file mode 100644
index 0000000000..c8bf871747
Binary files /dev/null and b/packages/webamp/demo/images/icons/winamp-playlist-32x32.png differ
diff --git a/packages/webamp/demo/js/DemoDesktop.tsx b/packages/webamp/demo/js/DemoDesktop.tsx
index 47f7c56536..31e5242489 100644
--- a/packages/webamp/demo/js/DemoDesktop.tsx
+++ b/packages/webamp/demo/js/DemoDesktop.tsx
@@ -10,6 +10,9 @@ import DesktopLinkIcon from "./DesktopLinkIcon";
import museumIcon from "../images/icons/internet-folder-32x32.png";
import soundcloudIcon from "../images/icons/soundcloud-32x32.png";
import { SoundCloudPlaylist } from "./SoundCloud";
+import { getPlaylists, playlists } from "./Subsonic";
+import PlaylistIcon from "./PlaylistIcon";
+import SubsonicIcon from "./SubsonicIcon";
// import MilkIcon from "./MilkIcon";
interface Props {
@@ -64,6 +67,10 @@ const DemoDesktop = ({ webamp, soundCloudPlaylist }: Props) => {
/>
);
}
+ icons.push(SubsonicIcon());
+ playlists.forEach(list => {
+ icons.push(PlaylistIcon({ webamp: webamp, playlist: list }));
+ });
}
return (
{
+ function onOpen() {
+ getPlaylistTracks(playlist.id).then(tracks => {
+ webamp.setTracksToPlay(tracks);
+ });
+ }
+
+ return (
+
+ );
+};
+
+export default PlaylistIcon;
diff --git a/packages/webamp/demo/js/Subsonic.ts b/packages/webamp/demo/js/Subsonic.ts
new file mode 100644
index 0000000000..df0e0e962f
--- /dev/null
+++ b/packages/webamp/demo/js/Subsonic.ts
@@ -0,0 +1,83 @@
+import { URLTrack } from "../../js/types"
+const md5 = require("md5");
+export interface Playlist {
+ name: String, id: Number
+}
+var domain: string;
+var username: string;
+var password: string;
+export var playlists: Playlist[] = [];
+function getNonce(): string {
+ const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._~'
+ const result = new Array();
+ window.crypto.getRandomValues(new Uint8Array(32)).forEach(c =>
+ result.push(charset[c % charset.length]));
+ return result.join('');
+}
+function getAuthParams(): string {
+ const salt = getNonce();
+ const token = md5(password.concat(salt));
+ return `u=${username}&s=${salt}&t=${token}`;
+}
+/**
+ * attempt to detect if this is served from a subsonic compatible server, and try to get credentials automatically
+ * currently supports Funkwhale
+ */
+async function detectServer() {
+ let home = await fetch(window.location.origin);
+ if (!home.ok) { return; }
+ let t = await home.text();
+ const dom = new DOMParser().parseFromString(t, 'text/html');
+ if (null !== dom.querySelector("meta[name=generator][content=Funkwhale]")) {
+ let req = await fetch(`${window.location.origin}/api/v1/users/me/`);
+ if (!req.ok) { return; }
+ let userjson = await req.json();
+ req = await fetch(`${window.location.origin}/api/v1/users/${userjson.username}/subsonic-token/`);
+ if (!req.ok) { return; }
+ let subjson = await req.json();
+ setSubsonicServer(window.location.hostname, userjson.username, subjson.subsonic_api_token);
+ }
+}
+export function setSubsonicServer(newDomain: string, newUsername: string, newPassword: string) {
+ if (null !== newDomain && null !== newUsername && null !== newPassword) {
+ domain = newDomain;
+ username = newUsername;
+ password = newPassword;
+ getPlaylists().then(function (l) {
+ // redraw desktop to show playlist icons
+ window.dispatchEvent(new Event("resize"));
+ });
+ }
+}
+detectServer();
+export async function getPlaylists(): Promise
{
+ const parameters = new URLSearchParams(window.location.search);
+ if (undefined !== domain && undefined !== username && undefined !== password) {
+ let res = await fetch(`https://${domain}/rest/getPlaylists.view?f=json&${getAuthParams()}`);
+ if (res.ok) {
+ let lists = await res.json();
+ playlists = [];
+ for (const e of lists['subsonic-response']['playlists']['playlist']) {
+ playlists.push({ name: e.name, id: e.id });
+ }
+ };
+ }
+ return playlists;
+}
+export async function getPlaylistTracks(id: Number): Promise {
+ const output: URLTrack[] = [];
+ const parameters = new URLSearchParams(window.location.search);
+ let res = await fetch(`https://${domain}/rest/getPlaylist.view?f=json&id=${id}&${getAuthParams()}`);
+ if (res.ok) {
+ let lists = await res.json();
+ for (const e of lists['subsonic-response']['playlist']['entry']) {
+ output.push({
+ duration: e.duration,
+ defaultName: `${e.artist} - ${e.title}`,
+ url: `https://${domain}/rest/stream.view?id=${e.id}&${getAuthParams()}`,
+ //metaData: { artist: e.artist, title: e.title, album: e.album },
+ });
+ }
+ }
+ return output;
+}
diff --git a/packages/webamp/demo/js/SubsonicIcon.tsx b/packages/webamp/demo/js/SubsonicIcon.tsx
new file mode 100644
index 0000000000..be1592fd8f
--- /dev/null
+++ b/packages/webamp/demo/js/SubsonicIcon.tsx
@@ -0,0 +1,44 @@
+import icon from "../images/icons/subsonic-32x32.png";
+import DesktopIcon from "./DesktopIcon";
+import ReactDOM from "react-dom";
+import "../../css/subsonic-container.css";
+import { setSubsonicServer } from "./Subsonic";
+
+const container = document.createElement("div");
+container.id = "subsonic-container";
+const body = document.querySelector("body")?.appendChild(container);
+
+const SubsonicIcon = () => {
+ function onOpen() {
+ ReactDOM.render(
+ , container);
+ (document.getElementById("subsonic-domain") as HTMLInputElement).value = window.location.hostname;
+ document.getElementById("subsonic-connect")?.addEventListener("submit", function (e) {
+ e.preventDefault();
+ setSubsonicServer((document.getElementById("subsonic-domain") as HTMLInputElement).value,
+ (document.getElementById("subsonic-username") as HTMLInputElement).value,
+ (document.getElementById("subsonic-password") as HTMLInputElement).value);
+ ReactDOM.unmountComponentAtNode(container);
+ });
+ document.getElementById("subsonic-close")?.addEventListener("click", function (e) {
+ ReactDOM.unmountComponentAtNode(container);
+ });
+ }
+ return (
+
+ );
+};
+
+export default SubsonicIcon;
diff --git a/packages/webamp/package.json b/packages/webamp/package.json
index e635954362..ce3f289f79 100644
--- a/packages/webamp/package.json
+++ b/packages/webamp/package.json
@@ -141,7 +141,8 @@
"fscreen": "^1.0.2",
"invariant": "^2.2.3",
"jszip": "^3.1.3",
- "lodash": "^4.17.21",
+ "lodash": "^4.17.11",
+ "md5": "^2.3.0",
"milkdrop-preset-converter-aws": "^0.1.6",
"music-metadata-browser": "^0.6.1",
"react": "^17.0.1",