forked from Mastodon/mastoradio-la-radio-di-mastodon
better router, add sharing support
This commit is contained in:
parent
9fc0c52973
commit
3c2146127a
|
@ -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",
|
||||||
|
|
|
@ -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": "**"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
import Route from 'route-parser'
|
||||||
|
|
||||||
|
export const home = new Route('/')
|
||||||
|
export const share = new Route('/share/:domain/:id')
|
|
@ -1,4 +1,4 @@
|
||||||
<Radio />
|
<Radio share={null} />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Radio from '/components/Radio.svelte'
|
import Radio from '/components/Radio.svelte'
|
||||||
|
|
|
@ -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>
|
|
@ -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] }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const isKnown = (values) => {
|
||||||
|
if (cache.has(values)) {
|
||||||
|
console.log(`Drop already processed ${values.join(':')}`)
|
||||||
|
return true
|
||||||
} else {
|
} else {
|
||||||
|
cache.add(values)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (track.referer.credentials.type) {
|
switch (track.referer.credentials.type) {
|
||||||
default:
|
default:
|
||||||
throw new Error()
|
throw new Error()
|
||||||
|
|
||||||
case 'mastodon':
|
case 'mastodon':
|
||||||
const path = [
|
if (isKnown([
|
||||||
'referer',
|
'referer',
|
||||||
'mastodon',
|
'mastodon',
|
||||||
track.referer.credentials.domain
|
track.referer.credentials.domain,
|
||||||
]
|
track.referer.credentials.id
|
||||||
|
])) {
|
||||||
const id = track.referer.credentials.id
|
return false
|
||||||
|
}
|
||||||
if (known.has(path, id)) {
|
|
||||||
console.log(`Drop already processed referer ${id}`)
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track.media == null) {
|
||||||
return false
|
return false
|
||||||
} else {
|
|
||||||
known.add(path, id)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const knownByMedia = known => track => {
|
|
||||||
if (track !== null) {
|
|
||||||
switch (track.media.credentials.type) {
|
switch (track.media.credentials.type) {
|
||||||
default:
|
default:
|
||||||
throw new Error()
|
throw new Error()
|
||||||
|
|
||||||
case 'youtube':
|
case 'youtube':
|
||||||
const path = [
|
if (isKnown([
|
||||||
'media',
|
'media',
|
||||||
'youtube'
|
'youtube',
|
||||||
]
|
track.media.credentials.id
|
||||||
|
])) {
|
||||||
const id = track.media.credentials.id
|
|
||||||
|
|
||||||
if (known.has(path, id)) {
|
|
||||||
console.log(`Drop already processed media ${id}`)
|
|
||||||
return false
|
return false
|
||||||
} else {
|
}
|
||||||
known.add(path, id)
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
const completeTrack = async track => {
|
||||||
return false
|
const metadata = await fetchMetadata(track.media)
|
||||||
}
|
return { ...track, title: metadata.title }
|
||||||
}
|
}
|
||||||
|
|
||||||
const processReferer = track => {
|
export const urlsToMedia = urls => {
|
||||||
const urls = getUrls(track.content)
|
return execPipe(
|
||||||
|
|
||||||
const media = 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) => {
|
||||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue