forked from Mastodon/mastoradio-la-radio-di-mastodon
add wip controls and statuses buffer
This commit is contained in:
parent
408099ac29
commit
3783f0fa3a
|
@ -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",
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
121
src/store.js
121
src/store.js
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
64
src/util.js
64
src/util.js
|
@ -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 }
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue