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 {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." | ||||
| 	} | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -3,23 +3,33 @@ const {request} = require("../../lib/utils/request") | ||||
| const {proxy} = require("pinski/plugins") | ||||
| const sharp = require("sharp") | ||||
| 
 | ||||
| module.exports = [ | ||||
| 	{route: "/imageproxy", methods: ["GET"], code: async (input) => { | ||||
| 		/** @type {URL} */ | ||||
| 		// check url param exists
 | ||||
| 		const completeURL = input.url | ||||
| 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 [400, "Must supply `url` query parameter"] | ||||
| 	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 [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
 | ||||
| 		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
 | ||||
| 		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"] | ||||
| 	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) => { | ||||
| 			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) { | ||||
| 				/* | ||||
| @ -28,7 +38,7 @@ module.exports = [ | ||||
| 					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 => { | ||||
| 				return request(verifyResult.url).then(res => { | ||||
| 					const converter = sharp().resize(width, width, {position: "entropy"}) | ||||
| 					return { | ||||
| 						statusCode: 200, | ||||
| @ -41,9 +51,21 @@ module.exports = [ | ||||
| 				}) | ||||
| 			} else { | ||||
| 				// 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, { | ||||
| 				"Cache-Control": constants.caching.image_cache_control | ||||
| 			}) | ||||
| 		} | ||||
| 	}} | ||||
| 	} | ||||
| ] | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user