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",