refact status processing

This commit is contained in:
wryk 2020-02-15 23:12:53 +01:00
parent 307331855d
commit 49deded9bd
6 changed files with 183 additions and 92 deletions

View File

@ -3,8 +3,8 @@
{#if $next} {#if $next}
<div class="entry" on:click={() => select($next)}> <div class="entry" on:click={() => select($next)}>
<div class="title">{$next.metadata.title}</div> <div class="title">{$next.title}</div>
<div class="user">by {$next.status.account.acct}</div> <div class="user">by {$next.username}</div>
</div> </div>
{/if} {/if}
@ -15,10 +15,10 @@
<h6>HISTORY</h6> <h6>HISTORY</h6>
{#each history as track (track.status.id)} {#each history as track}
<div class="entry" class:active={track === $current} on:click={() => select(track)}> <div class="entry" class:active={track === $current} on:click={() => select(track)}>
<div class>{track.metadata.title}</div> <div class>{track.title}</div>
<div class>shared by {track.status.account.acct}</div> <div class>shared by {track.username}</div>
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -1,5 +1,5 @@
<svelte:head> <svelte:head>
<title>{`${ $current ? `${$current.metadata.title} ` : ''}Eldritch Radio`}</title> <title>{`${ $current ? `${$current.title} ` : ''}Eldritch Radio`}</title>
</svelte:head> </svelte:head>
<div class="app container"> <div class="app container">
@ -28,8 +28,8 @@
import Controls from '/components/Controls.svelte' import Controls from '/components/Controls.svelte'
import Queue from '/components/Queue.svelte' import Queue from '/components/Queue.svelte'
import Viewer from '/components/Viewer.svelte' import Viewer from '/components/Viewer.svelte'
import { hashtagIterator, combinedIterator } from '/services/mastodon.js' import { hashtagsIterator } from '/services/mastodon.js'
import { mkTracksIterator } from '/services/misc.js' import { tracksIterator } from '/services/misc.js'
import { domain, hashtags, queue, next, current, enqueueing, select } from '/store.js' import { domain, hashtags, queue, next, current, enqueueing, select } from '/store.js'
@ -37,13 +37,10 @@
let currentUnsubcribe = null let currentUnsubcribe = null
onMount(async () => { onMount(async () => {
// const iterator = mkTracksIterator(hashtagIterator(get(domain), get(hashtags)[0]))
const domainValue = get(domain) const domainValue = get(domain)
const hashtagsValue = get(hashtags) const hashtagsValue = get(hashtags)
const iterator = combinedIterator( const iterator = tracksIterator(hashtagsIterator(domainValue, hashtagsValue))
hashtagsValue.map(hashtag => mkTracksIterator(hashtagIterator(domainValue, hashtag)))
)
const { value: first } = await iterator.next() const { value: first } = await iterator.next()

View File

@ -1,7 +1,7 @@
<div class="playerBig"> <div class="playerBig">
<div class="playerBig__player"> <div class="playerBig__player">
<YoutubePlayer <YoutubePlayer
id={$current ? $current.data.id : null} id={$current ? $current.media.credentials.id : null}
class="playerBig__iframe" class="playerBig__iframe"
paused={$paused} paused={$paused}
muted={$muted} muted={$muted}
@ -38,7 +38,6 @@
import { secondsToElapsedTime } from '/services/misc.js' import { secondsToElapsedTime } from '/services/misc.js'
import { paused, muted, volume, current, selectNext, loading } from '/store.js' import { paused, muted, volume, current, selectNext, loading } from '/store.js'
let ready = null let ready = null
let ended = null let ended = null
let error = null let error = null

View File

@ -1,10 +1,8 @@
import Observable from 'core-js-pure/features/observable' import Observable from 'core-js-pure/features/observable'
import { observableToAsyncIterator } from '/services/misc.js' import { observableToAsyncIterator, raceIterator } from '/services/misc.js'
const LINK_RE = /<(.+?)>; rel="(\w+)"/gi const LINK_RE = /<(.+?)>; rel="(\w+)"/gi
export const fetchStatus = (domain, id) => fetch(`https://${domain}/api/v1/statuses/${id}`).then(x => x.json())
function parseLinkHeader(link) { function parseLinkHeader(link) {
const links = {} const links = {}
@ -15,6 +13,9 @@ function parseLinkHeader(link) {
return links return links
} }
export const fetchStatus = (domain, id) => fetch(`https://${domain}/api/v1/statuses/${id}`).then(x => x.json())
// Observable<{ domain : string, hashtag : string, status : Status}>
export const hashtagStreamingObservable = (domain, hashtag) => { export const hashtagStreamingObservable = (domain, hashtag) => {
return new Observable(observer => { return new Observable(observer => {
const onOpen = () => { const onOpen = () => {
@ -24,7 +25,7 @@ export const hashtagStreamingObservable = (domain, hashtag) => {
const onStatus = event => { const onStatus = event => {
const status = JSON.parse(event.data) const status = JSON.parse(event.data)
console.log(`Streaming ${domain} #${hashtag} : status ${status.id}`) console.log(`Streaming ${domain} #${hashtag} : status ${status.id}`)
observer.next(status) observer.next(processStatus(domain, status))
} }
const onError = error => { const onError = error => {
@ -60,29 +61,19 @@ export async function* hashtagTimelineIterator (domain, hashtag) {
console.log(`Timeline ${domain} #${hashtag} : fetched ${statuses.length} statuses`) console.log(`Timeline ${domain} #${hashtag} : fetched ${statuses.length} statuses`)
yield* statuses yield* statuses.map(status => processStatus(domain, status))
} }
} }
export async function* hashtagIterator(domain, hashtag) { export const hashtagIterator = (domain, hashtag) => {
const newerIterator = observableToAsyncIterator(hashtagStreamingObservable(domain, hashtag)) return raceIterator([
const olderIterator = hashtagTimelineIterator(domain, hashtag) observableToAsyncIterator(hashtagStreamingObservable(domain, hashtag)),
hashtagTimelineIterator(domain, hashtag)
const iterators = [newerIterator, olderIterator] ])
const values = iterators.map(iterator => iterator.next())
while (true) {
const promises = values.map((promise, index) => promise.then(result => ({ index, result })))
const { index, result: { done, value } } = await Promise.race(promises)
values[index] = iterators[index].next()
console.log(`Resolver ${domain} #${hashtag} : resolved with iterator ${index}`)
yield value
}
} }
export async function* combinedIterator(iterators) { export async function* hashtagsIterator (domain, hashtags) {
const iterators = hashtags.map(hashtag => hashtagIterator(domain, hashtag))
const values = iterators.map(iterator => iterator.next()) const values = iterators.map(iterator => iterator.next())
while (true) { while (true) {
@ -91,14 +82,24 @@ export async function* combinedIterator(iterators) {
const sorted = promisesValues const sorted = promisesValues
.sort((a, b) =>{ .sort((a, b) =>{
new Date(a.result.value.status.created_at) - new Date(b.result.value.status.created_at) a.result.value.date - b.result.value.date
}) })
const { index, result: { done, value } } = sorted[0] const { index, result: { done, value } } = sorted[0]
values[index] = iterators[index].next() values[index] = iterators[index].next()
console.log(`CombinedResolver : resolved with iterator ${index}`)
yield value yield value
} }
} }
const processStatus = (domain, status) => ({
title: null,
username: status.account.username,
date: new Date(status.createdAt),
content: status.content,
referer: {
url: status.url,
credentials: { type: 'mastodon', domain, id: status.id }
},
media: null
})

View File

@ -1,5 +1,5 @@
import getUrls from 'get-urls' import getUrls from 'get-urls'
import { execPipe, asyncFilter, asyncMap } from 'iter-tools' import { execPipe, asyncFilter, asyncMap, map, findOr } from 'iter-tools'
export const tap = f => x => { export const tap = f => x => {
f(x) f(x)
@ -80,65 +80,159 @@ export const secondsToElapsedTime = (seconds) => {
.join(':') .join(':')
} }
export async function* mkTracksIterator(statusesIterator) { export async function* raceIterator(iterators) {
const knownStatus = new Set() const values = iterators.map(iterator => iterator.next())
const knownYoutube = new Set()
const tracks = execPipe( while (true) {
statusesIterator, const promises = values.map((promise, index) => promise.then(result => ({ index, result })))
asyncFilter(status => { const { index, result: { done, value } } = await Promise.race(promises)
if (!status) {
console.error(`No status, should not happen here`)
return false
} else {
if (knownStatus.has(status.id)) {
console.log(`Drop already processed status ${status.id}`)
return false
} else {
knownStatus.add(status.id)
return true
}
}
}),
asyncMap(status => ({ status, data: mkData(status) })),
asyncFilter(({ status, data }) => {
if (!data) {
console.log(`Drop non processable status ${status.id}`)
return false
} else {
if (knownYoutube.has(data.id)) {
console.log(`Drop already processed youtube ${data.id}`)
return false
} else {
knownYoutube.add(data.id)
return true
}
}
}),
asyncMap(async ({ status, data }) => ({ status, data, metadata: await mkMetadata(data) }))
)
yield* tracks values[index] = iterators[index].next()
yield value
}
} }
function mkData(status) const mkMapSet = () => ({ set: new Set(), children: new Map() })
{
const urls = getUrls(status.content)
for (const urlAsString of urls) { const pathSet = () => {
const url = new URL(urlAsString) const root = mkMapSet()
if (['youtube.com', 'm.youtube.com', 'music.youtube.com'].includes(url.hostname) && url.searchParams.has('v')) { const has = (keys, value) => {
return { url: urlAsString, id: url.searchParams.get('v') } let x = root
} else if (url.hostname === 'youtu.be') {
return { url: urlAsString, id: url.pathname.substring(1) } for (const key of keys) {
if (x.children.has(key)) {
x = x.children.get(key)
} else {
return false
}
} }
return x.set.has(value)
} }
return null const add = (keys, value) => {
let x = root
for (const key of keys) {
if (!x.children.has(key)) {
x.children.set(key, mkMapSet())
}
x = x.children.get(key)
}
x.set.add(value)
}
return { root, has, add }
} }
async function mkMetadata(entry) { export async function* tracksIterator(statusesIterator) {
return fetch(`https://noembed.com/embed?url=https://www.youtube.com/watch?v=${entry.id}`) const known = pathSet()
.then(response => response.json())
yield* execPipe(
statusesIterator,
asyncFilter(knownByReferer(known)),
asyncMap(processReferer),
asyncFilter(knownByMedia(known)),
asyncMap(processMedia)
)
}
const knownByReferer = known => track => {
if (!track) {
console.error(`No status, should not happen here`)
return false
} else {
switch (track.referer.credentials.type) {
default:
throw new Error()
case 'mastodon':
const path = [
'referer',
'mastodon',
track.referer.credentials.domain
]
const id = track.referer.credentials.id
if (known.has(path, id)) {
console.log(`Drop already processed referer ${id}`)
return false
} else {
known.add(path, id)
return true
}
}
}
}
const knownByMedia = known => track => {
if (track !== null) {
switch (track.media.credentials.type) {
default:
throw new Error()
case 'youtube':
const path = [
'media',
'youtube'
]
const id = track.media.credentials.id
if (known.has(path, id)) {
console.log(`Drop already processed media ${id}`)
return false
} else {
known.add(path, id)
return true
}
}
} else {
return false
}
}
const processReferer = track => {
const urls = getUrls(track.content)
const media = execPipe(
urls,
map(parseSource),
findOr(null, x => x !== null)
)
if (media) {
return { ...track, media }
} else {
return null
}
}
const processMedia = async track => {
const metadata = await fetchMetadata(track.media)
return { ...track, title: metadata.title }
}
const parseSource = (url) => {
const { hostname, pathname, searchParams } = new URL(url)
if (['youtube.com', 'm.youtube.com', 'music.youtube.com'].includes(hostname) && searchParams.has('v')) {
return { url, credentials: { type: 'youtube', id: searchParams.get('v') } }
} else if (hostname === 'youtu.be') {
return { url, credentials: { type: 'youtube', id: pathname.substring(1) } }
} else {
return null
}
}
const fetchMetadata = (media) => {
switch (media.credentials.type) {
case 'youtube':
return fetch(`https://noembed.com/embed?url=https://www.youtube.com/watch?v=${media.credentials.id}`)
.then(response => response.json())
}
} }

View File

@ -31,7 +31,7 @@ export const canNext = derived([queue, index], ([$queue, $index]) => $index !==
export const select = track => { export const select = track => {
console.log(`Select ${track.metadata.title}`) console.log(`Select ${track.title}`)
current.set(track) current.set(track)
} }