Last active
November 1, 2024 16:06
-
-
Save bkacjios/2ab4c0e91476368f20b0 to your computer and use it in GitHub Desktop.
A simple command line tool for uploading files to imgur, whether it be through an account or anonymously.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/luajit | |
local https = require"ssl.https" | |
local json = require"json" | |
local ltn12 = require"ltn12" | |
local socket = require"socket" | |
local url = require"socket.url" | |
local lgi = require"lgi" | |
local mime = require"mime" | |
local GLib = lgi.require("GLib") | |
local notify = lgi.require("Notify") | |
notify.init("imgur") | |
math.randomseed(socket.gettime()) | |
local HOME = os.getenv("HOME") | |
local imgur = { | |
client_id = "bc79e7073db68d0", | |
client_secret = "046f28f131f882bebf6ca1cef72b563d3ee60e81", | |
-- BAD TOUCH | |
copy_url = false, | |
open_url = false, | |
save_path = nil, | |
login = false, | |
album = nil, | |
credentials_file = HOME .. "/.config/imgur-upload.conf", | |
credentials = {}, | |
files = {}, | |
version = "0.4", | |
debug = true, | |
notification = nil, | |
upload = { | |
start = socket.gettime(), | |
speed = 0, | |
samples = 0, | |
last_percent = 0, | |
size = 0, | |
uploaded = 0, | |
} | |
} | |
function debug(message) | |
if imgur.debug then | |
print(message) | |
end | |
end | |
local charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" | |
function imgur.random_string(len) | |
local str = "" | |
for i=1,len or 10 do | |
local char = math.random(1,#charset) | |
str = str .. string.sub(charset, char, char) | |
end | |
return str | |
end | |
function imgur.file_exists(name) | |
local f = io.open(name,"r") | |
if f ~= nil then io.close(f) return true else return false end | |
end | |
function imgur.tmp_file(folder,filename) | |
local filename = filename or (imgur.random_string() .. ".png") | |
local path = string.format("%s/%s", folder, filename) | |
if imgur.file_exists(path) then | |
-- Try to generate a new random file | |
return imgur.tmp_file(folder,nil) | |
end | |
return io.open(path,"wb") | |
end | |
function imgur.notify_start() | |
imgur.notification = notify.Notification.new("Imgur Uploader", "Starting upload..", "shotwell") | |
imgur.notification:show() | |
imgur.upload.start = socket.gettime() | |
imgur.upload.speed = 0 | |
imgur.upload.samples = 0 | |
imgur.upload.last_percent = 0 | |
end | |
function imgur.print_progress(cur, max) | |
local maxbars = 30 | |
local perc = cur/max | |
local bars = string.rep("=", math.floor(perc*maxbars)) | |
local spaces = string.rep(" ", math.ceil((1-perc)*maxbars)) | |
-- Stuff to debug upload speed | |
imgur.upload.samples = imgur.upload.samples + 1 | |
imgur.upload.speed = cur / (socket.gettime() - imgur.upload.start) | |
local percNum = math.floor(perc*100) | |
if imgur.upload.last_percent ~= percNum then -- Only update when a new bar is needed | |
io.stdout:write(("%4s%% [%s>%s] %4.1f Mbps %s"):format(percNum, bars, spaces, imgur.upload.speed/125000, percNum < 100 and "\r" or "\n")) | |
io.stdout:flush() | |
imgur.upload.last_percent = percNum | |
end | |
end | |
function imgur.notify_percent(cur, max) | |
imgur.print_progress(cur, max) | |
if not imgur.notification then return end | |
local var = GLib.Variant("i", math.floor(cur/max*100)) | |
--imgur.notification:set_hint_int32("value", math.floor(cur/max*100)) | |
imgur.notification:set_hint("value", var) | |
imgur.notification:show() | |
end | |
function imgur.notify_end(title, message) | |
if not imgur.notification then return end | |
imgur.notification:clear_hints() | |
imgur.notification.summary = title | |
imgur.notification.body = message | |
imgur.notification:show() | |
end | |
function imgur.encodepost(args) | |
if args == nil or next(args) == nil then -- no args or empty args? | |
return "" | |
end | |
local strp = "" | |
for key, vals in pairs(args) do | |
if type(vals) ~= "table" then | |
vals = {vals} | |
end | |
for i,val in ipairs(vals) do | |
if type(val) == "boolean" then | |
strp = strp.."&"..key.."="..( val and 1 or 0 ) | |
elseif val and #val > 0 then | |
strp = strp.."&"..key.."="..url.escape(val) | |
end | |
end | |
end | |
-- remove first & | |
return string.sub(strp,2) | |
end | |
function imgur.load_token() | |
local f, err = io.open(imgur.credentials_file, "r") | |
if not f then return end | |
local credentials = {} | |
local pattern = "([%w_]+)%s*=%s*([%w_]+)" | |
for k, v in string.gmatch(f:read("*all"), pattern) do | |
credentials[k] = v | |
end | |
f:close() | |
return credentials | |
end | |
function imgur.check_token() | |
imgur.credentials = imgur.load_token() | |
local time = os.time() | |
if imgur.credentials and imgur.credentials.refresh_token then | |
if time > tonumber(imgur.credentials.token_expire_time) then | |
imgur.refresh_token() | |
end | |
else | |
imgur.aquire_token() | |
end | |
end | |
function imgur.refresh_token() | |
local data = imgur.post("https://api.imgur.com/oauth2/token", { | |
refresh_token = imgur.credentials.refresh_token, | |
client_id = imgur.client_id, | |
client_secret = imgur.client_secret, | |
grant_type = "refresh_token", | |
}) | |
if not data.status then | |
imgur.credentials = { | |
access_token = data.access_token, | |
refresh_token = data.refresh_token, | |
token_expire_time = os.time() + data.expires_in, | |
} | |
end | |
imgur.save_token() | |
end | |
function imgur.aquire_token() | |
local authorize_url = ("https://api.imgur.com/oauth2/authorize?client_id=%s&response_type=pin"):format(imgur.client_id) | |
print "Please go to" | |
print(authorize_url) | |
print "and grant access to this application." | |
io.stdout:write("Enter PIN: ") | |
local pin = io.read() | |
print("PIN", pin) | |
local data = imgur.post("https://api.imgur.com/oauth2/token", { | |
client_id = imgur.client_id, | |
client_secret = imgur.client_secret, | |
grant_type = "pin", | |
pin = pin, | |
}) | |
if not data.status then | |
imgur.credentials = { | |
access_token = data.access_token, | |
refresh_token = data.refresh_token, | |
token_expire_time = os.time() + data.expires_in, | |
} | |
end | |
imgur.save_token() | |
end | |
function imgur.save_token() | |
local f = assert(io.open(imgur.credentials_file, "w")) | |
local data = {} | |
for k,v in pairs(imgur.credentials) do | |
table.insert(data, ("%s = %s"):format(k,v)) | |
end | |
f:write(table.concat(data, "\n")) | |
f:close() | |
end | |
local chunk, src_err | |
local ret, snk_err | |
-- Allows us to calculate how far along in the upload we are | |
function imgur.step(src, snk) | |
chunk, src_err = src() | |
ret, snk_err = snk(chunk, src_err) | |
if chunk and ret then | |
imgur.upload.uploaded = imgur.upload.uploaded + #chunk | |
imgur.notify_percent(imgur.upload.uploaded, imgur.upload.size) | |
return 1 | |
else return nil, src_err or snk_err end | |
end | |
function imgur.upload_image(image) | |
local parameters = imgur.encodepost({ | |
["image"] = mime.b64(image), | |
["type"] = "base64", | |
["album"] = imgur.album, | |
}) | |
imgur.upload.size = #parameters | |
imgur.upload.uploaded = 0 | |
print(("Uploading file (%.2f MB)"):format(#parameters/125000)) | |
local token = imgur.credentials.access_token | |
local response = {} -- for the response body | |
local result, respcode, respheaders, respstatus = https.request{ | |
url = "https://api.imgur.com/3/image", | |
method = "POST", | |
source = ltn12.source.string(parameters), | |
headers = { | |
["content-length"] = tostring(#parameters), | |
["content-type"] = "application/x-www-form-urlencoded", | |
["Authorization"] = imgur.login and ("Bearer " .. token) or ("Client-ID " .. imgur.client_id), | |
}, | |
step = imgur.step, | |
sink = ltn12.sink.table(response), | |
} | |
local resp = table.concat(response) | |
debug(("Upload response: %s"):format(resp)) | |
return json.decode(resp) | |
end | |
function imgur.post(url, data) | |
data = imgur.encodepost(data) | |
local response = {} -- for the response body | |
local result, respcode, respheaders, respstatus = https.request{ | |
url = url, | |
method = "POST", | |
source = ltn12.source.string(data), | |
headers = { | |
["content-length"] = tostring(#data), | |
["content-type"] = "application/x-www-form-urlencoded", | |
}, | |
sink = ltn12.sink.table(response), | |
} | |
return json.decode(table.concat(response)) | |
end | |
function imgur.error(message) | |
io.stderr:write(("%s\n"):format(message)) | |
end | |
function imgur.notify(title, message) | |
os.execute(("notify-send -i shotwell %q %q"):format(title, message)) | |
end | |
function imgur.print_help(prog) | |
print("Usage: " .. prog .. " [options] [files ...]" .. [[ | |
Options | |
-V, --version Output the version number | |
-h, --help Show this help text | |
-, --stdin Upload raw image data from stdin | |
-l, --login Connect to an imgur account | |
-a, --album Upload the images to an album | |
-c, --clipboard Copy the image URL to clipboard | |
-o, --open Open the image URL in your browser | |
-s, --save Save the image to a file | |
-d, --debug Debug messages]]) | |
end | |
function main(args) | |
local prog = args[0]:match("([^/\\]+)$") | |
if #args <= 0 then | |
imgur.print_help(prog) | |
return | |
end | |
local arg | |
local skip = false | |
for i=1,#args do | |
if not skip then | |
arg = args[i] | |
if arg == "-h" or arg == "--help" then | |
imgur.print_help(prog) | |
elseif arg == "-V" or arg == "--version" then | |
print("version: " .. imgur.version) | |
elseif arg == "-l" or arg == "--login" then | |
imgur.login = true | |
elseif arg == "-a" or arg == "--album" then | |
skip = true | |
imgur.album = args[i+1] | |
print("Upload images to album: " .. imgur.album) | |
elseif arg == "-c" or arg == "--clipboard" then | |
imgur.copy_url = true | |
elseif arg == "-o" or arg == "--open" then | |
imgur.open_url = true | |
elseif arg == '-s' or arg == "--save" then | |
skip = true | |
imgur.save_path = args[i+1] | |
print("Save images to path: " .. imgur.save_path) | |
elseif arg == '-' or arg == "--stdin" then | |
table.insert(imgur.files, io.stdin) | |
elseif arg == "-d" or arg == "--debug" then | |
imgur.debug = true | |
else | |
local file, err = io.open(arg, "rb") | |
if file then | |
table.insert(imgur.files, file) | |
else | |
imgur.error(err) | |
end | |
end | |
else | |
skip = false | |
end | |
end | |
if imgur.login then | |
imgur.check_token() | |
end | |
for _,file in pairs(imgur.files) do | |
local raw = file:read("*all") | |
file:close() | |
if #raw > 0 then | |
imgur.notify_start() | |
local filename | |
local result = imgur.upload_image(raw) | |
if result.success then | |
print "Upload complete.." | |
if imgur.copy_url then | |
os.execute(("echo -n %q | xclip -selection clipboard"):format(result.data.link)) | |
end | |
filename = result.data.link:match("([^/\\]+)$") | |
imgur.notify_end("Upload complete", result.data.link) | |
print(result.data.link) | |
if imgur.open_url then | |
os.execute(("xdg-open %q"):format(result.data.link)) | |
end | |
else | |
imgur.notify_end("Upload failed", result.data.error or "No result data") | |
imgur.error(("Upload failed: %s"):format(result.data.error)) | |
end | |
if imgur.save_path then | |
local folderpath = string.format("%s/%s", imgur.save_path, os.date("%Y-%m")) | |
os.execute(string.format("mkdir -p %q", folderpath)) | |
-- If we failed to upload, we generate our own filename | |
local tmp = imgur.tmp_file(folderpath,filename) | |
if tmp then | |
tmp:write(raw) | |
tmp:close() | |
else | |
imgur.error(("Save failed: %s"):format(err)) | |
end | |
end | |
end | |
end | |
end | |
main(arg) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment