summary refs log tree commit diff
path: root/dot_config/mpv/scripts/playlistmanager.lua
diff options
context:
space:
mode:
Diffstat (limited to 'dot_config/mpv/scripts/playlistmanager.lua')
-rw-r--r--dot_config/mpv/scripts/playlistmanager.lua990
1 files changed, 990 insertions, 0 deletions
diff --git a/dot_config/mpv/scripts/playlistmanager.lua b/dot_config/mpv/scripts/playlistmanager.lua
new file mode 100644
index 0000000..baa7c6d
--- /dev/null
+++ b/dot_config/mpv/scripts/playlistmanager.lua
@@ -0,0 +1,990 @@
+local settings = {
+
+  -- #### FUNCTIONALITY SETTINGS
+
+  --navigation keybindings force override only while playlist is visible
+  --if "no" then you can display the playlist by any of the navigation keys
+  dynamic_binds = true,
+
+  -- to bind multiple keys separate them by a space
+  key_moveup = "UP",
+  key_movedown = "DOWN",
+  key_selectfile = "RIGHT LEFT",
+  key_unselectfile = "",
+  key_playfile = "ENTER",
+  key_removefile = "BS",
+  key_closeplaylist = "ESC",
+
+  --replaces matches on filenames based on extension, put as empty string to not replace anything
+  --replace rules are executed in provided order
+  --replace rule key is the pattern and value is the replace value
+  --uses :gsub('pattern', 'replace'), read more http://lua-users.org/wiki/StringLibraryTutorial
+  --'all' will match any extension or protocol if it has one
+  --uses json and parses it into a lua table to be able to support .conf file
+
+  filename_replace = "",
+
+--[=====[ START OF SAMPLE REPLACE, to use remove start and end line
+  --Sample replace: replaces underscore to space on all files
+  --for mp4 and webm; remove extension, remove brackets and surrounding whitespace, change dot between alphanumeric to space
+  filename_replace = [[
+    [
+      {
+        "ext": { "all": true},
+        "rules": [
+          { "_" : " " }
+        ]
+      },{
+        "ext": { "mp4": true, "mkv": true },
+        "rules": [
+          { "^(.+)%..+$": "%1" },
+          { "%s*[%[%(].-[%]%)]%s*": "" },
+          { "(%w)%.(%w)": "%1 %2" }
+        ]
+      },{
+        "protocol": { "http": true, "https": true },
+        "rules": [
+          { "^%a+://w*%.?": "" }
+        ]
+      }
+    ]
+  ]],
+--END OF SAMPLE REPLACE ]=====]
+
+  --json array of filetypes to search from directory
+  loadfiles_filetypes = [[
+    [
+      "jpg", "jpeg", "png", "tif", "tiff", "gif", "webp", "svg", "bmp",
+      "mp3", "wav", "ogm", "flac", "m4a", "wma", "ogg", "opus",
+      "mkv", "avi", "mp4", "ogv", "webm", "rmvb", "flv", "wmv", "mpeg", "mpg", "m4v", "3gp"
+    ]
+  ]],
+
+  --loadfiles at startup if there is 0 or 1 items in playlist, if 0 uses worḱing dir for files
+  loadfiles_on_start = false,
+
+  --sort playlist on mpv start
+  sortplaylist_on_start = false,
+
+  --sort playlist when files are added to playlist
+  sortplaylist_on_file_add = false,
+
+  --use alphanumerical sort
+  alphanumsort = true,
+
+  --"linux | windows | auto"
+  system = "auto",
+
+  --Use ~ for home directory. Leave as empty to use mpv/playlists
+  playlist_savepath = "",
+
+  --save playlist automatically after current file was unloaded
+  save_playlist_on_file_end = false,
+
+
+  --show playlist or filename every time a new file is loaded
+  --2 shows playlist, 1 shows current file(filename strip applied) as osd text, 0 shows nothing
+  --instead of using this you can also call script-message playlistmanager show playlist/filename
+  --ex. KEY playlist-next ; script-message playlistmanager show playlist
+  show_playlist_on_fileload = 0,
+
+  --sync cursor when file is loaded from outside reasons(file-ending, playlist-next shortcut etc.)
+  --has the sideeffect of moving cursor if file happens to change when navigating
+  --good side is cursor always following current file when going back and forth files with playlist-next/prev
+  sync_cursor_on_load = true,
+
+  --playlist open key will toggle visibility instead of refresh, best used with long timeout
+  open_toggles = true,
+
+  --allow the playlist cursor to loop from end to start and vice versa
+  loop_cursor = true,
+
+
+  --####  VISUAL SETTINGS
+
+  --prefer to display titles for following files: "all", "url", "none". Sorting still uses filename.
+  prefer_titles = "url",
+
+  --call youtube-dl to resolve the titles of urls in the playlist
+  resolve_titles = false,
+
+  --osd timeout on inactivity, with high value on this open_toggles is good to be true
+  playlist_display_timeout = 10,
+
+  --amount of entries to show before slicing. Optimal value depends on font/video size etc.
+  showamount = 16,
+
+  --font size scales by window, if false requires larger font and padding sizes
+  scale_playlist_by_window=true,
+  --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua
+  --example {\\fnUbuntu\\fs10\\b0\\bord1} equals: font=Ubuntu, size=10, bold=no, border=1
+  --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags
+  --undeclared tags will use default osd settings
+  --these styles will be used for the whole playlist
+  style_ass_tags = "{}",
+  --paddings from top left corner
+  text_padding_x = 10,
+  text_padding_y = 30,
+
+  --set title of window with stripped name
+  set_title_stripped = false,
+  title_prefix = "",
+  title_suffix = " - mpv",
+
+  --slice long filenames, and how many chars to show
+  slice_longfilenames = false,
+  slice_longfilenames_amount = 70,
+
+  --Playlist header template
+  --%mediatitle or %filename = title or name of playing file
+  --%pos = position of playing file
+  --%cursor = position of navigation
+  --%plen = playlist length
+  --%N = newline
+  playlist_header = "[%cursor/%plen]",
+
+  --Playlist file templates
+  --%pos = position of file with leading zeros
+  --%name = title or name of file
+  --%N = newline
+  --you can also use the ass tags mentioned above. For example:
+  --  selected_file="{\\c&HFF00FF&}➔ %name"   | to add a color for selected file. However, if you
+  --  use ass tags you need to reset them for every line (see https://github.com/jonniek/mpv-playlistmanager/issues/20)
+  normal_file = "○ %name",
+  hovered_file = "● %name",
+  selected_file = "➔ %name",
+  playing_file = "▷ %name",
+  playing_hovered_file = "▶ %name",
+  playing_selected_file = "➤ %name",
+
+
+  -- what to show when playlist is truncated
+  playlist_sliced_prefix = "...",
+  playlist_sliced_suffix = "..."
+
+}
+local opts = require("mp.options")
+opts.read_options(settings, "playlistmanager", function(list) update_opts(list) end)
+
+local utils = require("mp.utils")
+local msg = require("mp.msg")
+local assdraw = require("mp.assdraw")
+
+
+--check os
+if settings.system=="auto" then
+  local o = {}
+  if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then
+    settings.system = "windows"
+  else
+    settings.system = "linux"
+  end
+end
+
+--global variables
+local playlist_visible = false
+local strippedname = nil
+local path = nil
+local directory = nil
+local filename = nil
+local pos = 0
+local plen = 0
+local cursor = 0
+--table for saved media titles for later if we prefer them
+local url_table = {}
+-- table for urls that we have request to be resolved to titles
+local requested_urls = {}
+--state for if we sort on playlist size change
+local sort_watching = false
+
+local filetype_lookup = {}
+
+function update_opts(changelog)
+  msg.verbose('updating options')
+
+  --parse filename json
+  if changelog.filename_replace then
+    if(settings.filename_replace~="") then
+      settings.filename_replace = utils.parse_json(settings.filename_replace)
+    else
+      settings.filename_replace = false
+    end
+  end
+
+  --parse loadfiles json
+  if changelog.loadfiles_filetypes then
+    settings.loadfiles_filetypes = utils.parse_json(settings.loadfiles_filetypes)
+
+    filetype_lookup = {}
+    --create loadfiles set
+    for _, ext in ipairs(settings.loadfiles_filetypes) do
+      filetype_lookup[ext] = true
+    end
+  end
+
+  if changelog.resolve_titles then
+    resolve_titles()
+  end
+
+  if changelog.playlist_display_timeout then
+    keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds)
+    keybindstimer:kill()
+  end
+
+  if playlist_visible then showplaylist() end
+end
+
+update_opts({filename_replace = true, loadfiles_filetypes = true})
+
+function on_loaded()
+  filename = mp.get_property("filename")
+  path = mp.get_property('path')
+  --if not a url then join path with working directory
+  if not path:match("^%a%a+:%/%/") then
+    path = utils.join_path(mp.get_property('working-directory'), path)
+    directory = utils.split_path(path)
+  else
+    directory = nil
+  end
+
+  refresh_globals()
+  if settings.sync_cursor_on_load then
+    cursor=pos
+    --refresh playlist if cursor moved
+    if playlist_visible then draw_playlist() end
+  end
+
+  local media_title = mp.get_property("media-title")
+  if path:match('^https?://') and not url_table[path] and path ~= media_title then
+    url_table[path] = media_title
+  end
+
+  strippedname = stripfilename(mp.get_property('media-title'))
+  if settings.show_playlist_on_fileload == 2 then
+    showplaylist()
+  elseif settings.show_playlist_on_fileload == 1 then
+    mp.commandv('show-text', strippedname)
+  end
+  if settings.set_title_stripped then
+    mp.set_property("title", settings.title_prefix..strippedname..settings.title_suffix)
+  end
+
+  local didload = false
+  if settings.loadfiles_on_start and plen == 1 then
+    didload = true --save reference for sorting
+    msg.info("Loading files from playing files directory")
+    playlist()
+  end
+
+  --if we promised to sort files on launch do it
+  if promised_sort then
+    promised_sort = false
+    msg.info("Your playlist is sorted before starting playback")
+    if didload then sortplaylist() else sortplaylist(true) end
+  end
+
+  --if we promised to listen and sort on playlist size increase do it
+  if promised_sort_watch then
+    promised_sort_watch = false
+    sort_watching = true
+    msg.info("Added files will be automatically sorted")
+    mp.observe_property('playlist-count', "number", autosort)
+  end
+end
+
+function on_closed()
+  if settings.save_playlist_on_file_end then save_playlist() end
+  strippedname = nil
+  path = nil
+  directory = nil
+  filename = nil
+  if playlist_visible then showplaylist() end
+end
+
+function refresh_globals()
+  pos = mp.get_property_number('playlist-pos', 0)
+  plen = mp.get_property_number('playlist-count', 0)
+end
+
+function escapepath(dir, escapechar)
+  return string.gsub(dir, escapechar, '\\'..escapechar)
+end
+
+--strip a filename based on its extension or protocol according to rules in settings
+function stripfilename(pathfile, media_title)
+  if pathfile == nil then return '' end
+  local ext = pathfile:match("^.+%.(.+)$")
+  local protocol = pathfile:match("^(%a%a+)://")
+  if not ext then ext = "" end
+  local tmp = pathfile
+  if settings.filename_replace and not media_title then
+    for k,v in ipairs(settings.filename_replace) do
+      if ( v['ext'] and (v['ext'][ext] or (ext and not protocol and v['ext']['all'])) )
+      or ( v['protocol'] and (v['protocol'][protocol] or (protocol and not ext and v['protocol']['all'])) ) then
+        for ruleindex, indexrules in ipairs(v['rules']) do
+          for rule, override in pairs(indexrules) do
+            tmp = tmp:gsub(rule, override)
+          end
+        end
+      end
+    end
+  end
+  if settings.slice_longfilenames and tmp:len()>settings.slice_longfilenames_amount+5 then
+    tmp = tmp:sub(1, settings.slice_longfilenames_amount).." ..."
+  end
+  return tmp
+end
+
+--gets a nicename of playlist entry at 0-based position i
+function get_name_from_index(i, notitle)
+  refresh_globals()
+  if plen <= i then msg.error("no index in playlist", i, "length", plen); return nil end
+  local _, name = nil
+  local title = mp.get_property('playlist/'..i..'/title')
+  local name = mp.get_property('playlist/'..i..'/filename')
+
+  local should_use_title = settings.prefer_titles == 'all' or name:match('^https?://') and settings.prefer_titles == 'url'
+  --check if file has a media title stored or as property
+  if not title and should_use_title then
+    local mtitle = mp.get_property('media-title')
+    if i == pos and mp.get_property('filename') ~= mtitle then
+      if not url_table[name] then
+        url_table[name] = mtitle
+      end
+      title = mtitle
+    elseif url_table[name] then
+      title = url_table[name]
+    end
+  end
+
+  --if we have media title use a more conservative strip
+  if title and not notitle and should_use_title then return stripfilename(title, true) end
+
+  --remove paths if they exist, keeping protocols for stripping
+  if string.sub(name, 1, 1) == '/' or name:match("^%a:[/\\]") then
+    _, name = utils.split_path(name)
+  end
+  return stripfilename(name)
+end
+
+function parse_header(string)
+  local esc_title = stripfilename(mp.get_property("media-title"), true):gsub("%%", "%%%%")
+  local esc_file = stripfilename(mp.get_property("filename")):gsub("%%", "%%%%")
+  return string:gsub("%%N", "\\N")
+               :gsub("%%pos", mp.get_property_number("playlist-pos",0)+1)
+               :gsub("%%plen", mp.get_property("playlist-count"))
+               :gsub("%%cursor", cursor+1)
+               :gsub("%%mediatitle", esc_title)
+               :gsub("%%filename", esc_file)
+               -- undo name escape
+               :gsub("%%%%", "%%")
+end
+
+function parse_filename(string, name, index)
+  local base = tostring(plen):len()
+  local esc_name = stripfilename(name):gsub("%%", "%%%%")
+  return string:gsub("%%N", "\\N")
+               :gsub("%%pos", string.format("%0"..base.."d", index+1))
+               :gsub("%%name", esc_name)
+               -- undo name escape
+               :gsub("%%%%", "%%")
+end
+
+function parse_filename_by_index(index)
+  local template = settings.normal_file
+
+  local is_idle = mp.get_property_native('idle-active')
+  local position = is_idle and -1 or pos
+
+  if index == position then
+    if index == cursor then
+      if selection then
+        template = settings.playing_selected_file
+      else
+        template = settings.playing_hovered_file
+      end
+    else
+      template = settings.playing_file
+    end
+  elseif index == cursor then
+    if selection then
+      template = settings.selected_file
+    else
+      template = settings.hovered_file
+    end
+  end
+
+  return parse_filename(template, get_name_from_index(index), index)
+end
+
+
+function draw_playlist()
+  refresh_globals()
+  local ass = assdraw.ass_new()
+  ass:new_event()
+  ass:pos(settings.text_padding_x, settings.text_padding_y)
+  ass:append(settings.style_ass_tags)
+
+  if settings.playlist_header ~= "" then
+    ass:append(parse_header(settings.playlist_header).."\\N")
+  end
+  local start = cursor - math.floor(settings.showamount/2)
+  local showall = false
+  local showrest = false
+  if start<0 then start=0 end
+  if plen <= settings.showamount then
+    start=0
+    showall=true
+  end
+  if start > math.max(plen-settings.showamount-1, 0) then
+    start=plen-settings.showamount
+    showrest=true
+  end
+  if start > 0 and not showall then ass:append(settings.playlist_sliced_prefix.."\\N") end
+  for index=start,start+settings.showamount-1,1 do
+    if index == plen then break end
+
+    ass:append(parse_filename_by_index(index).."\\N")
+    if index == start+settings.showamount-1 and not showall and not showrest then
+      ass:append(settings.playlist_sliced_suffix)
+    end
+  end
+  local w, h = mp.get_osd_size()
+  if settings.scale_playlist_by_window then w,h = 0, 0 end
+  mp.set_osd_ass(w, h, ass.text)
+end
+
+function toggle_playlist()
+  if settings.open_toggles then
+    if playlist_visible then
+      remove_keybinds()
+      return
+    end
+  end
+  showplaylist()
+end
+
+function showplaylist(duration)
+  refresh_globals()
+  if plen == 0 then return end
+  playlist_visible = true
+  add_keybinds()
+
+  draw_playlist()
+  keybindstimer:kill()
+  if duration then
+    keybindstimer = mp.add_periodic_timer(duration, remove_keybinds)
+  else
+    keybindstimer:resume()
+  end
+end
+
+selection=nil
+function selectfile()
+  refresh_globals()
+  if plen == 0 then return end
+  if not selection then
+    selection=cursor
+  else
+    selection=nil
+  end
+  showplaylist()
+end
+
+function unselectfile()
+  selection=nil
+  showplaylist()
+end
+
+function removefile()
+  refresh_globals()
+  if plen == 0 then return end
+  selection = nil
+  if cursor==pos then mp.command("script-message unseenplaylist mark true \"playlistmanager avoid conflict when removing file\"") end
+  mp.commandv("playlist-remove", cursor)
+  if cursor==plen-1 then cursor = cursor - 1 end
+  showplaylist()
+end
+
+function moveup()
+  refresh_globals()
+  if plen == 0 then return end
+  if cursor~=0 then
+    if selection then mp.commandv("playlist-move", cursor,cursor-1) end
+    cursor = cursor-1
+  elseif settings.loop_cursor then
+    if selection then mp.commandv("playlist-move", cursor,plen) end
+    cursor = plen-1
+  end
+  showplaylist()
+end
+
+function movedown()
+  refresh_globals()
+  if plen == 0 then return end
+  if cursor ~= plen-1 then
+    if selection then mp.commandv("playlist-move", cursor,cursor+2) end
+    cursor = cursor + 1
+  elseif settings.loop_cursor then
+    if selection then mp.commandv("playlist-move", cursor,0) end
+    cursor = 0
+  end
+  showplaylist()
+end
+
+function write_watch_later(force_write)
+  if mp.get_property_bool("save-position-on-quit") or force_write then
+    mp.command("write-watch-later-config")
+  end
+end
+
+function playlist_next(force_write)
+  write_watch_later(force_write)
+  mp.commandv("playlist-next", "weak")
+end
+
+function playlist_prev(force_write)
+  write_watch_later(force_write)
+  mp.commandv("playlist-prev", "weak")
+end
+
+function playfile()
+  refresh_globals()
+  if plen == 0 then return end
+  selection = nil
+  local is_idle = mp.get_property_native('idle-active')
+  if cursor ~= pos or is_idle then
+    write_watch_later()
+    mp.set_property("playlist-pos", cursor)
+  else
+    if cursor~=plen-1 then
+      cursor = cursor + 1
+    end
+    write_watch_later()
+    mp.commandv("playlist-next", "weak")
+  end
+  if settings.show_playlist_on_fileload ~= 2 then
+    remove_keybinds()
+  end
+end
+
+function get_files_windows(dir)
+  local args = {
+    'powershell', '-NoProfile', '-Command', [[& {
+          Trap {
+              Write-Error -ErrorRecord $_
+              Exit 1
+          }
+          $path = "]]..dir..[["
+          $escapedPath = [WildcardPattern]::Escape($path)
+          cd $escapedPath
+
+          $list = (Get-ChildItem -File | Sort-Object { [regex]::Replace($_.Name, '\d+', { $args[0].Value.PadLeft(20) }) }).Name
+          $string = ($list -join "/")
+          $u8list = [System.Text.Encoding]::UTF8.GetBytes($string)
+          [Console]::OpenStandardOutput().Write($u8list, 0, $u8list.Length)
+      }]]
+  }
+  local process = utils.subprocess({ args = args, cancellable = false })
+  return parse_files(process, '%/')
+end
+
+function get_files_linux(dir)
+  local args = { 'ls', '-1pv', dir }
+  local process = utils.subprocess({ args = args, cancellable = false })
+  return parse_files(process, '\n')
+end
+
+function parse_files(res, delimiter)
+  if not res.error and res.status == 0 then
+    local valid_files = {}
+    for line in res.stdout:gmatch("[^"..delimiter.."]+") do
+      local ext = line:match("^.+%.(.+)$")
+      if ext and filetype_lookup[ext:lower()] then
+        table.insert(valid_files, line)
+      end
+    end
+    return valid_files, nil
+  else
+    return nil, res.error
+  end
+end
+
+--Creates a playlist of all files in directory, will keep the order and position
+--For exaple, Folder has 12 files, you open the 5th file and run this, the remaining 7 are added behind the 5th file and prior 4 files before it
+function playlist(force_dir)
+  refresh_globals()
+  if not directory and plen > 0 then return end
+  local hasfile = true
+  if plen == 0 then
+    hasfile = false
+    dir = mp.get_property('working-directory')
+  else
+    dir = directory
+  end
+  if force_dir then dir = force_dir end
+
+  local files, error
+  if settings.system == "linux" then
+    files, error = get_files_linux(dir)
+  else
+    files, error = get_files_windows(dir)
+  end
+
+  local c, c2 = 0,0
+  if files then
+    local cur = false
+    local filename = mp.get_property("filename")
+    for _, file in ipairs(files) do
+      local appendstr = "append"
+      if not hasfile then
+        cur = true
+        appendstr = "append-play"
+        hasfile = true
+      end
+      if cur == true then
+        mp.commandv("loadfile", utils.join_path(dir, file), appendstr)
+        msg.info("Appended to playlist: " .. file)
+        c2 = c2 + 1
+      elseif file ~= filename then
+          mp.commandv("loadfile", utils.join_path(dir, file), appendstr)
+          msg.info("Prepended to playlist: " .. file)
+          mp.commandv("playlist-move", mp.get_property_number("playlist-count", 1)-1,  c)
+          c = c + 1
+      else
+        cur = true
+      end
+    end
+    if c2 > 0 or c>0 then
+      mp.osd_message("Added "..c + c2.." files to playlist")
+    else
+      mp.osd_message("No additional files found")
+    end
+    cursor = mp.get_property_number('playlist-pos', 1)
+  else
+    msg.error("Could not scan for files: "..(error or ""))
+  end
+  if sort_watching then
+    msg.info("Ignoring directory structure and using playlist sort")
+    sortplaylist()
+  end
+  refresh_globals()
+  if playlist_visible then showplaylist() end
+  return c + c2
+end
+
+function parse_home(path)
+  if not path:find("^~") then
+    return path
+  end
+  local home_dir = os.getenv("HOME") or os.getenv("USERPROFILE")
+  if not home_dir then
+    local drive = os.getenv("HOMEDRIVE")
+    local path = os.getenv("HOMEPATH")
+    if drive and path then
+      home_dir = utils.join_path(drive, path)
+    else
+      msg.error("Couldn't find home dir.")
+      return nil
+    end
+  end
+  local result = path:gsub("^~", home_dir)
+  return result
+end
+
+--saves the current playlist into a m3u file
+function save_playlist()
+  local length = mp.get_property_number('playlist-count', 0)
+  if length == 0 then return end
+
+  --get playlist save path
+  local savepath
+  if settings.playlist_savepath == nil or settings.playlist_savepath == "" then
+    savepath = mp.command_native({"expand-path", "~~home/"}).."/playlists"
+  else
+    savepath = parse_home(settings.playlist_savepath)
+    if savepath == nil then return end
+  end
+
+  --create savepath if it doesn't exist
+  if utils.readdir(savepath) == nil then
+    local windows_args = {'powershell', '-NoProfile', '-Command', 'mkdir', savepath}
+    local unix_args = { 'mkdir', savepath }
+    local args = settings.system == 'windows' and windows_args or unix_args
+    local res = utils.subprocess({ args = args, cancellable = false })
+    if res.status ~= 0 then
+      msg.error("Failed to create playlist save directory "..savepath..". Error: "..(res.error or "unknown"))
+      return
+    end
+  end
+
+  local date = os.date("*t")
+  local datestring = ("%02d-%02d-%02d_%02d-%02d-%02d"):format(date.year, date.month, date.day, date.hour, date.min, date.sec)
+
+  local savepath = utils.join_path(savepath, datestring.."_playlist-size_"..length..".m3u")
+  local file, err = io.open(savepath, "w")
+  if not file then
+    msg.error("Error in creating playlist file, check permissions. Error: "..(err or "unknown"))
+  else
+    local i=0
+    while i < length do
+      local pwd = mp.get_property("working-directory")
+      local filename = mp.get_property('playlist/'..i..'/filename')
+      local fullpath = filename
+      if not filename:match("^%a%a+:%/%/") then
+        fullpath = utils.join_path(pwd, filename)
+      end
+      local title = mp.get_property('playlist/'..i..'/title')
+      if title then file:write("#EXTINF:,"..title.."\n") end
+      file:write(fullpath, "\n")
+      i=i+1
+    end
+    msg.info("Playlist written to: "..savepath)
+    file:close()
+  end
+end
+
+function alphanumsort(a, b)
+  local function padnum(d)
+    local dec, n = string.match(d, "(%.?)0*(.+)")
+    return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n)
+  end
+  return tostring(a):lower():gsub("%.?%d+",padnum)..("%3d"):format(#b)
+       < tostring(b):lower():gsub("%.?%d+",padnum)..("%3d"):format(#a)
+end
+
+function dosort(a,b)
+  if settings.alphanumsort then
+    return alphanumsort(a,b)
+  else
+    return a < b
+  end
+end
+
+function sortplaylist(startover)
+  local length = mp.get_property_number('playlist-count', 0)
+  if length < 2 then return end
+  --use insertion sort on playlist to make it easy to order files with playlist-move
+  for outer=1, length-1, 1 do
+    local outerfile = get_name_from_index(outer, true)
+    local inner = outer - 1
+    while inner >= 0 and dosort(outerfile, get_name_from_index(inner, true)) do
+      inner = inner - 1
+    end
+    inner = inner + 1
+    if outer ~= inner then
+      mp.commandv('playlist-move', outer, inner)
+    end
+  end
+  cursor = mp.get_property_number('playlist-pos', 0)
+  if startover then
+    mp.set_property('playlist-pos', 0)
+  end
+  if playlist_visible then showplaylist() end
+end
+
+function autosort(name, param)
+  if param == 0 then return end
+  if plen < param then
+    msg.info("Playlistmanager autosorting playlist")
+    refresh_globals()
+    sortplaylist()
+  end
+end
+
+function reverseplaylist()
+  local length = mp.get_property_number('playlist-count', 0)
+  if length < 2 then return end
+  for outer=1, length-1, 1 do
+    mp.commandv('playlist-move', outer, 0)
+  end
+  if playlist_visible then showplaylist() end
+end
+
+function shuffleplaylist()
+  refresh_globals()
+  if plen < 2 then return end
+  mp.command("playlist-shuffle")
+  math.randomseed(os.time())
+  mp.commandv("playlist-move", pos, math.random(0, plen-1))
+  mp.set_property('playlist-pos', 0)
+  refresh_globals()
+  if playlist_visible then showplaylist() end
+end
+
+function bind_keys(keys, name, func, opts)
+  if not keys then
+    mp.add_forced_key_binding(keys, name, func, opts)
+    return
+  end
+  local i = 1
+  for key in keys:gmatch("[^%s]+") do
+    local prefix = i == 1 and '' or i
+    mp.add_forced_key_binding(key, name..prefix, func, opts)
+    i = i + 1
+  end
+end
+
+function unbind_keys(keys, name)
+  if not keys then
+    mp.remove_key_binding(name)
+    return
+  end
+  local i = 1
+  for key in keys:gmatch("[^%s]+") do
+    local prefix = i == 1 and '' or i
+    mp.remove_key_binding(name..prefix)
+    i = i + 1
+  end
+end
+
+function add_keybinds()
+  bind_keys(settings.key_moveup, 'moveup', moveup, "repeatable")
+  bind_keys(settings.key_movedown, 'movedown', movedown, "repeatable")
+  bind_keys(settings.key_selectfile, 'selectfile', selectfile)
+  bind_keys(settings.key_unselectfile, 'unselectfile', unselectfile)
+  bind_keys(settings.key_playfile, 'playfile', playfile)
+  bind_keys(settings.key_removefile, 'removefile', removefile, "repeatable")
+  bind_keys(settings.key_closeplaylist, 'closeplaylist', remove_keybinds)
+end
+
+function remove_keybinds()
+  keybindstimer:kill()
+  keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds)
+  keybindstimer:kill()
+  mp.set_osd_ass(0, 0, "")
+  playlist_visible = false
+  if settings.dynamic_binds then
+    unbind_keys(settings.key_moveup, 'moveup')
+    unbind_keys(settings.key_movedown, 'movedown')
+    unbind_keys(settings.key_selectfile, 'selectfile')
+    unbind_keys(settings.key_unselectfile, 'unselectfile')
+    unbind_keys(settings.key_playfile, 'playfile')
+    unbind_keys(settings.key_removefile, 'removefile')
+    unbind_keys(settings.key_closeplaylist, 'closeplaylist')
+  end
+end
+
+keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds)
+keybindstimer:kill()
+
+if not settings.dynamic_binds then
+  add_keybinds()
+end
+
+if settings.loadfiles_on_start and mp.get_property_number('playlist-count', 0) == 0 then
+  playlist()
+end
+
+promised_sort_watch = false
+if settings.sortplaylist_on_file_add then
+  promised_sort_watch = true
+end
+
+promised_sort = false
+if settings.sortplaylist_on_start then
+  promised_sort = true
+end
+
+mp.observe_property('playlist-count', "number", function()
+  if playlist_visible then showplaylist() end
+  if settings.prefer_titles == 'none' then return end
+  -- resolve titles
+  resolve_titles()
+end)
+
+--resolves url titles by calling youtube-dl
+function resolve_titles()
+  if not settings.resolve_titles then return end
+  local length = mp.get_property_number('playlist-count', 0)
+  if length < 2 then return end
+  local i=0
+  -- loop all items in playlist because we can't predict how it has changed
+  while i < length do
+    local filename = mp.get_property('playlist/'..i..'/filename')
+    local title = mp.get_property('playlist/'..i..'/title')
+    if i ~= pos
+      and filename
+      and filename:match('^https?://')
+      and not title
+      and not url_table[filename]
+      and not requested_urls[filename]
+    then
+      requested_urls[filename] = true
+
+      local args = { 'youtube-dl', '--no-playlist', '--flat-playlist', '-sJ', filename }
+      local req = mp.command_native_async(
+        {
+          name = "subprocess",
+          args = args,
+          playback_only = false,
+          capture_stdout = true
+        }, function (success, res)
+            if res.killed_by_us then
+              msg.verbose('Request to resolve url title ' .. filename .. ' timed out')
+              return
+            end
+            if res.status == 0 then
+              local json, err = utils.parse_json(res.stdout)
+              if not err then
+                local is_playlist = json['_type'] and json['_type'] == 'playlist'
+                local title = (is_playlist and '[playlist]: ' or '') .. json['title']
+                msg.verbose(filename .. " resolved to '" .. title .. "'")
+                url_table[filename] = title
+                refresh_globals()
+                if playlist_visible then showplaylist() end
+                return
+              else
+                msg.error("Failed parsing json, reason: "..(err or "unknown"))
+              end
+            else
+              msg.error("Failed to resolve url title "..filename.." Error: "..(res.error or "unknown"))
+            end
+          end)
+
+      mp.add_timeout(5, function()
+        mp.abort_async_command(req)
+      end)
+
+    end
+    i=i+1
+  end
+end
+
+--script message handler
+function handlemessage(msg, value, value2)
+  if msg == "show" and value == "playlist" then
+    if value2 ~= "toggle" then
+      showplaylist(value2)
+      return
+    else
+      toggle_playlist()
+      return
+    end
+  end
+  if msg == "show" and value == "filename" and strippedname and value2 then
+    mp.commandv('show-text', strippedname, tonumber(value2)*1000 ) ; return
+  end
+  if msg == "show" and value == "filename" and strippedname then
+    mp.commandv('show-text', strippedname ) ; return
+  end
+  if msg == "sort" then sortplaylist(value) ; return end
+  if msg == "shuffle" then shuffleplaylist() ; return end
+  if msg == "reverse" then reverseplaylist() ; return end
+  if msg == "loadfiles" then playlist(value) ; return end
+  if msg == "save" then save_playlist() ; return end
+  if msg == "playlist-next" then playlist_next(true) ; return end
+  if msg == "playlist-prev" then playlist_prev(true) ; return end
+end
+
+mp.register_script_message("playlistmanager", handlemessage)
+
+mp.add_key_binding("CTRL+p", "sortplaylist", sortplaylist)
+mp.add_key_binding("CTRL+P", "shuffleplaylist", shuffleplaylist)
+mp.add_key_binding("CTRL+R", "reverseplaylist", reverseplaylist)
+mp.add_key_binding("P", "loadfiles", playlist)
+mp.add_key_binding("p", "saveplaylist", save_playlist)
+mp.add_key_binding("SHIFT+ENTER", "showplaylist", toggle_playlist)
+
+mp.register_event("file-loaded", on_loaded)
+mp.register_event("end-file", on_closed)