1
0
mirror of https://git.sr.ht/~cadence/bibliogram synced 2024-11-23 00:27:30 +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 constants = require("../constants")
const {proxyImage, proxyExtendedOwner} = require("../utils/proxyurl") const {proxyImage, proxyVideo} = require("../utils/proxyurl")
class TimelineBaseMethods { class TimelineBaseMethods {
constructor() { constructor() {
@ -21,10 +21,18 @@ class TimelineBaseMethods {
} }
} }
isVideo() {
return this.data.__typename === "GraphVideo"
}
getDisplayUrlP() { getDisplayUrlP() {
return proxyImage(this.data.display_url) return proxyImage(this.data.display_url)
} }
getVideoUrlP() {
return proxyVideo(this.data.video_url)
}
getAlt() { getAlt() {
return this.data.accessibility_caption || "No image description available." 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() { async fetchFeedData() {
const children = await this.fetchChildren() const children = await this.fetchChildren()
return { return {

View File

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

View File

@ -3,23 +3,33 @@ const {request} = require("../../lib/utils/request")
const {proxy} = require("pinski/plugins") const {proxy} = require("pinski/plugins")
const sharp = require("sharp") const sharp = require("sharp")
module.exports = [ const VERIFY_SUCCESS = Symbol("VERIFY_SUCCESS")
{route: "/imageproxy", methods: ["GET"], code: async (input) => {
/** @type {URL} */ /**
// check url param exists * Check that a resource is on Instagram.
const completeURL = input.url * @param {URL} completeURL
*/
function verifyURL(completeURL) {
const params = completeURL.searchParams const params = completeURL.searchParams
if (!params.get("url")) return [400, "Must supply `url` query parameter"] if (!params.get("url")) return {status: "fail", value: [400, "Must supply `url` query parameter"]}
try { try {
var url = new URL(params.get("url")) var url = new URL(params.get("url"))
} catch (e) { } catch (e) {
return [400, "`url` query parameter is not a valid URL"] return {status: "fail", value: [400, "`url` query parameter is not a valid URL"]}
} }
// check url protocol // check url protocol
if (url.protocol !== "https:") return [400, "URL protocol must be `https:`"] if (url.protocol !== "https:") return {status: "fail", value: [400, "URL protocol must be `https:`"]}
// check url host // check url host
if (!["fbcdn.net", "cdninstagram.com"].some(host => url.host.endsWith(host))) return [400, "URL host is not allowed"] if (!["fbcdn.net", "cdninstagram.com"].some(host => url.host.endsWith(host))) return {status: "fail", value: [400, "URL host is not allowed"]}
if (!["png", "jpg"].some(ext => url.pathname.endsWith(ext))) return [400, "URL extension is not allowed"] return {status: "ok", url}
}
module.exports = [
{
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") const width = +params.get("width")
if (typeof width === "number" && !isNaN(width) && width > 0) { if (typeof width === "number" && !isNaN(width) && width > 0) {
/* /*
@ -28,7 +38,7 @@ module.exports = [
Some thumbnails aren't square and would otherwise be stretched on the page without this. 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>. 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 => { return request(verifyResult.url).then(res => {
const converter = sharp().resize(width, width, {position: "entropy"}) const converter = sharp().resize(width, width, {position: "entropy"})
return { return {
statusCode: 200, statusCode: 200,
@ -41,9 +51,21 @@ module.exports = [
}) })
} else { } else {
// No specific size was requested, so just stream proxy the file directly. // No specific size was requested, so just stream proxy the file directly.
return proxy(verifyResult.url, {
"Cache-Control": constants.caching.image_cache_control
})
}
}
},
{
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, { return proxy(url, {
"Cache-Control": constants.caching.image_cache_control "Cache-Control": constants.caching.image_cache_control
}) })
} }
}} }
] ]

View File

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

View File

@ -22,5 +22,8 @@ html
if post.getCaption() if post.getCaption()
p.description= post.getCaption() p.description= post.getCaption()
section.images-gallery section.images-gallery
for image in post.children for entry in post.children
img(src=image.getDisplayUrlP() alt=image.getAlt() width=image.data.dimensions.width height=image.data.dimensions.height).sized-image 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) @media screen and (max-width: $layout-a-max)
flex: 1 flex: 1
.sized-image .sized-image, .sized-video
color: #eee color: #eee
background-color: #3b3c3d background-color: #3b3c3d
max-height: 94vh max-height: 94vh
max-width: 100% max-width: 100%
width: auto
height: auto
&:not(:last-child) &:not(:last-child)
margin-bottom: 10px margin-bottom: 10px
.sized-image
width: auto
height: auto
.sized-video
width: 100%
height: 100%
.error-page .error-page
box-sizing: border-box box-sizing: border-box
min-height: 100vh min-height: 100vh