diff --git a/src/lib/structures/TimelineBaseMethods.js b/src/lib/structures/TimelineBaseMethods.js
index 467004f..a176a3a 100644
--- a/src/lib/structures/TimelineBaseMethods.js
+++ b/src/lib/structures/TimelineBaseMethods.js
@@ -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."
}
diff --git a/src/lib/structures/TimelineEntry.js b/src/lib/structures/TimelineEntry.js
index f4690f3..26fba3d 100644
--- a/src/lib/structures/TimelineEntry.js
+++ b/src/lib/structures/TimelineEntry.js
@@ -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 {
diff --git a/src/lib/utils/proxyurl.js b/src/lib/utils/proxyurl.js
index 94d6a90..17ce50c 100644
--- a/src/lib/utils/proxyurl.js
+++ b/src/lib/utils/proxyurl.js
@@ -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
diff --git a/src/site/api/proxy.js b/src/site/api/proxy.js
index 1e15820..6a7b251 100644
--- a/src/site/api/proxy.js
+++ b/src/site/api/proxy.js
@@ -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 .
+ */
+ 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 .
- */
- 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
})
}
- }}
+ }
]
diff --git a/src/site/api/routes.js b/src/site/api/routes.js
index 9f961a0..f756786 100644
--- a/src/site/api/routes.js
+++ b/src/site/api/routes.js
@@ -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) {
diff --git a/src/site/pug/post.pug b/src/site/pug/post.pug
index ef595e7..4bd8986 100644
--- a/src/site/pug/post.pug
+++ b/src/site/pug/post.pug
@@ -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
diff --git a/src/site/sass/main.sass b/src/site/sass/main.sass
index de85d61..be26e19 100644
--- a/src/site/sass/main.sass
+++ b/src/site/sass/main.sass
@@ -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