add wip controls and statuses buffer

This commit is contained in:
wryk 2020-01-09 20:31:12 +01:00
parent 408099ac29
commit 3783f0fa3a
7 changed files with 202 additions and 83 deletions

10
package-lock.json generated
View File

@ -2312,6 +2312,11 @@
"whatwg-url": "^7.0.0" "whatwg-url": "^7.0.0"
} }
}, },
"date-fns": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.9.0.tgz",
"integrity": "sha512-khbFLu/MlzLjEzy9Gh8oY1hNt/Dvxw3J6Rbc28cVoYWQaC1S3YI4xwkF9ZWcjDLscbZlY9hISMr66RFzZagLsA=="
},
"deasync": { "deasync": {
"version": "0.1.19", "version": "0.1.19",
"resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.19.tgz", "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.19.tgz",
@ -3620,11 +3625,6 @@
"integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
"dev": true "dev": true
}, },
"get-youtube-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-youtube-id/-/get-youtube-id-1.0.1.tgz",
"integrity": "sha512-5yidLzoLXbtw82a/Wb7LrajkGn29BM6JuLWeHyNfzOGp1weGyW4+7eMz6cP23+etqj27VlOFtq8fFFDMLq/FXQ=="
},
"getpass": { "getpass": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",

View File

@ -19,8 +19,8 @@
}, },
"dependencies": { "dependencies": {
"@babel/runtime-corejs3": "^7.7.7", "@babel/runtime-corejs3": "^7.7.7",
"date-fns": "^2.9.0",
"get-urls": "^9.2.0", "get-urls": "^9.2.0",
"get-youtube-id": "^1.0.1",
"yt-player": "^3.4.3" "yt-player": "^3.4.3"
}, },
"browserslist": [ "browserslist": [

View File

@ -6,38 +6,55 @@
</header> </header>
<section class="player"> <section class="player">
{#if selectedEntry} {#if $selectedEntry}
Playing <a href={selectedEntry.url}>{selectedEntry.id}</a> Playing <a href={$selectedEntry.url}>{$selectedEntry.id}</a>
<YoutubeViewer bind:videoId={selectedEntry.id}></YoutubeViewer> <YoutubeViewer bind:videoId={$selectedEntry.id} bind:playing={$playing}></YoutubeViewer>
{:else} {:else}
Loading ... Loading ...
{/if} {/if}
<div>
<button>⏮️</button>
<button on:click={() => $playing = !$playing}>{#if $playing}⏸️{:else}▶️{/if}</button>
<button on:click={() => selectedEntry.next()}>⏭️</button>
</div>
<div>
<button>🔇 / 🔊</button>
<input type="range" min="0" max="100" value="80">
</div>
<div>
<button></button>
<button>🔁</button>
</div>
</section> </section>
<section class="queue"> <section class="queue">
<div>
{#if $entries} {#if $entries}
{#await $entries}
Loading radio please wait
{:then entries}
<ul> <ul>
{#each entries as entry} {#each $entries as entry}
<li class="entry" class:active={entry === selectedEntry} on:click={selectEntry(entry)}> <li class="entry" class:active={entry === $selectedEntry} on:click={() => $selectedEntry = entry}>
<div>{entry.url}</div>
<b>{entry.status.account.acct}</b> <b>{entry.status.account.acct}</b>
<small>{entry.tags}</small> <small>{entry.tags}</small>
</li> </li>
{/each} {/each}
</ul> </ul>
{:catch error}
Oops, something went wrong : {error}
{/await}
{:else} {:else}
Your queue Your queue
{/if} {/if}
</div>
{#if $loading}
LOADING ...
{:else}
<button on:click={() => entries.load(3)}>LOAD MOAR</button>
{/if}
<header> <header>
<a href="https://{$domain}/">{$domain}</a> - {@html $hashtags.map(hashtag => `<a href="https://${$domain}/tags/${hashtag}">#${hashtag}</a>`)} <a href="https://{$domain}/">{$domain}</a> - {@html $hashtags.map(hashtag => `<a href="https://${$domain}/tags/${hashtag}">#${hashtag}</a>`)}
<button on:click={() => entries.load()}>LOAD MOAR</button>
</header> </header>
</section> </section>
</main> </main>
@ -46,16 +63,19 @@
<script> <script>
import { onMount } from 'svelte' import { onMount } from 'svelte'
import YoutubeViewer from './YoutubeViewer.svelte' import YoutubeViewer from './YoutubeViewer.svelte'
import { domain, hashtags, entries } from './store.js' import { domain, hashtags, playing, loading, entry as selectedEntry, entries } from './store.js'
let selectedEntry = null
const selectEntry = entry => {
selectedEntry = entry
}
onMount(() => { onMount(() => {
entries.load() const unsub = entries.subscribe(async (xs) => {
if (xs.length) {
const [firstEntry] = xs
selectedEntry.set(firstEntry)
unsub()
}
})
entries.load(7)
}) })
</script> </script>
@ -96,6 +116,11 @@
.entry { .entry {
cursor: pointer; cursor: pointer;
border: 1px solid black;
}
.entry:hover {
background-color: plum;
} }
.entry.active { .entry.active {

View File

@ -9,6 +9,7 @@
import YoutubePlayer from 'yt-player' import YoutubePlayer from 'yt-player'
export let videoId export let videoId
export let playing
let element let element
let player let player
@ -17,9 +18,17 @@
player.load(videoId, true) player.load(videoId, true)
} }
$: if (player) {
if (playing) {
player.play()
} else {
player.pause()
}
}
onMount(() => { onMount(() => {
player = new YoutubePlayer(element, { player = new YoutubePlayer(element, {
autoplay: true, autoplay: playing,
controls: true, // debug only controls: true, // debug only
keyboard: false, keyboard: false,
fullscreen: false, fullscreen: false,

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title> <title>Eldritch Radio</title>
</head> </head>
<body> <body>
<script src="main.js"></script> <script src="main.js"></script>

View File

@ -1,5 +1,5 @@
import { writable, get } from 'svelte/store' import { writable, get } from 'svelte/store'
import { getUrls, getYoutubeId, isSupportedUrl, intersection } from './util.js' import * as util from './util.js'
export const domain = writable('eldritch.cafe') export const domain = writable('eldritch.cafe')
@ -10,62 +10,91 @@ export const hashtags = writable([
'pouetradio' 'pouetradio'
]) ])
export const entries = entriesStore() export const playing = writable(true)
function entriesStore() { export const loading = writable(false)
let loading = false
let next = `https://eldritch.cafe/api/v1/timelines/tag/np`
export const entries = entriesStore(loading)
export const entry = entryStore(entries)
function entryStore(entries) {
const store = writable(null) const store = writable(null)
const { set, subscribe } = store const { set, update, subscribe } = store
const load = async () => { const next = async () => {
if (loading) { const entriesList = await get(entries)
update(oldEntry => {
if (entriesList.length === 0) {
return null
}
const index = entriesList.indexOf(oldEntry)
if (index === -1) {
return null
}
const nextIndex = index + 1
if (nextIndex === entriesList.length - 1) {
entries.load(1)
}
return entriesList[nextIndex]
})
}
return { subscribe, set, next }
}
async function* loader(loading) {
loading.set(true)
let { statuses, nextLink, previousLink } = await util.fetchTimeline('https://eldritch.cafe/api/v1/timelines/tag/np')
loading.set(false)
yield* util.statusesToEntries(statuses)
while (true) {
loading.set(true)
const timeline = await util.fetchTimeline(nextLink)
loading.set(false)
nextLink = timeline.nextLink
yield* util.statusesToEntries(timeline.statuses)
}
}
function entriesStore(loading) {
const entriesSteam = loader(loading)
const store = writable([])
const { update, subscribe } = store
const load = async (number) => {
if (get(loading)) {
return return
} }
loading = true for (let i = 0; i < number; i++) {
const iteratorResult = await entriesSteam.next()
const responseP = fetch(next) if (iteratorResult.value) {
update(entries => [...entries, iteratorResult.value])
responseP.then(response => {
next = Array.from(getUrls(response.headers.get('link')))[0] // need to better parse that
})
const entriesP = responseP
.then(response => response.json())
.then(statuses => {
return statuses
.map(status => {
const [url] = Array.from(getUrls(status.content)).filter(isSupportedUrl)
return { status, url }
})
.filter(entry => entry.url != null)
.map(({ status, url }) => {
const id = getYoutubeId(url)
const tags = intersection(status.tags.map(tag => tag.name), [
'np',
'nowplaying',
'tootradio',
'pouetradio'
])
return { status, url, id, tags }
})
})
const previousEntriesP = get(store)
if (previousEntriesP) {
const [previousEntries, entries] = await Promise.all([previousEntriesP, entriesP])
set(Promise.resolve([...previousEntries, ...entries]))
} else { } else {
set(entriesP) break
}
} }
loading = false
} }
return { subscribe, load } return { subscribe, load }
} }

View File

@ -1,10 +1,66 @@
export { default as getUrls } from 'get-urls' import getUrls from 'get-urls'
export { default as getYoutubeId } from 'get-youtube-id'
export function isSupportedUrl(url) { export function isSupportedUrl(urlAsString) {
return (new URL(url)).hostname === 'youtube.com' const url = new URL(urlAsString)
const hosts = [
'youtube.com',
'music.youtube.host'
]
return hosts.includes(url.hostname) && url.searchParams.has('v')
}
export function getYoutubeVideoId(urlAsString) {
return new URL(urlAsString).searchParams.get('v')
} }
export function intersection(xs, ys) { export function intersection(xs, ys) {
return xs.filter(x => ys.includes(x)); return xs.filter(x => ys.includes(x));
} }
export async function fetchTimeline(url) {
const urlBuilder = new URL(url)
urlBuilder.searchParams.set('limit', 40)
const response = await fetch(url)
const statuses = await response.json()
const { next, previous } = parseLinkHeader(response.headers.get('link'))
return { statuses, nextLink: next, previousLink: previous }
}
const LINK_RE = /<(.+?)>; rel="(\w+)"/gi
export function parseLinkHeader(link) {
const links = {}
for (const [ , url, name ] of link.matchAll(LINK_RE)) {
links[name] = url
}
return links
}
export function statusesToEntries(statuses) {
const entries = []
return statuses
.map(status => {
const [url] = Array.from(getUrls(status.content)).filter(isSupportedUrl)
return { status, url }
})
.filter(entry => entry.url != null)
.map(({ status, url }) => {
const id = getYoutubeVideoId(url)
const tags = intersection(status.tags.map(tag => tag.name), [
'np',
'nowplaying',
'tootradio',
'pouetradio'
])
return { status, url, id, tags }
})
}