yt finished, cleanup

This commit is contained in:
Ondrej Zara 2020-03-16 22:57:13 +01:00
parent 4e10590646
commit cf46a0ffb1
No known key found for this signature in database
GPG key ID: B0A5751E616840C5
23 changed files with 301 additions and 199 deletions

View file

@ -70,7 +70,6 @@ select {
@import "font.less";
@import "icons.less";
@import "mixins.less";
@import "search.less";
@import "art.less";
@import "variables.less";
@ -84,7 +83,9 @@ select {
@import "elements/yt.less";
@import "elements/range.less";
@import "elements/playlist.less";
@import "elements/search.less";
@import "elements/library.less";
@import "elements/tag.less";
@import "elements/back.less";
@import "elements/path.less";
@import "elements/yt-result.less";

View file

@ -15,13 +15,4 @@ cyp-library {
}
}
}
form {
.item;
.flex-row;
button:first-of-type {
margin-left: var(--icon-spacing);
}
}
}

View file

@ -37,7 +37,7 @@ cyp-player {
.timeline {
flex: none;
height: 24px;
height: var(--icon-size);
margin-bottom: 4px;
.flex-row;

View 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%; }
}

View file

@ -0,0 +1,8 @@
cyp-yt-result {
.item;
cursor: default;
button .icon {
width: var(--icon-size);
}
}

View file

@ -1,32 +1,8 @@
cyp-yt {
header {
border-bottom: 1px solid var(--fg);
button + button {
margin-left: 16px;
}
}
.clear {
margin-left: auto;
}
pre {
margin: 0.5em 0.5ch;
flex-grow: 1;
overflow: auto;
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%; }
}

View file

@ -1,5 +1,5 @@
.icon {
width: 24px;
width: var(--icon-size);
flex: none;
path, polygon, circle {

View file

@ -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;
}
}

View file

