From 3783f0fa3ab5bf00a1ef0842da15b1e64eadd877 Mon Sep 17 00:00:00 2001 From: wryk Date: Thu, 9 Jan 2020 20:31:12 +0100 Subject: [PATCH] add wip controls and statuses buffer --- package-lock.json | 10 ++-- package.json | 2 +- src/App.svelte | 71 +++++++++++++++------- src/YoutubeViewer.svelte | 11 +++- src/index.html | 2 +- src/store.js | 125 ++++++++++++++++++++++++--------------- src/util.js | 64 ++++++++++++++++++-- 7 files changed, 202 insertions(+), 83 deletions(-) diff --git a/package-lock.json b/package-lock.json index d41e541..4f4076e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2312,6 +2312,11 @@ "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": { "version": "0.1.19", "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.19.tgz", @@ -3620,11 +3625,6 @@ "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", "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": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", diff --git a/package.json b/package.json index fb3f69a..ba66d4b 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ }, "dependencies": { "@babel/runtime-corejs3": "^7.7.7", + "date-fns": "^2.9.0", "get-urls": "^9.2.0", - "get-youtube-id": "^1.0.1", "yt-player": "^3.4.3" }, "browserslist": [ diff --git a/src/App.svelte b/src/App.svelte index 304f475..ff11287 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -6,38 +6,55 @@
- {#if selectedEntry} - Playing {selectedEntry.id} - + {#if $selectedEntry} + Playing {$selectedEntry.id} + {:else} Loading ... {/if} + +
+ + + +
+ +
+ + +
+ +
+ + +
- {#if $entries} - {#await $entries} - Loading radio please wait - {:then entries} +
+ {#if $entries}
    - {#each entries as entry} -
  • + {#each $entries as entry} +
  • $selectedEntry = entry}> +
    {entry.url}
    {entry.status.account.acct} {entry.tags}
  • {/each}
- {:catch error} - Oops, something went wrong : {error} - {/await} + {:else} + Your queue + {/if} +
+ + {#if $loading} + LOADING ... {:else} - Your queue + {/if}
{$domain} - {@html $hashtags.map(hashtag => `#${hashtag}`)} - -
@@ -46,16 +63,19 @@ @@ -96,6 +116,11 @@ .entry { cursor: pointer; + border: 1px solid black; + } + + .entry:hover { + background-color: plum; } .entry.active { diff --git a/src/YoutubeViewer.svelte b/src/YoutubeViewer.svelte index cb47ddb..e154de1 100644 --- a/src/YoutubeViewer.svelte +++ b/src/YoutubeViewer.svelte @@ -9,6 +9,7 @@ import YoutubePlayer from 'yt-player' export let videoId + export let playing let element let player @@ -17,9 +18,17 @@ player.load(videoId, true) } + $: if (player) { + if (playing) { + player.play() + } else { + player.pause() + } + } + onMount(() => { player = new YoutubePlayer(element, { - autoplay: true, + autoplay: playing, controls: true, // debug only keyboard: false, fullscreen: false, diff --git a/src/index.html b/src/index.html index 6bc2b84..6d81e5a 100644 --- a/src/index.html +++ b/src/index.html @@ -4,7 +4,7 @@ - Document + Eldritch Radio diff --git a/src/store.js b/src/store.js index 524a26e..22be305 100644 --- a/src/store.js +++ b/src/store.js @@ -1,5 +1,5 @@ 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') @@ -10,62 +10,91 @@ export const hashtags = writable([ 'pouetradio' ]) -export const entries = entriesStore() +export const playing = writable(true) -function entriesStore() { - let loading = false - let next = `https://eldritch.cafe/api/v1/timelines/tag/np` +export const loading = writable(false) +export const entries = entriesStore(loading) + +export const entry = entryStore(entries) + + + + + +function entryStore(entries) { const store = writable(null) - const { set, subscribe } = store + const { set, update, subscribe } = store - const load = async () => { - if (loading) { + const next = async () => { + 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 } - loading = true + for (let i = 0; i < number; i++) { + const iteratorResult = await entriesSteam.next() - const responseP = fetch(next) - - 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 { - set(entriesP) + if (iteratorResult.value) { + update(entries => [...entries, iteratorResult.value]) + } else { + break + } } - - loading = false } return { subscribe, load } -} \ No newline at end of file +} + diff --git a/src/util.js b/src/util.js index 4d34d8d..c39744f 100644 --- a/src/util.js +++ b/src/util.js @@ -1,10 +1,66 @@ -export { default as getUrls } from 'get-urls' -export { default as getYoutubeId } from 'get-youtube-id' +import getUrls from 'get-urls' -export function isSupportedUrl(url) { - return (new URL(url)).hostname === 'youtube.com' +export function isSupportedUrl(urlAsString) { + 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) { 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 } + }) } \ No newline at end of file