forked from Mastodon/mastoradio-la-radio-di-mastodon
quick url matcher/transformer refact
This commit is contained in:
parent
87c0398f0a
commit
e6e88fddd5
|
@ -11,9 +11,12 @@
|
|||
<div>
|
||||
{#each $entries as entry}
|
||||
<div class="entry" class:active={entry === $currentEntry} on:click={() => $currentEntry = entry}>
|
||||
<div>{entry.metadata.title}</div>
|
||||
<b>{entry.status.account.username} <small>{entry.status.account.acct}</small></b>
|
||||
<small>{entry.tags}</small>
|
||||
<div>{entry.data.metadata.title}</div>
|
||||
|
||||
<div>
|
||||
<small>{entry.url}</small>
|
||||
<b>{entry.status.account.username} <small>{entry.status.account.acct}</small></b>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -34,13 +37,12 @@
|
|||
import { playing, loading, entry as currentEntry, entries } from '/store.js'
|
||||
|
||||
onMount(() => {
|
||||
const unsub = entries.subscribe(async (xs) => {
|
||||
const unsubscribe = entries.subscribe(async (xs) => {
|
||||
if (xs.length) {
|
||||
const [firstEntry] = xs
|
||||
currentEntry.set(firstEntry)
|
||||
unsub()
|
||||
unsubscribe()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
entries.load(7)
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
$: updateVolume($volume)
|
||||
|
||||
const updateEntry = (entry) => {
|
||||
if (player && entry) player.load(entry.id, get(playing))
|
||||
if (player && entry) player.load(entry.data.id, get(playing))
|
||||
}
|
||||
|
||||
const updatePlaying = (playing) => {
|
||||
|
@ -37,7 +37,7 @@
|
|||
width: 300,
|
||||
height: 300,
|
||||
autoplay: $playing,
|
||||
controls: true, // debug only
|
||||
controls: false,
|
||||
keyboard: false,
|
||||
fullscreen: false,
|
||||
modestBranding: true,
|
||||
|
@ -49,7 +49,11 @@
|
|||
updateVolume($volume)
|
||||
|
||||
player.on('ended', () => entry.next())
|
||||
player.on('unplayable', () => entry.next())
|
||||
|
||||
player.on('unplayable', (...args) => {
|
||||
console.log('unplayable', ...args)
|
||||
entry.next()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
13
src/store.js
13
src/store.js
|
@ -1,9 +1,9 @@
|
|||
import { writable, get } from 'svelte/store'
|
||||
import * as util from '/util.js'
|
||||
|
||||
export const domain = writable('eldritch.cafe')
|
||||
export const domain = writableLocalStorage('domain', 'eldritch.cafe')
|
||||
|
||||
export const hashtags = writable([
|
||||
export const hashtags = writableLocalStorage('hashtags', [
|
||||
'np',
|
||||
'nowplaying',
|
||||
'tootradio',
|
||||
|
@ -14,7 +14,7 @@ export const playing = writable(true)
|
|||
export const muted = writableLocalStorage('muted', false)
|
||||
export const volume = writableLocalStorage('volume', 100)
|
||||
|
||||
export const entries = entriesStore()
|
||||
export const entries = entriesStore(get(domain), get(hashtags))
|
||||
export const entry = entryStore(entries)
|
||||
|
||||
|
||||
|
@ -23,7 +23,7 @@ export const entry = entryStore(entries)
|
|||
function writableLocalStorage(key, value) {
|
||||
const item = JSON.parse(localStorage.getItem(key))
|
||||
const store = writable(item === null ? value : item)
|
||||
const unsubscribe = store.subscribe(x => localStorage.setItem(key, x))
|
||||
const unsubscribe = store.subscribe(x => localStorage.setItem(key, JSON.stringify(x)))
|
||||
return store
|
||||
}
|
||||
|
||||
|
@ -80,8 +80,8 @@ function entryStore(entries) {
|
|||
return { subscribe, set: select, previous, next }
|
||||
}
|
||||
|
||||
function entriesStore() {
|
||||
const entriesSteam = util.statusesToEntries(util.statusesStreaming())
|
||||
function entriesStore(domain, hashtags) {
|
||||
const entriesSteam = util.statusesToEntries(util.statusesStreaming(domain, hashtags))
|
||||
|
||||
const store = writable([])
|
||||
const { update, subscribe } = store
|
||||
|
@ -91,6 +91,7 @@ function entriesStore() {
|
|||
const iteratorResult = await entriesSteam.next()
|
||||
|
||||
if (iteratorResult.value) {
|
||||
// console.log(iteratorResult.value)
|
||||
update(entries => [...entries, iteratorResult.value])
|
||||
} else {
|
||||
break
|
||||
|
|
100
src/util.js
100
src/util.js
|
@ -1,8 +1,11 @@
|
|||
import getUrls from 'get-urls'
|
||||
import { pipe, asyncFilter, asyncMap, asyncTap, asyncTake } from 'iter-tools'
|
||||
import YouTubePlayer from 'yt-player'
|
||||
|
||||
export async function* statusesStreaming() {
|
||||
let { statuses, nextLink, previousLink } = await fetchTimeline('https://eldritch.cafe/api/v1/timelines/tag/np')
|
||||
export async function* statusesStreaming(domain, [hashtag]) {
|
||||
const initialLink = `https://${domain}/api/v1/timelines/tag/${hashtag}?limit=40`
|
||||
|
||||
let { statuses, nextLink, previousLink } = await fetchTimeline(initialLink)
|
||||
|
||||
yield* statuses
|
||||
|
||||
|
@ -15,50 +18,10 @@ export async function* statusesStreaming() {
|
|||
}
|
||||
|
||||
export const statusesToEntries = pipe(
|
||||
asyncMap(status => ({ status, urls: Array.from(getUrls(status.content)).filter(isSupportedUrl) })),
|
||||
asyncFilter(entry => entry.urls.length > 0),
|
||||
asyncMap(async ({ status, urls }) => {
|
||||
const [url] = urls
|
||||
const id = getYoutubeVideoId(url)
|
||||
|
||||
const tags = intersection(status.tags.map(tag => tag.name), [
|
||||
'np',
|
||||
'nowplaying',
|
||||
'tootradio',
|
||||
'pouetradio'
|
||||
])
|
||||
|
||||
const metadata = await fetchYoutubeMetadata(id)
|
||||
|
||||
return { status, url, id, tags, metadata }
|
||||
})
|
||||
asyncMap(statusToEntry),
|
||||
asyncFilter(entry => entry.type !== 'unsupported')
|
||||
)
|
||||
|
||||
function fetchYoutubeMetadata(id) {
|
||||
return fetch(`https://noembed.com/embed?url=https://www.youtube.com/watch?v=${id}`)
|
||||
.then(response => response.json())
|
||||
|
||||
}
|
||||
|
||||
export function isSupportedUrl(urlAsString) {
|
||||
const url = new URL(urlAsString)
|
||||
|
||||
const hosts = [
|
||||
'youtube.com',
|
||||
'music.youtube.com'
|
||||
]
|
||||
|
||||
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)
|
||||
|
@ -72,7 +35,7 @@ export async function fetchTimeline(url) {
|
|||
|
||||
const LINK_RE = /<(.+?)>; rel="(\w+)"/gi
|
||||
|
||||
export function parseLinkHeader(link) {
|
||||
function parseLinkHeader(link) {
|
||||
const links = {}
|
||||
|
||||
for (const [ , url, name ] of link.matchAll(LINK_RE)) {
|
||||
|
@ -80,4 +43,51 @@ export function parseLinkHeader(link) {
|
|||
}
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
async function statusToEntry(status) {
|
||||
const urls = getUrls(status.content)
|
||||
|
||||
for await (const url of urls) {
|
||||
const { type, data } = await urlToEntry(url)
|
||||
|
||||
if (type !== 'unsupported') {
|
||||
return { status, url, type, data }
|
||||
}
|
||||
}
|
||||
|
||||
return { type: 'unsupported' }
|
||||
}
|
||||
|
||||
async function urlToEntry(urlAsString) {
|
||||
const url = new URL(urlAsString)
|
||||
|
||||
console.log(url.hostname)
|
||||
|
||||
if (['youtube.com', 'music.youtube.com'].includes(url.hostname) && url.searchParams.has('v')) {
|
||||
return await mkYoutubeEntry(url.searchParams.get('v'))
|
||||
} else if (url.hostname === 'youtu.be') {
|
||||
return await mkYoutubeEntry(url.pathname.substring(1))
|
||||
} else {
|
||||
return { type: 'unsupported' }
|
||||
}
|
||||
}
|
||||
|
||||
async function mkYoutubeEntry(id) {
|
||||
return {
|
||||
type: 'youtube',
|
||||
data: {
|
||||
id,
|
||||
metadata: await fetchYoutubeMetadata(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fetchYoutubeMetadata(id) {
|
||||
return fetch(`https://noembed.com/embed?url=https://www.youtube.com/watch?v=${id}`)
|
||||
.then(response => response.json())
|
||||
}
|
||||
|
||||
export function intersection(xs, ys) {
|
||||
return xs.filter(x => ys.includes(x))
|
||||
}
|
Loading…
Reference in New Issue