Skip to content

Instantly share code, notes, and snippets.

@bkacjios
Last active November 1, 2024 16:06
Show Gist options
  • Save bkacjios/2ab4c0e91476368f20b0 to your computer and use it in GitHub Desktop.
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.
#!/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