---@diagnostic disable: need-check-nil function descriptor() return { title = "VLC Subsonic client", version = "1.0", author = "jrosh", shortdesc = "VLC Subsonic client", description = "Play music from your subsonic server with VLC as client", } end function activate() if (read_config_file()) then open_playlists_dialog() else open_login_dialog() end end function deactivate() dlg:delete() end function open_login_dialog() dlg = vlc.dialog(descriptor().title) dlg:add_label("

Enter your server details

", 1,1,1,1) local host_w = dlg:add_text_input("", 1,2,1,1) local user_w = dlg:add_text_input("", 1,3,1,1) local pass_w = dlg:add_text_input("", 1,4,1,1) local status_label = dlg:add_label("", 1,7,1,1) local test_button dlg:add_button("Test connection", function() local host = host_w:get_text() local user = user_w:get_text() local pass = pass_w:get_text() status_label:set_text("") local result = test_connection(host, user, pass) status_label:set_text(result) end, 1,5,1,1) local save_button dlg:add_button("Save config and login", function() local host = host_w:get_text() local user = user_w:get_text() local pass = pass_w:get_text() --local encryption_key = generateRandomAESKey() --local encrypted_pass = encrypt_password(pass, encryption_key) --write_config_file(host, user, encrypted_pass, encryption_key) write_config_file(host, user, pass) status_label:set_text("") status_label:set_text("Config saved, please reload the extension") dlg:del_widget(host_w, user_w, pass_w, test_button, save_button) open_playlists_dialog() end, 1,6,1,1) dlg:show() end function test_connection(hostname, username, password) local url_s = generate_salt(6) local url_t = calculate_md5(password .. url_s) local url = hostname.."/rest/ping?&u="..username.."&t="..url_t.."&s="..url_s.."&v=1.16.1&c=vlc" local stream = vlc.stream(url) if not stream then vlc.msg.err("Failed to open stream for URL: " .. url) return nil end local chunk_data = "" local testing_chunk = stream:read(1024) if testing_chunk then chunk_data = (chunk_data .. testing_chunk:gsub("\n", "")) else vlc.msg.err("Failed to read data from stream") end local response = chunk_data:match('status="([^"]+)"') return response end function open_playlists_dialog() if not dlg then dlg = vlc.dialog(descriptor().title) end config = read_config_file() -- keep global at this position --password = decrypt_password(config['password'], os.getenv("VLC_SUBSONIC_EXTENSION")) local url_s = generate_salt(6) local url_t = calculate_md5(config["password"] .. url_s) --local url = string.format("%s/rest/getPlaylists?&u=%s&t=%s&s=%s&v=1.16.1&c=vlc", config['hostname'], config['username'], url_t, url_s ) local url = config['hostname'] .. "/rest/getPlaylists?&u=" .. config['username'] .. "&t=" .. url_t .. "&s="..url_s.."&v=1.16.1&c=vlc" local playlist_table = fetch_xml_data(url) local playlists = {} local names = {} local ids = {} -- occurrences of name="value" and 'playlist id="value"' playlist_table:gsub('name="([^"]*)"', function(name) table.insert(names, name) end) playlist_table:gsub('playlist%s+id="([^"]*)"', function(playlist_id) table.insert(ids, playlist_id) end) -- combining names and ids into the playlists table for i = 1, math.max(#names, #ids) do local name = names[i] or "Unnamed" local playlist_id = ids[i] or "No ID" table.insert(playlists, {name = name, playlist_id = playlist_id}) end -- display playlists in the grid local col = 0 local row = 0 for index, playlist in ipairs(playlists) do row = ( index % 10 ) + 2 col = math.ceil(index / 10) dlg:add_button(playlist.name, function() add_to_local_playlist(playlist.playlist_id) end, col, row) end dlg:add_label("

Playlists

", 1,1, col) dlg:show() end function add_to_local_playlist(playlist_id) local url_s = generate_salt(6) local url_t = calculate_md5(config['password'] .. url_s) local playlist_url = config['hostname'] .. "/rest/getPlaylist?id="..playlist_id.."&u=".. config['username'] .."&t="..url_t.."&s="..url_s.."&v=1.16.1&c=vlc" local songs_table = fetch_xml_data(playlist_url) vlc.playlist.clear() for song in string.gmatch(songs_table, "") do local id = string.match(song, 'id="([^"]-)"') local title = string.match(song, 'title="([^"]-)"') local url_s = generate_salt(6) local url_t = calculate_md5(config['password'] .. url_s) local song_url = config['hostname'] .. "/rest/stream?id=".. id .."&u=".. config['username'] .."&t="..url_t.."&s="..url_s.."&v=1.16.1&c=vlc" vlc.playlist.add({{path=song_url, title=title}}) end --vlc.playlist.goto(0) <- doesn't launch the extension. goto() was added as an internal lua function from 5.2 but VLC sould use 5.1. idk whats going on end function fetch_xml_data(url) local stream = vlc.stream(url) if not stream then vlc.msg.err("Failed to open stream for URL: " .. url) return nil end local xml_data = "" -- Read a initial chunk of data to see if we get a response local initial_chunk = stream:read(1024) -- Read the first 1024 bytes if initial_chunk then xml_data = (xml_data .. initial_chunk:gsub("\n", "")) -- Append the initial chunk else vlc.msg.err("Failed to read data from stream") end -- Continue reading until EOF while true do local chunk = stream:read(1024) -- Read more data in chunks if not chunk or chunk == "" then break end xml_data = (xml_data .. chunk:gsub("\n", "")) -- Append each chunk to the complete data end return xml_data end function calculate_md5(str) local command = string.format("echo -n %s | openssl dgst -md5", str) local handle = io.popen(command) local md5hash = handle:read("*a") handle:close() return md5hash:match("= (%x+)") end function generate_salt(length) local salt = "" for i = 1, length do local char = string.char(math.random() < 0.33 and math.random(48, 57) or math.random() < 0.66 and math.random(65, 90) or math.random(97, 122)) salt = salt .. char end return salt end function file_exists(filename) local file = io.open(filename, "rb") if file then file:close() end return file ~= nil end function read_config_file() local config_file = vlc.config.configdir() .. "/vlc_subsonic.conf" local config = {} local file = io.open(config_file, "r") if not file then return nil end for line in file:lines() do local key, value = line:match("([^=]+)=([^=]*)") if key and value then key = key:match("^%s*(.-)%s*$") value = value:match("^%s*(.-)%s*$") config[key] = value end end file:close() return config end function write_config_file(hostname, username, password) local config_file = vlc.config.configdir() .. "/vlc_subsonic.conf" local file, err = io.open(config_file, "w") if file then io.output(file) io.write("hostname=" .. hostname .. "\n") io.write("username=" .. username .. "\n") io.write("password=" .. password) file:close() else vlc.msg.err("Error: Could not open file for writing. " .. err) end end -- not in use from here function encrypt_password(password, key) print(password .. " -> " .. key) local command = string.format("echo -n '%s' | openssl enc -base64 -aes-256-cbc -e -pass pass:%s -pbkdf2", password, key) local handle = io.popen(command) local encryptedPassword = handle:read("*a") handle:close() return encryptedPassword:gsub("%s+", "") end function decrypt_password(encryptedPassword, key) print(encryptedPassword .. " -> " .. key) local command = string.format("echo '%s' | openssl enc -base64 -aes-256-cbc -d -pass pass:%s -pbkdf2", encryptedPassword, key) local handle = io.popen(command) local decryptedPassword = handle:read("*a") handle:close() return decryptedPassword:gsub("%s+", "") end function generateRandomAESKey() local key = {} math.randomseed(os.time()) for i = 1, 16 do local byte = math.random(0, 255) table.insert(key, string.char(byte)) end return table.concat(key) end function set_or_update_env_var(var_value) --doesnt work (writes new key each time) local homerc = vlc.config.homedir() .. "/.bashrc" local file = io.open(homerc, "r") local rccontent = file:read("*a") file:close() local new_env_var = string.format("export VLC_SUBSONIC_EXTENSION=\"%s\"", var_value) if rccontent:find("^export VLC_SUBSONIC_EXTENSION=") then rccontent = rccontent:gsub("export VLC_SUBSONIC_EXTENSION=\"[^\"]*\"", new_env_var) else rccontent = rccontent .. "\n" .. new_env_var end local file = io.open(homerc, "w") file:write(rccontent) file:close() end