First commit: /a/, /gallery/, images, gifv
This commit is contained in:
commit
7c2e53c6e4
|
@ -0,0 +1,2 @@
|
|||
dist
|
||||
node_modules
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "imgur-proxy",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"build": "npx tsc",
|
||||
"watch": "npx tsc --watch",
|
||||
"test": "npx mocha -r ts-node/register test/**/*.test.ts",
|
||||
"dev:tsc": "tsc --watch -p .",
|
||||
"dev:serve": "nodemon -e js -w dist dist/index.js",
|
||||
"dev": "run-p dev:*"
|
||||
},
|
||||
"author": "3np",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@types/hapi__hapi": "^20.0.9",
|
||||
"@types/hapi__inert": "^5.2.3",
|
||||
"@types/hapi__vision": "^5.5.3",
|
||||
"@types/node": "^16.10.3",
|
||||
"@types/pug": "^2.0.5",
|
||||
"nodemon": "^2.0.13",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/hapi": "^20.2.0",
|
||||
"@hapi/inert": "^6.0.4",
|
||||
"@hapi/vision": "^6.1.0",
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"got": "^11.8.2",
|
||||
"hpagent": "^0.1.2",
|
||||
"pug": "^3.0.2"
|
||||
}
|
||||
}
|
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
port: process.env.RIMGU_PORT || 8080,
|
||||
host: process.env.RIMGU_HOST || 'localhost',
|
||||
address: process.env.RIMGU_ADDRESS || '127.0.0.1',
|
||||
http_proxy: process.env.RIMGU_HTTP_PROXY || null,
|
||||
https_proxy: process.env.RIMGU_HTTPS_PROXY || null,
|
||||
imgur_client_id: process.env.RIMGU_IMGUR_CLIENT_ID || null,
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
import cheerio from 'cheerio';
|
||||
import got, { Response } from 'got';
|
||||
import { HttpsProxyAgent, HttpProxyAgent } from 'hpagent';
|
||||
import { globalAgent as httpGlobalAgent } from 'http';
|
||||
import { globalAgent as httpsGlobalAgent } from 'https';
|
||||
|
||||
import CONFIG from './config';
|
||||
|
||||
const GALLERY_JSON_REGEX = /window\.postDataJSON=(".*")$/;
|
||||
|
||||
const agent = {
|
||||
http: CONFIG.http_proxy
|
||||
? new HttpProxyAgent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 256,
|
||||
maxFreeSockets: 256,
|
||||
scheduling: 'lifo',
|
||||
proxy: CONFIG.http_proxy,
|
||||
})
|
||||
: httpGlobalAgent,
|
||||
https: CONFIG.https_proxy
|
||||
? new HttpsProxyAgent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 256,
|
||||
maxFreeSockets: 256,
|
||||
scheduling: 'lifo',
|
||||
proxy: CONFIG.https_proxy,
|
||||
})
|
||||
: httpsGlobalAgent
|
||||
};
|
||||
|
||||
export const fetchComments = async (galleryID: string): Promise<Comment[]> => {
|
||||
// https://api.imgur.com/comment/v1/comments?client_id=${CLIENT_ID}%5Bpost%5D=eq%3Ag1bk7CB&include=account%2Cadconfig&per_page=30&sort=best
|
||||
const response = await got(`https://api.imgur.com/comment/v1/comments?client_id=${CONFIG.imgur_client_id}&filter%5Bpost%5D=eq%3A${galleryID}&include=account%2Cadconfig&per_page=30&sort=best`);
|
||||
return JSON.parse(response.body).data;
|
||||
}
|
||||
|
||||
export const fetchGallery = async (galleryID: string): Promise<Gallery> => {
|
||||
// https://imgur.com/gallery/g1bk7CB
|
||||
const response = await got(`https://imgur.com/gallery/${galleryID}`, { agent });
|
||||
const $ = cheerio.load(response.body);
|
||||
const postDataScript = $('head script:first-of-type').html();
|
||||
if (!postDataScript) {
|
||||
throw new Error('Could not find gallery data');
|
||||
}
|
||||
const postDataMatches = postDataScript.match(GALLERY_JSON_REGEX);
|
||||
if (!postDataMatches || postDataMatches.length < 2) {
|
||||
throw new Error('Could not parse gallery data');
|
||||
}
|
||||
const postData = JSON.parse(JSON.parse(postDataMatches[1]));
|
||||
return postData;
|
||||
};
|
||||
|
||||
export const fetchAlbumURL = async (albumID: string): Promise<string> => {
|
||||
// https://imgur.com/a/DfEsrAB
|
||||
const response = await got(`https://imgur.com/a/${albumID}`, { agent });
|
||||
const $ = cheerio.load(response.body);
|
||||
const url = $('head meta[property="og:image"]').attr('content')?.replace(/\/\?.*$/, '');
|
||||
if (!url) {
|
||||
throw new Error('Could not read image url');
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export const fetchMedia = async (filename: string): Promise<Response<string>> =>
|
||||
await got(`https://i.imgur.com/${filename}`, { agent });
|
|
@ -0,0 +1,45 @@
|
|||
import Hapi = require('@hapi/hapi');
|
||||
import '@hapi/vision';
|
||||
import { fetchAlbumURL, fetchComments, fetchGallery, fetchMedia } from './fetchers';
|
||||
import * as util from './util';
|
||||
|
||||
export const handleMedia = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
||||
const {
|
||||
baseName,
|
||||
extension,
|
||||
} = request.params;
|
||||
const result = await fetchMedia(`${baseName}.${extension}`);
|
||||
const response = h.response(result.rawBody)
|
||||
.header('Content-Type', result.headers["content-type"] || `image/${extension}`);
|
||||
return response;
|
||||
};
|
||||
|
||||
export const handleAlbum = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
||||
// https://imgur.com/a/DfEsrAB
|
||||
const url = await fetchAlbumURL(request.params.albumID);
|
||||
return h.view('album', {
|
||||
url,
|
||||
util,
|
||||
});
|
||||
};
|
||||
|
||||
export const handleUser = (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
||||
// https://imgur.com/user/MomBotNumber5
|
||||
throw new Error('not implemented');
|
||||
};
|
||||
|
||||
export const handleTag = (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
||||
// https://imgur.com/t/funny
|
||||
throw new Error('not implemented');
|
||||
};
|
||||
|
||||
export const handleGallery = async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
|
||||
const galleryID = request.params.galleryID;
|
||||
const gallery = await fetchGallery(galleryID);
|
||||
const comments = await fetchComments(galleryID);
|
||||
return h.view('gallery', {
|
||||
...gallery,
|
||||
comments,
|
||||
util,
|
||||
});
|
||||
};
|
|
@ -0,0 +1,74 @@
|
|||
'use strict';
|
||||
|
||||
import Hapi = require('@hapi/hapi');
|
||||
import Path = require('path');
|
||||
import { handleAlbum, handleGallery, handleMedia, handleTag, handleUser } from './handlers';
|
||||
|
||||
import CONFIG from './config';
|
||||
|
||||
const init = async () => {
|
||||
const server = Hapi.server({
|
||||
port: CONFIG.port,
|
||||
host: CONFIG.host,
|
||||
address: CONFIG.address,
|
||||
routes: {
|
||||
files: {
|
||||
relativeTo: Path.join(__dirname, 'static')
|
||||
}
|
||||
}
|
||||
});
|
||||
await server.register(require('@hapi/vision'));
|
||||
await server.register(require('@hapi/inert'));
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/css/{param*}',
|
||||
handler: ({
|
||||
directory: {
|
||||
path: Path.join(__dirname, 'static/css')
|
||||
}
|
||||
} as any)
|
||||
});
|
||||
server.views({
|
||||
engines: {
|
||||
pug: require('pug')
|
||||
},
|
||||
relativeTo: __dirname,
|
||||
path: 'templates',
|
||||
});
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/{baseName}.{extension}',
|
||||
handler: handleMedia,
|
||||
});
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/a/{albumID?}',
|
||||
handler: handleAlbum,
|
||||
});
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/t/{tagID?}',
|
||||
handler: handleTag,
|
||||
});
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/user/{userID?}',
|
||||
handler: handleUser,
|
||||
});
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/gallery/{galleryID}',
|
||||
handler: handleGallery,
|
||||
});
|
||||
|
||||
await server.start();
|
||||
console.log('Server running on %s', server.info.uri);
|
||||
};
|
||||
|
||||
process.on('unhandledRejection', (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
init();
|
|
@ -0,0 +1,77 @@
|
|||
interface Account {
|
||||
id: number;
|
||||
username: string;
|
||||
avatar_url: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface Gallery {
|
||||
id: string;
|
||||
title: string;
|
||||
account: Account;
|
||||
media: Media[];
|
||||
tags: Tag[];
|
||||
cover: Media;
|
||||
}
|
||||
|
||||
type MediaMimeType = 'image/jpeg' | 'image/png' | 'image/gif';
|
||||
type MediaType = 'image';
|
||||
type MediaExt = 'jpeg' | 'png' | 'gif';
|
||||
|
||||
interface Tag {
|
||||
tag: string;
|
||||
display: string;
|
||||
background_id: string;
|
||||
accent: string;
|
||||
is_promoted: boolean;
|
||||
}
|
||||
|
||||
interface Media {
|
||||
id: string;
|
||||
account_id: number;
|
||||
mime_type: MediaMimeType;
|
||||
type: MediaType;
|
||||
name: string;
|
||||
basename: string;
|
||||
url: string;
|
||||
ext: MediaExt;
|
||||
width: number;
|
||||
height: number;
|
||||
size: number;
|
||||
metadata: {
|
||||
title: string;
|
||||
description: string;
|
||||
is_animated: boolean;
|
||||
is_looping: boolean;
|
||||
duration: number;
|
||||
has_sound: boolean;
|
||||
},
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
type MediaPlatform = 'ios' | 'android' | 'api' | 'web';
|
||||
interface Comment {
|
||||
id: number;
|
||||
parent_id: number;
|
||||
comment: string;
|
||||
account_id: number;
|
||||
post_id: string;
|
||||
upvote_count: number;
|
||||
downvote_count: number;
|
||||
point_count: number;
|
||||
vote: null; // ?
|
||||
platform_id: number;
|
||||
platform: MediaPlatform;
|
||||
created_at: string;
|
||||
updated_at: "2021-10-01T00:08:51Z",
|
||||
deleted_at: null,
|
||||
next: null; //?
|
||||
comments: Comment[];
|
||||
account: {
|
||||
id: number;
|
||||
username: string;
|
||||
avatar: string;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export const proxyURL = (url: string): string =>
|
||||
url.replace(/^https?:\/\/[^.]*\.imgur.com\//, '/');
|
||||
|
||||
export const linkify = (content: string) =>
|
||||
content.replace(
|
||||
/https?:\/\/[^.]*\.imgur.com\/([\/_a-zA-Z0-9-]+)\.gifv/g,
|
||||
'<video src="/$1.mp4" class="commentVideo commentObject" loop="" autoplay=""></video>'
|
||||
).replace(
|
||||
/https?:\/\/[^.]*\.imgur.com\/([\/_a-zA-Z0-9-]+\.[a-z0-9A-Z]{2,6})/g,
|
||||
'<a href="/$1" target="_blank"><img class="commentImage commentObject" src="/$1" loading="lazy" /></a>'
|
||||
);
|
|
@ -0,0 +1,4 @@
|
|||
img.album-img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
.UserAvatar {
|
||||
display: block;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.TagPill {
|
||||
box-shadow: 0 5px 5px rgba(0,0,0,.25);
|
||||
border-radius: 54px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
letter-spacing: .02em;
|
||||
color: #eff1f4;
|
||||
text-shadow: 0 1px 4px #000;
|
||||
padding: 8px 30px;
|
||||
display: inline-block;
|
||||
font-family: Proxima Nova Bold,Helvetica Neue,Helvetica,Arial,sans-serif;
|
||||
transition: box-shadow .2s ease-out;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
.GalleryComment-avatar-bar .avatar span {
|
||||
display: block;
|
||||
background-color: grey;
|
||||
border-radius: 100%;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.GalleryComment-byLine .author-name {
|
||||
text-overflow: ellipsis;
|
||||
color: #01b96b;
|
||||
font-family: Proxima Nova Bold,Helvetica Neue,Helvetica,Arial,sans-serif;
|
||||
}
|
||||
|
||||
.GalleryComment-avatar-bar .avatar {
|
||||
margin: 2px 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.GalleryComment-byLine {
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
color: #b4b9c2;
|
||||
}
|
||||
|
||||
.GalleryComment-avatar-bar {
|
||||
width: 24px;
|
||||
margin-right: 8px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.GalleryComment-byLine .Meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
*, ::after, ::before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.GalleryComment-replies {
|
||||
padding-left: 32px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.GalleryComment-body .commentObject {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
max-height: 100px;
|
||||
min-width: 50px;
|
||||
max-width: 500px;
|
||||
padding: 5px 0 0;
|
||||
}
|
||||
|
||||
.GalleryComment-body {
|
||||
font-size: 15px;
|
||||
line-height: 150%;
|
||||
overflow-wrap: break-word;
|
||||
color: #eff1f4;
|
||||
}
|
||||
|
||||
.GalleryComment-actions .points {
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.GalleryComment-actions .actions-btn {
|
||||
color: #b4b9c2;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.Vote {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.GalleryComment-actions .actions-btn.vote-btn {
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.GalleryComment-actions .actions-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
color: #b4b9c2;
|
||||
outline: none;
|
||||
/* cursor: pointer; */
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
min-height: 19px;
|
||||
border-radius: 3px;
|
||||
height: 26px;
|
||||
justify-content: center;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.GalleryComment-actions {
|
||||
display: flex;
|
||||
margin: 4px 0 8px -5px;
|
||||
align-items: center;
|
||||
color: #b4b9c2;
|
||||
font-family: Proxima Nova Bold,Helvetica Neue,Helvetica,Arial,sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
html
|
||||
head
|
||||
title imgur-proxy
|
||||
include includes/head.pug
|
||||
body
|
||||
img(src=util.proxyURL(url), alt='' class='album-img')
|
|
@ -0,0 +1,71 @@
|
|||
mixin commentbox(comment)
|
||||
div(class='GalleryComment')
|
||||
div(class='GalleryComment-wrapper')
|
||||
div(class='GalleryComment-content')
|
||||
div(class='GalleryComment-byLine')
|
||||
div(class='Meta')
|
||||
div(class='GalleryComment-avatar-bar')
|
||||
div(class='avatar')
|
||||
a(title='View profile of '+comment.account.username, href='/user/'+comment.account.username)
|
||||
span(title=comment.account.username, style='background-image: url("' + util.proxyURL(comment.account.avatar) + '");')
|
||||
a(class='author-name', title='View profile of '+comment.account.username, href='/user/'+comment.account.username) #{comment.account.username}
|
||||
span(class="date", title=comment.created_at)
|
||||
span(class="delimiter") •
|
||||
span #{comment.created_at} via <a class="platform bold" href="/apps">#{comment.platform}</a>
|
||||
div(class='GalleryComment-body')
|
||||
span(class='Linkify')
|
||||
| !{util.linkify(comment.comment)}
|
||||
div(class='GalleryComment-actions')
|
||||
div(class='vote-btn upvote actions-btn' title='Upvotes')
|
||||
div(class='Vote Vote-up')
|
||||
svg(width='16', height='16', viewBox='0 0 16 16', fill='none', xmlns='http://www.w3.org/2000/svg')
|
||||
title Upvotes
|
||||
| <path fill="none" stroke="#B4B9C2" stroke-width="2" fill-rule="evenodd" clip-rule="evenodd" d="M7.197 2.524a1.2 1.2 0 011.606 0c.521.46 1.302 1.182 2.363 2.243a29.617 29.617 0 012.423 2.722c.339.435.025 1.028-.526 1.028h-2.397v4.147c0 .524-.306.982-.823 1.064-.417.066-1.014.122-1.843.122s-1.427-.056-1.843-.122c-.517-.082-.824-.54-.824-1.064V8.517H2.937c-.552 0-.865-.593-.527-1.028.52-.669 1.32-1.62 2.423-2.722a52.996 52.996 0 012.364-2.243z"></path>
|
||||
.points + #{comment.upvote_count}
|
||||
div(class='vote-btn down actions-btn' title='Downvotes')
|
||||
div(class='Vote Vote-down')
|
||||
svg(width='16', height='16', viewBox='0 0 16 16', fill='none', xmlns='http://www.w3.org/2000/svg')
|
||||
title Downvotes
|
||||
| <path fill="none" stroke="#B4B9C2" stroke-width="2" fill-rule="evenodd" clip-rule="evenodd" d="M8.803 13.476a1.2 1.2 0 01-1.606 0 53.03 53.03 0 01-2.364-2.243 29.613 29.613 0 01-2.422-2.722c-.339-.435-.025-1.028.526-1.028h2.397V3.336c0-.524.306-.982.823-1.064A11.874 11.874 0 018 2.15c.829 0 1.427.056 1.843.122.517.082.824.54.824 1.064v4.147h2.396c.552 0 .865.593.527 1.028-.52.669-1.32 1.62-2.423 2.722a53.038 53.038 0 01-2.364 2.243z"></path>
|
||||
.points - #{comment.downvote_count}
|
||||
.points = #{comment.point_count}
|
||||
div(class='GalleryComment-replies')
|
||||
each reply in comment.comments
|
||||
+commentbox(reply)
|
||||
html
|
||||
head
|
||||
title imgur-proxy
|
||||
include includes/head.pug
|
||||
body
|
||||
div(class='Gallery-Content')
|
||||
div(class='Gallery-Header')
|
||||
div(class='Gallery-Title')
|
||||
span #{title}
|
||||
div(class='Gallery-Byline')
|
||||
a(class='author-link' title='View profile of '+account.username, href='/user/'+account.username)
|
||||
span(class='UserAvatar', title=account.username, style='background-image: url("' + util.proxyURL(account.avatar_url) + '");')
|
||||
div(class='Info-Wrapper')
|
||||
div(class='Info')
|
||||
a(class='author-name' title='View profile of '+account.username, href='/user/'+account.username) #{account.username}
|
||||
div(class='Meta')
|
||||
span #{view_count} Views
|
||||
span(class='delimiter') •
|
||||
span(title=created_at) #{created_at}
|
||||
div(class='Gallery-ContentWrapper')
|
||||
div(class='Gallery-Content--media')
|
||||
div(class='imageContainer')
|
||||
img(src=util.proxyURL(cover.url))
|
||||
div(class='Gallery-Content--tags')
|
||||
each tag in tags
|
||||
a(class='TagPill'
|
||||
style='background: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)) repeat scroll 0% 0%, rgba(0, 0, 0, 0) url("/' + tag.background_id + '_d.jpg?maxwidth=200&fidelity=grand") repeat scroll 0% 0%;'
|
||||
href='/t/'+tag.tag) #{tag.tag}
|
||||
div(class='CommentsList')
|
||||
div(class='CommentsList-headline')
|
||||
div(class='CommentsList-headline--counter')
|
||||
span #{comments.length} Comments
|
||||
div
|
||||
div(class='CommentsList-comments')
|
||||
div(class='CommentsList-comments--container')
|
||||
each comment in comments
|
||||
+commentbox(comment)
|
|
@ -0,0 +1,2 @@
|
|||
link(rel="stylesheet", type="text/css", href="/css/styles.css")
|
||||
link(rel="stylesheet", type="text/css", href="/css/custom.css")
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2019",
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"declaration": true,
|
||||
"sourceMap": false,
|
||||
"outDir": "dist",
|
||||
"typeRoots": [
|
||||
"src/types/"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue