1
0
mirror of https://git.sr.ht/~cadence/bibliogram synced 2024-11-22 16:17:29 +00:00

Add video support (experimental!)

This commit is contained in:
Cadence Fish 2020-01-30 04:20:20 +13:00
parent 95cc416e08
commit a5ab771969
No known key found for this signature in database
GPG Key ID: 81015DF9AA8607E1
7 changed files with 96 additions and 43 deletions

View File

@ -1,5 +1,5 @@
const constants = require("../constants")
const {proxyImage, proxyExtendedOwner} = require("../utils/proxyurl")
const {proxyImage, proxyVideo} = require("../utils/proxyurl")
class TimelineBaseMethods {
constructor() {
@ -21,10 +21,18 @@ class TimelineBaseMethods {
}
}
isVideo() {
return this.data.__typename === "GraphVideo"
}
getDisplayUrlP() {
return proxyImage(this.data.display_url)
}
getVideoUrlP() {
return proxyVideo(this.data.video_url)
}
getAlt() {
return this.data.accessibility_caption || "No image description available."
}

View File

@ -203,6 +203,12 @@ class TimelineEntry extends TimelineBaseMethods {
}
}
fetchVideoURL() {
if (!this.isVideo()) return Promise.resolve(null)
else if (this.data.video_url) return Promise.resolve(this.getVideoUrlP())
else return this.update().then(() => this.getVideoUrlP())
}
async fetchFeedData() {
const children = await this.fetchChildren()
return {

View File

@ -5,6 +5,12 @@ function proxyImage(url, width) {
return "/imageproxy?"+params.toString()
}
function proxyVideo(url) {
const params = new URLSearchParams()
params.set("url", url)
return "/videoproxy?"+params.toString()
}
/**
* @param {import("../types").ExtendedOwner} owner
*/
@ -15,4 +21,5 @@ function proxyExtendedOwner(owner) {
}
module.exports.proxyImage = proxyImage
module.exports.proxyVideo = proxyVideo
module.exports.proxyExtendedOwner = proxyExtendedOwner

View File

@ -3,47 +3,69 @@ const {request} = require("../../lib/utils/request")
const {proxy} = require("pinski/plugins")
const sharp = require("sharp")
const VERIFY_SUCCESS = Symbol("VERIFY_SUCCESS")
/**
* Check that a resource is on Instagram.
* @param {URL} completeURL
*/
function verifyURL(completeURL) {
const params = completeURL.searchParams
if (!params.get("url")) return {status: "fail", value: [400, "Must supply `url` query parameter"]}
try {
var url = new URL(params.get("url"))
} catch (e) {
return {status: "fail", value: [400, "`url` query parameter is not a valid URL"]}
}
// check url protocol
if (url.protocol !== "https:") return {status: "fail", value: [400, "URL protocol must be `https:`"]}
// check url host
if (!["fbcdn.net", "cdninstagram.com"].some(host => url.host.endsWith(host))) return {status: "fail", value: [400, "URL host is not allowed"]}
return {status: "ok", url}
}
module.exports = [
{route: "/imageproxy", methods: ["GET"], code: async (input) => {
/** @type {URL} */
// check url param exists
const completeURL = input.url
const params = completeURL.searchParams
if (!params.get("url")) return [400, "Must supply `url` query parameter"]
try {
var url = new URL(params.get("url"))
} catch (e) {
return [400, "`url` query parameter is not a valid URL"]
{
route: "/imageproxy", methods: ["GET"], code: async (input) => {
const verifyResult = verifyURL(input.url)
if (verifyResult.status !== "ok") return verifyResult.value
if (!["png", "jpg"].some(ext => verifyResult.url.pathname.endsWith(ext))) return [400, "URL extension is not allowed"]
const params = input.url.searchParams
const width = +params.get("width")
if (typeof width === "number" && !isNaN(width) && width > 0) {
/*
This uses sharp to force crop the image to a square.
"entropy" seems to currently work better than "attention" on the thumbnail of this shortcode: B55yH20gSl0
Some thumbnails aren't square and would otherwise be stretched on the page without this.
If I cropped the images client side, it would have to be done with CSS background-image, which means no <img srcset>.
*/
return request(verifyResult.url).then(res => {
const converter = sharp().resize(width, width, {position: "entropy"})
return {
statusCode: 200,
contentType: "image/jpeg",
headers: {
"Cache-Control": constants.caching.image_cache_control
},
stream: res.body.pipe(converter)
}
})
} else {
// No specific size was requested, so just stream proxy the file directly.
return proxy(verifyResult.url, {
"Cache-Control": constants.caching.image_cache_control
})
}
}
// check url protocol
if (url.protocol !== "https:") return [400, "URL protocol must be `https:`"]
// check url host
if (!["fbcdn.net", "cdninstagram.com"].some(host => url.host.endsWith(host))) return [400, "URL host is not allowed"]
if (!["png", "jpg"].some(ext => url.pathname.endsWith(ext))) return [400, "URL extension is not allowed"]
const width = +params.get("width")
if (typeof width === "number" && !isNaN(width) && width > 0) {
/*
This uses sharp to force crop the image to a square.
"entropy" seems to currently work better than "attention" on the thumbnail of this shortcode: B55yH20gSl0
Some thumbnails aren't square and would otherwise be stretched on the page without this.
If I cropped the images client side, it would have to be done with CSS background-image, which means no <img srcset>.
*/
return request(url).then(res => {
const converter = sharp().resize(width, width, {position: "entropy"})
return {
statusCode: 200,
contentType: "image/jpeg",
headers: {
"Cache-Control": constants.caching.image_cache_control
},
stream: res.body.pipe(converter)
}
})
} else {
// No specific size was requested, so just stream proxy the file directly.
},
{
route: "/videoproxy", methods: ["GET"], code: async (input) => {
const verifyResult = verifyURL(input.url)
if (verifyResult.status !== "ok") return verifyResult.value
const url = verifyResult.url
if (!["mp4"].some(ext => url.pathname.endsWith(ext))) return [400, "URL extension is not allowed"]
return proxy(url, {
"Cache-Control": constants.caching.image_cache_control
})
}
}}
}
]

View File

@ -92,6 +92,7 @@ module.exports = [
return getOrFetchShortcode(fill[0]).then(async post => {
await post.fetchChildren()
await post.fetchExtendedOwnerP() // parallel await is okay since intermediate fetch result is cached
if (post.isVideo()) await post.fetchVideoURL()
return render(200, "pug/post.pug", {post})
}).catch(error => {
if (error === constants.symbols.NOT_FOUND) {

View File

@ -22,5 +22,8 @@ html
if post.getCaption()
p.description= post.getCaption()
section.images-gallery
for image in post.children
img(src=image.getDisplayUrlP() alt=image.getAlt() width=image.data.dimensions.width height=image.data.dimensions.height).sized-image
for entry in post.children
if entry.isVideo()
video(src=entry.getVideoUrlP() controls preload="auto" width=entry.data.dimensions.width height=entry.data.dimensions.height).sized-video
else
img(src=entry.getDisplayUrlP() alt=entry.getAlt() width=entry.data.dimensions.width height=entry.data.dimensions.height).sized-image

View File

@ -267,17 +267,23 @@ body
@media screen and (max-width: $layout-a-max)
flex: 1
.sized-image
.sized-image, .sized-video
color: #eee
background-color: #3b3c3d
max-height: 94vh
max-width: 100%
width: auto
height: auto
&:not(:last-child)
margin-bottom: 10px
.sized-image
width: auto
height: auto
.sized-video
width: 100%
height: 100%
.error-page
box-sizing: border-box
min-height: 100vh