yt finished, cleanup
This commit is contained in:
parent
4e10590646
commit
cf46a0ffb1
23 changed files with 301 additions and 199 deletions
|
@ -70,7 +70,6 @@ select {
|
||||||
@import "font.less";
|
@import "font.less";
|
||||||
@import "icons.less";
|
@import "icons.less";
|
||||||
@import "mixins.less";
|
@import "mixins.less";
|
||||||
@import "search.less";
|
|
||||||
@import "art.less";
|
@import "art.less";
|
||||||
@import "variables.less";
|
@import "variables.less";
|
||||||
|
|
||||||
|
@ -84,7 +83,9 @@ select {
|
||||||
@import "elements/yt.less";
|
@import "elements/yt.less";
|
||||||
@import "elements/range.less";
|
@import "elements/range.less";
|
||||||
@import "elements/playlist.less";
|
@import "elements/playlist.less";
|
||||||
|
@import "elements/search.less";
|
||||||
@import "elements/library.less";
|
@import "elements/library.less";
|
||||||
@import "elements/tag.less";
|
@import "elements/tag.less";
|
||||||
@import "elements/back.less";
|
@import "elements/back.less";
|
||||||
@import "elements/path.less";
|
@import "elements/path.less";
|
||||||
|
@import "elements/yt-result.less";
|
||||||
|
|
|
@ -15,13 +15,4 @@ cyp-library {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
form {
|
|
||||||
.item;
|
|
||||||
.flex-row;
|
|
||||||
|
|
||||||
button:first-of-type {
|
|
||||||
margin-left: var(--icon-spacing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ cyp-player {
|
||||||
|
|
||||||
.timeline {
|
.timeline {
|
||||||
flex: none;
|
flex: none;
|
||||||
height: 24px;
|
height: var(--icon-size);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
.flex-row;
|
.flex-row;
|
||||||
|
|
||||||
|
|
23
app/css/elements/search.less
Normal file
23
app/css/elements/search.less
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
cyp-search {
|
||||||
|
form {
|
||||||
|
.item;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
button:first-of-type {
|
||||||
|
margin-left: var(--icon-spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending form {
|
||||||
|
background-image: linear-gradient(var(--primary), var(--primary));
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 25% var(--border-width);
|
||||||
|
animation: bar ease-in-out 3s alternate infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes bar {
|
||||||
|
0% { background-position: 0 100%; }
|
||||||
|
100% { background-position: 100% 100%; }
|
||||||
|
}
|
8
app/css/elements/yt-result.less
Normal file
8
app/css/elements/yt-result.less
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
cyp-yt-result {
|
||||||
|
.item;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
button .icon {
|
||||||
|
width: var(--icon-size);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,32 +1,8 @@
|
||||||
cyp-yt {
|
cyp-yt {
|
||||||
header {
|
|
||||||
border-bottom: 1px solid var(--fg);
|
|
||||||
|
|
||||||
button + button {
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
margin: 0.5em 0.5ch;
|
margin: 0.5em 0.5ch;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.pending header {
|
|
||||||
background-image: linear-gradient(var(--primary), var(--primary));
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 25% 4px;
|
|
||||||
animation: bar ease-in-out 3s alternate infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bar {
|
|
||||||
0% { background-position: 0 100%; }
|
|
||||||
100% { background-position: 100% 100%; }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.icon {
|
.icon {
|
||||||
width: 24px;
|
width: var(--icon-size);
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|
||||||
path, polygon, circle {
|
path, polygon, circle {
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
.search {
|
|
||||||
.flex-row;
|
|
||||||
margin-left: auto;
|
|
||||||
transition: all 300ms;
|
|
||||||
width: 32px;
|
|
||||||
max-width: 20ch;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 32px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
color: inherit;
|
|
||||||
background-color: inherit;
|
|
||||||
border-bottom: 1px solid var(--fg);
|
|
||||||
width: 0;
|
|
||||||
padding: 0;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
@breakpoint-menu: 480px;
|
@breakpoint-menu: 480px;
|
||||||
|
|
||||||
cyp-app {
|
cyp-app {
|
||||||
|
--icon-size: 24px;
|
||||||
--icon-spacing: 4px;
|
--icon-spacing: 4px;
|
||||||
--primary: rgb(var(--primary-raw));
|
--primary: rgb(var(--primary-raw));
|
||||||
--primary-tint: rgba(var(--primary-raw), 0.1);
|
--primary-tint: rgba(var(--primary-raw), 0.1);
|
||||||
|
|
File diff suppressed because one or more lines are too long
187
app/cyp.js
187
app/cyp.js
|
@ -654,7 +654,6 @@ async function initMpd() {
|
||||||
await init();
|
await init();
|
||||||
return mpd;
|
return mpd;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
|
||||||
return mpdMock;
|
return mpdMock;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -675,6 +674,7 @@ class App extends HTMLElement {
|
||||||
const names = children.map(node => node.nodeName.toLowerCase())
|
const names = children.map(node => node.nodeName.toLowerCase())
|
||||||
.filter(name => name.startsWith("cyp-"));
|
.filter(name => name.startsWith("cyp-"));
|
||||||
const unique = new Set(names);
|
const unique = new Set(names);
|
||||||
|
console.log(unique);
|
||||||
|
|
||||||
const promises = [...unique].map(name => customElements.whenDefined(name));
|
const promises = [...unique].map(name => customElements.whenDefined(name));
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
@ -712,10 +712,12 @@ class Selection {
|
||||||
this._component = component;
|
this._component = component;
|
||||||
/** @type {"single" | "multi"} */
|
/** @type {"single" | "multi"} */
|
||||||
this._mode = mode;
|
this._mode = mode;
|
||||||
this._items = []; // FIXME ukladat skutecne HTML? co kdyz nastane refresh?
|
this._items = [];
|
||||||
this._node = node("cyp-commands", {hidden:true});
|
this._node = node("cyp-commands", {hidden:true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appendTo(parent) { parent.appendChild(this._node); }
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
while (this._items.length) { this.remove(this._items[0]); }
|
while (this._items.length) { this.remove(this._items[0]); }
|
||||||
}
|
}
|
||||||
|
@ -770,18 +772,16 @@ class Selection {
|
||||||
}
|
}
|
||||||
|
|
||||||
_show() {
|
_show() {
|
||||||
const parent = this._component.closest("cyp-app").querySelector("footer"); // FIXME jde lepe?
|
|
||||||
parent.appendChild(this._node);
|
|
||||||
this._node.offsetWidth; // FIXME jde lepe?
|
|
||||||
this._node.hidden = false;
|
this._node.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_hide() {
|
_hide() {
|
||||||
this._node.hidden = true;
|
this._node.hidden = true;
|
||||||
this._node.remove();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("cyp-commands", class extends HTMLElement {});
|
||||||
|
|
||||||
class Component extends HTMLElement {
|
class Component extends HTMLElement {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
super();
|
super();
|
||||||
|
@ -789,6 +789,10 @@ class Component extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
if (this.selection) {
|
||||||
|
const parent = this._app.querySelector("footer");
|
||||||
|
this.selection.appendTo(parent);
|
||||||
|
}
|
||||||
this._app.addEventListener("load", _ => this._onAppLoad());
|
this._app.addEventListener("load", _ => this._onAppLoad());
|
||||||
this._app.addEventListener("component-change", _ => {
|
this._app.addEventListener("component-change", _ => {
|
||||||
const component = this._app.component;
|
const component = this._app.component;
|
||||||
|
@ -814,6 +818,13 @@ class Menu extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onAppLoad() {
|
||||||
|
this._app.addEventListener("queue-length-change", e => {
|
||||||
|
this.querySelector(".queue-length").textContent = `(${e.detail})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
async _activate(component) {
|
async _activate(component) {
|
||||||
const app = await this._app;
|
const app = await this._app;
|
||||||
app.setAttribute("component", component);
|
app.setAttribute("component", component);
|
||||||
|
@ -1157,8 +1168,8 @@ class Queue extends Component {
|
||||||
let songs = await this._mpd.listQueue();
|
let songs = await this._mpd.listQueue();
|
||||||
this._buildSongs(songs);
|
this._buildSongs(songs);
|
||||||
|
|
||||||
// FIXME pubsub?
|
let e = new CustomEvent("queue-length-change", {detail:songs.length});
|
||||||
document.querySelector("#queue-length").textContent = `(${songs.length})`;
|
this._app.dispatchEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateCurrent() {
|
_updateCurrent() {
|
||||||
|
@ -1366,6 +1377,54 @@ class Settings extends Component {
|
||||||
|
|
||||||
customElements.define("cyp-settings", Settings);
|
customElements.define("cyp-settings", Settings);
|
||||||
|
|
||||||
|
class Search extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._built = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() { return this._input.value.trim(); }
|
||||||
|
set value(value) { this._input.value = value; }
|
||||||
|
get _input() { return this.querySelector("input"); }
|
||||||
|
|
||||||
|
onSubmit() {}
|
||||||
|
focus() { this._input.focus(); }
|
||||||
|
pending(pending) { this.classList.toggle("pending", pending); }
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
if (this._built) { return; }
|
||||||
|
|
||||||
|
const form = node("form", {}, "", this);
|
||||||
|
node("input", {type:"text"}, "", form);
|
||||||
|
button({icon:"magnify"}, "", form);
|
||||||
|
|
||||||
|
form.addEventListener("submit", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.onSubmit();
|
||||||
|
});
|
||||||
|
|
||||||
|
this._built = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("cyp-search", Search);
|
||||||
|
|
||||||
|
class YtResult extends Item {
|
||||||
|
constructor(title) {
|
||||||
|
super();
|
||||||
|
this._title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.appendChild(icon("magnify"));
|
||||||
|
this._buildTitle(this._title);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("cyp-yt-result", YtResult);
|
||||||
|
|
||||||
const decoder = new TextDecoder("utf-8");
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
|
||||||
function decodeChunk(byteArray) {
|
function decodeChunk(byteArray) {
|
||||||
|
@ -1374,57 +1433,52 @@ function decodeChunk(byteArray) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class YT extends Component {
|
class YT extends Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._search = new Search();
|
||||||
|
|
||||||
|
this._search.onSubmit = _ => {
|
||||||
|
let query = this._search.value;
|
||||||
|
query && this._doSearch(query);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
const form = node("form", {}, "", this);
|
this._clear();
|
||||||
const input = node("input", {type:"text"}, "", form);
|
|
||||||
button({icon:"magnify"}, "", form);
|
|
||||||
form.addEventListener("submit", e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const query = input.value.trim();
|
|
||||||
if (!query.length) { return; }
|
|
||||||
this._doSearch(query, form);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async _doSearch(query, form) {
|
|
||||||
let response = await fetch(`/youtube?q=${encodeURIComponent(query)}`);
|
|
||||||
let data = await response.json();
|
|
||||||
|
|
||||||
clear(this);
|
|
||||||
this.appendChild(form);
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_download() {
|
|
||||||
let url = prompt("Please enter a YouTube URL:");
|
|
||||||
if (!url) { return; }
|
|
||||||
|
|
||||||
this._post(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
_search() {
|
|
||||||
let q = prompt("Please enter a search string:");
|
|
||||||
if (!q) { return; }
|
|
||||||
|
|
||||||
this._post(`ytsearch:${q}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_clear() {
|
_clear() {
|
||||||
clear(this.querySelector("pre"));
|
clear(this);
|
||||||
|
this.appendChild(this._search);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _post(q) {
|
async _doSearch(query) {
|
||||||
let pre = this.querySelector("pre");
|
this._clear();
|
||||||
clear(pre);
|
this._search.pending(true);
|
||||||
|
|
||||||
this.classList.add("pending");
|
let response = await fetch(`/youtube?q=${encodeURIComponent(query)}`);
|
||||||
|
let results = await response.json();
|
||||||
|
|
||||||
|
this._search.pending(false);
|
||||||
|
|
||||||
|
results.forEach(result => {
|
||||||
|
let node = new YtResult(result.title);
|
||||||
|
this.appendChild(node);
|
||||||
|
node.addButton("download", () => this._download(result.id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async _download(id) {
|
||||||
|
this._clear();
|
||||||
|
|
||||||
|
let pre = node("pre", {}, "", this);
|
||||||
|
this._search.pending(true);
|
||||||
|
|
||||||
let body = new URLSearchParams();
|
let body = new URLSearchParams();
|
||||||
body.set("q", q);
|
body.set("id", id);
|
||||||
let response = await fetch("/youtube", {method:"POST", body});
|
let response = await fetch("/youtube", {method:"POST", body});
|
||||||
|
|
||||||
let reader = response.body.getReader();
|
let reader = response.body.getReader();
|
||||||
|
@ -1436,7 +1490,7 @@ class YT extends Component {
|
||||||
}
|
}
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
|
|
||||||
this.classList.remove("pending");
|
this._search.pending(false);
|
||||||
|
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
this._mpd.command(`update ${escape(ytPath)}`);
|
this._mpd.command(`update ${escape(ytPath)}`);
|
||||||
|
@ -1444,7 +1498,10 @@ class YT extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onComponentChange(c, isThis) {
|
_onComponentChange(c, isThis) {
|
||||||
|
const wasHidden = this.hidden;
|
||||||
this.hidden = !isThis;
|
this.hidden = !isThis;
|
||||||
|
|
||||||
|
if (!wasHidden && isThis) { this._showRoot(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1560,6 +1617,13 @@ class Library extends Component {
|
||||||
super({selection:"multi"});
|
super({selection:"multi"});
|
||||||
this._stateStack = [];
|
this._stateStack = [];
|
||||||
this._initCommands();
|
this._initCommands();
|
||||||
|
|
||||||
|
this._search = new Search();
|
||||||
|
this._search.onSubmit = _ => {
|
||||||
|
let query = this._search.value;
|
||||||
|
if (query.length < 3) { return; }
|
||||||
|
this._doSearch(query);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_popState() {
|
_popState() {
|
||||||
|
@ -1642,29 +1706,26 @@ class Library extends Component {
|
||||||
_showSearch(query = "") {
|
_showSearch(query = "") {
|
||||||
clear(this);
|
clear(this);
|
||||||
|
|
||||||
const form = node("form", {}, "", this);
|
this.appendChild(this._search);
|
||||||
const input = node("input", {type:"text", value:query}, "", form);
|
this._search.value = query;
|
||||||
button({icon:"magnify"}, "", form);
|
this._search.focus();
|
||||||
form.addEventListener("submit", e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const query = input.value.trim();
|
|
||||||
if (query.length < 3) { return; }
|
|
||||||
this._doSearch(query, form);
|
|
||||||
});
|
|
||||||
|
|
||||||
input.focus();
|
query && this._search.onSubmit();
|
||||||
if (query) { this._doSearch(query, form); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _doSearch(query, form) {
|
async _doSearch(query) {
|
||||||
let state = this._stateStack[this._stateStack.length-1];
|
let state = this._stateStack[this._stateStack.length-1];
|
||||||
state.query = query;
|
state.query = query;
|
||||||
|
|
||||||
|
clear(this);
|
||||||
|
this.appendChild(this._search);
|
||||||
|
this._search.pending(true);
|
||||||
|
|
||||||
const songs1 = await this._mpd.searchSongs({"AlbumArtist": query});
|
const songs1 = await this._mpd.searchSongs({"AlbumArtist": query});
|
||||||
const songs2 = await this._mpd.searchSongs({"Album": query});
|
const songs2 = await this._mpd.searchSongs({"Album": query});
|
||||||
const songs3 = await this._mpd.searchSongs({"Title": query});
|
const songs3 = await this._mpd.searchSongs({"Title": query});
|
||||||
clear(this);
|
|
||||||
this.appendChild(form);
|
this._search.pending(false);
|
||||||
|
|
||||||
this._aggregateSearch(songs1, "AlbumArtist");
|
this._aggregateSearch(songs1, "AlbumArtist");
|
||||||
this._aggregateSearch(songs2, "Album");
|
this._aggregateSearch(songs2, "Album");
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
<button data-for="queue" data-icon="music">
|
<button data-for="queue" data-icon="music">
|
||||||
<div>
|
<div>
|
||||||
<span>Queue</span>
|
<span>Queue</span>
|
||||||
<span id="queue-length"></span>
|
<span class="queue-length"></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button data-for="playlists" data-icon="playlist-music"><span>Playlists</span></button>
|
<button data-for="playlists" data-icon="playlist-music"><span>Playlists</span></button>
|
||||||
|
|
|
@ -8,6 +8,10 @@ export default class Component extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
if (this.selection) {
|
||||||
|
const parent = this._app.querySelector("footer");
|
||||||
|
this.selection.appendTo(parent);
|
||||||
|
}
|
||||||
this._app.addEventListener("load", _ => this._onAppLoad());
|
this._app.addEventListener("load", _ => this._onAppLoad());
|
||||||
this._app.addEventListener("component-change", _ => {
|
this._app.addEventListener("component-change", _ => {
|
||||||
const component = this._app.component;
|
const component = this._app.component;
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
export const artSize = 96;
|
export const artSize = 96;
|
||||||
export const ytPath = "_youtube";
|
export const ytPath = "_youtube";
|
||||||
export const locale = "cs";
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ async function initMpd() {
|
||||||
await mpd.init();
|
await mpd.init();
|
||||||
return mpd;
|
return mpd;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
|
||||||
return mpdMock;
|
return mpdMock;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +34,7 @@ class App extends HTMLElement {
|
||||||
const names = children.map(node => node.nodeName.toLowerCase())
|
const names = children.map(node => node.nodeName.toLowerCase())
|
||||||
.filter(name => name.startsWith("cyp-"));
|
.filter(name => name.startsWith("cyp-"));
|
||||||
const unique = new Set(names);
|
const unique = new Set(names);
|
||||||
|
console.log(unique);
|
||||||
|
|
||||||
const promises = [...unique].map(name => customElements.whenDefined(name));
|
const promises = [...unique].map(name => customElements.whenDefined(name));
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Tag from "./tag.js";
|
||||||
import Path from "./path.js";
|
import Path from "./path.js";
|
||||||
import Back from "./back.js";
|
import Back from "./back.js";
|
||||||
import Song from "./song.js";
|
import Song from "./song.js";
|
||||||
|
import Search from "./search.js";
|
||||||
import { escape, serializeFilter } from "../mpd.js";
|
import { escape, serializeFilter } from "../mpd.js";
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,6 +37,13 @@ class Library extends Component {
|
||||||
super({selection:"multi"});
|
super({selection:"multi"});
|
||||||
this._stateStack = [];
|
this._stateStack = [];
|
||||||
this._initCommands();
|
this._initCommands();
|
||||||
|
|
||||||
|
this._search = new Search();
|
||||||
|
this._search.onSubmit = _ => {
|
||||||
|
let query = this._search.value;
|
||||||
|
if (query.length < 3) { return; }
|
||||||
|
this._doSearch(query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_popState() {
|
_popState() {
|
||||||
|
@ -118,29 +126,26 @@ class Library extends Component {
|
||||||
_showSearch(query = "") {
|
_showSearch(query = "") {
|
||||||
html.clear(this);
|
html.clear(this);
|
||||||
|
|
||||||
const form = html.node("form", {}, "", this);
|
this.appendChild(this._search);
|
||||||
const input = html.node("input", {type:"text", value:query}, "", form);
|
this._search.value = query;
|
||||||
html.button({icon:"magnify"}, "", form);
|
this._search.focus();
|
||||||
form.addEventListener("submit", e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const query = input.value.trim();
|
|
||||||
if (query.length < 3) { return; }
|
|
||||||
this._doSearch(query, form);
|
|
||||||
});
|
|
||||||
|
|
||||||
input.focus();
|
query && this._search.onSubmit();
|
||||||
if (query) { this._doSearch(query, form); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _doSearch(query, form) {
|
async _doSearch(query) {
|
||||||
let state = this._stateStack[this._stateStack.length-1];
|
let state = this._stateStack[this._stateStack.length-1];
|
||||||
state.query = query;
|
state.query = query;
|
||||||
|
|
||||||
|
html.clear(this);
|
||||||
|
this.appendChild(this._search);
|
||||||
|
this._search.pending(true);
|
||||||
|
|
||||||
const songs1 = await this._mpd.searchSongs({"AlbumArtist": query});
|
const songs1 = await this._mpd.searchSongs({"AlbumArtist": query});
|
||||||
const songs2 = await this._mpd.searchSongs({"Album": query});
|
const songs2 = await this._mpd.searchSongs({"Album": query});
|
||||||
const songs3 = await this._mpd.searchSongs({"Title": query});
|
const songs3 = await this._mpd.searchSongs({"Title": query});
|
||||||
html.clear(this);
|
|
||||||
this.appendChild(form);
|
this._search.pending(false);
|
||||||
|
|
||||||
this._aggregateSearch(songs1, "AlbumArtist");
|
this._aggregateSearch(songs1, "AlbumArtist");
|
||||||
this._aggregateSearch(songs2, "Album");
|
this._aggregateSearch(songs2, "Album");
|
||||||
|
|
|
@ -10,6 +10,13 @@ class Menu extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onAppLoad() {
|
||||||
|
this._app.addEventListener("queue-length-change", e => {
|
||||||
|
this.querySelector(".queue-length").textContent = `(${e.detail})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
async _activate(component) {
|
async _activate(component) {
|
||||||
const app = await this._app;
|
const app = await this._app;
|
||||||
app.setAttribute("component", component);
|
app.setAttribute("component", component);
|
||||||
|
|
|
@ -52,8 +52,8 @@ class Queue extends Component {
|
||||||
let songs = await this._mpd.listQueue();
|
let songs = await this._mpd.listQueue();
|
||||||
this._buildSongs(songs);
|
this._buildSongs(songs);
|
||||||
|
|
||||||
// FIXME pubsub?
|
let e = new CustomEvent("queue-length-change", {detail:songs.length});
|
||||||
document.querySelector("#queue-length").textContent = `(${songs.length})`;
|
this._app.dispatchEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateCurrent() {
|
_updateCurrent() {
|
||||||
|
|
33
app/js/elements/search.js
Normal file
33
app/js/elements/search.js
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import * as html from "../html.js";
|
||||||
|
|
||||||
|
export default class Search extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._built = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() { return this._input.value.trim(); }
|
||||||
|
set value(value) { this._input.value = value; }
|
||||||
|
get _input() { return this.querySelector("input"); }
|
||||||
|
|
||||||
|
onSubmit() {}
|
||||||
|
focus() { this._input.focus(); }
|
||||||
|
pending(pending) { this.classList.toggle("pending", pending); }
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
if (this._built) { return; }
|
||||||
|
|
||||||
|
const form = html.node("form", {}, "", this);
|
||||||
|
html.node("input", {type:"text"}, "", form);
|
||||||
|
html.button({icon:"magnify"}, "", form);
|
||||||
|
|
||||||
|
form.addEventListener("submit", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.onSubmit();
|
||||||
|
});
|
||||||
|
|
||||||
|
this._built = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("cyp-search", Search);
|
19
app/js/elements/yt-result.js
Normal file
19
app/js/elements/yt-result.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import Item from "../item.js";
|
||||||
|
import * as html from "../html.js";
|
||||||
|
|
||||||
|
|
||||||
|
export default class YtResult extends Item {
|
||||||
|
constructor(title) {
|
||||||
|
super();
|
||||||
|
this._title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.appendChild(html.icon("magnify"));
|
||||||
|
this._buildTitle(this._title);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClick() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("cyp-yt-result", YtResult);
|
|
@ -2,6 +2,8 @@ import * as html from "../html.js";
|
||||||
import * as conf from "../conf.js";
|
import * as conf from "../conf.js";
|
||||||
import { escape } from "../mpd.js";
|
import { escape } from "../mpd.js";
|
||||||
import Component from "../component.js";
|
import Component from "../component.js";
|
||||||
|
import Search from "./search.js";
|
||||||
|
import Result from "./yt-result.js";
|
||||||
|
|
||||||
|
|
||||||
const decoder = new TextDecoder("utf-8");
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
@ -12,57 +14,52 @@ function decodeChunk(byteArray) {
|
||||||
}
|
}
|
||||||
|
|
||||||
class YT extends Component {
|
class YT extends Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._search = new Search();
|
||||||
|
|
||||||
|
this._search.onSubmit = _ => {
|
||||||
|
let query = this._search.value;
|
||||||
|
query && this._doSearch(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
const form = html.node("form", {}, "", this);
|
this._clear();
|
||||||
const input = html.node("input", {type:"text"}, "", form);
|
|
||||||
html.button({icon:"magnify"}, "", form);
|
|
||||||
form.addEventListener("submit", e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const query = input.value.trim();
|
|
||||||
if (!query.length) { return; }
|
|
||||||
this._doSearch(query, form);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async _doSearch(query, form) {
|
|
||||||
let response = await fetch(`/youtube?q=${encodeURIComponent(query)}`);
|
|
||||||
let data = await response.json();
|
|
||||||
|
|
||||||
html.clear(this);
|
|
||||||
this.appendChild(form);
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_download() {
|
|
||||||
let url = prompt("Please enter a YouTube URL:");
|
|
||||||
if (!url) { return; }
|
|
||||||
|
|
||||||
this._post(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
_search() {
|
|
||||||
let q = prompt("Please enter a search string:");
|
|
||||||
if (!q) { return; }
|
|
||||||
|
|
||||||
this._post(`ytsearch:${q}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_clear() {
|
_clear() {
|
||||||
html.clear(this.querySelector("pre"));
|
html.clear(this);
|
||||||
|
this.appendChild(this._search);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _post(q) {
|
async _doSearch(query) {
|
||||||
let pre = this.querySelector("pre");
|
this._clear();
|
||||||
html.clear(pre);
|
this._search.pending(true);
|
||||||
|
|
||||||
this.classList.add("pending");
|
let response = await fetch(`/youtube?q=${encodeURIComponent(query)}`);
|
||||||
|
let results = await response.json();
|
||||||
|
|
||||||
|
this._search.pending(false);
|
||||||
|
|
||||||
|
results.forEach(result => {
|
||||||
|
let node = new Result(result.title);
|
||||||
|
this.appendChild(node);
|
||||||
|
node.addButton("download", () => this._download(result.id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async _download(id) {
|
||||||
|
this._clear();
|
||||||
|
|
||||||
|
let pre = html.node("pre", {}, "", this);
|
||||||
|
this._search.pending(true);
|
||||||
|
|
||||||
let body = new URLSearchParams();
|
let body = new URLSearchParams();
|
||||||
body.set("q", q);
|
body.set("id", id);
|
||||||
let response = await fetch("/youtube", {method:"POST", body});
|
let response = await fetch("/youtube", {method:"POST", body});
|
||||||
|
|
||||||
let reader = response.body.getReader();
|
let reader = response.body.getReader();
|
||||||
|
@ -74,7 +71,7 @@ class YT extends Component {
|
||||||
}
|
}
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
|
|
||||||
this.classList.remove("pending");
|
this._search.pending(false);
|
||||||
|
|
||||||
if (response.status == 200) {
|
if (response.status == 200) {
|
||||||
this._mpd.command(`update ${escape(conf.ytPath)}`);
|
this._mpd.command(`update ${escape(conf.ytPath)}`);
|
||||||
|
@ -82,7 +79,10 @@ class YT extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onComponentChange(c, isThis) {
|
_onComponentChange(c, isThis) {
|
||||||
|
const wasHidden = this.hidden;
|
||||||
this.hidden = !isThis;
|
this.hidden = !isThis;
|
||||||
|
|
||||||
|
if (!wasHidden && isThis) { this._showRoot(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,12 @@ export default class Selection {
|
||||||
this._component = component;
|
this._component = component;
|
||||||
/** @type {"single" | "multi"} */
|
/** @type {"single" | "multi"} */
|
||||||
this._mode = mode;
|
this._mode = mode;
|
||||||
this._items = []; // FIXME ukladat skutecne HTML? co kdyz nastane refresh?
|
this._items = [];
|
||||||
this._node = html.node("cyp-commands", {hidden:true});
|
this._node = html.node("cyp-commands", {hidden:true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appendTo(parent) { parent.appendChild(this._node); }
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
while (this._items.length) { this.remove(this._items[0]); }
|
while (this._items.length) { this.remove(this._items[0]); }
|
||||||
}
|
}
|
||||||
|
@ -66,14 +68,12 @@ export default class Selection {
|
||||||
}
|
}
|
||||||
|
|
||||||
_show() {
|
_show() {
|
||||||
const parent = this._component.closest("cyp-app").querySelector("footer"); // FIXME jde lepe?
|
|
||||||
parent.appendChild(this._node);
|
|
||||||
this._node.offsetWidth; // FIXME jde lepe?
|
|
||||||
this._node.hidden = false;
|
this._node.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_hide() {
|
_hide() {
|
||||||
this._node.hidden = true;
|
this._node.hidden = true;
|
||||||
this._node.remove();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("cyp-commands", class extends HTMLElement {});
|
15
index.js
15
index.js
|
@ -14,8 +14,9 @@ function searchYoutube(q, response) {
|
||||||
response.setHeader("Content-Type", "text/plain"); // necessary for firefox to read by chunks
|
response.setHeader("Content-Type", "text/plain"); // necessary for firefox to read by chunks
|
||||||
|
|
||||||
console.log("YouTube searching", q);
|
console.log("YouTube searching", q);
|
||||||
q = escape(`ytsearch10:${q}`);
|
q = escape(`ytsearch3:${q}`);
|
||||||
const command = `${cmd} -j ${q} | jq "{id,title}" | jq -s .`;
|
const command = `${cmd} -j ${q} | jq "{id,title}" | jq -s .`;
|
||||||
|
|
||||||
require("child_process").exec(command, {}, (error, stdout, stderr) => {
|
require("child_process").exec(command, {}, (error, stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log("error", error);
|
console.log("error", error);
|
||||||
|
@ -28,14 +29,14 @@ function searchYoutube(q, response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function downloadYoutube(q, response) {
|
function downloadYoutube(id, response) {
|
||||||
response.setHeader("Content-Type", "text/plain"); // necessary for firefox to read by chunks
|
response.setHeader("Content-Type", "text/plain"); // necessary for firefox to read by chunks
|
||||||
|
|
||||||
console.log("YouTube downloading", q);
|
console.log("YouTube downloading", id);
|
||||||
let args = [
|
let args = [
|
||||||
"-f", "bestaudio",
|
"-f", "bestaudio",
|
||||||
"-o", `${__dirname}/_youtube/%(title)s-%(id)s.%(ext)s`,
|
"-o", `${__dirname}/_youtube/%(title)s-%(id)s.%(ext)s`,
|
||||||
q
|
id
|
||||||
]
|
]
|
||||||
let child = require("child_process").spawn(cmd, args);
|
let child = require("child_process").spawn(cmd, args);
|
||||||
|
|
||||||
|
@ -70,9 +71,9 @@ function handleYoutubeDownload(request, response) {
|
||||||
request.setEncoding("utf8");
|
request.setEncoding("utf8");
|
||||||
request.on("data", chunk => str += chunk);
|
request.on("data", chunk => str += chunk);
|
||||||
request.on("end", () => {
|
request.on("end", () => {
|
||||||
let q = require("querystring").parse(str)["id"];
|
let id = require("querystring").parse(str)["id"];
|
||||||
if (q) {
|
if (id) {
|
||||||
downloadYoutube(q, response);
|
downloadYoutube(id, response);
|
||||||
} else {
|
} else {
|
||||||
response.writeHead(404);
|
response.writeHead(404);
|
||||||
response.end();
|
response.end();
|
||||||
|
|
Loading…
Reference in a new issue