better router, add sharing support

This commit is contained in:
wryk 2020-02-16 17:02:39 +01:00
parent 9fc0c52973
commit 3c2146127a
12 changed files with 170 additions and 136 deletions

5
package-lock.json generated
View File

@ -8047,6 +8047,11 @@
"inherits": "^2.0.1" "inherits": "^2.0.1"
} }
}, },
"route-parser": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/route-parser/-/route-parser-0.0.5.tgz",
"integrity": "sha1-fR0J0zXkkJQDHqFpkaSnmwG74fQ="
},
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",

View File

@ -27,13 +27,14 @@
"date-fns": "^2.9.0", "date-fns": "^2.9.0",
"get-urls": "^9.2.0", "get-urls": "^9.2.0",
"iter-tools": "^7.0.0-rc.0", "iter-tools": "^7.0.0-rc.0",
"route-parser": "0.0.5",
"svelte-routing": "^1.4.0" "svelte-routing": "^1.4.0"
}, },
"browserslist": [ "browserslist": [
"last 1 chrome versions" "last 1 chrome versions"
], ],
"staticFiles": { "staticFiles": {
"staticPath": "public", "staticPath": "static",
"watcherGlob": "**" "watcherGlob": "**"
} }
} }

View File

@ -1,10 +1,19 @@
<Router> <Router>
<Route path="/share/:domain/:statusId" component={Share} /> {#each routes as { duplex, component }}
<Route path="/" component={Home} /> <Route path={duplex.spec} component={component} />
{/each}
</Router> </Router>
<script> <script>
import { Router, Route } from 'svelte-routing' import { Router, Route } from 'svelte-routing'
import { home, share } from '/routes.js'
import Home from '/routes/Home.svelte' import Home from '/routes/Home.svelte'
import Share from '/routes/Share.svelte' import Share from '/routes/Share.svelte'
const pair = (duplex, component) => ({ duplex, component })
const routes = [
pair(share, Share),
pair(home, Home)
]
</script> </script>

View File

@ -4,7 +4,7 @@
{#if $next} {#if $next}
<div class="entry" on:click={() => select($next)}> <div class="entry" on:click={() => select($next)}>
<div class="title">{$next.title}</div> <div class="title">{$next.title}</div>
<div class="user">by {$next.username}</div> <div class="user">by {$next.referer.username}</div>
</div> </div>
{/if} {/if}
@ -18,7 +18,7 @@
{#each history as track} {#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.title}</div> <div class>{track.title}</div>
<div class>shared by {track.username}</div> <div class>shared by {track.referer.username}</div>
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -21,26 +21,38 @@
<script> <script>
import { onMount, onDestroy } from 'svelte' import { onMount, onDestroy } from 'svelte'
import { get } from 'svelte/store' import { derived, get } from 'svelte/store'
import Header from '/components/layout/Header.svelte' import Header from '/components/layout/Header.svelte'
import Footer from '/components/layout/Footer.svelte' import Footer from '/components/layout/Footer.svelte'
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 { hashtagsIterator } from '/services/mastodon.js' import { radioIterator, radioShareIterator } from '/services/radio.js'
import { tracksIterator } from '/services/misc.js' import { fetchStatus } from '/services/mastodon.js'
import { domain, hashtags, queue, next, current, enqueueing, select } from '/store.js' import { domain, hashtags, queue, next, current, enqueueing, select } from '/store.js'
import DeepSet from '/services/deep-set.js'
export let share
const cache = new DeepSet()
let nextUnsubcribe = null let nextUnsubcribe = null
let currentUnsubcribe = null let currentUnsubcribe = null
onMount(async () => { onMount(async () => {
const domainValue = get(domain) let iterator
const hashtagsValue = get(hashtags)
const iterator = tracksIterator(hashtagsIterator(domainValue, hashtagsValue)) if (share != null) {
const track = await fetchStatus(share.domain, share.id)
iterator = radioShareIterator(track, get(domain), get(hashtags), cache)
} else {
iterator = radioIterator(get(domain), get(hashtags), cache)
}
// generated multiples times cannot usable and don't free resources
// const iterator = derived([domain, hashtags], ([$domain, $hashtags]) => radioIterator($domain, $hashtags))
const { value: first } = await iterator.next() const { value: first } = await iterator.next()

4
src/routes.js Normal file
View File

@ -0,0 +1,4 @@
import Route from 'route-parser'
export const home = new Route('/')
export const share = new Route('/share/:domain/:id')

View File

@ -1,4 +1,4 @@
<Radio /> <Radio share={null} />
<script> <script>
import Radio from '/components/Radio.svelte' import Radio from '/components/Radio.svelte'

View File

@ -1,16 +1,14 @@
<Radio /> <Radio share={refererCredentials} />
<script> <script>
import { onMount } from 'svelte'
import Radio from '/components/Radio.svelte' import Radio from '/components/Radio.svelte'
import { fetchStatus } from '/services/mastodon.js'
export let domain export let domain
export let statusId export let id
const refererCredentials = {
onMount(async () => { type: 'mastodon',
const status = await fetchStatus(domain, statusId) domain,
console.log(`Status ${statusId} from ${domain} shared`) id
}) }
</script> </script>

41
src/services/deep-set.js Normal file
View File

@ -0,0 +1,41 @@
export default class DeepSet {
constructor() {
this.map = new Map()
this.set = new Set()
}
_reduce(path) {
return path.reduce((context, key) => {
if (context.map.has(key)) {
return context.map.get(key)
} else {
const newContext = new DeepSet()
context.map.set(key, newContext)
return newContext
}
}, this).set
}
has(values) {
const { keys, value } = destruct(values)
return this._reduce(keys).has(value)
}
add(values) {
const { keys, value } = destruct(values)
return this._reduce(keys).add(value)
}
}
const destruct = xs => {
switch (xs.length) {
case 0:
return { keys: [], value: undefined }
case 1:
return { keys: [], value: xs[0] }
default:
return { keys: xs.slice(0, xs.length - 1), value: xs.slice(-1)[0] }
}
}

View File

@ -1,5 +1,6 @@
import Observable from 'core-js-pure/features/observable' import Observable from 'core-js-pure/features/observable'
import { observableToAsyncIterator, raceIterator } from '/services/misc.js' import getUrls from 'get-urls'
import { observableToAsyncIterator, raceIterator, urlsToMedia } from '/services/misc.js'
const LINK_RE = /<(.+?)>; rel="(\w+)"/gi const LINK_RE = /<(.+?)>; rel="(\w+)"/gi
@ -13,7 +14,9 @@ function parseLinkHeader(link) {
return links return links
} }
export const fetchStatus = (domain, id) => fetch(`https://${domain}/api/v1/statuses/${id}`).then(x => x.json()) export const fetchStatus = (domain, id) => fetch(`https://${domain}/api/v1/statuses/${id}`)
.then(response => response.json())
.then(status => processStatus(domain, status))
// Observable<{ domain : string, hashtag : string, status : Status}> // Observable<{ domain : string, hashtag : string, status : Status}>
export const hashtagStreamingObservable = (domain, hashtag) => { export const hashtagStreamingObservable = (domain, hashtag) => {
@ -43,6 +46,7 @@ export const hashtagStreamingObservable = (domain, hashtag) => {
eventSource.removeEventListener('open', onOpen) eventSource.removeEventListener('open', onOpen)
eventSource.removeEventListener('update', onStatus) eventSource.removeEventListener('update', onStatus)
eventSource.removeEventListener('error', onError) eventSource.removeEventListener('error', onError)
eventSource.close()
} }
}) })
} }
@ -92,14 +96,13 @@ export async function* hashtagsIterator (domain, hashtags) {
} }
const processStatus = (domain, status) => ({ const processStatus = (domain, status) => ({
title: null, title: '',
username: status.account.username,
date: new Date(status.createdAt), date: new Date(status.createdAt),
content: status.content,
referer: { referer: {
username: status.account.username,
url: status.url, url: status.url,
credentials: { type: 'mastodon', domain, id: status.id } credentials: { type: 'mastodon', domain, id: status.id }
}, },
media: null media: urlsToMedia(getUrls(status.content))
}) })

