import * as _ from 'lodash'; import Axios from 'axios'; import { domain } from '../../bbcode/core'; export type UrlSolverCallback = (url: string, match: RegExpMatchArray) => Promise; export interface UrlSolver { matcher: RegExp; solver: UrlSolverCallback; } export class ImageUrlMutator { private solvers: UrlSolver[] = []; private static readonly IMGUR_CLIENT_ID = 'd60e27140a73b2e'; private static readonly IMGUR_IMAGE_URL_REGEX = /^https?:\/\/i.imgur.com\/([a-zA-Z0-9]+)(\.[a-z0-9A-Z]+)(.*)$/; private debug: boolean; private static SUPPRESSOR_DOMAINS = [ 'vimeo.com', 'gfycat.com' ]; constructor(debug: boolean) { this.debug = debug; this.init(); } protected init(): void { // this.add( // /^https?:\/\/.*twitter.com/, // async(): Promise => 'https://i.imgur.com/ScNLbsp.png' // ); this.add( /^https?:\/\/rule34video.com\/videos\/([0-9a-zA-Z-_]+)/, async(_url: string, match: RegExpMatchArray): Promise => { const videoId = match[1]; return `https://rule34video.com/embed/${videoId}`; } ); this.add( /^https?:\/\/(www.)?pornhub.com\/view_video.php\?viewkey=([a-z0-9A-Z]+)/, async(_url: string, match: RegExpMatchArray): Promise => { // https://www.pornhub.com/view_video.php?viewkey=ph5e11b975327f2 // https://www.pornhub.com/embed/ph5e11b975327f2 const videoId = match[2]; return `https://pornhub.com/embed/${videoId}`; } ); this.add( /^https?:\/\/(www.)?pornhub.com\/gif\/([a-z0-9A-Z]+)/, async(_url: string, match: RegExpMatchArray): Promise => { const gifId = match[2]; return `https://pornhub.com/embedgif/${gifId}`; } ); this.add( /^https?:\/\/(www.|v3.)?redgifs.com\/watch\/([a-z0-9A-Z]+)/, async(_url: string, match: RegExpMatchArray): Promise => { const redgifId = match[2]; return `https://www.redgifs.com/ifr/${redgifId}?controls=0&hd=1`; } ); this.add( /^https?:\/\/media[0-9]?.giphy.com\/media\/(.+)$/, async(_url: string, match: RegExpMatchArray): Promise => { const giphyUri = match[1]; return `https://i.giphy.com/media/${giphyUri}`; } ); this.add( /^https?:\/\/(www.)?gfycat.com\/([a-z0-9A-Z\-]+)\/?$/, async(_url: string, match: RegExpMatchArray): Promise => { const gfyId = match[2]; return `https://gfycat.com/ifr/${gfyId}?controls=0&hd=1`; } ); this.add( /^https?:\/\/e621.net\/(posts|post\/show)\/([0-9]+)/, async(url: string, match: RegExpMatchArray): Promise => { const galleryId = match[2]; try { const result = await Axios.get( `https://e621.net/posts/${galleryId}.json`, { // headers: { // 'User-Agent': 'F-List-Rising-Client/1.0' // } } ); const imageUrl = _.get(result, 'data.post.file.url') as string; return imageUrl || url; } catch(err) { console.error('E621 API Failure', url, err); return url; } } ); this.add( /^https?:\/\/((m|www).)?imgur.com\/gallery\/([a-zA-Z0-9]+)/, async(url: string, match: RegExpMatchArray): Promise => { // Imgur Gallery const galleryId = match[3]; try { const result = await Axios.get( `https://api.imgur.com/3/gallery/${galleryId}/images`, { headers: { Authorization: `Client-ID ${ImageUrlMutator.IMGUR_CLIENT_ID}` } } ); const imageUrl = _.get(result, 'data.data.0.link', null); if (!imageUrl) { return url; } const imageCount = _.get(result, 'data.data.length', 1); if (this.debug) console.log('Imgur gallery', url, imageUrl, imageCount); return this.getOptimizedImgurUrlFromUrl(`${imageUrl}?flist_gallery_image_count=${imageCount}`); } catch (err) { console.error('Imgur Gallery Failure', url, err); return url; } } ); this.add( /^https?:\/\/((m|www).)?imgur.com\/a\/([a-zA-Z0-9]+)/, async(url: string, match: RegExpMatchArray): Promise => { // Imgur Album const albumId = match[3]; try { const result = await Axios.get( `https://api.imgur.com/3/album/${albumId}/images`, { headers: { Authorization: `Client-ID ${ImageUrlMutator.IMGUR_CLIENT_ID}` } } ); const imageUrl = _.get(result, 'data.data.0.link', null); if (!imageUrl) { return url; } const imageCount = _.get(result, 'data.data.length', 1); if (this.debug) console.log('Imgur album', url, imageUrl, imageCount); return this.getOptimizedImgurUrlFromUrl(`${imageUrl}?flist_gallery_image_count=${imageCount}`); } catch (err) { console.error('Imgur Album Failure', url, err); return url; } } ); // must be AFTER gallery & album test this.add( /^https?:\/\/((m|www).)?imgur.com\/([a-zA-Z0-9]+)/, async(url: string, match: RegExpMatchArray): Promise => { // Single Imgur Image const imageId = match[3]; try { const result = await Axios.get( `https://api.imgur.com/3/image/${imageId}`, { headers: { Authorization: `Client-ID ${ImageUrlMutator.IMGUR_CLIENT_ID}` } } ); const imageUrl = _.get(result, 'data.data.link', url); if (this.debug) console.log('Imgur image', url, imageUrl); return this.getOptimizedImgurUrlFromUrl(imageUrl as string); } catch (err) { console.error('Imgur Image Failure', url, err); return url; } } ); // Load large thumbnail instead of the full size picture when possible this.add(ImageUrlMutator.IMGUR_IMAGE_URL_REGEX, async(_url: string, match: RegExpMatchArray) => this.getOptimizedImgUrlFromMatch(match) ); } getOptimizedImgUrlFromMatch(match: RegExpMatchArray): string { const imageId = match[1]; const ext = match[2]; const rest = match[3]; const finalExt = ((ext === '.gif') || (ext === '.gifv')) ? '.mp4' : ext; return `https://i.imgur.com/${imageId}${((imageId.length <= 7) && (finalExt !== '.mp4')) ? 'l' : ''}${finalExt}${rest}`; } getOptimizedImgurUrlFromUrl(url: string): string { const m = url.match(ImageUrlMutator.IMGUR_IMAGE_URL_REGEX); return m ? this.getOptimizedImgUrlFromMatch(m) : url; } setDebug(debug: boolean): void { this.debug = debug; } protected add(matcher: RegExp, solver: UrlSolverCallback): void { this.solvers.push({matcher, solver}); } async resolve(url: string): Promise { const match = _.find(this.solvers, (s: UrlSolver) => url.match(s.matcher)) as (UrlSolver | undefined); return this.attachSuppressor( match ? await match.solver(url, url.match(match.matcher) as RegExpMatchArray) : url ); // return this.attachSuppressor(await match.solver(url, url.match(match.matcher) as RegExpMatchArray)); } attachSuppressor(url: string): string { const host = domain(url); if (_.indexOf(ImageUrlMutator.SUPPRESSOR_DOMAINS, host) < 0) { return url; } const u = new URL(url); u.searchParams.set('__x-suppress__', '1'); return u.toString(); } }