@ -1,6 +1,7 @@
@breakpoint-menu: 480px;
cyp-app {
--icon-size: 24px;
--icon-spacing: 4px;
--primary: rgb(var(--primary-raw));
--primary-tint: rgba(var(--primary-raw), 0.1);

File diff suppressed because one or more lines are too long

View file

@ -654,7 +654,6 @@ async function initMpd() {
await init();
return mpd;
} catch (e) {
console.error(e);
return mpdMock;
}
}
@ -675,6 +674,7 @@ class App extends HTMLElement {
const names = children.map(node => node.nodeName.toLowerCase())
.filter(name => name.startsWith("cyp-"));
const unique = new Set(names);
console.log(unique);
const promises = [...unique].map(name => customElements.whenDefined(name));
await Promise.all(promises);
@ -712,10 +712,12 @@ class Selection {
this._component = component;
/** @type {"single" | "multi"} */
this._mode = mode;
this._items = []; // FIXME ukladat skutecne HTML? co kdyz nastane refresh?
this._items = [];
this._node = node("cyp-commands", {hidden:true});
}
appendTo(parent) { parent.appendChild(this._node); }
clear() {
while (this._items.length) { this.remove(this._items[0]); }
}
@ -770,18 +772,16 @@ class Selection {
}
_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;
}
_hide() {
this._node.hidden = true;
this._node.remove();
}
}
customElements.define("cyp-commands", class extends HTMLElement {});
class Component extends HTMLElement {
constructor(options = {}) {
super();
@ -789,6 +789,10 @@ class Component extends HTMLElement {
}
connectedCallback() {
if (this.selection) {
const parent = this._app.querySelector("footer");
this.selection.appendTo(parent);
}
this._app.addEventListener("load", _ => this._onAppLoad());
this._app.addEventListener("component-change", _ => {
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) {
const app = await this._app;
app.setAttribute("component", component);
@ -1157,8 +1168,8 @@ class Queue extends Component {
let songs = await this._mpd.listQueue();
this._buildSongs(songs);
// FIXME pubsub?
document.querySelector("#queue-length").textContent = `(${songs.length})`;
let e = new CustomEvent("queue-length-change", {detail:songs.length});
this._app.dispatchEvent(e);
}
_updateCurrent() {
@ -1366,6 +1377,54 @@ class Settings extends Component {
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");
function decodeChunk(byteArray) {
@ -1374,57 +1433,52 @@ function decodeChunk(byteArray) {
}
class YT extends Component {
constructor() {
super();
this._search = new Search();
this._search.onSubmit = _ => {
let query = this._search.value;
query && this._doSearch(query);
};
}
connectedCallback() {
super.connectedCallback();
const form = node("form", {}, "", this);
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}`);
this._clear();
}
_clear() {
clear(this.querySelector("pre"));
clear(this);
this.appendChild(this._search);
}
async _post(q) {
let pre = this.querySelector("pre");
clear(pre);
async _doSearch(query) {
this._clear();
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();
body.set("q", q);
body.set("id", id);
let response = await fetch("/youtube", {method:"POST", body});
let reader = response.body.getReader();
@ -1436,7 +1490,7 @@ class YT extends Component {
}
reader.releaseLock();
this.classList.remove("pending");
this._search.pending(false);
if (response.status == 200) {
this._mpd.command(`update ${escape(ytPath)}`);
@ -1444,7 +1498,10 @@ class YT extends Component {
}
_onComponentChange(c, isThis) {
const wasHidden = this.hidden;
this.hidden = !isThis;
if (!wasHidden && isThis) { this._showRoot(); }
}
}
@ -1560,6 +1617,13 @@ class Library extends Component {
super({selection:"multi"});
this._stateStack = [];
this._initCommands();
this._search = new Search();
this._search.onSubmit = _ => {
let query = this._search.value;
if (query.length < 3) { return; }
this._doSearch(query);
};
}
_popState() {
@ -1642,29 +1706,26 @@ class Library extends Component {
_showSearch(query = "") {
clear(this);
const form = node("form", {}, "", this);
const input = node("input", {type:"text", value:query}, "", form);
button({icon:"magnify"}, "", form);
form.addEventListener("submit", e => {
e.preventDefault();
const query = input.value.trim();
if (query.length < 3) { return; }
this._doSearch(query, form);
});
this.appendChild(this._search);
this._search.value = query;
this._search.focus();
input.focus();
if (query) { this._doSearch(query, form); }
query && this._search.onSubmit();
}
async _doSearch(query, form) {
async _doSearch(query) {
let state = this._stateStack[this._stateStack.length-1];
state.query = query;
clear(this);
this.appendChild(this._search);
this._search.pending(true);
const songs1 = await this._mpd.searchSongs({"AlbumArtist": query});
const songs2 = await this._mpd.searchSongs({"Album": query});
const songs3 = await this._mpd.searchSongs({"Title": query});
clear(this);
this.appendChild(form);
this._search.pending(false);
this._aggregateSearch(songs1, "AlbumArtist");
this._aggregateSearch(songs2, "Album");

View file

@ -76,7 +76,7 @@
<button data-for="queue" data-icon="music">
<div>
<span>Queue</span>
<span id="queue-length"></span>
<span class="queue-length"></span>
</div>
</button>
<button data-for="playlists" data-icon="playlist-music"><span>Playlists</span></button>

View file

@ -8,6 +8,10 @@ export default class Component extends HTMLElement {
}
connectedCallback() {
if (this.selection) {
const parent = this._app.querySelector("footer");
this.selection.appendTo(parent);
}
this._app.addEventListener("load", _ => this._onAppLoad());
this._app.addEventListener("component-change", _ => {
const component = this._app.component;

View file

@ -1,3 +1,2 @@
export const artSize = 96;
export const ytPath = "_youtube";
export const locale = "cs";

View file

@ -14,7 +14,6 @@ async function initMpd() {
await mpd.init();
return mpd;
} catch (e) {
console.error(e);
return mpdMock;
}
}
@ -35,6 +34,7 @@ class App extends HTMLElement {
const names = children.map(node => node.nodeName.toLowerCase())
.filter(name => name.startsWith("cyp-"));
const unique = new Set(names);
console.log(unique);
const promises = [...unique].map(name => customElements.whenDefined(name));
await Promise.all(promises);

View file

@ -4,6 +4,7 @@ import Tag from "./tag.js";
import Path from "./path.js";
import Back from "./back.js";
import Song from "./song.js";
import Search from "./search.js";
import { escape, serializeFilter } from "../mpd.js";
@ -36,6 +37,13 @@ class Library extends Component {
super({selection:"multi"});
this._stateStack = [];
this._initCommands();
this._search = new Search();
this._search.onSubmit = _ => {
let query = this._search.value;
if (query.length < 3) { return; }
this._doSearch(query);
}
}
_popState() {
@ -118,29 +126,26 @@ class Library extends Component {
_showSearch(query = "") {
html.clear(this);
const form = html.node("form", {}, "", this);
const input = html.node("input", {type:"text", value:query}, "", form);
html.button({icon:"magnify"}, "", form);
form.addEventListener("submit", e => {
e.preventDefault();
const query = input.value.trim();
if (query.length < 3) { return; }
this._doSearch(query, form);
});
this.appendChild(this._search);
this._search.value = query;
this._search.focus();
input.focus();
if (query) { this._doSearch(query, form); }
query && this._search.onSubmit();
}
async _doSearch(query, form) {
async _doSearch(query) {
let state = this._stateStack[this._stateStack.length-1];
state.query = query;
html.clear(this);
this.appendChild(this._search);
this._search.pending(true);
const songs1 = await this._mpd.searchSongs({"AlbumArtist": query});
const songs2 = await this._mpd.searchSongs({"Album": 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(songs2, "Album");

View file

@ -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) {
const app = await this._app;
app.setAttribute("component", component);

View file

@ -52,8 +52,8 @@ class Queue extends Component {
let songs = await this._mpd.listQueue();
this._buildSongs(songs);
// FIXME pubsub?
document.querySelector("#queue-length").textContent = `(${songs.length})`;
let e = new CustomEvent("queue-length-change", {detail:songs.length});
this._app.dispatchEvent(e);
}
_updateCurrent() {

33
app/js/elements/search.js Normal file
View 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);

View 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);

View file

@ -2,6 +2,8 @@ import * as html from "../html.js";
import * as conf from "../conf.js";
import { escape } from "../mpd.js";
import Component from "../component.js";
import Search from "./search.js";
import Result from "./yt-result.js";
const decoder = new TextDecoder("utf-8");
@ -12,57 +14,52 @@ function decodeChunk(byteArray) {
}
class YT extends Component {
constructor() {
super();
this._search = new Search();
this._search.onSubmit = _ => {
let query = this._search.value;
query && this._doSearch(query);
}
}
connectedCallback() {
super.connectedCallback();
const form = html.node("form", {}, "", this);
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}`);
this._clear();
}
_clear() {
html.clear(this.querySelector("pre"));
html.clear(this);
this.appendChild(this._search);
}
async _post(q) {
let pre = this.querySelector("pre");
html.clear(pre);
async _doSearch(query) {
this._clear();
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();
body.set("q", q);
body.set("id", id);
let response = await fetch("/youtube", {method:"POST", body});
let reader = response.body.getReader();
@ -74,7 +71,7 @@ class YT extends Component {
}
reader.releaseLock();
this.classList.remove("pending");
this._search.pending(false);
if (response.status == 200) {
this._mpd.command(`update ${escape(conf.ytPath)}`);
@ -82,7 +79,10 @@ class YT extends Component {
}
_onComponentChange(c, isThis) {
const wasHidden = this.hidden;
this.hidden = !isThis;
if (!wasHidden && isThis) { this._showRoot(); }
}
}

View file

@ -8,10 +8,12 @@ export default class Selection {
this._component = component;
/** @type {"single" | "multi"} */
this._mode = mode;
this._items = []; // FIXME ukladat skutecne HTML? co kdyz nastane refresh?
this._items = [];
this._node = html.node("cyp-commands", {hidden:true});
}
appendTo(parent) { parent.appendChild(this._node); }
clear() {
while (this._items.length) { this.remove(this._items[0]); }
}
@ -66,14 +68,12 @@ export default class Selection {
}
_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;
}
_hide() {
this._node.hidden = true;
this._node.remove();
}
}
customElements.define("cyp-commands", class extends HTMLElement {});

View file

@ -14,8 +14,9 @@ function searchYoutube(q, response) {
response.setHeader("Content-Type", "text/plain"); // necessary for firefox to read by chunks
console.log("YouTube searching", q);
q = escape(`ytsearch10:${q}`);
q = escape(`ytsearch3:${q}`);
const command = `${cmd} -j ${q} | jq "{id,title}" | jq -s .`;
require("child_process").exec(command, {}, (error, stdout, stderr) => {
if (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
console.log("YouTube downloading", q);
console.log("YouTube downloading", id);
let args = [
"-f", "bestaudio",
"-o", `${__dirname}/_youtube/%(title)s-%(id)s.%(ext)s`,
q
id
]
let child = require("child_process").spawn(cmd, args);
@ -70,9 +71,9 @@ function handleYoutubeDownload(request, response) {
request.setEncoding("utf8");
request.on("data", chunk => str += chunk);
request.on("end", () => {
let q = require("querystring").parse(str)["id"];
if (q) {
downloadYoutube(q, response);
let id = require("querystring").parse(str)["id"];
if (id) {
downloadYoutube(id, response);
} else {
response.writeHead(404);
response.end();