View File

@ -1,4 +1,3 @@
import getUrls from 'get-urls'
import { execPipe, asyncFilter, asyncMap, map, findOr } from 'iter-tools' import { execPipe, asyncFilter, asyncMap, map, findOr } from 'iter-tools'
export const tap = f => x => { export const tap = f => x => {
@ -92,129 +91,82 @@ export async function* raceIterator(iterators) {
} }
} }
const mkMapSet = () => ({ set: new Set(), children: new Map() }) export async function* tracksIterator(statusesIterator, cache) {
const pathSet = () => {
const root = mkMapSet()
const has = (keys, value) => {
let x = root
for (const key of keys) {
if (x.children.has(key)) {
x = x.children.get(key)
} else {
return false
}
}
return x.set.has(value)
}
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 }
}
export async function* tracksIterator(statusesIterator) {
const known = pathSet()
yield* execPipe( yield* execPipe(
statusesIterator, statusesIterator,
asyncFilter(knownByReferer(known)), asyncFilter(track => track != null), // should not be necessary
asyncMap(processReferer), asyncFilter(notKnown(cache)),
asyncFilter(knownByMedia(known)), asyncMap(completeTrack)
asyncMap(processMedia)
) )
} }
const knownByReferer = known => track => { const notKnown = cache => track => {
if (!track) { if (!track) {
console.error(`No status, should not happen here`) console.error(`No track, 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 return false
} }
const isKnown = (values) => {
if (cache.has(values)) {
console.log(`Drop already processed ${values.join(':')}`)
return true
} else {
cache.add(values)
return false
}
}
switch (track.referer.credentials.type) {
default:
throw new Error()
case 'mastodon':
if (isKnown([
'referer',
'mastodon',
track.referer.credentials.domain,
track.referer.credentials.id
])) {
return false
}
break;
}
if (track.media == null) {
return false
}
switch (track.media.credentials.type) {
default:
throw new Error()
case 'youtube':
if (isKnown([
'media',
'youtube',
track.media.credentials.id
])) {
return false
}
break
}
return true
} }
const processReferer = track => { const completeTrack = async track => {
const urls = getUrls(track.content) const metadata = await fetchMetadata(track.media)
return { ...track, title: metadata.title }
}
const media = execPipe( export const urlsToMedia = urls => {
return execPipe(
urls, urls,
map(parseSource), map(parseSource),
findOr(null, x => x !== null) 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 parseSource = (url) => {

9
src/services/radio.js Normal file
View File

@ -0,0 +1,9 @@
import { asyncPrepend } from 'iter-tools'
import { hashtagsIterator } from '/services/mastodon.js'
import { tracksIterator } from '/services/misc.js'
export const radioIterator = (domain, hashtags, cache) =>
tracksIterator(hashtagsIterator(domain, hashtags), cache)
export const radioShareIterator = (track, domain, hashtags, cache) =>
tracksIterator(asyncPrepend(track, hashtagsIterator(domain, hashtags)), cache)