cleanup schema
This commit is contained in:
parent
c43b455505
commit
f60bc914c1
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"downlevelIteration": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue