Skip to content

Commit

Permalink
Implement file routes and fix other bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
bcomnes committed Jul 19, 2022
1 parent bffc61b commit c494c0d
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 199 deletions.
2 changes: 2 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default async function App (fastify, opts) {
dir: join(__dirname, 'routes'),
options: Object.assign({}, opts)
})

// setTimeout(() => { console.log(fastify.printRoutes()) }, 500)
}

export const options = {
Expand Down
2 changes: 1 addition & 1 deletion fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ processes = []
FASTIFY_OPTIONS = 'true'
ENV = 'production'
NODE_ENV = 'production'
DOMAIN = 'breadcrum.net'
HOST = 'breadcrum.net'
REGISTRATION = 0

[deploy]
Expand Down
16 changes: 15 additions & 1 deletion lib/run-yt-dlp.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ try {
binPath = path.join(__dirname, '../yt-dlp')
}

export async function getYTDLPUrl ({
url
}) {
const ytDlpWrap = new YTDlpWrap(binPath)
const args = [
url,
'-f',
'best[ext=mp4]/ba[ext=m4a]'
]

const metadata = await ytDlpWrap.getVideoInfo(args)
return metadata
}

export function runYTDLP ({
userId,
bookmarkId,
Expand All @@ -42,7 +56,7 @@ export function runYTDLP ({
const args = [
url,
'-f',
'best[ext=mp4]/ba'
'best[ext=mp4]/ba[ext=m4a]'
]

try {
Expand Down
11 changes: 11 additions & 0 deletions lib/temp-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This is just a temp cache till we
// get redis caching working

import LRU from 'lru-cache'

export const cache = new LRU({
max: 10000,
ttl: 1000 * 60 * 20, // 20 mins,
updateAgeOnGet: false,
ttlAutopurge: true
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"highlight.js": "^11.5.0",
"jsonfeed-to-rss": "^3.0.6",
"local-storage-proxy": "^4.0.3",
"lru-cache": "^7.13.1",
"mine.css": "^7.0.0",
"p-queue": "^7.2.0",
"pg": "^8.6.0",
Expand Down
2 changes: 1 addition & 1 deletion plugins/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const schema = {
type: 'string',
default: 'postgres://postgres@localhost/breadcrum'
},
DOMAIN: {
HOST: {
// Hostname and port (if needed)
type: 'string',
default: 'localhost:3000'
Expand Down
2 changes: 1 addition & 1 deletion plugins/redirector.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default fp(async function (fastify, opts) {
fastify.config.ENV === 'production' &&
(request.headers?.host?.startsWith('www.') || request.headers?.host?.endsWith('.fly.dev'))
) {
return reply.redirect(301, 'https://' + fastify.config.DOMAIN + request.url)
return reply.redirect(301, `${fastify.config.TRANSPORT}://${fastify.config.HOST}${request.url}`)
}
})
}, {
Expand Down
2 changes: 1 addition & 1 deletion routes/api/bookmarks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ export default async function bookmarkRoutes (fastify, opts) {

return {
status: 'ok',
site_url: `https://${fastify.config.domain}/bookmarks/b?id=${bookmark.id}`
site_url: `${fastify.config.TRANSPORT}://${fastify.config.HOST}/bookmarks/b?id=${bookmark.id}`
}
})
}
Expand Down
259 changes: 259 additions & 0 deletions routes/api/feeds/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import SQL from '@nearform/sql'
import jsonfeedToRSS from 'jsonfeed-to-rss'
import cleanDeep from 'clean-deep'
import { getYTDLPUrl } from '../../../lib/run-yt-dlp.js'
import { cache } from '../../../lib/temp-cache.js'

export default async function podcastFeedsRoutes (fastify, opts) {
fastify.get(
'/:feed',
{
preHandler: fastify.auth([fastify.basicAuth]),
schema: {
parms: {
type: 'object',
properties: {
feed: {
type: 'string',
format: 'uuid'
}
},
required: ['feed']
},
querystring: {
type: 'object',
properties: {
format: {
enum: ['json', 'rss']
}
}
}
}
},
async function getFeedHandler (request, reply) {
const { userId, token: userProvidedToken } = request.feedTokenUser
const { feed: feedId } = request.params
const { format } = request.query
const accept = request.accepts()
if (!userId) throw new Error('missing authenticated feed userId')

const episodesQuery = SQL`
select
e.id,
e.created_at,
e.updated_at,
e.url as src_url,
e.type,
e.medium,
e.size_in_bytes,
e.duration_in_seconds,
e.mime_type,
e.explicit,
e.author_name,
e.filename,
e.ext,
e.src_type,
e.ready,
bm.id as bookmark_id,
bm.url as bookmark_url,
bm.title,
bm.note
from episodes e
join bookmarks bm
on bm.id = e.bookmark_id
where e.owner_id = ${userId}
and bm.owner_id = ${userId}
and e.podcast_feed_id = ${feedId}
and e.ready = true
and e.error is null
order by e.created_at desc, bm.title desc, e.filename desc
fetch first 100 rows only;
`

const feedQuery = SQL`
select
pf.id,
pf.created_at,
pf.updated_at,
pf.title,
pf.description,
pf.image_url,
pf.explicit,
pf.token,
u.username as owner_name
from podcast_feeds pf
join users u
on pf.owner_id = u.id
where pf.id = ${feedId}
and pf.owner_id = ${userId}
fetch first row only;
`

const [episodesResults, feedResults] = await Promise.all([
fastify.pg.query(episodesQuery),
fastify.pg.query(feedQuery)
])

const pf = feedResults.rows.pop()
if (!pf) {
return reply.notFound(`podcast feed ${feedId} not found`)
}

const episodes = episodesResults.rows

const fallbackImg = `${fastify.config.TRANSPORT}://${fastify.config.HOST}/static/bread.png`

const jsonfeed = {
version: 'https://jsonfeed.org/version/1',
title: pf.title || `${pf.owner_name}'s breadcrum feed`,
home_page_url: `${fastify.config.TRANSPORT}://${fastify.config.HOST}/feeds?id=${feedId}`, // TODO a page with the feed contents
description: pf.description ?? `This is ${pf.owner_name}'s default podcast feed. Customize this description on the feed's home page.`,
icon: pf.image_url ?? fallbackImg,
favicon: pf.image_url ?? fallbackImg,
feed_url: `${fastify.config.TRANSPORT}://${userId}:${userProvidedToken}@${fastify.config.HOST}/api/feeds/${pf.id}`,
author: {
name: pf.username,
url: `${fastify.config.TRANSPORT}://${fastify.config.HOST}/bookmarks`,
avatar: pf.image_url ?? fallbackImg
},
_itunes: {
explicit: pf.explicit
},
items: episodes.map(ep => {
const redirectUrl = `${fastify.config.TRANSPORT}://${userId}:${userProvidedToken}@${fastify.config.HOST}/api/feeds/${pf.id}/episodes/${ep.id}`

return {
id: ep.id,
url: `${fastify.config.TRANSPORT}://${fastify.config.HOST}/bookmarks/view/?id=${ep.bookmark_id}`,
title: ep.title,
content_text: ep.note,
date_published: ep.created_at,
attachments: cleanDeep([{
url: redirectUrl,
mime_type: `${ep.src_type}/${ep.ext}`,
title: ep.filename,
duration_in_seconds: ep.duration_in_seconds
}])
}
})
}

// TODO: caching

// Querystring overrides accept header
if (format) {
switch (format) {
case 'rss': {
reply.type('application/rss+xml')
const rss = jsonfeedToRSS(jsonfeed, {
itunes: true
})
return rss
}
case 'json':
default: {
return jsonfeed
}
}
}

switch (accept.type(['rss', 'json'])) {
case 'json': {
reply.type('application/json')
return jsonfeed
}
case 'rss':
default: {
reply.type('application/rss+xml')
const rss = jsonfeedToRSS(jsonfeed, {
itunes: true
})
return rss
}
}
}
)

fastify.get(
'/:feed/episodes/:episode',
{
preHandler: fastify.auth([fastify.basicAuth]),
schema: {
parms: {
type: 'object',
properties: {
feed: {
type: 'string',
format: 'uuid'
},
episodes: {
type: 'string',
format: 'uuid'
}
},
required: ['feed', 'episode']
}
}
},
async function episodeHandler (request, reply) {
const { userId, token: userProvidedToken } = request.feedTokenUser
const { feed: feedId, episode: episodeId } = request.params
if (!userId) throw new Error('missing authenticated feed userId')

const cacheKey = ['file', userId, userProvidedToken, feedId, episodeId].join(':')

const cachedUrl = await cache.get(cacheKey)

if (cachedUrl) {
reply.header('fly-cache-status', 'HIT')
return reply.redirect(302, cachedUrl)
} else {
reply.header('fly-cache-status', 'MISS')
}

const episodeQuery = SQL`
select
e.id,
e.created_at,
e.updated_at,
e.url as src_url,
e.type,
e.medium,
e.size_in_bytes,
e.duration_in_seconds,
e.mime_type,
e.explicit,
e.author_name,
e.filename,
e.ext,
e.src_type,
e.ready,
bm.id as bookmark_id,
bm.url as bookmark_url,
bm.title,
bm.note
from episodes e
join bookmarks bm
on bm.id = e.bookmark_id
where e.owner_id = ${userId}
and bm.owner_id = ${userId}
and e.podcast_feed_id = ${feedId}
and e.ready = true
and e.error is null
and e.id = ${episodeId}
fetch first 1 rows only;
`

const results = await fastify.pg.query(episodeQuery)
const episode = results.rows.pop()

if (!episode) {
return reply.notFound(`episide ${episodeId} not found in feed ${feedId}`)
}

const metadata = await getYTDLPUrl({ url: episode.src_url })
await cache.set(cacheKey, metadata.urls, metadata.urls)
reply.redirect(302, metadata.urls)
}
)
}

0 comments on commit c494c0d

Please sign in to comment.