initial commit
This commit is contained in:
commit
f895fe5348
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
venv/*
|
||||
helloworld/__pycache__/*
|
||||
testdata/*
|
||||
.vscode/*
|
7
CHANGELOG.md
Normal file
7
CHANGELOG.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
This project uses Semantic Versioning
|
||||
|
||||
## 0.0.0
|
||||
|
||||
Initial release
|
66
imgmine/__init__.py
Normal file
66
imgmine/__init__.py
Normal file
@ -0,0 +1,66 @@
|
||||
from gevent import monkey
|
||||
from gevent import sleep
|
||||
monkey.patch_all()
|
||||
from threading import Thread
|
||||
from os import remove, mkdir, path
|
||||
from shutil import rmtree
|
||||
from glob import glob
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from bottle import route, run
|
||||
from bottle import static_file
|
||||
from bottle import SimpleTemplate
|
||||
|
||||
from .get import get
|
||||
from .config import IMAGE_CACHE, SINGLE_IMAGE_DELETE_AFTER_SECS, ALBUM_DELETE_AFTER_SECS, template_dir
|
||||
|
||||
|
||||
def album(id):
|
||||
req_id = str(uuid4())
|
||||
req = IMAGE_CACHE
|
||||
|
||||
get("/a/" + id, req)
|
||||
|
||||
imgs = glob(req + "*")
|
||||
for c, img in enumerate(imgs):
|
||||
imgs[c] = img.replace(IMAGE_CACHE, '/')
|
||||
with open(f'{template_dir}gallery.html', 'r') as img_view:
|
||||
tpl = SimpleTemplate(img_view)
|
||||
return tpl.render(imgs=imgs)
|
||||
|
||||
@route('/')
|
||||
@route('')
|
||||
def home():
|
||||
return static_file("index.html", root=template_dir)
|
||||
|
||||
@route('/static/<file>')
|
||||
def static(file=''):
|
||||
return static_file(file, root=template_dir)
|
||||
|
||||
|
||||
@route('/gallery/<id>')
|
||||
def gallery(id=''):
|
||||
return album(id)
|
||||
|
||||
@route('/a/<id>')
|
||||
def gallery(id=''):
|
||||
return album(id)
|
||||
|
||||
|
||||
@route('/<img>')
|
||||
def hello(img=''):
|
||||
|
||||
if not path.exists(IMAGE_CACHE + img):
|
||||
get(img, IMAGE_CACHE)
|
||||
return static_file(img, root=IMAGE_CACHE)
|
||||
|
||||
|
||||
def start_server():
|
||||
try:
|
||||
rmtree(IMAGE_CACHE)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
mkdir(IMAGE_CACHE)
|
||||
|
||||
run(server='gevent')
|
BIN
imgmine/__pycache__/__init__.cpython-39.pyc
Normal file
BIN
imgmine/__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
imgmine/__pycache__/config.cpython-39.pyc
Normal file
BIN
imgmine/__pycache__/config.cpython-39.pyc
Normal file
Binary file not shown.
BIN
imgmine/__pycache__/get.cpython-39.pyc
Normal file
BIN
imgmine/__pycache__/get.cpython-39.pyc
Normal file
Binary file not shown.
4
imgmine/config.py
Normal file
4
imgmine/config.py
Normal file
@ -0,0 +1,4 @@
|
||||
IMAGE_CACHE = '/tmp/imgmine-imgur-images/'
|
||||
SINGLE_IMAGE_DELETE_AFTER_SECS = 60
|
||||
ALBUM_DELETE_AFTER_SECS = 120
|
||||
template_dir = 'imgmine/web/'
|
64
imgmine/get.py
Normal file
64
imgmine/get.py
Normal file
@ -0,0 +1,64 @@
|
||||
import sys
|
||||
from shutil import rmtree
|
||||
from os import remove
|
||||
from threading import Thread
|
||||
|
||||
import requests
|
||||
import bs4
|
||||
from gevent import sleep
|
||||
|
||||
from .config import SINGLE_IMAGE_DELETE_AFTER_SECS, ALBUM_DELETE_AFTER_SECS
|
||||
|
||||
def delete_file(path):
|
||||
sleep(SINGLE_IMAGE_DELETE_AFTER_SECS)
|
||||
print('Erasing', path)
|
||||
try:
|
||||
remove(path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def error(msg):
|
||||
sys.stderr.write(msg + "\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
def get(url: str, write_dir: str, delete=True):
|
||||
if not url.startswith('https://imgur.com/'):
|
||||
url = 'https://imgur.com/' + url
|
||||
found_url = ''
|
||||
|
||||
print('it not album', url)
|
||||
album = False
|
||||
if "gallery" in url:
|
||||
url = url.replace("gallery", "a")
|
||||
print('it album')
|
||||
if "/a/" in url:
|
||||
print('it album')
|
||||
album = True
|
||||
if not url.endswith("blog"):
|
||||
url += "/layout/blog"
|
||||
|
||||
if not album:
|
||||
print('getting img', url)
|
||||
url = 'https://i.imgur.com/' + url.rsplit('/', 1)[-1].replace('jpeg', '').replace('jpg', '')
|
||||
with open(f'{write_dir}/{url[-12:]}', 'wb') as img:
|
||||
img.write(requests.get(url).content)
|
||||
if delete:
|
||||
Thread(target=delete_file, args=[f"{write_dir}/{url[-12:]}"]).start()
|
||||
else:
|
||||
print('Detecting album/gallery images', url)
|
||||
soup = bs4.BeautifulSoup(requests.get(url).text, 'html.parser')
|
||||
for count, el in enumerate(soup.select('.post-image meta[itemprop="contentUrl"]'), start=1):
|
||||
try:
|
||||
found_url = "https:" + el['content']
|
||||
except KeyError:
|
||||
error("Could not obtain url for detected image")
|
||||
continue
|
||||
print(f"Downloading image {count}: {found_url}")
|
||||
|
||||
print("Writing image", f"{write_dir}{found_url[-11:]}")
|
||||
with open(f"{write_dir}{found_url[-11:]}", "wb") as f:
|
||||
f.write(requests.get(found_url).content)
|
||||
if delete:
|
||||
Thread(target=delete_file, args=[f"{write_dir}{found_url[-11:]}"]).start()
|
||||
|
25
imgmine/web/gallery.html
Normal file
25
imgmine/web/gallery.html
Normal file
@ -0,0 +1,25 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charself="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>imgmine - minimal & private imgur proxy</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📷</text></svg>">
|
||||
<link rel="stylesheet" href="/static/theme.css">
|
||||
</head>
|
||||
<body>
|
||||
% for img in imgs:
|
||||
<img src="{{img}}" loading="lazy">
|
||||
<br>
|
||||
% end
|
||||
<footer>
|
||||
<small>
|
||||
This website does not claim ownership of any media.
|
||||
<br>This service simply acts as a proxy to Imgur.com and does not store images aside from a temporary cache.
|
||||
<br>Abusive images should be reported to imgur. This website does not create new images/comments.
|
||||
</small>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
41
imgmine/web/index.html
Normal file
41
imgmine/web/index.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charself="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>imgmine - minimal & private imgur proxy</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📷</text></svg>">
|
||||
<link rel="stylesheet" href="/static/theme.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
imgmine
|
||||
</h1>
|
||||
<hr>
|
||||
<p>
|
||||
A minimalist read-only Imgur proxy insipired by software like <a href="https://invidio.us/">Invidious</a>, <a href="https://nitter.net/">Nitter</a>, and <a href="https://bibliogram.art/">Bibliogram</a>.
|
||||
</p>
|
||||
<p>
|
||||
This project was started because <a href="https://chaoswebs.net/">Kevin Froman</a> got fed up with Imgur breaking with Tor because of cow-dung JS.
|
||||
</p>
|
||||
<h2>Feature roadmap</h2>
|
||||
<p>Features are in order of priority of most to least</p>
|
||||
<ul>
|
||||
<li><input type="checkbox" checked> Image proxying</li>
|
||||
<li><input type="checkbox" checked> Gallery/album proxying</li>
|
||||
<li><input type="checkbox"> Proper order of gallery posts</li>
|
||||
<li><input type="checkbox"> Image description and author info</li>
|
||||
<li><input type="checkbox"> GIFs/videos</li>
|
||||
<li><input type="checkbox"> Comments... maybe</li>
|
||||
</ul>
|
||||
<footer>
|
||||
<small>
|
||||
This website does not claim ownership of any media.
|
||||
<br>This service simply acts as a proxy to Imgur.com and does not store images aside from a temporary cache.
|
||||
<br>Abusive images should be reported to imgur. This website does not create new images/comments.
|
||||
</small>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
14
imgmine/web/theme.css
Normal file
14
imgmine/web/theme.css
Normal file
@ -0,0 +1,14 @@
|
||||
body{
|
||||
background-color: rgba(8, 6, 37, 0.781);
|
||||
color: white;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
a, a:visited {
|
||||
color: white;
|
||||
}
|
||||
|
||||
ul{
|
||||
list-style-type: none;
|
||||
}
|
13
run_tests.sh
Executable file
13
run_tests.sh
Executable file
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
ran=0
|
||||
SECONDS=0 ;
|
||||
close () {
|
||||
exit 10;
|
||||
}
|
||||
|
||||
for f in tests/*.py; do
|
||||
python3 "$f" || close # if needed
|
||||
let "ran++"
|
||||
done
|
||||
echo "ran $ran test files successfully in $SECONDS seconds"
|
||||
rm -f *.dat
|
7
tests/test.py
Normal file
7
tests/test.py
Normal file
@ -0,0 +1,7 @@
|
||||
import unittest
|
||||
class TestBasic(unittest.TestCase):
|
||||
|
||||
def test_basic(self):
|
||||
self.assertTrue(True)
|
||||
|
||||
unittest.main()
|
Loading…
Reference in New Issue
Block a user