diff --git a/package-lock.json b/package-lock.json index 7455361..2f167c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8886,6 +8886,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "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": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", diff --git a/package.json b/package.json index a285328..015a7a5 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "parcel-plugin-svelte": "^4.0.5", "postcss-input-range": "^4.0.0", "sass": "^1.25.0", - "svelte": "^3.18.2" + "svelte": "^3.18.2", + "typescript": "^3.8.2" }, "dependencies": { "core-js-pure": "^3.6.4", diff --git a/src/components/Queue.svelte b/src/components/Queue.svelte index a702680..6cebbbb 100644 --- a/src/components/Queue.svelte +++ b/src/components/Queue.svelte @@ -4,12 +4,12 @@
select($next)}>
- {#if $next}{$next.title}{/if} + {#if $next}{$next.media.title}{/if}
{#if $next} - shared by {$next.referer.username} • - + shared by {$next.referer.username} • + {/if}
@@ -25,10 +25,10 @@ {#each history as track}
select(track)}>
-
{track.title}
+
{track.media.title}
- shared by {track.referer.username} • - + shared by {track.referer.username} • +
diff --git a/src/components/Radio.svelte b/src/components/Radio.svelte index 63fe54e..dd22f51 100644 --- a/src/components/Radio.svelte +++ b/src/components/Radio.svelte @@ -1,5 +1,5 @@ - {`${ $current ? `${$current.title} ∴ ` : ''}Eldritch Radio`} + {`${ $current ? `${$current.media.title} ∴ ` : ''}Eldritch Radio`}
@@ -69,7 +69,7 @@ return $iterator.next().then(({ done, value }) => { enqueueing.set(false) return value - }) + }).catch(console.error) } else { return $nextPromise } @@ -94,7 +94,7 @@ const select = track => { - console.log(`Select ${track.title}`) + console.log(`Select ${track.media.title}`) current.set(track) } diff --git a/src/components/Viewer.svelte b/src/components/Viewer.svelte index d10a4a7..54f2549 100644 --- a/src/components/Viewer.svelte +++ b/src/components/Viewer.svelte @@ -28,7 +28,7 @@
-
{#if $current}{$current.title}{/if}
+
{#if $current}{$current.media.title}{/if}
{#if $current}shared by {$current.referer.username}{/if}
diff --git a/src/services/mastodon.js b/src/services/mastodon.js index 4ca5657..247a1d4 100644 --- a/src/services/mastodon.js +++ b/src/services/mastodon.js @@ -4,11 +4,11 @@ import { urlsToMedia } from '/services/misc.js' const LINK_RE = /<(.+?)>; rel="(\w+)"/gi -function parseLinkHeader(link) { - const links = {} +function parseLinkHeader(linkHeader) { + const links = new Map() - for (const [ , url, name ] of link.matchAll(LINK_RE)) { - links[name] = url + for (const [ , url, name ] of linkHeader.matchAll(LINK_RE)) { + links.set(name, url) } return links @@ -74,7 +74,7 @@ export async function* hashtagTimelineIterator (domain, hashtag) { const response = await fetch(nextLink) nextLink = response.headers.has('link') - ? parseLinkHeader(response.headers.get('link')).next + ? parseLinkHeader(response.headers.get('link')).get('next') : null const statuses = await response.json() @@ -132,15 +132,11 @@ export async function* hashtagsIterator(domain, hashtags) { } } - const processStatus = (domain, status) => ({ - title: '', + username: status.account.username, + content: status.content, date: new Date(status.created_at), - referer: { - username: status.account.username, - url: status.url, - credentials: { type: 'mastodon', domain, id: status.id } - }, - media: urlsToMedia(getUrls(status.content)) + url: status.url, + credentials: { type: 'mastodon', domain, id: status.id } }) diff --git a/src/services/misc.js b/src/services/misc.js index db7e2e3..ca87ba0 100644 --- a/src/services/misc.js +++ b/src/services/misc.js @@ -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 => { f(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 = () => { let resolve let reject @@ -28,42 +19,15 @@ export const defer = () => { return { resolve, reject, promise } } -export async function* observableToAsyncIterator(observable) { - const buffer = [defer()] +export const queue = () => { + const deferred = defer() + let promise = deferred.promise - const next = value => { - buffer[buffer.length - 1].resolve(value) - buffer.push(defer()) + const enqueue = f => { + promise = promise.then(tap(f)) } - const complete = value => { - 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)) + return { enqueue, run: deferred.resolve } } export const secondsToElapsedTime = (seconds) => { @@ -79,120 +43,64 @@ export const secondsToElapsedTime = (seconds) => { .join(':') } -export async function* raceIterator(iterators) { - const values = iterators.map(iterator => iterator.next()) - - 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) => { +export async function* tracksIterator(refererGenerator, cache) { + const notKnow = (values) => { if (cache.has(values)) { console.log(`Drop already processed ${values.join(':')}`) - return true + return false } else { cache.add(values) - return false + return true } } - switch (track.referer.credentials.type) { - default: - throw new Error() + try { + yield* execPipe( + 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 (isKnown([ - 'referer', - 'mastodon', - track.referer.credentials.domain, - track.referer.credentials.id - ])) { - return false - } + 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 + } + }), + 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; - } - - 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 completeTrack = async track => { - const metadata = await fetchMetadata(track.media) - return { - ...track, - title: metadata.title, - cover: metadata.thumbnail_url + return { + shareUrl: `${location.origin}${share.reverse({ domain: referer.credentials.domain, id: referer.credentials.id })}`, + referer, + media: { + title: metadata.title, + cover: metadata.thumbnail_url, + url: mediaUrl, + credentials: mediaCredentials + } + } + }) + ) + } finally { + refererGenerator.return() } } -export const urlsToMedia = urls => { - return execPipe( - urls, - 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()) - } +const fetchMetadata = (credentials) => { + return fetch(`https://noembed.com/embed?url=https://www.youtube.com/watch?v=${credentials.id}`) + .then(response => response.json()) } \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..96cd46c --- /dev/null +++ b/src/types.ts @@ -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 +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bd0f9c0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "lib": [ + "dom", + "esnext" + ], + "downlevelIteration": true + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file