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"
|
||||
}
|
||||
},
|
||||
"route-parser": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/route-parser/-/route-parser-0.0.5.tgz",
|
||||
"integrity": "sha1-fR0J0zXkkJQDHqFpkaSnmwG74fQ="
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
|
|
|
@ -27,13 +27,14 @@
|
|||
"date-fns": "^2.9.0",
|
||||
"get-urls": "^9.2.0",
|
||||
"iter-tools": "^7.0.0-rc.0",
|
||||
"route-parser": "0.0.5",
|
||||
"svelte-routing": "^1.4.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
],
|
||||
"staticFiles": {
|
||||
"staticPath": "public",
|
||||
"staticPath": "static",
|
||||
"watcherGlob": "**"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
<Router>
|
||||
<Route path="/share/:domain/:statusId" component={Share} />
|
||||
<Route path="/" component={Home} />
|
||||
{#each routes as { duplex, component }}
|
||||
<Route path={duplex.spec} component={component} />
|
||||
{/each}
|
||||
</Router>
|
||||
|
||||
<script>
|
||||
import { Router, Route } from 'svelte-routing'
|
||||
import { home, share } from '/routes.js'
|
||||
import Home from '/routes/Home.svelte'
|
||||
import Share from '/routes/Share.svelte'
|
||||
|
||||
const pair = (duplex, component) => ({ duplex, component })
|
||||
|
||||
const routes = [
|
||||
pair(share, Share),
|
||||
pair(home, Home)
|
||||
]
|
||||
</script>
|
|
@ -4,7 +4,7 @@
|
|||
{#if $next}
|
||||
<div class="entry" on:click={() => select($next)}>
|
||||
<div class="title">{$next.title}</div>
|
||||
<div class="user">by {$next.username}</div>
|
||||
<div class="user">by {$next.referer.username}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
|||
{#each history as track}
|
||||
<div class="entry" class:active={track === $current} on:click={() => select(track)}>
|
||||
<div class>{track.title}</div>
|
||||
<div class>shared by {track.username}</div>
|
||||
<div class>shared by {track.referer.username}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -21,26 +21,38 @@
|
|||
|
||||
<script>
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { get } from 'svelte/store'
|
||||
import { derived, get } from 'svelte/store'
|
||||
|
||||
import Header from '/components/layout/Header.svelte'
|
||||
import Footer from '/components/layout/Footer.svelte'
|
||||
import Controls from '/components/Controls.svelte'
|
||||
import Queue from '/components/Queue.svelte'
|
||||
import Viewer from '/components/Viewer.svelte'
|
||||
import { hashtagsIterator } from '/services/mastodon.js'
|
||||
import { tracksIterator } from '/services/misc.js'
|
||||
import { radioIterator, radioShareIterator } from '/services/radio.js'
|
||||
import { fetchStatus } from '/services/mastodon.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 currentUnsubcribe = null
|
||||
|
||||
onMount(async () => {
|
||||
const domainValue = get(domain)
|
||||
const hashtagsValue = get(hashtags)
|
||||
let iterator
|
||||
|
||||
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()
|
||||
|
||||
|
|
|
@ -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>
|
||||
import Radio from '/components/Radio.svelte'
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
<Radio />
|
||||
<Radio share={refererCredentials} />
|
||||
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import Radio from '/components/Radio.svelte'
|
||||
import { fetchStatus } from '/services/mastodon.js'
|
||||
|
||||
export let domain
|
||||
export let statusId
|
||||
export let id
|
||||
|
||||
|
||||
onMount(async () => {
|
||||
const status = await fetchStatus(domain, statusId)
|
||||
console.log(`Status ${statusId} from ${domain} shared`)
|
||||
})
|
||||
const refererCredentials = {
|
||||
type: 'mastodon',
|
||||
domain,
|
||||
id
|
||||
}
|
||||
</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 { 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
|
||||
|
||||
|
@ -13,7 +14,9 @@ function parseLinkHeader(link) {
|
|||
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}>
|
||||
export const hashtagStreamingObservable = (domain, hashtag) => {
|
||||
|
@ -43,6 +46,7 @@ export const hashtagStreamingObservable = (domain, hashtag) => {
|
|||
eventSource.removeEventListener('open', onOpen)
|
||||
eventSource.removeEventListener('update', onStatus)
|
||||
eventSource.removeEventListener('error', onError)
|
||||
eventSource.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -92,14 +96,13 @@ export async function* hashtagsIterator (domain, hashtags) {
|
|||
}
|
||||
|
||||
const processStatus = (domain, status) => ({
|
||||
title: null,
|
||||
username: status.account.username,
|
||||
title: '',
|
||||
date: new Date(status.createdAt),
|
||||
content: status.content,
|
||||
referer: {
|
||||
username: status.account.username,
|
||||
url: status.url,
|
||||
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'
|
||||
|
||||
export const tap = f => x => {
|
||||
|
@ -92,129 +91,82 @@ export async function* raceIterator(iterators) {
|
|||
}
|
||||
}
|
||||
|
||||
const mkMapSet = () => ({ set: new Set(), children: new Map() })
|
||||
|
||||
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()
|
||||
|
||||
export async function* tracksIterator(statusesIterator, cache) {
|
||||
yield* execPipe(
|
||||
statusesIterator,
|
||||
asyncFilter(knownByReferer(known)),
|
||||
asyncMap(processReferer),
|
||||
asyncFilter(knownByMedia(known)),
|
||||
asyncMap(processMedia)
|
||||
asyncFilter(track => track != null), // should not be necessary
|
||||
asyncFilter(notKnown(cache)),
|
||||
asyncMap(completeTrack)
|
||||
)
|
||||
}
|
||||
|
||||
const knownByReferer = known => track => {
|
||||
const notKnown = cache => 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 {
|
||||
console.error(`No track, should not happen here`)
|
||||
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 urls = getUrls(track.content)
|
||||
const completeTrack = async track => {
|
||||
const metadata = await fetchMetadata(track.media)
|
||||
return { ...track, title: metadata.title }
|
||||
}
|
||||
|
||||
const media = execPipe(
|
||||
export const urlsToMedia = urls => {
|
||||
return 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) => {
|
||||
|
|
|
@ -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