From 8d1cb733def3f8682b1ac6578d990bc9bf4cd495 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 20 Apr 2021 20:40:12 -0400 Subject: [PATCH 1/6] Add subsonic playlist functionality Connects to a Subsonic server and displays the user's playlists as icons on the demo desktop. The playlists can be activated and loaded into Webamp and played from Subsonic. (Creating and saving playlists not supported.) This should live in a media library window mentioned in #627 but fake files are good enough for now. The URL must have parameters u, s, t, and (optionally) d for the username, salt, token, and domain respectively (if omitted, defaults to current page's domain). Token is md5(user's password + salt) in hexadecimal encoding, see http://www.subsonic.org/pages/api.jsp for more. Subsonic server may need proper CORS configuration. I built and used this with a personal Funkwhale instance. --- .../images/icons/winamp-playlist-32x32.png | Bin 0 -> 393 bytes packages/webamp/demo/js/DemoDesktop.tsx | 5 ++ packages/webamp/demo/js/PlaylistIcon.tsx | 28 ++++++++++++ packages/webamp/demo/js/Subsonic.ts | 43 ++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 packages/webamp/demo/images/icons/winamp-playlist-32x32.png create mode 100644 packages/webamp/demo/js/PlaylistIcon.tsx create mode 100644 packages/webamp/demo/js/Subsonic.ts 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 0000000000000000000000000000000000000000..c8bf8717479ed871ab9eb497a094a6bb0d555737 GIT binary patch literal 393 zcmV;40e1e0P)q1QE=J9QsopSy>;;a_&um!E^wU=$2+*N00000NkvXXu0mjfOyH&J literal 0 HcmV?d00001 diff --git a/packages/webamp/demo/js/DemoDesktop.tsx b/packages/webamp/demo/js/DemoDesktop.tsx index 47f7c56536..699232b3e8 100644 --- a/packages/webamp/demo/js/DemoDesktop.tsx +++ b/packages/webamp/demo/js/DemoDesktop.tsx @@ -10,6 +10,8 @@ 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 } from "./Subsonic"; +import PlaylistIcon from "./PlaylistIcon"; // import MilkIcon from "./MilkIcon"; interface Props { @@ -64,6 +66,9 @@ const DemoDesktop = ({ webamp, soundCloudPlaylist }: Props) => { /> ); } + for (const list of getPlaylists()) { + 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..0a2628f280 --- /dev/null +++ b/packages/webamp/demo/js/Subsonic.ts @@ -0,0 +1,43 @@ +import { PlaylistTrack, URLTrack } from "../../js/types" +export interface Playlist { + name: String, id: Number +} +const playlists: Playlist[] = []; +export function getPlaylists(): Playlist[] { + if (0 === playlists.length) { + const parameters = new URLSearchParams(window.location.search); + if (null !== parameters.get("u") && null !== parameters.get("s") && null !== parameters.get("t")) { + const params = `u=${parameters.get("u")}&s=${parameters.get("s")}&t=${parameters.get("t")}`; + const server = `https://${parameters.get("d") || window.location.hostname}/rest/`; + fetch(`${server}getPlaylists.view?f=json&${params}`).then(res => { + if (res.ok) { + res.json().then(lists => { + for (const e of lists['subsonic-response']['playlists']['playlist']) { + playlists.push({ name: e.name, id: e.id }); + } + window.dispatchEvent(new Event("resize")); + }); + }; + }); + } + } + return playlists; +} +export async function getPlaylistTracks(id: Number): Promise { + const output: URLTrack[] = []; + const parameters = new URLSearchParams(window.location.search); + const server = `https://${parameters.get("d") || window.location.hostname}/rest/`; + const params = `u=${parameters.get("u")}&s=${parameters.get("s")}&t=${parameters.get("t")}`; + let res = await fetch(`${server}getPlaylist.view?f=json&id=${id}&${params}`); + 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: `${server}stream.view?id=${e.id}&${params}`, + metaData: { artist: e.artist, title: e.title, album: e.album } + }); + } + } + return output; +} From c95bf0c2cc844a0a56333a36f894eb0e981fc1af Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 24 Apr 2021 18:15:22 -0400 Subject: [PATCH 2/6] Add form to connect to Subsonic Adds form to connect to Subsonic. Try to detect Subsonic login automatically. Correct font stack for desktop icons. --- packages/webamp/css/subsonic-container.css | 8 ++ packages/webamp/demo/css/page.css | 2 +- .../demo/images/icons/subsonic-32x32.png | Bin 0 -> 1469 bytes packages/webamp/demo/js/DemoDesktop.tsx | 8 +- packages/webamp/demo/js/PlaylistIcon.tsx | 3 +- packages/webamp/demo/js/Subsonic.ts | 83 +++++++++++++----- packages/webamp/demo/js/SubsonicIcon.tsx | 44 ++++++++++ packages/webamp/package.json | 1 + 8 files changed, 121 insertions(+), 28 deletions(-) create mode 100644 packages/webamp/css/subsonic-container.css create mode 100644 packages/webamp/demo/images/icons/subsonic-32x32.png create mode 100644 packages/webamp/demo/js/SubsonicIcon.tsx 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 55dc701be8..89e61381e3 100644 --- a/packages/webamp/demo/css/page.css +++ b/packages/webamp/demo/css/page.css @@ -161,7 +161,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 0000000000000000000000000000000000000000..e3d4f27efef4ae28e22fd864fc9430a1ba92ed6f GIT binary patch literal 1469 zcmV;u1w#6XP)O?8Xg zil7ET{a~vBrKKNq+b>p8^n?8r7OGH-EQ<*4A}p(jrihA4X(I>sfBxq`@T(n)gW~X8A99h?XEJ2&QZzBg z+)D^kKNo=5c^lgZE)Wk0`V3~}a+ZHEJ}^<_RxIAY2cQR^etZG?sl#YH(yKfgpX zpP;QbU-a<-nsg9HiQfCs8Hh$7n1djoshe?^?cc~2dg;I-iO*@iW{l_9F9Q_kfvaF& zN=8)G77_Qt90(Uel=OBQcK4xeo1YD?WJ&X7wAm0h-~~{q!*JnKhD%Q~&KN@s3rs6N z9K-?u9mL@zHqE#kViSL8`WGwH4$B-^6jxkT>VZ=A1rLKCz^OWv>K>=-vmBcFt31I0 zjaI!75LLB-l_D}S55g>@qMwTFpdkHJIF3V4*5TUbI@!2V@B}w7C>BY}kg_0`P*yc6 z>#{Mr8WuCk4Jx!FWJN=MTwrs6ZK`^&h`6fSBsz#A{Z#Z}Zes+CGAHQd5w^D9&g!gE zQ$c`w5T?JTPzP#ts0L6Elu97@T1BP8WVy%_bN9$GhV&USvV(%EwgVLrDX8i-z$4Mj z$2fumRA^4@;l5lqqvaACvW>JP6i;P=rj`jg&=7%9D=c8l2yp?UNko!(nP$4v`*dhx zkL=LBs$v73z)iqz5&1|}CxJUeWN(aH@fiT@Sb@tMVM(%)$!fr8)sO@NAQn}s9)$6a zDM%V3RS2^pC`55hIzvm>ZcU^bMD%YUAF1jsTCZ)!vR?<@2X0Z-b|dR30C2qIQm!cD zb3Vnuus#J>q@X(mD;#J~!n&3a%yII1fAXL;8Ids}W<8qKASCEi4JKT}XbI|mH^9Eu&nEx{I_3)pEH=Ph zjO$aB5tg=B$WMZo(R|mMMoiA50Bm@uIJIJR6Ib0)W%jI7x&hPzIPJrnA4Ts>R&ury zyGX;Emtxt=81mIl{ZDE$V-K_u9~V~TSo+z%lZ^g5#TpxGlXxWpK77s4RDqZUNgu?V zt)ii@7bGl2Yh1~DhRO0#fIGjwY6Up}jzyY~1SVp-4~uQ|F{4?oKQ5#jATWZzrJ1#D z0htbE?(gZ2_YQjaLk+B20X6sAja&%R9{??JpE=*+zzL6h>p$Qt$abv;PKwBks$LP1 z@U58iQ_=g`Lf;|>lYn#zy+B$ml$a=qxM9|_IC#S2 zaB-KsaQZC}u*A=k{)o)f}u{R=FUJo1+k%D}`MWB;A z*+37y-~I?L*&m+oihkMOjWfRB_W{5+ XkSBWZK9M7200000NkvXXu0mjfs05!O literal 0 HcmV?d00001 diff --git a/packages/webamp/demo/js/DemoDesktop.tsx b/packages/webamp/demo/js/DemoDesktop.tsx index 699232b3e8..31e5242489 100644 --- a/packages/webamp/demo/js/DemoDesktop.tsx +++ b/packages/webamp/demo/js/DemoDesktop.tsx @@ -10,8 +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 } from "./Subsonic"; +import { getPlaylists, playlists } from "./Subsonic"; import PlaylistIcon from "./PlaylistIcon"; +import SubsonicIcon from "./SubsonicIcon"; // import MilkIcon from "./MilkIcon"; interface Props { @@ -66,9 +67,10 @@ const DemoDesktop = ({ webamp, soundCloudPlaylist }: Props) => { /> ); } - for (const list of getPlaylists()) { + icons.push(SubsonicIcon()); + playlists.forEach(list => { icons.push(PlaylistIcon({ webamp: webamp, playlist: list })); - } + }); } return (
{ - if (res.ok) { - res.json().then(lists => { - for (const e of lists['subsonic-response']['playlists']['playlist']) { - playlists.push({ name: e.name, id: e.id }); - } - window.dispatchEvent(new Event("resize")); - }); - }; - }); - } +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); - const server = `https://${parameters.get("d") || window.location.hostname}/rest/`; - const params = `u=${parameters.get("u")}&s=${parameters.get("s")}&t=${parameters.get("t")}`; - let res = await fetch(`${server}getPlaylist.view?f=json&id=${id}&${params}`); + 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: `${server}stream.view?id=${e.id}&${params}`, + url: `https://${domain}/rest/stream.view?id=${e.id}&${getAuthParams()}`, metaData: { artist: e.artist, title: e.title, album: e.album } }); } 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 e3e738a342..0f68448261 100644 --- a/packages/webamp/package.json +++ b/packages/webamp/package.json @@ -142,6 +142,7 @@ "invariant": "^2.2.3", "jszip": "^3.1.3", "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", From 7760774cf38186cbdde2219499b8ca82cd6005c1 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 20 Apr 2021 20:40:12 -0400 Subject: [PATCH 3/6] Add subsonic playlist functionality Connects to a Subsonic server and displays the user's playlists as icons on the demo desktop. The playlists can be activated and loaded into Webamp and played from Subsonic. (Creating and saving playlists not supported.) This should live in a media library window mentioned in #627 but fake files are good enough for now. The URL must have parameters u, s, t, and (optionally) d for the username, salt, token, and domain respectively (if omitted, defaults to current page's domain). Token is md5(user's password + salt) in hexadecimal encoding, see http://www.subsonic.org/pages/api.jsp for more. Subsonic server may need proper CORS configuration. I built and used this with a personal Funkwhale instance. --- .../images/icons/winamp-playlist-32x32.png | Bin 0 -> 393 bytes packages/webamp/demo/js/DemoDesktop.tsx | 5 ++ packages/webamp/demo/js/PlaylistIcon.tsx | 28 ++++++++++++ packages/webamp/demo/js/Subsonic.ts | 43 ++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 packages/webamp/demo/images/icons/winamp-playlist-32x32.png create mode 100644 packages/webamp/demo/js/PlaylistIcon.tsx create mode 100644 packages/webamp/demo/js/Subsonic.ts 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 0000000000000000000000000000000000000000..c8bf8717479ed871ab9eb497a094a6bb0d555737 GIT binary patch literal 393 zcmV;40e1e0P)q1QE=J9QsopSy>;;a_&um!E^wU=$2+*N00000NkvXXu0mjfOyH&J literal 0 HcmV?d00001 diff --git a/packages/webamp/demo/js/DemoDesktop.tsx b/packages/webamp/demo/js/DemoDesktop.tsx index 47f7c56536..699232b3e8 100644 --- a/packages/webamp/demo/js/DemoDesktop.tsx +++ b/packages/webamp/demo/js/DemoDesktop.tsx @@ -10,6 +10,8 @@ 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 } from "./Subsonic"; +import PlaylistIcon from "./PlaylistIcon"; // import MilkIcon from "./MilkIcon"; interface Props { @@ -64,6 +66,9 @@ const DemoDesktop = ({ webamp, soundCloudPlaylist }: Props) => { /> ); } + for (const list of getPlaylists()) { + 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..0a2628f280 --- /dev/null +++ b/packages/webamp/demo/js/Subsonic.ts @@ -0,0 +1,43 @@ +import { PlaylistTrack, URLTrack } from "../../js/types" +export interface Playlist { + name: String, id: Number +} +const playlists: Playlist[] = []; +export function getPlaylists(): Playlist[] { + if (0 === playlists.length) { + const parameters = new URLSearchParams(window.location.search); + if (null !== parameters.get("u") && null !== parameters.get("s") && null !== parameters.get("t")) { + const params = `u=${parameters.get("u")}&s=${parameters.get("s")}&t=${parameters.get("t")}`; + const server = `https://${parameters.get("d") || window.location.hostname}/rest/`; + fetch(`${server}getPlaylists.view?f=json&${params}`).then(res => { + if (res.ok) { + res.json().then(lists => { + for (const e of lists['subsonic-response']['playlists']['playlist']) { + playlists.push({ name: e.name, id: e.id }); + } + window.dispatchEvent(new Event("resize")); + }); + }; + }); + } + } + return playlists; +} +export async function getPlaylistTracks(id: Number): Promise { + const output: URLTrack[] = []; + const parameters = new URLSearchParams(window.location.search); + const server = `https://${parameters.get("d") || window.location.hostname}/rest/`; + const params = `u=${parameters.get("u")}&s=${parameters.get("s")}&t=${parameters.get("t")}`; + let res = await fetch(`${server}getPlaylist.view?f=json&id=${id}&${params}`); + 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: `${server}stream.view?id=${e.id}&${params}`, + metaData: { artist: e.artist, title: e.title, album: e.album } + }); + } + } + return output; +} From 5d4c2e102e3b43f6208d5936fe637f5ff28af95a Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 24 Apr 2021 18:15:22 -0400 Subject: [PATCH 4/6] Add form to connect to Subsonic Adds form to connect to Subsonic. Try to detect Subsonic login automatically. Correct font stack for desktop icons. --- packages/webamp/css/subsonic-container.css | 8 ++ packages/webamp/demo/css/page.css | 2 +- .../demo/images/icons/subsonic-32x32.png | Bin 0 -> 1469 bytes packages/webamp/demo/js/DemoDesktop.tsx | 8 +- packages/webamp/demo/js/PlaylistIcon.tsx | 3 +- packages/webamp/demo/js/Subsonic.ts | 83 +++++++++++++----- packages/webamp/demo/js/SubsonicIcon.tsx | 44 ++++++++++ packages/webamp/package.json | 1 + 8 files changed, 121 insertions(+), 28 deletions(-) create mode 100644 packages/webamp/css/subsonic-container.css create mode 100644 packages/webamp/demo/images/icons/subsonic-32x32.png create mode 100644 packages/webamp/demo/js/SubsonicIcon.tsx 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 55dc701be8..89e61381e3 100644 --- a/packages/webamp/demo/css/page.css +++ b/packages/webamp/demo/css/page.css @@ -161,7 +161,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 0000000000000000000000000000000000000000..e3d4f27efef4ae28e22fd864fc9430a1ba92ed6f GIT binary patch literal 1469 zcmV;u1w#6XP)O?8Xg zil7ET{a~vBrKKNq+b>p8^n?8r7OGH-EQ<*4A}p(jrihA4X(I>sfBxq`@T(n)gW~X8A99h?XEJ2&QZzBg z+)D^kKNo=5c^lgZE)Wk0`V3~}a+ZHEJ}^<_RxIAY2cQR^etZG?sl#YH(yKfgpX zpP;QbU-a<-nsg9HiQfCs8Hh$7n1djoshe?^?cc~2dg;I-iO*@iW{l_9F9Q_kfvaF& zN=8)G77_Qt90(Uel=OBQcK4xeo1YD?WJ&X7wAm0h-~~{q!*JnKhD%Q~&KN@s3rs6N z9K-?u9mL@zHqE#kViSL8`WGwH4$B-^6jxkT>VZ=A1rLKCz^OWv>K>=-vmBcFt31I0 zjaI!75LLB-l_D}S55g>@qMwTFpdkHJIF3V4*5TUbI@!2V@B}w7C>BY}kg_0`P*yc6 z>#{Mr8WuCk4Jx!FWJN=MTwrs6ZK`^&h`6fSBsz#A{Z#Z}Zes+CGAHQd5w^D9&g!gE zQ$c`w5T?JTPzP#ts0L6Elu97@T1BP8WVy%_bN9$GhV&USvV(%EwgVLrDX8i-z$4Mj z$2fumRA^4@;l5lqqvaACvW>JP6i;P=rj`jg&=7%9D=c8l2yp?UNko!(nP$4v`*dhx zkL=LBs$v73z)iqz5&1|}CxJUeWN(aH@fiT@Sb@tMVM(%)$!fr8)sO@NAQn}s9)$6a zDM%V3RS2^pC`55hIzvm>ZcU^bMD%YUAF1jsTCZ)!vR?<@2X0Z-b|dR30C2qIQm!cD zb3Vnuus#J>q@X(mD;#J~!n&3a%yII1fAXL;8Ids}W<8qKASCEi4JKT}XbI|mH^9Eu&nEx{I_3)pEH=Ph zjO$aB5tg=B$WMZo(R|mMMoiA50Bm@uIJIJR6Ib0)W%jI7x&hPzIPJrnA4Ts>R&ury zyGX;Emtxt=81mIl{ZDE$V-K_u9~V~TSo+z%lZ^g5#TpxGlXxWpK77s4RDqZUNgu?V zt)ii@7bGl2Yh1~DhRO0#fIGjwY6Up}jzyY~1SVp-4~uQ|F{4?oKQ5#jATWZzrJ1#D z0htbE?(gZ2_YQjaLk+B20X6sAja&%R9{??JpE=*+zzL6h>p$Qt$abv;PKwBks$LP1 z@U58iQ_=g`Lf;|>lYn#zy+B$ml$a=qxM9|_IC#S2 zaB-KsaQZC}u*A=k{)o)f}u{R=FUJo1+k%D}`MWB;A z*+37y-~I?L*&m+oihkMOjWfRB_W{5+ XkSBWZK9M7200000NkvXXu0mjfs05!O literal 0 HcmV?d00001 diff --git a/packages/webamp/demo/js/DemoDesktop.tsx b/packages/webamp/demo/js/DemoDesktop.tsx index 699232b3e8..31e5242489 100644 --- a/packages/webamp/demo/js/DemoDesktop.tsx +++ b/packages/webamp/demo/js/DemoDesktop.tsx @@ -10,8 +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 } from "./Subsonic"; +import { getPlaylists, playlists } from "./Subsonic"; import PlaylistIcon from "./PlaylistIcon"; +import SubsonicIcon from "./SubsonicIcon"; // import MilkIcon from "./MilkIcon"; interface Props { @@ -66,9 +67,10 @@ const DemoDesktop = ({ webamp, soundCloudPlaylist }: Props) => { /> ); } - for (const list of getPlaylists()) { + icons.push(SubsonicIcon()); + playlists.forEach(list => { icons.push(PlaylistIcon({ webamp: webamp, playlist: list })); - } + }); } return (
{ - if (res.ok) { - res.json().then(lists => { - for (const e of lists['subsonic-response']['playlists']['playlist']) { - playlists.push({ name: e.name, id: e.id }); - } - window.dispatchEvent(new Event("resize")); - }); - }; - }); - } +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); - const server = `https://${parameters.get("d") || window.location.hostname}/rest/`; - const params = `u=${parameters.get("u")}&s=${parameters.get("s")}&t=${parameters.get("t")}`; - let res = await fetch(`${server}getPlaylist.view?f=json&id=${id}&${params}`); + 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: `${server}stream.view?id=${e.id}&${params}`, + url: `https://${domain}/rest/stream.view?id=${e.id}&${getAuthParams()}`, metaData: { artist: e.artist, title: e.title, album: e.album } }); } 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 e3e738a342..0f68448261 100644 --- a/packages/webamp/package.json +++ b/packages/webamp/package.json @@ -142,6 +142,7 @@ "invariant": "^2.2.3", "jszip": "^3.1.3", "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", From da6279d1b5764b1762dc7f0c24ee9b9fa6cc0c31 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 27 Apr 2021 19:27:41 -0400 Subject: [PATCH 5/6] Add icon select effect borrowed from Windows 93 https://www.windows93.net/ --- packages/webamp/demo/css/page.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/webamp/demo/css/page.css b/packages/webamp/demo/css/page.css index 55dc701be8..f292f41aea 100644 --- a/packages/webamp/demo/css/page.css +++ b/packages/webamp/demo/css/page.css @@ -5,6 +5,10 @@ body { font-size: 15px; } +::selection{ + background: none; +} + #browser-compatibility { display: none; position: absolute; @@ -176,3 +180,7 @@ body { background-color: #000080; border-color: white; } + +.desktop-icon.selected img { + filter: brightness(0.35) sepia(100%) hue-rotate(148deg) saturate(10); +} From ff0962023b89287d90d1b33e4b22112bca1b3ca0 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 27 Apr 2021 23:27:47 -0400 Subject: [PATCH 6/6] Don't pass metadata Don't pass metadata so music-metadata-browser will scan and load more data --- packages/webamp/demo/js/Subsonic.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/webamp/demo/js/Subsonic.ts b/packages/webamp/demo/js/Subsonic.ts index 64a4450c91..df0e0e962f 100644 --- a/packages/webamp/demo/js/Subsonic.ts +++ b/packages/webamp/demo/js/Subsonic.ts @@ -1,4 +1,4 @@ -import { PlaylistTrack, URLTrack } from "../../js/types" +import { URLTrack } from "../../js/types" const md5 = require("md5"); export interface Playlist { name: String, id: Number @@ -72,9 +72,10 @@ export async function getPlaylistTracks(id: Number): Promise { let lists = await res.json(); for (const e of lists['subsonic-response']['playlist']['entry']) { output.push({ - duration: e.duration, defaultName: `${e.artist} - ${e.title}`, + 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 } + //metaData: { artist: e.artist, title: e.title, album: e.album }, }); } }