mirror of
				https://git.sr.ht/~cadence/bibliogram
				synced 2025-10-31 03:25:36 +00:00 
			
		
		
		
	Add video support (experimental!)
This commit is contained in:
		
							parent
							
								
									95cc416e08
								
							
						
					
					
						commit
						a5ab771969
					
				| @ -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." | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -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 { | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
| @ -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 | ||||||
| 			}) | 			}) | ||||||
| 		} | 		} | ||||||
| 	}} | 	} | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -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) { | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user