cleanup schema

This commit is contained in:
wryk 2020-02-22 03:39:15 +01:00
parent c43b455505
commit f60bc914c1
9 changed files with 129 additions and 173 deletions

6
package-lock.json generated
View File

@ -8886,6 +8886,12 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true "dev": true
}, },
"typescript": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.2.tgz",
"integrity": "sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==",
"dev": true
},
"typescript-compare": { "typescript-compare": {
"version": "0.0.2", "version": "0.0.2",
"resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",

View File

@ -20,7 +20,8 @@
"parcel-plugin-svelte": "^4.0.5", "parcel-plugin-svelte": "^4.0.5",
"postcss-input-range": "^4.0.0", "postcss-input-range": "^4.0.0",
"sass": "^1.25.0", "sass": "^1.25.0",
"svelte": "^3.18.2" "svelte": "^3.18.2",
"typescript": "^3.8.2"
}, },
"dependencies": { "dependencies": {
"core-js-pure": "^3.6.4", "core-js-pure": "^3.6.4",

View File

@ -4,12 +4,12 @@
<div class="track" on:click={() => select($next)}> <div class="track" on:click={() => select($next)}>
<div class="track__main"> <div class="track__main">
<div class="track__title" class:placeholder={!$next}> <div class="track__title" class:placeholder={!$next}>
{#if $next}{$next.title}{/if} {#if $next}{$next.media.title}{/if}
</div> </div>
<div class="track__subtitle" class:placeholder={!$next}> <div class="track__subtitle" class:placeholder={!$next}>
{#if $next} {#if $next}
shared by {$next.referer.username} shared by {$next.referer.username}
<DistanceDate date={$next.date} /> <DistanceDate date={$next.referer.date} />
{/if} {/if}
</div> </div>
</div> </div>
@ -25,10 +25,10 @@
{#each history as track} {#each history as track}
<div class="track" class:track--active={track === $current} class:track--playing={!$paused} on:click={() => select(track)}> <div class="track" class:track--active={track === $current} class:track--playing={!$paused} on:click={() => select(track)}>
<div class="track__main"> <div class="track__main">
<div class="track__title">{track.title}</div> <div class="track__title">{track.media.title}</div>
<div class="track__subtitle"> <div class="track__subtitle">
shared by {track.referer.username} shared by {track.referer.username}
<DistanceDate date={track.date} /> <DistanceDate date={track.referer.date} />
</div> </div>
</div> </div>
<button class="track__menu" aria-label="track menu"><IconMenu></IconMenu></button> <button class="track__menu" aria-label="track menu"><IconMenu></IconMenu></button>

View File

@ -1,5 +1,5 @@
<svelte:head> <svelte:head>
<title>{`${ $current ? `${$current.title} ` : ''}Eldritch Radio`}</title> <title>{`${ $current ? `${$current.media.title} ` : ''}Eldritch Radio`}</title>
</svelte:head> </svelte:head>
<div class="app container"> <div class="app container">
@ -69,7 +69,7 @@
return $iterator.next().then(({ done, value }) => { return $iterator.next().then(({ done, value }) => {
enqueueing.set(false) enqueueing.set(false)
return value return value
}) }).catch(console.error)
} else { } else {
return $nextPromise return $nextPromise
} }
@ -94,7 +94,7 @@
const select = track => { const select = track => {
console.log(`Select ${track.title}`) console.log(`Select ${track.media.title}`)
current.set(track) current.set(track)
} }

View File

@ -28,7 +28,7 @@
<div class="playerTrack"> <div class="playerTrack">
<div class="playerTrack__infos"> <div class="playerTrack__infos">
<div class="playerTrack__name" class:placeholder={!$current}>{#if $current}{$current.title}{/if}</div> <div class="playerTrack__name" class:placeholder={!$current}>{#if $current}{$current.media.title}{/if}</div>
<div class="playerTrack__referer" class:placeholder={!$current}> <div class="playerTrack__referer" class:placeholder={!$current}>
{#if $current}shared by <span class="playerTrack__username">{$current.referer.username}</span>{/if} {#if $current}shared by <span class="playerTrack__username">{$current.referer.username}</span>{/if}
</div> </div>

View File

@ -4,11 +4,11 @@ import { urlsToMedia } from '/services/misc.js'
const LINK_RE = /<(.+?)>; rel="(\w+)"/gi const LINK_RE = /<(.+?)>; rel="(\w+)"/gi
function parseLinkHeader(link) { function parseLinkHeader(linkHeader) {
const links = {} const links = new Map()
for (const [ , url, name ] of link.matchAll(LINK_RE)) { for (const [ , url, name ] of linkHeader.matchAll(LINK_RE)) {
links[name] = url links.set(name, url)
} }
return links return links
@ -74,7 +74,7 @@ export async function* hashtagTimelineIterator (domain, hashtag) {
const response = await fetch(nextLink) const response = await fetch(nextLink)
nextLink = response.headers.has('link') nextLink = response.headers.has('link')
? parseLinkHeader(response.headers.get('link')).next ? parseLinkHeader(response.headers.get('link')).get('next')
: null : null
const statuses = await response.json() const statuses = await response.json()
@ -132,15 +132,11 @@ export async function* hashtagsIterator(domain, hashtags) {
} }
} }
const processStatus = (domain, status) => ({ const processStatus = (domain, status) => ({
title: '', username: status.account.username,
content: status.content,
date: new Date(status.created_at), date: new Date(status.created_at),
referer: { url: status.url,
username: status.account.username, credentials: { type: 'mastodon', domain, id: status.id }
url: status.url,
credentials: { type: 'mastodon', domain, id: status.id }
},
media: urlsToMedia(getUrls(status.content))
}) })

View File

@ -1,21 +1,12 @@
import { execPipe, asyncFilter, asyncMap, map, findOr } from 'iter-tools' import getUrls from 'get-urls'
import { execPipe, asyncFilter, asyncMap, map, take, filter, asyncFlatMap, toArray } from 'iter-tools'
import { share } from '/routes.js'
export const tap = f => x => { export const tap = f => x => {
f(x) f(x)
return x return x
} }
export const queue = () => {
const deferred = defer()
let promise = deferred.promise
const enqueue = f => {
promise = promise.then(tap(f))
}
return { enqueue, run: deferred.resolve }
}
export const defer = () => { export const defer = () => {
let resolve let resolve
let reject let reject
@ -28,42 +19,15 @@ export const defer = () => {
return { resolve, reject, promise } return { resolve, reject, promise }
} }
export async function* observableToAsyncIterator(observable) { export const queue = () => {
const buffer = [defer()] const deferred = defer()
let promise = deferred.promise
const next = value => { const enqueue = f => {
buffer[buffer.length - 1].resolve(value) promise = promise.then(tap(f))
buffer.push(defer())
} }
const complete = value => { return { enqueue, run: deferred.resolve }
buffer[buffer.length - 1].resolve(value)
}
const error = (error) => {
buffer[buffer.length - 1].reject(error)
}
const subscription = observable.subscribe({ next, error, complete })
try {
while (true) {
const value = await buffer[0].promise
buffer.shift()
if (buffer.length) {
yield value
} else {
return value
}
}
} finally {
subscription.unsubscribe()
}
}
export function intersection(xs, ys) {
return xs.filter(x => ys.includes(x))
} }
export const secondsToElapsedTime = (seconds) => { export const secondsToElapsedTime = (seconds) => {
@ -79,120 +43,64 @@ export const secondsToElapsedTime = (seconds) => {
.join(':') .join(':')
} }
export async function* raceIterator(iterators) { export async function* tracksIterator(refererGenerator, cache) {
const values = iterators.map(iterator => iterator.next()) const notKnow = (values) => {
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()
yield value
}
}
export async function* tracksIterator(statusesGenerator, cache) {
try {
yield* execPipe(
statusesGenerator,
asyncFilter(track => track != null), // should not be necessary
asyncFilter(notKnown(cache)),
asyncMap(completeTrack)
)
} finally {
statusesGenerator.return()
}
}
const notKnown = cache => track => {
if (!track) {
console.error(`No track, should not happen here`)
return false
}
const isKnown = (values) => {
if (cache.has(values)) { if (cache.has(values)) {
console.log(`Drop already processed ${values.join(':')}`) console.log(`Drop already processed ${values.join(':')}`)
return true return false
} else { } else {
cache.add(values) cache.add(values)
return false return true
} }
} }
switch (track.referer.credentials.type) { try {
default: yield* execPipe(
throw new Error() refererGenerator,
asyncFilter(({ credentials: { domain, id } }) => notKnow(['referer', 'mastodon', domain, id])),
asyncFlatMap(referer => {
return execPipe(
referer.content,
getUrls,
map(url => {
const { hostname, pathname, searchParams } = new URL(url)
case 'mastodon': if (['youtube.com', 'm.youtube.com', 'music.youtube.com'].includes(hostname) && searchParams.has('v')) {
if (isKnown([ return { url, credentials: { type: 'youtube', id: searchParams.get('v') } }
'referer', } else if (hostname === 'youtu.be') {
'mastodon', return { url, credentials: { type: 'youtube', id: pathname.substring(1) } }
track.referer.credentials.domain, } else {
track.referer.credentials.id return null
])) { }
return false }),
} filter(media => media !== null),
map(({ url, credentials }) => ({ referer, mediaUrl: url, mediaCredentials: credentials })),
take(1),
toArray
)
}),
asyncFilter(({ mediaCredentials: { id }}) => notKnow(['media', 'youtube', id])),
asyncMap(async ({ referer, mediaUrl, mediaCredentials }) => {
const metadata = await fetchMetadata(mediaCredentials)
break; return {
} shareUrl: `${location.origin}${share.reverse({ domain: referer.credentials.domain, id: referer.credentials.id })}`,
referer,
if (track.media == null) { media: {
return false title: metadata.title,
} cover: metadata.thumbnail_url,
url: mediaUrl,
switch (track.media.credentials.type) { credentials: mediaCredentials
default: }
throw new Error() }
})
case 'youtube': )
if (isKnown([ } finally {
'media', refererGenerator.return()
'youtube',
track.media.credentials.id
])) {
return false
}
break
}
return true
}
const completeTrack = async track => {
const metadata = await fetchMetadata(track.media)
return {
...track,
title: metadata.title,
cover: metadata.thumbnail_url
} }
} }
export const urlsToMedia = urls => { const fetchMetadata = (credentials) => {
return execPipe( return fetch(`https://noembed.com/embed?url=https://www.youtube.com/watch?v=${credentials.id}`)
urls, .then(response => response.json())
map(parseSource),
findOr(null, x => x !== null)
)
}
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())
}
} }

32
src/types.ts Normal file
View File

@ -0,0 +1,32 @@
export type Track = {
referer: Referer
media: Media
}
export type Referer = {
username: string
content: string
date: Date
credentials: RefererCredentials
}
export type RefererCredentials = Mastodon
export type Mastodon = {
type: 'mastodon'
domain: string
id: string
}
export type Media = {
title: string
cover: string
credentials: MediaCredentials
}
export type MediaCredentials = Youtube
export type Youtube = {
type: 'youtube'
id: string
}

13
tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"strict": true,
"lib": [
"dom",
"esnext"
],
"downlevelIteration": true
},
"include": [
"src/**/*"
]
}