7 Commits
v1.0 ... v3.0

Author SHA1 Message Date
16f32bdcdd complete refactor see README for more info 2024-07-27 09:26:54 -04:00
878f0faaf7 add some nil checks, and make lssi work with eatit changes 2023-10-17 22:24:49 -04:00
6f091a7648 Major changes!
- add name expansion gh:po5/thumbfast -> https://github.com/po5/thumbfast
- allow multiple files to be downloaded to one dir
- add pin option to stop updates on specific plugins
2023-10-17 22:03:41 -04:00
96c3e6f3e6 add nil checks 2023-10-15 07:04:07 -04:00
25765c4ace fix indentation 2023-10-15 07:02:40 -04:00
b6dff07a12 fix typo and make commands appear nicer 2023-07-28 19:24:44 -04:00
af125691f7 add bootstrap script and better instructions for installation 2023-07-28 19:23:26 -04:00
9 changed files with 387 additions and 422 deletions

View File

@ -1,6 +1,6 @@
Eat It - a Mpv plugin manager
Copyright © 2023 squibid
Copyright (c) 2024 squibid
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@ -1,29 +1,63 @@
# Eat It
Eat It is a plugin manager for mpv which allows you to declare what files you
want from git repos.
Preface - eatit is a "plugin manager" for mpv it's sole purpose is to provide a
declerative way to install and update your plugins. Loading the plugins is done
by mpv itself.
## Installation
put this script into your ~/.config/mpv/scripts/ directory:
```lua
-- install eat-it on startup
local mp = require("mp")
local utils = require("mp.utils")
local path = mp.comand_native({ "expand-path", "~~/scripts/eat-it" })
## Installing Eat It
To install Eat It simply put the `eatit.lua` file into your mpv scripts
directory (~/.config/mpv/scripts by default on linux). After you have done that
Eat It is installed, however it won't do anything without the `eatit-cfg.lua`,
which has to be placed in the root of your mpv config dir (~/.config/mpv by
default on linux).
Alternatively you can run these two commands:
```sh
curl -L https://git.squi.bid/eat-it/plain/eatit.lua -o ~/.config/mpv/scripts/eatit.lua
curl -L https://git.squi.bid/eat-it/plain/eatit-cfg.lua -o ~/.config/mpv/eatit-cfg.lua
if not utils.readdir(path) then
mp.command_native_async({
name = "subprocess",
playback_only = false,
args = { "git", "clone", "--depth=1", "https://git.squi.bid/dep", path }
})
end
```
## Setup
in ~/.config/mpv/eatit-cfg.lua put:
```lua
return {
-- list of packages
}
```
### Package Spec
```lua
{
-- [string] Specifies the full name of the package (required)
"user/package",
-- [string] Overrides the url of the git repo to clone
-- by default eatit tries https://github.com/user/package.git
url = "",
-- [boolean] whether to ignore updates
pin = true,
-- [string] git branch to clone
branch = "",
-- [table] table of files to copy
files = {
[""] = ""
},
-- [function] code to run when installing/updating the package note due to how
-- mpv works eatit cannot change the working directory while running the setup
-- function
setup = function()
end
}
```
## FAQ
Q: Where does the name 'Eat It' come from?
Q: Where does the name "Eat It" come from?
A: The plugin manager eats all the useless files and keeps the ones you want,
*and Weird Al is a funny guy*
## TODO
- make an auto install script that can be put into a mpv config
~~also a refrence to Weird Al's song "Eat It"~~
## ALTERNATIVES
[email me](mailto:me@zacharyscheiman.com) if you know of any alternatives
[email me](mailto:me@zacharyscheiman.com) if you know of any other alternatives
- [mpv_manager](https://github.com/po5/mpv_manager)

View File

@ -1,40 +0,0 @@
--[[
Eat It - a Mpv plugin manager
Copyright © 2023 squibid
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
]]
-- NOTE: The variables in this file need to be global in order to be read
-- after being called with dofile()
plugins = { -- the plugins you want to load
{ 'https://git.squi.bid/eat-it', -- required, specifies the git repo
file = 'eatit.lua', -- required, specifies the desired file
dir = 'scripts', -- optional, sets the dest dir
branch = 'master', -- optional, sets the desired branch
},
}
-- options for eat it
opts = {
bind = 'U',
logging = { -- options for logging
log = true,
logdate = '[%H:%M:%S]:',
logfile = '~~/eatit.log',
},
dl = { -- options for dealing with the git repos
dir = '/tmp/mpv-eatit',
powerwash = false, -- if true the dl dir gets deleted after mpv closes
}
}

184
eatit.lua
View File

@ -1,184 +0,0 @@
--[[
Eat It - a Mpv plugin manager
Copyright © 2023 squibid
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
]]
local mp = require('mp')
-- load the config file
dofile(mp.command_native({'expand-path', '~~/eatit-cfg.lua'}))
-- helper functions --
local function tablelength(T)
local count = 0
for _ in pairs(T) do count = count + 1 end
return count
end
local function fileexists(name)
local ok, err, code = os.rename(name, name)
if not ok then
if code == 13 then
-- Permission denied, but it exists
return true
end
end
return ok, err
end
local function testforslash(str)
if string.match(str, '/') then
return string.match(str, '/([^/]+)$')
else
return str
end
end
local function run(cmd)
local x = io.popen(cmd)
local y = x:read("*a")
x:close()
return y
end
local function openlog()
if opts.logging.log then -- log if asked to
-- get our logfile's full path
fn = mp.command_native({'expand-path', opts.logging.logfile})
f = io.open(fn, 'a') -- open file buffer
io.output(f) -- set it as default
end
end
local function logwrite(string)
if opts.logging.log then
io.write(os.date(opts.logging.logdate) .. ' ' .. string .. '\n')
end
end
local function closelog()
if opts.logging.log then
io.close(f)
end
end
-- get the requested git repos
local function clonegit(plugdir, i)
logwrite('downloading ' .. plugins[i][1])
-- clone the repo
-- BUG: logwriting the git command doens't actually log the output
run('git -C ' .. opts.dl.dir .. ' clone ' .. plugins[i][1])
run('git -C ' .. plugdir .. ' checkout -q ' .. plugins[i]['branch'])
end
-- check for updates
local function checkupdates(plugdir)
local localhash = run('git -C ' .. plugdir .. ' log -1 --format=format:"%H"')
local remotehash = run('git -C ' .. plugdir .. ' rev-parse $(git -C ' ..
plugdir .. ' branch -r) | tail -1')
if localhash ~= remotehash then return true else return false end
end
-- start install
local function startinstall()
-- let the user know that we are starting install
logwrite('Starting Download...')
mp.osd_message('Downloading plugins!')
-- start iterating through plugins
for i = 1, tablelength(plugins) do
-- check if the user has defined a file for the current plugin
if not plugins[i]['file'] then
logwrite('WARNING! Git repo "' .. plugins[i][1] ..
'" doesn\'t have a specified file. Skipping download')
-- FIXME: this results in the last plugin being downloaded & copied twice
i = i + 1 -- if not we go to the next plugin
end
-- if no destdir defined we set it to the root of the mpv config dir
if not plugins[i]['dir'] then
plugins[i]['dir'] = '~~/'
end
-- get the plugins tmp download dir
local plugdir = opts.dl.dir .. '/' ..
string.match(plugins[i][1], '/([^/]+)$'):gsub('.git', '')
-- if no specified branch we use the default
if not plugins[i]['branch'] then
plugins[i]['branch'] = testforslash(
run('git -C ' .. plugdir .. ' symbolic-ref refs/remotes/origin/HEAD')
)
end
-- get the file's dir
local pluginfile = opts.dl.dir .. '/' ..
string.match(plugins[i][1], '/([^/]+)$'):gsub('.git', '') ..
'/' .. plugins[i]['file']
-- get the dest dir
local destfile = mp.command_native({'expand-path', '~~/'}) ..
'/' .. plugins[i]['dir'] ..
'/' .. testforslash(plugins[i]['file'])
if fileexists(plugdir .. '/') then
-- if we need to update, update
if checkupdates(plugdir) then
logwrite(plugins[i]['file'] .. ' is updating.')
-- make sure we are on the main branch
run('git -C ' .. plugdir .. ' checkout -q ' .. plugins[i]['branch'])
run('git -C ' .. plugdir .. ' pull') -- get the latest commits
else
logwrite(plugins[i]['file'] .. ' is up to date!')
end
else
clonegit(plugdir, i)
end
-- copy the file contents over to the desired location
local infile = io.open(pluginfile, 'r')
local outfile = io.open(destfile, 'w')
outfile:write(infile:read('*a'))
outfile:close()
infile:close()
end
end
local function initupdate()
openlog()
logwrite('# of plugins defined in table: ' .. tablelength(plugins))
os.execute('mkdir -p ' .. opts.dl.dir) -- make download dir
startinstall()
-- closing/removing everything
if opts.dl.powerwash == true then
logwrite('powerwashing the tmp dir "' .. opts.dl.dir .. '"')
os.execute('rm -rf ' .. opts.dl.dir)
end
closelog()
end
-- remove logfile on startup
if opts.logging.log then
openlog()
os.remove(fn)
closelog()
end
mp.add_key_binding(opts.bind, 'UpdatePlugins', initupdate)

247
main.lua Normal file
View File

@ -0,0 +1,247 @@
-- Copyright (c) 2024 squibid, see LICENSE file for more info
local mp = require('mp')
local msg = require('mp.msg')
local utils = require('mp.utils')
local proc = require('proc')
-- load the config file
local config = dofile(mp.command_native({"expand-path", "~~/eatit-cfg.lua"}))
if not config or type(config) ~= "table" then
msg.fatal("no config provided, bailing out")
return
end
local base_dir = mp.command_native({"expand-path", "~~cache/plugins"})
local packages = {}
-- make sure the base directory exists (*nix only)
if utils.file_info(base_dir) == nil then
proc.exec({ "mkdir", "-p", base_dir }, {}, function(err, message)
if err then
msg.fatal(string.format("failed to create plugin directory: %s", err))
return
end
end)
end
--- regiester a new package spec
---@param spec table package spec from config
---@return table package
local function register_pkg(spec)
if type(spec) ~= "table" then
spec = { spec }
end
local id = spec[1]
local package = packages[id]
if not package then
package = {
id = id,
exists = false,
setup = false
}
packages[id] = package
end
package.name = string.sub(package.id, string.find(package.id, "%/") + 1, #package.id)
package.url = spec.url or ("https://github.com/"..package.id..".git")
package.branch = spec.branch
package.files = spec.files
package.dir = package.files and utils.join_path(base_dir, package.name) or utils.join_path(mp.command_native({ 'expand-path', "~~/scripts" }), package.name)
package.pin = spec.pin
package.exists = utils.file_info(package.dir) ~= nil
-- validate that all files have been installed
if type(package.files) == "table" then
for filename, dest in pairs(package.files) do
if not utils.file_info(utils.join_path(package.dir, filename)) or
not utils.file_info(utils.join_path(mp.command_native({ "expand-path", dest }), filename)) then
package.exists = false
break
end
end
end
package.on_setup = spec.setup
return package
end
--- run package setup
---@param package table package
local function setup_package(package)
if type(package.on_setup) ~= "function" then
return
end
local ok, err = pcall(package.on_setup, package.dir)
if not ok then
msg.warn(string.format("error when running setup on '%s': %s", package.id, err))
return
end
package.setup = true
end
--- copy all files according to package spec
---@param package table package
local function copy_files(package)
--- copy src to dest
---@param src string path to src file
---@param dest string path to dest file
local function cp(src, dest)
local i = io.open(src, 'r')
if not i then return end
local o = io.open(dest, 'w')
if not o then return end
o:write(i:read('*a'))
o:close()
i:close()
end
if type(package.files) == "table" then
for name, loc in pairs(package.files) do
local path = mp.command_native({'expand-path', loc})
local dest = utils.join_path(path, name)
local src = utils.join_path(package.dir, name)
if not utils.file_info(src) then
msg.warn(string.format("file %s not found", name))
return
end
local ok, err = pcall(cp, src, dest)
if not ok then
msg.warn(string.format("failed to copy %s: %s", name, utils.to_string(err)))
end
end
end
end
local function validate_package()
end
--- download or update package
---@param package table package
---@param cb function callback
local function sync(package, cb)
if package.exists then
if package.pin then
cb()
return
end
--- generic error
---@param err any error
local function log_err(err)
msg.error(string.format("failed to update %s; reason: %s", package.id, err))
end
-- get current head commit hash
proc.git_rev_parse(package.dir, "HEAD", function(err, before)
if err then
log_err(before)
cb(err)
return
end
-- get the latest commit hash
proc.git_fetch(package.dir, "origin", package.branch or "HEAD", function(err, message)
if err then
log_err(message)
cb(err)
return
end
-- check the latest and current against eachother
proc.git_rev_parse(package.dir, "FETCH_HEAD", function(err, after)
if err then
log_err(after)
cb(err)
return
elseif before == after then
msg.info(string.format("skipped %s", package.id))
cb(err)
return
end
-- switch HEAD to new commit
proc.git_reset(package.dir, after, function(err, message)
if err then
log_err(message)
return
end
setup_package(package)
copy_files(package)
msg.info(string.format("updated %s; %s -> %s", package.id, before, after))
cb(err)
end)
end)
end)
end)
else
-- clone repo since it doesn't exist
proc.git_clone(package.dir, package.url, package.branch, function(err, message)
if err then
msg.error(string.format("failed to install %s; reason: %s", package.id, utils.to_string(message)))
else
setup_package(package)
copy_files(package)
package.exists = true
msg.info(string.format("installed %s", package.id))
end
cb(err)
end)
end
end
--- sync a list of plugins
---@param list table list of packages
---@param cb function callback
local function sync_list(list, cb)
local progress = 0
for i in pairs(list) do
sync(list[i], function()
progress = progress + 1
cb()
end)
end
end
--- check if package spec should be synced
---@param package table package
---@return boolean
local function should_sync(package)
if config.sync == "new" or config.sync == nil then
return not package.exists
else
return config.sync == "always"
end
end
-- register all packages
for i = 1, #config do
local ok, err = pcall(register_pkg, config[i])
if not ok then
msg.warn(string.format("%s: %s", err, config[i].as))
end
end
-- check for package updates
local targets = {}
for i in pairs(packages) do
if should_sync(packages[i]) then
targets[#targets + 1] = packages[i]
end
end
sync_list(targets, function() end)
-- register script message for keybinding
mp.register_script_message("eatit-sync", function(name, value)
sync_list(packages, function() end)
end)

View File

@ -1,28 +0,0 @@
# Modules
Modules are extentions to the EatIt plugin manager that aren't necessary for
running it. Every module will be in it's own sub directory in order to keep
documentation relative to the individual modules.
## List of modules
- `lssi.lua` Lua Script Script Injector for modifing files post install
## Installing
Installing a module can be done through EatIt like so:
```lua
plugins = { -- the plugins you want to load
{ 'https://git.squi.bid/eat-it',
file = 'modules/moddir/module.lua',
dir = 'scripts',
},
}
```
## For Devs
Please add a table in the global scope called 'mod' as this may be used in the
future. It should look like this:
```lua
mod = {
version = 'version',
author = 'autor name',
}
```

View File

@ -1,16 +0,0 @@
# Using lssi
Any plugin can be modified by lssi by doing the following in the eatit-cfg.lua:
```lua
plugins = { -- the plugins you want to load
{ 'https://git.squi.bid/eat-it',
file = 'eatit.lua',
dir = 'scripts',
lssi = {
-- code line
{ 'print("hello world")', 'G' },
}
},
}
```
The line option can be 'g' for top of file, 'G' for bottom of file, or any
number in between.

View File

@ -1,133 +0,0 @@
--[[
Eat It - a Mpv plugin manager
Copyright © 2023 squibid
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
]]
-- NOTE: This is a POC and will most likely be reimplimented using diff files
-- with git
-- or we might want to generate a diff file from the requested changes
-- and check if the diffs match in content
--[[
Lua Script Script Injector
Takes an input file and code then, it outputs a file with your code in there.
]]--
local mp = require('mp')
mod = {
version = 'ALPHA 1', -- the current version of lssi
author = 'squibid',
}
-- load the eatit config file
dofile(mp.command_native({'expand-path', '~~/eatit-cfg.lua'}))
-- helper functions
local function tablelength(T)
local count = 0
for _ in pairs(T) do count = count + 1 end
return count
end
local function fileexists(name)
local f = io.open(name, 'r')
if f ~= nil then io.close(f) return true else return false end
end
local function logwrite(string)
if opts.logging.log then
io.write(os.date(opts.logging.logdate) .. ' ' .. string .. '\n')
end
end
local function openlog()
if opts.logging.log then -- log if asked to
-- get our logfile's full path
fn = mp.command_native({'expand-path', opts.logging.logfile})
f = io.open(fn, 'a') -- open file buffer
io.output(f) -- set it as default
end
end
local function closelog()
if opts.logging.log then
io.close(f)
end
end
local function inject(infile, l, outfile)
local inf = io.open(infile, 'r')
local infcont = {}
for i in inf:lines() do
table.insert(infcont, i)
end
inf:close()
-- don't do anything if there is already code injected into the file
if string.find(infcont[1], "-- code injected by lssi") then
logwrite('code is already injected into ' .. infile)
return
end
logwrite('Injecting code into ' .. infile)
for i in pairs(l) do
-- add requested line below existing line
if l[i][2] == 'G' then
infcont[tablelength(infcont)] = infcont[tablelength(infcont)] .. '\n' .. l[i][1]
elseif l[i][2] == 'g' then
infcont[1] = l[i][1] .. '\n' .. infcont[1]
else
infcont[l[i][2]] = (infcont[l[i][2]]) .. '\n' .. l[i][1]
end
end
local outf = io.open(outfile, 'w')
-- we inject metadata to prevent writing to the file more than once
infcont[1] = "-- code injected by lssi " .. mod.version .. '\n' .. infcont[1]
for i, v in ipairs(infcont) do
outf:write(v .. '\n')
end
io.close(outf)
end
local function checkandinject()
openlog()
for i = 1, tablelength(plugins) do
-- get the file we want to inject our code into
local f = mp.command_native({'expand-path', '~~/'}) ..
'/' .. plugins[i]['dir'] ..
'/' .. plugins[i]['file']
-- check if the plugin has been configured with lssi
if plugins[i]['lssi'] then
-- and the file we are trying to modify actually exists
if fileexists(f) then
-- inject it! no going back now
inject(f, plugins[i]['lssi'], f)
else
logwrite('Failed to inject code into "' ..
plugins[i]['file'] .. '" file does not exist')
end
end
end
closelog()
end
checkandinject()

85
proc.lua Normal file
View File

@ -0,0 +1,85 @@
-- Copyright (c) 2024 squibid, see LICENSE file for more info
local mp = require('mp')
local M = {}
--- run a system binary
---@param args table command with it's options
---@param env table key value pair of envvars
---@param cb function callback
function M.exec(args, env, cb)
local res_env = {}
for i, v in pairs(env) do
res_env[#res_env + 1] = i.."="..v
end
--- run callback
---@param success boolean if the command ran successfully
---@param result table|nil results
---@param error string|nil error string or nil
local function callback(success, result, error)
local output
if result then
-- combine both stdout and stderr
output = result.stdout..result.stderr
end
cb(not success, output)
end
mp.command_native_async({
name = "subprocess",
playback_only = false,
capture_stdout = true,
capture_stderr = true,
env = res_env,
args = args
}, callback)
end
---@type table git environment
local git_env = { GIT_TERMINAL_PROMPT = 0 }
--- git rev parse
---@param dir string directory
---@param arg string arg
---@param cb function callback
function M.git_rev_parse(dir, arg, cb)
local cmd = { "git", "-C", dir, "rev-parse", "--short", arg }
M.exec(cmd, git_env, cb)
end
--- git clone
---@param dir string directory
---@param url string url
---@param branch string branch
---@param cb function callback
function M.git_clone(dir, url, branch, cb)
local cmd = { "git", "clone", "--depth=1", "--recurse-submodules", "--shallow-submodules", url, dir }
if branch then
cmd[#cmd + 1] = "--branch="..branch
end
M.exec(cmd, git_env, cb)
end
--- git fetch
---@param dir string directory
---@param remote string remote
---@param refspec string refspec
---@param cb function callback
function M.git_fetch(dir, remote, refspec, cb)
local cmd = { "git", "-C", dir, "fetch", "--depth=1", "--recurse-submodules", remote, refspec }
M.exec(cmd, git_env, cb)
end
--- git reset
---@param dir string dir
---@param treeish string treeish
---@param cb function callback
function M.git_reset(dir, treeish, cb)
local cmd = { "git", "-C", dir, "reset", "--hard", "--recurse-submodules", treeish, "--" }
M.exec(cmd, git_env, cb)
end
return M