First commit: /a/, /gallery/, images, gifv

This commit is contained in:
3nprob
2021-10-06 18:43:59 +09:00
commit 7c2e53c6e4
19 changed files with 6559 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
dist
node_modules
+5995
View File
File diff suppressed because it is too large Load Diff
+37
View File
@@ -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
+8
View File
@@ -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,
};
+68
View File
@@ -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 });
+45
View File
@@ -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,
});
};
+74
View File
@@ -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();
+77
View File
@@ -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;
}
}
+11
View File
@@ -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>'
);
+4
View File
@@ -0,0 +1,4 @@
img.album-img {
max-width: 100%;
max-height: 100%;
}
+139
View File
@@ -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;
}
+6
View File
@@ -0,0 +1,6 @@
html
head
title imgur-proxy
include includes/head.pug
body
img(src=util.proxyURL(url), alt='' class='album-img')
+71
View File
@@ -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)
+2
View File
@@ -0,0 +1,2 @@
link(rel="stylesheet", type="text/css", href="/css/styles.css")
link(rel="stylesheet", type="text/css", href="/css/custom.css")
Executable
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2019",
"noImplicitAny": true,
"strictNullChecks": true,
"declaration": true,
"sourceMap": false,
"outDir": "dist",
"typeRoots": [
"src/types/"
]
},
"include": [
"src/**/*"
]
}