Repository: VideoPlayerCode/mpv-tools
Branch: master
Commit: 39e49a4e17d5
Files: 24
Total size: 148.6 KB
Directory structure:
gitextract_7aqr3828/
├── .gitignore
├── README.md
├── script-settings/
│ ├── Blackbox.conf.example
│ ├── Colorbox.conf.example
│ ├── Leapfrog.conf.example
│ └── README.md
└── scripts/
├── Blackbox.js
├── Colorbox.js
├── Gallerizer.js
├── Leapfrog.js
├── auto-keep-gui-open.lua
├── cycle-video-rotate.lua
├── modules.js/
│ ├── AssFormat.js
│ ├── MicroUtils.js
│ ├── Options.js
│ ├── PathIndex.js
│ ├── PathTools.js
│ ├── PlaylistManager.js
│ ├── PseudoRandom.js
│ ├── RandomCycle.js
│ ├── SelectionMenu.js
│ └── Stack.js
├── multi-command-if.lua
└── quick-scale.lua
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# temporary editor files
*.swp
.\#*
*.sublime-project
*.sublime-workspace
.idea
# operating system cache files
.DS_Store
Desktop.ini
Thumbs.db
================================================
FILE: README.md
================================================
## VideoPlayerCode's MPV Utilities
* `[ Lua ]` **[auto-keep-gui-open.lua](https://github.com/VideoPlayerCode/mpv-tools/blob/master/scripts/auto-keep-gui-open.lua)**
Intelligently switches mpv's "keep-open" behavior based on whether you are
running in video-mode or audio-only mode.
* `[ JS ] TOP!` **[Blackbox.js](https://github.com/VideoPlayerCode/mpv-tools/blob/master/scripts/Blackbox.js)**
Advanced, modular media browser, file manager and playlist manager for mpv.
* `[ JS ] TOP!` **[Colorbox.js](https://github.com/VideoPlayerCode/mpv-tools/blob/master/scripts/Colorbox.js)**
Apply color correction presets.
* `[ Lua ]` **[cycle-video-rotate.lua](https://github.com/VideoPlayerCode/mpv-tools/blob/master/scripts/cycle-video-rotate.lua)**
Allows you to perform video rotation which perfectly cycles through all 360
degrees without any glitches.
* `[ JS ] TOP!` **[Gallerizer.js](https://github.com/VideoPlayerCode/mpv-tools/blob/master/scripts/Gallerizer.js)**
Image gallery autoloader for mpv.
* `[ JS ] TOP!` **[Leapfrog.js](https://github.com/VideoPlayerCode/mpv-tools/blob/master/scripts/Leapfrog.js)**
Effortlessly jump through your playlist, with your own custom jump size and
direction, including the ability to jump randomly. Excellent when queuing lots
of images and using mpv as an image viewer.
* `[ Lua ] TOP!` **[multi-command-if.lua](https://github.com/VideoPlayerCode/mpv-tools/blob/master/scripts/multi-command-if.lua)**
Very powerful conditional logic and multiple action engine for your
keybindings, without having to write a single line of code!
* `[ Lua ] TOP!` **[quick-scale.lua](https://github.com/VideoPlayerCode/mpv-tools/blob/master/scripts/quick-scale.lua)**
Quickly scale the video player to a target size, with full control over target
scale and max scale. Helps you effortlessly resize a video to fit on your
desktop, or any other video dimensions you need!
* `[ JS ] TOP!` **[VideoPlayerCode's Modules.js (for developers)](https://github.com/VideoPlayerCode/mpv-tools/tree/master/scripts/modules.js)**
Tons of pre-written, open source JavaScript modules which helps you rapidly
create your own JS user scripts (including a very helpful [script config](https://github.com/VideoPlayerCode/mpv-tools/blob/master/scripts/modules.js/Options.js)
system based on mpv's Lua `mp.options` API). All modules are free to use (and
extend) in your own scripts!
### Download
Easily download all scripts as a zip file: [mpv-tools-master.zip](https://github.com/VideoPlayerCode/mpv-tools/archive/master.zip).
### Requirements
You need the [mpv.io](http://mpv.io) media player, built with Lua (`.lua`) and
JavaScript (`.js`) support. If you want to use any of the JavaScript utilities,
you must _also_ download the `scripts/modules.js/` folder and place it within
your own `scripts/` folder, exactly how it's laid out in this project. It
contains important sublibraries that are shared by all of my `.js` utilities.
### License
[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
### Author
VideoPlayerCode (https://github.com/VideoPlayerCode)
================================================
FILE: script-settings/Blackbox.conf.example
================================================
# Menu appearance.
auto_close=0
max_lines=12
font_size=40
help_hint=no
# Favorite paths.
favorites={/home/example/Movies}+{/home/example/Downloads}+{/home/example/Documents}+{/}
# Additional file-inclusion pattern (regex).
# This particular pattern also adds ".rar" and ".zip" files to the file browser.
include_regex=\.(?:rar|zip)$
# Key rebindings.
#
# Recommendation:
# Bind the main "Blackbox" menu command (via input.conf)
# to "z" and use the left-hand navigation below.
keys_menu_up={up}+{w}
keys_menu_down={down}+{s}
keys_menu_up_fast={shift+up}+{shift+w}
keys_menu_down_fast={shift+down}+{shift+s}
keys_menu_left={left}+{a}
keys_menu_right={right}+{d}
keys_menu_open={enter}+{e}
keys_menu_undo={bs}+{r}
keys_menu_help={h}
keys_menu_close={esc}+{c}
================================================
FILE: script-settings/Colorbox.conf.example
================================================
# Presets.
# Format: v1; contrast; brightness; gamma; saturation; hue; sharpen; title
presets[]=v1; 6; -2; 0; -4; 0; 0.6; Subtle depth enhancer (desat)
presets[]=v1; 6; -2; 0; 4; 0; 0.6; Subtle depth enhancer (sat)
presets[]=v1; 7; -8; 6; -10; 5; 1.3; Clarity darker
presets[]=v1; 6; -5; 2; -10; 5; 1.3; Clarity natural/dark
presets[]=v1; 6; -3; 1; -10; 5; 1.3; Clarity medium
presets[]=v1; 6; 0; 1; -5; 5; 1.3; Clarity light
presets[]=v1; 6; 1; 4; -5; 5; 1.3; Clarity lighter
presets[]=v1; 52; -11; -2; -6; 4; 1.3; Lift vintage flatness
presets[]=v1; 21; 3; -2; -30; 8; 1.1; Yellow de-glow and modernize
# Load preset at startup.
startup_preset=Clarity medium
# Menu appearance.
auto_close=0
max_lines=16
font_size=30
font_alpha=0.7
# Key rebindings.
keys_menu_up={up}+{w}
keys_menu_down={down}+{s}
keys_menu_up_fast={shift+up}+{shift+w}
keys_menu_down_fast={shift+down}+{shift+s}
keys_menu_left={left}+{a}
keys_menu_right={right}+{d}
keys_menu_open={enter}+{e}
keys_menu_undo={bs}+{r}
keys_menu_help={h}
keys_menu_close={esc}+{c}
================================================
FILE: script-settings/Leapfrog.conf.example
================================================
# Tells Leapfrog to use a small fontsize and adds some transparency.
# Read the Leapfrog.js code for more details about these options.
font_size=14
font_alpha=0.8
================================================
FILE: script-settings/README.md
================================================
## Script Configurations
This folder contains example configuration files. To enable them on your own
system, simply create the `script-settings/` folder in your mpv config path,
exactly as shown here (side by side with your `scripts/` folder).
The configurations automatically follow the `<current script name>.conf` format,
so if you've renamed any of the scripts after downloading them, you'll also have
to rename their configuration files to match the new name that you've given the
scripts.
To enable any of these example configurations, simply rename the demo extensions
from `.conf.example` to `.conf`.
================================================
FILE: scripts/Blackbox.js
================================================
/*
* BLACKBOX.JS
*
* Description: Advanced, modular media browser, file manager and playlist
* manager for mpv.
* Version: 1.3.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
// Read the bottom of this file for configuration and script setup instructions.
/* jshint -W097 */
/* global mp, require, setTimeout, clearTimeout */
'use strict';
var Options = require('Options'),
Utils = require('MicroUtils'),
Ass = require('AssFormat'),
PathIndex = require('PathIndex'),
PathTools = require('PathTools'),
PlaylistManager = require('PlaylistManager'),
SelectionMenu = require('SelectionMenu');
var Blackbox = function(options)
{
options = options || {};
// List of system folders to ignore, keyed by parent path.
this.ignorePaths = {
'/': {
// General Linux system paths (some are used by macOS too).
'bin':1,
'boot':1,
'cdrom':1,
'dev':1,
'etc':1,
'lib':1,
'lib32':1,
'lib64':1,
'lost+found':1,
'opt':1,
'proc':1,
'root':1,
'run':1,
'sbin':1,
'selinux':1,
'snap':1,
'srv':1,
'sys':1,
'tmp':1,
'usr':1,
'var':1,
// Useless macOS-specific system paths.
'cores':1,
'net':1,
'private':1,
'Library':1, // System data and preference library (useless).
'System':1 // Operating system (useless).
}
};
if (PathTools.isMac()) // Ignore "/home" on macOS (but not on Unix/Linux).
this.ignorePaths['/'].home = 1;
// Media file detection regex (pre-compiled since MuJS regexps are slow).
this.mediaRgx = /\.(?:mp[234]|m4[av]|mpe?g|m[12]v|web[mp]|mk(?:[av]|3d)|h?264|qt|mov|avi|xvid|divx|wm[av]|asf|pcm|flac|aiff|wav|aac|dts|e?ac3|dat|bin|vob|vcd|mt[sv]|m2ts?|ts|flv|f4[vp]|rm(?:vb)?|3(?:gp|iv)|h?dv|og[gmv]|jpe?g|png|bmp|gif)(?:\.part)?$/i;
// Include the user's custom inclusion regex (if available and non-empty).
var includeRgx = options.includeRegex ? Utils.trim(options.includeRegex) : null;
if (includeRgx) {
try {
var userRgx = new RegExp(includeRgx), // Throws if invalid.
mergedRgx = '(?:'+this.mediaRgx.source+'|'+userRgx.source+')';
this.mediaRgx = new RegExp(mergedRgx, 'i'); // Throws if invalid.
} catch (e) {
throw 'User\'s include-regex: "'+e.message.replace('regular expression: ', '')+'"';
}
}
// Active file browser path.
this.currentPath = null;
// Favorite media paths and/or files.
this.favoritePaths = [];
// Initialize the navigation menu and its callbacks.
this.currentPage = null;
this.currentPageIdx = -1;
this.pageList = ['files', 'favorites', 'playlist'];
this.lastPageSelection = {}; // Tracks last-used selection on each page.
this.currentlyPlayingStr = '[Currently Playing]';
this.clearPlaylistStr = '[Clear Playlist]';
this.clearPlaylistId = -999;
this.showHelpHint = typeof options.showHelpHint === 'boolean' ?
options.showHelpHint : true;
this.menu = new SelectionMenu({ // Throws if bindings are illegal.
maxLines: options.maxLines,
menuFontSize: options.menuFontSize,
autoCloseDelay: options.autoCloseDelay,
keyRebindings: options.keyRebindings
});
this.menu.setMetadata({type:null});
this.lastNavTime = 0;
this._registerCallbacks();
// Only use menu text colors while mpv is rendering in GUI mode (non-CLI).
var self = this;
this.menu.setUseTextColors(mp.get_property_bool('vo-configured'));
mp.observe_property('vo-configured', 'bool', function(name, value) {
self.menu.setUseTextColors(value);
});
// Detect when the playlist contents or its position changes, with slight
// throttling to avoid massive processing during rapid-fire events, such as
// when the playlist is full of invalid files that are blazed through.
// NOTE: This property changes when contents OR current file changes.
var playlistTimer = null;
mp.observe_property('playlist', 'native', function(name, value) {
if (playlistTimer !== null) {
clearTimeout(playlistTimer);
playlistTimer = null;
}
if (self.menu.isMenuActive() && self.currentPage === 'playlist')
playlistTimer = setTimeout(function() {
playlistTimer = null;
if (self.menu.isMenuActive() && self.currentPage === 'playlist')
// We're refreshing page while it's active. Keep selection.
self.navigatePlaylist(value, true);
}, 150);
});
// Register all of the favorite paths/files in this media browser instance.
var favOpt = options.favoritePaths;
if (favOpt && favOpt.length) {
try {
for (var i = 0; i < favOpt.length; ++i) {
this.addFavorite(favOpt[i]); // Throws.
}
} catch (e) { // Treat as non-fatal.
this._showError('Blackbox: Invalid favorites option value: '+e+'.', 3);
}
}
};
Blackbox.prototype.flipPage = function(forcePage)
{
if (forcePage === this.currentPage || (forcePage === 'none' && this.currentPage === null))
return; // No-op: We're already marked as being on that exact page!
// Save state from the currently active page (if any).
switch (this.currentPage) {
case 'files':
case 'favorites':
// Be sure that we only save the selection if the menu CONTAINS data for
// the page that we're "leaving". It may have simply failed to swap page
// (loading the "currentPage" data) and now swapping back to old page!
if (this.menu.getMetadata().type === this.currentPage)
this.lastPageSelection[this.currentPage] = this.menu.getSelectedItem();
break;
case 'playlist':
// We do NOT save position on the playlist page. Instead, we always
// select the currently active file every time we enter the page.
break;
}
// Cycle to the next (or target) page index.
if (typeof forcePage === 'string') {
var forceIdx = -1;
for (var i = 0; i < this.pageList.length; ++i)
if (this.pageList[i] === forcePage) {
forceIdx = i;
break;
}
this.currentPageIdx = forceIdx;
}
else
++this.currentPageIdx;
// Verify the page index and activate the page.
if (this.currentPageIdx >= 0 && this.currentPageIdx < this.pageList.length)
this.currentPage = this.pageList[this.currentPageIdx];
else {
this.currentPage = null;
this.currentPageIdx = -1;
}
};
Blackbox.prototype.addFavorite = function(path)
{
// Do some basic validation, but don't check for duplicates or path
// existence (we could verify that by trying to read the path, but people
// may want to include paths/files that are not always available).
var pathSep = PathTools.pathSep(),
pl = path.length;
if (!pl)
throw 'Empty path';
if (pl > 1 && path.charAt(pl - 1) === pathSep) // Allows "/" (root).
throw 'Trailing "'+pathSep+'" in path "'+path+'"';
this.favoritePaths.push(path);
};
Blackbox.prototype._showError = function(err, durationSec)
{
err = typeof err === 'string' ? err : '_showError: No error string.';
durationSec = typeof durationSec === 'number' ? durationSec : 2;
if (durationSec < 1)
durationSec = 1;
mp.msg.error(err);
if (this.menu.isMenuActive())
this.menu.showMessage(err, Math.ceil(durationSec * 1000));
else
mp.osd_message(err, durationSec);
};
Blackbox.prototype._registerCallbacks = function()
{
var browser = this;
var processSelection = function() {
var selection = browser.getSelection();
if (!selection.targetPath)
return null; // Abort since there is no selection.
var targetType = null,
targetPath = selection.targetPath,
selectEntry = null;
switch (selection.menuPage) {
case 'files':
break;
case 'favorites':
if (targetPath === browser.currentlyPlayingStr) {
var playingFile = PlaylistManager.getCurrentlyPlayingLocal(true);
if (playingFile.pathName) {
targetPath = playingFile.pathName;
if (playingFile.baseName)
// Select currently playing file when navigating to dir.
selectEntry = playingFile.baseName;
} else {
browser.menu.showMessage('No local file is playing.');
return null;
}
}
break;
case 'playlist':
targetType = 'playlist';
selectEntry = selection.item; // Filename/title of playlist item.
break;
default:
mp.msg.error('processSelection: Unknown menu page: '+selection.menuPage);
return null; // Unknown page.
}
// It's important that we check for existence. Favorites in particular
// may contain unavailable favorite locations (such as network shares),
// but folders can also contain stale file lists that were indexed
// before something in the directory was deleted or modified.
// NOTE: The reason why there's no dedicated "refresh folder" action
// is because it's easy to press "left + right" to re-open a dir.
if (!targetType) { // No other type assigned. So it's a disk-file.
targetType = PathTools.getPathInfo(targetPath);
if (targetType === 'missing') {
mp.msg.error('Blackbox: Unable to access "'+targetPath+'".');
browser._showError(
// Since it's missing, we'll have to guess type. Only the
// files-page will be able to guess that things are files...
// The favorites-page assumes everything missing = folder!
'Blackbox: '+(selection.itemType === 'file' ? 'File' : 'Target')+
' is missing or unreadable.'+
(selection.menuPage === 'files' ? ' Re-indexing directory...' : ''),
0.8
);
if (selection.menuPage === 'files') {
// NOTE: If the WHOLE dir is missing, this shows an error:
browser.navigateDir(browser.currentPath, null, true); // Force re-index.
}
return null;
}
}
// The target exists and has been successfully analyzed.
return {
targetType: targetType,
targetPath: targetPath,
selectEntry: selectEntry
};
};
var playlistRemove = function(itemPos, showRemoval) {
// Remove the targeted playlist item (regardless of who/what added it).
var playlistCount = mp.get_property_number('playlist-count');
if (playlistCount >= 2) {
var removePos = itemPos < 0 ? playlistCount - 1 : itemPos,
removeItem = PlaylistManager.getPlaylist(removePos);
if (!removeItem)
return; // Safeguard against failure to fetch that offset.
// Refuse to remove currently playing item if last (would quit mpv).
if (removeItem.current && removePos === (playlistCount - 1)) {
browser.menu.showMessage('Cannot remove the final playlist item, because it is playing.');
return;
}
// NOTE: If this HAD been out-of-index, mpv ignores this command.
mp.commandv('playlist-remove', removePos);
// Determine whether the menu selection prefix should be cleared.
var c = browser.menu.useTextColors,
removedTitle = removeItem.title ? removeItem.title :
browser._shrinkFilename(removeItem.filename),
clearSelectionPrefix = false;
if (browser.currentPage !== 'playlist' && !PathTools.isWebURL(removeItem.filename)) {
var lastFullPath = PathTools.makePathAbsolute(removeItem.filename);
if (lastFullPath === browser.getSelection().targetPath)
// Exact playlist path matches the selected file.
clearSelectionPrefix = true;
}
if (showRemoval) {
// Show the basename and playlist-pos of the removed file.
browser.menu.showMessage(
'Removed #'+(removePos + 1)+' from playlist'+
(!removedTitle.length ? '.' : ':\n'+Ass.startSeq(c)+Ass.yellow(c)+
'"'+Ass.esc(removedTitle, c)+'"'+Ass.stopSeq(c)),
750, // Show msg for 0.75s to avoid mass-deletion accidents.
clearSelectionPrefix // Remove menu prefix after de-queue?
);
} else if (clearSelectionPrefix) {
// Don't show message, but we should at least clear prefix.
browser.menu.renderMenu(null, 1); // 1 = Only redraw if open.
}
} else if (playlistCount === 1) { // Shows if 1 left, does nothing if 0.
// Don't remove the only remaining playlist item (would quit mpv).
browser.menu.showMessage('Cannot remove the only remaining playlist item.');
}
};
// browser.menu.setCallbackMenuShow(function() {});
browser.menu.setCallbackMenuHide(function() {
browser.flipPage('none'); // Force-flip in case menu was closed via timeout.
});
browser.menu.setCallbackMenuLeft(function() {
if (browser.currentPage !== 'files' && browser.currentPage !== 'playlist')
return;
// Throttle navigation speed to avoid blazing backwards when held down.
var newNavTime = mp.get_time_ms();
if (newNavTime - browser.lastNavTime < 200) // 0.2s
return;
switch (browser.currentPage) {
case 'files':
// Navigate to parent folder, but no higher than drive root.
var parentPath = PathTools.getParentPath(browser.currentPath);
// Only reindex folder if path is new, otherwise just render.
if (parentPath.newPath !== browser.currentPath)
// NOTE: If parent dir is deleted/unreadable, this shows an error:
browser.navigateDir(
parentPath.newPath,
// Select the directory we've just left, for easy navigation.
parentPath.previousDir ? parentPath.previousDir+'/' : null
);
else
browser.menu.renderMenu();
break;
case 'playlist':
var selection = processSelection();
if (selection && selection.targetType === 'playlist' && selection.targetPath >= 1)
// We have a verified playlist index (selection). De-queue it.
playlistRemove(selection.targetPath - 1, false); // No msg.
break;
default:
mp.msg.error('Unknown menu page: '+browser.currentPage);
}
// Update navigation throttling timestamp.
browser.lastNavTime = newNavTime;
});
browser.menu.setCallbackMenuRight(function() {
var selection = processSelection();
if (!selection)
return;
switch (selection.targetType) {
case 'dir':
// Navigate into the directory. Huge dirs might take a few seconds.
browser.menu.renderMenu('[loading...]'); // Show a selection prefix.
browser.navigateDir(
selection.targetPath,
selection.selectEntry, // Filename to select, or null.
selection.selectEntry !== null // Force refresh if selection.
);
// Update navigation throttling timestamp, with a slight reduction
// since we've just entered this folder and may wanna leave it fast.
browser.lastNavTime = mp.get_time_ms() - 50;
break;
case 'file':
// Refuse to double-queue the last playlist item.
var lastItem = PlaylistManager.getPlaylist(-1);
if (lastItem && !PathTools.isWebURL(lastItem.filename) &&
selection.targetPath === PathTools.makePathAbsolute(lastItem.filename)) {
browser.menu.renderMenu('[already queued]');
return;
}
// Add the file to the playlist queue and show OSD hint.
mp.commandv('loadfile', selection.targetPath, 'append-play');
browser.menu.renderMenu('[added to playlist!]');
break;
case 'playlist':
if (selection.targetPath >= 1) // Ensure target isn't a special ID.
// We have a verified playlist index (selection). Jump to it.
// NOTE: If this HAD been out-of-index, mpv ignores the command.
mp.set_property('playlist-pos-1', selection.targetPath);
break;
default:
mp.msg.error('Unknown selection type: '+selection.targetType);
}
});
browser.menu.setCallbackMenuOpen(function() {
var selection = processSelection();
if (!selection)
return;
switch (selection.targetType) {
case 'dir':
case 'file':
// We absolutely MUST place the player into "idle if there is no
// file to play" mode! Otherwise it QUITS if dir/file is empty/bad!
mp.set_property('idle', 'yes');
// Replace whole playlist with selected file/folder (recursively).
// NOTE: Recursively loading folders is a non-blocking function call
// but may freeze the player while mpv scans files in large trees!
mp.commandv('loadfile', selection.targetPath, 'replace');
browser.menu.hideMenu();
// Show feedback to tell the user what is being queued...
var c = browser.menu.useTextColors,
osdText = Ass.startSeq(c)+Ass.scale(90, c)+
'Queueing media file'+(selection.targetType === 'dir' ? 's' : ''),
baseName = browser._shrinkFilename(selection.targetPath); // Name of dir/file.
if (baseName)
osdText += (selection.targetType === 'dir' ? ' in' : '')+':\n'+
Ass.scale(60, c)+Ass.yellow(c)+'"'+Ass.esc(baseName, c)+'"';
else
osdText += '...';
osdText += Ass.stopSeq(c);
mp.osd_message(osdText, 2); // Use regular OSD due to hiding menu.
break;
case 'playlist':
if (selection.targetPath >= 1) // Ensure target isn't a special ID.
// We have a verified playlist index (selection). Jump to it.
// NOTE: If this HAD been out-of-index, mpv ignores the command.
mp.set_property('playlist-pos-1', selection.targetPath);
else if (selection.targetPath === browser.clearPlaylistId)
// This command clears the playlist, except the playing file.
mp.commandv('playlist-clear');
break;
default:
mp.msg.error('Unknown selection type: '+selection.targetType);
}
});
browser.menu.setCallbackMenuUndo(function() {
playlistRemove(-1, true); // Remove the last playlist item. Show msg.
});
};
Blackbox.prototype._shrinkFilename = function(path)
{
if (!PathTools.isWebURL(path)) // Shrink local filenames.
path = PathTools.getBasename(path);
return path;
};
Blackbox.prototype._generateLegalPath = function(path)
{
var overrideSelect = false;
if (typeof path !== 'string') // Re-use current if no new path provided.
path = this.currentPath; // NOTE: Is null if no path navigated yet.
if (typeof path !== 'string' || !path.length) {
overrideSelect = true;
// Grab current local playlist file (if any) and make its path absolute.
var playingFile = PlaylistManager.getCurrentlyPlayingLocal(true);
// Mark the media filename for selection, to auto-select the currently
// playing local file the 1st time user opens the browser.
if (playingFile.pathName) {
path = playingFile.pathName;
if (playingFile.baseName)
overrideSelect = playingFile.baseName;
}
}
if (typeof path !== 'string' || !path.length) {
if (overrideSelect === false) // Only toggle if no name-override exists.
overrideSelect = true;
// As a last resort, begin navigation in mpv's current working
// directory. This is necessary if there was no playlist, or if the
// current file was a web URL, or if it was a relative file which
// isn't within any subfolder of the working dir.
path = PathTools.getCwd(true); // Throws.
}
return {
path: path,
overrideSelect: overrideSelect
};
};
Blackbox.prototype.getSelection = function()
{
var selection = {
menuPage: this.currentPage,
item: null,
itemType: null,
targetPath: null
};
var menuMetadata = this.menu.getMetadata();
if (!menuMetadata || menuMetadata.type !== selection.menuPage)
selection.menuPage = null; // Menu does not have data for this page.
if (selection.menuPage === null)
return selection;
var selectedItem = this.menu.getSelectedItem();
switch (selection.menuPage) {
case 'files':
var isDir = selectedItem.substring(selectedItem.length - 1) === '/';
if (isDir)
selectedItem = selectedItem.substring(0, selectedItem.length - 1);
selection.item = selectedItem;
selection.itemType = isDir ? 'dir' : 'file';
if (selectedItem.length)
selection.targetPath = PathTools.getSubPath(this.currentPath, selectedItem);
break;
case 'favorites':
selection.item = selectedItem;
selection.itemType = 'favorite';
selection.targetPath = selection.item !== this.currentlyPlayingStr ?
PathTools.makePathAbsolute(selection.item) : selection.item;
break;
case 'playlist':
var itemTitle = '',
itemIndex = null,
match = selectedItem.match(/^=?(\d+)=?: (.*)$/); // Parse selection.
if (match) {
// Validate real playlist index and ensure the item is in there.
// NOTE: Finds new idx if been moved by -2 or +2 in real playlist.
var playlist = mp.get_property_native('playlist');
if (playlist && playlist.length) {
var plTitle = match[2],
plIndex = parseInt(match[1], 10) - 1, // 1-index to 0-index.
testOffsets = [0, -1, 1, -2, 2];
for (var i = 0; i < testOffsets.length; ++i) {
var testIdx = plIndex + testOffsets[i];
if (testIdx < 0 || testIdx >= playlist.length)
continue;
// Verify via same title/filename-shrink as playlist page.
// NOTE: This ensures we've found the selected item even if
// the user's playlist manager page is slightly stale.
var thisName = playlist[testIdx].title ? playlist[testIdx].title :
this._shrinkFilename(playlist[testIdx].filename);
if (thisName === plTitle) {
// We have found the exact item the user had selected.
itemTitle = thisName;
itemIndex = testIdx + 1; // 0-index to 1-index.
break;
}
}
}
} else if (selectedItem === this.clearPlaylistStr) {
itemTitle = this.clearPlaylistStr;
itemIndex = this.clearPlaylistId; // Special, negative ID number.
}
selection.item = itemTitle; // Simplified title/filename of the item.
selection.itemType = 'playlist';
selection.targetPath = itemIndex; // Verified, 1-indexed offset (or ID).
break;
default:
mp.msg.error('getSelection: Unknown menu page: '+selection.menuPage);
selection.menuPage = null; // We can't parse selection for this page.
}
return selection;
};
Blackbox.prototype.navigateFav = function(selectEntry)
{
this.flipPage('favorites'); // Force flip to save old state.
// Only build the menu options if we aren't already viewing that data.
if (this.menu.getMetadata().type !== 'favorites') {
var i, fav,
menuOptions = [],
initialSelectionIdx = 0;
menuOptions.push(this.currentlyPlayingStr);
if (selectEntry !== this.currentlyPlayingStr)
++initialSelectionIdx; // Start at 2nd item (after "current").
for (i = 0; i < this.favoritePaths.length; ++i) {
fav = this.favoritePaths[i];
menuOptions.push(fav);
if (selectEntry === fav)
initialSelectionIdx = menuOptions.length - 1;
}
this.menu.getMetadata().type = 'favorites';
this.menu.setTitle((menuOptions.length === 0 ? '[empty] ' : '')+'Blackbox Favorites');
this.menu.setOptions(menuOptions, initialSelectionIdx);
}
this.menu.renderMenu();
};
Blackbox.prototype.navigatePlaylist = function(playlist, keepPosition)
{
this.flipPage('playlist'); // Force flip to the correct page if not already.
// NOTE: The playlist page must be refreshed every time we're called, to
// ensure that it always has a fresh, up-to-date playback/list state.
// Read the current playlist property value if not provided to us.
playlist = playlist || mp.get_property_native('playlist');
var i, entryPath, entryIsPlaying, entryIndex, entryText,
extraMenuOptions = 0,
activeIndex = null,
menuOptions = [],
initialSelectionIdx = 0;
if (playlist && playlist.length >= 2) { // Only if 2+ items in playlist.
menuOptions.push(this.clearPlaylistStr);
++extraMenuOptions;
++initialSelectionIdx; // Start at 2nd item (after "clear playlist").
}
if (keepPosition && this.menu.getMetadata().type === 'playlist')
initialSelectionIdx = this.menu.selectionIdx; // Stay at same position.
if (playlist)
for (i = 0; i < playlist.length; ++i) {
entryPath = playlist[i].title ? playlist[i].title :
this._shrinkFilename(playlist[i].filename);
entryIsPlaying = playlist[i].current;
entryIndex = i + 1;
if (entryIsPlaying)
activeIndex = entryIndex;
entryText = (entryIsPlaying ? '='+entryIndex+'=' : entryIndex)+': '+entryPath;
menuOptions.push(entryText);
if (entryIsPlaying && !keepPosition) // Found the playing item?
initialSelectionIdx = menuOptions.length - 1;
}
// Select maximum possible entry if desired target is gone (such as the last
// entry being deleted while hovering over it (in "keep position" mode)).
if (initialSelectionIdx >= menuOptions.length)
initialSelectionIdx = menuOptions.length ? menuOptions.length - 1 : 0;
this.menu.getMetadata().type = 'playlist';
this.menu.setTitle(
'['+(
menuOptions.length <= extraMenuOptions ? 'empty' :
(activeIndex !== null ? activeIndex+'/' : 'x')+
(menuOptions.length - extraMenuOptions) // Don't count extras.
)+'] Playlist Manager'
);
this.menu.setOptions(menuOptions, initialSelectionIdx);
this.menu.renderMenu();
};
Blackbox.prototype.navigateDir = function(path, selectEntry, forceRefresh)
{
var oldPage = this.currentPage;
this.flipPage('files'); // Force flip to save old state.
try {
// Transform the incoming path (uses playlist path or cwd if no path).
var legalPathInfo = this._generateLegalPath(path); // Throws if cwdfail.
path = legalPathInfo.path;
// The transformation may have picked a different path and told us which
// filename to select in that case, since we obviously can't use
// "selectEntry" if the path has been changed by the transformation.
// NOTE: The only real case where this happens is during the first
// browsing, where either the current playlist item's path or cwd is
// chosen as the startpoint. It tries to select the playing filename.
if (legalPathInfo.overrideSelect !== false)
selectEntry = typeof legalPathInfo.overrideSelect === 'string' ?
legalPathInfo.overrideSelect : null;
// NOTE: If the last-assigned menu data was "files" and the path has not
// changed, we don't waste time reindexing since it's already loaded,
// and the user's last selection is already active (no need to restore).
if (forceRefresh || this.menu.getMetadata().type !== 'files' || this.currentPath !== path) {
var i, dir, file,
dirContents = new PathIndex(path, { // Throws if bad path.
skipDotfiles: true,
fileFilterRgx: this.mediaRgx // Show only media-ext files.
}),
ignorePaths = this.ignorePaths[path],
menuOptions = [],
initialSelectionIdx = 0;
for (i = 0; i < dirContents.dirs.length; ++i) {
dir = dirContents.dirs[i];
if (ignorePaths && ignorePaths[dir])
continue; // Hide (skip) this directory.
dir += '/'; // Append slash to signify that it is a directory.
menuOptions.push(dir);
if (selectEntry === dir)
initialSelectionIdx = menuOptions.length - 1;
}
for (i = 0; i < dirContents.files.length; ++i) {
file = dirContents.files[i];
menuOptions.push(file);
if (selectEntry === file)
initialSelectionIdx = menuOptions.length - 1;
}
var helpPrefix = '';
if (this.showHelpHint && this.currentPath === null) { // 1st browse.
var helpKeys = this.menu.keyBindings['Menu-Help'].keys;
if (helpKeys.length)
helpPrefix = '['+helpKeys[0]+' for help] ';
}
this.currentPath = path;
this.lastPageSelection.files = null; // Forget saved selection after path-change.
this.menu.getMetadata().type = 'files';
this.menu.setTitle(helpPrefix+(menuOptions.length === 0 ? '[empty] ' : '')+path);
this.menu.setOptions(menuOptions, initialSelectionIdx);
}
this.menu.renderMenu();
} catch (e) {
// Restore the previous page (if different from current).
// NOTE: If we're already in the filebrowser, path stays where it was!
this.flipPage(oldPage);
this._showError('Blackbox: Unable to access directory "'+path+'".', 0.8);
}
};
Blackbox.prototype.switchMenu = function(forcePage)
{
if (typeof forcePage === 'string') {
// We're being asked to go to a specific page. Toggle if already there.
if (forcePage === this.currentPage)
forcePage = 'none';
this.flipPage(forcePage);
} else {
// Flip to the next menu page.
this.flipPage();
// NOTE: The method below can be used for skipping past empty pages.
// if (this.currentPage === 'favorites' && !this.favoritePaths.length)
// this.flipPage();
}
// Render the page, or hide menu if there were no more pages/toggled off.
if (this.currentPage === null)
this.menu.hideMenu();
else {
// Stop any lingering menu-message before swapping the page.
this.menu.stopMessage();
switch (this.currentPage) {
case 'favorites':
this.navigateFav(this.lastPageSelection.favorites);
break;
case 'files':
this.navigateDir(this.currentPath, this.lastPageSelection.files);
break;
case 'playlist':
this.navigatePlaylist(); // Selects the currently active file.
break;
default:
mp.msg.error('switchMenu: Unknown menu page: '+this.currentPage);
}
}
};
(function() {
// Read user configuration (uses defaults for any unconfigured options).
// * You can override these values via the configuration system, as follows:
// - Via permanent file: `<mpv config dir>/script-settings/Blackbox.conf`
// - Command override: `mpv --script-opts=Blackbox-favorites="{/path1}+{/path2}"`
// - Or by editing this file directly (not recommended, makes your updates harder).
var userConfig = new Options.advanced_options({
// How long to keep the menu open while you are idle.
// * (float/int) Ex: `10` (ten seconds), `0` (to disable autoclose).
auto_close: 5,
// Maximum number of file selection lines to show at a time.
// * (int) Ex: `20` (twenty lines). Cannot be lower than 3.
max_lines: 10,
// What font size to use for the menu text. Large sizes look the best.
// * (int) Ex: `42` (font size fourtytwo). Cannot be lower than 1.
font_size: 40,
// Whether to show the "[h for help]" hint on the first launch.
// * (bool) Ex: `yes` (enable) or `no` (disable).
help_hint: true,
// List of paths (and/or files) to show in the favorites menu, each delimited by `{}` and plus signs.
// * (string) Ex: `{/home/foo}+{/mnt}+{/media}+{/bunny.avi}` to add three paths and a file.
// - To get to your favorites, press the "Blackbox" hotkey twice.
favorites: '',
// (Advanced users) Optional regex for including more files in browser.
// * (string) Ex: `\.(?:rar|zip)$` to include rar and zip files too.
// - Note that the regex is ALWAYS case-insensitive and can't use flags.
// - There is no need for special "double string escaping". Just write
// the regex as you would write any other native JavaScript regex. But
// with the difference that you don't need to escape forward slashes.
// - The regex is matched against the whole filename (not the path), and
// it's your job to ensure it matches what you want, such as anchoring
// it to the extension as in the example above (by writing a period
// followed by your match followed by $ for end-of-filename anchor).
// - Your regex will be faster if you use non-capturing groups via `?:`
// such as `(?:hello)` instead of capturing groups `(hello)`.
// - Lastly, be aware that MuJS (mpv's JavaScript engine) is slow, so
// if your regex includes tons of files, the browser may slow down.
include_regex: '',
// Keybindings. You can bind any action to multiple keys simultaneously.
// * (string) Ex: `{up}`, `{up}+{shift+w}` or `{x}+{+}` (binds to "x" and the plus key).
// - Note that all "shift variants" MUST be specified as "shift+<key>".
'keys_menu_up': '{up}',
'keys_menu_down': '{down}',
'keys_menu_up_fast': '{shift+up}',
'keys_menu_down_fast': '{shift+down}',
'keys_menu_left': '{left}',
'keys_menu_right': '{right}',
'keys_menu_open': '{enter}',
'keys_menu_undo': '{bs}',
'keys_menu_help': '{h}',
'keys_menu_close': '{esc}'
});
// Create and initialize the media browser instance.
try {
var browser = new Blackbox({ // Throws.
autoCloseDelay: userConfig.getValue('auto_close'),
maxLines: userConfig.getValue('max_lines'),
menuFontSize: userConfig.getValue('font_size'),
showHelpHint: userConfig.getValue('help_hint'),
favoritePaths: userConfig.getMultiValue('favorites'),
includeRegex: userConfig.getValue('include_regex'),
keyRebindings: {
'Menu-Up': userConfig.getMultiValue('keys_menu_up'),
'Menu-Down': userConfig.getMultiValue('keys_menu_down'),
'Menu-Up-Fast': userConfig.getMultiValue('keys_menu_up_fast'),
'Menu-Down-Fast': userConfig.getMultiValue('keys_menu_down_fast'),
'Menu-Left': userConfig.getMultiValue('keys_menu_left'),
'Menu-Right': userConfig.getMultiValue('keys_menu_right'),
'Menu-Open': userConfig.getMultiValue('keys_menu_open'),
'Menu-Undo': userConfig.getMultiValue('keys_menu_undo'),
'Menu-Help': userConfig.getMultiValue('keys_menu_help'),
'Menu-Close': userConfig.getMultiValue('keys_menu_close')
}
});
} catch (e) {
mp.msg.error('Blackbox: '+e+'.');
mp.osd_message('Blackbox: '+e+'.', 3);
throw e; // Critical init error. Stop script execution.
}
// Provide the bindable mpv command which opens/cycles through the menu.
// * Bind this via input.conf: `ctrl+b script-binding Blackbox`.
// - To get to your favorites (if you've added some), press this key twice.
mp.add_key_binding(null, 'Blackbox', function() {
browser.switchMenu();
});
// Provide bindings that go directly to (or toggle off) each specific page.
mp.add_key_binding(null, 'Blackbox_Files', function() {
browser.switchMenu('files');
});
mp.add_key_binding(null, 'Blackbox_Favorites', function() {
browser.switchMenu('favorites');
});
mp.add_key_binding(null, 'Blackbox_Playlist', function() {
browser.switchMenu('playlist');
});
})();
================================================
FILE: scripts/Colorbox.js
================================================
/*
* COLORBOX.JS
*
* Description: Apply color correction presets.
* Version: 1.0.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
// Read the bottom of this file for configuration and script setup instructions.
/* jshint -W097 */
/* global mp, require, setTimeout */
'use strict';
var Options = require('Options'),
Utils = require('MicroUtils'),
Ass = require('AssFormat'),
SelectionMenu = require('SelectionMenu');
(function() {
var userConfig = new Options.advanced_options({
presets: [],
startup_preset: '',
auto_close: 5,
max_lines: 10,
font_size: 40,
font_alpha: 1.0,
'keys_menu_up': '{up}',
'keys_menu_down': '{down}',
'keys_menu_up_fast': '{shift+up}',
'keys_menu_down_fast': '{shift+down}',
'keys_menu_left': '{left}',
'keys_menu_right': '{right}',
'keys_menu_open': '{enter}',
'keys_menu_undo': '{bs}',
'keys_menu_help': '{h}',
'keys_menu_close': '{esc}'
});
var menu = new SelectionMenu({ // Throws if bindings are illegal.
maxLines: userConfig.getValue('max_lines'),
menuFontAlpha: userConfig.getValue('font_alpha'),
menuFontSize: userConfig.getValue('font_size'),
autoCloseDelay: userConfig.getValue('auto_close'),
keyRebindings: {
'Menu-Up': userConfig.getMultiValue('keys_menu_up'),
'Menu-Down': userConfig.getMultiValue('keys_menu_down'),
'Menu-Up-Fast': userConfig.getMultiValue('keys_menu_up_fast'),
'Menu-Down-Fast': userConfig.getMultiValue('keys_menu_down_fast'),
'Menu-Left': userConfig.getMultiValue('keys_menu_left'),
'Menu-Right': userConfig.getMultiValue('keys_menu_right'),
'Menu-Open': userConfig.getMultiValue('keys_menu_open'),
'Menu-Undo': userConfig.getMultiValue('keys_menu_undo'),
'Menu-Help': userConfig.getMultiValue('keys_menu_help'),
'Menu-Close': userConfig.getMultiValue('keys_menu_close')
}
});
var reloadTitle = '[Reload Configuration]',
resetTitle = '[Reset Image Settings]';
var buildMenuOptions = function(presets)
{
var i, len, parts, title, values,
presetCache = {},
menuOpts = [];
menuOpts.push({
menuText: reloadTitle,
preset: 'reload'
});
menuOpts.push({
menuText: resetTitle,
preset: 'reset'
});
for (i = 0, len = presets.length; i < len; ++i) {
parts = presets[i].split(';');
title = 'Invalid preset';
values = null;
if (parts.length) {
switch(parts[0]) {
case 'v1':
if (parts.length < 7)
break;
title = parts.slice(7).join(';').replace(/^\s+|\s+$/g, '');
if (!title.length)
title = 'Untitled preset';
values = {
contrast: parseFloat(parts[1]),
brightness: parseFloat(parts[2]),
gamma: parseFloat(parts[3]),
saturation: parseFloat(parts[4]),
hue: parseFloat(parts[5]),
sharpen: parseFloat(parts[6])
};
presetCache[title] = values;
break;
}
}
menuOpts.push({
'menuText': title,
'preset': values
});
}
var paddedIdx,
missingPadLen,
totalPadLen = (String(menuOpts.length - 2)).length;
if (totalPadLen < 2)
totalPadLen = 2;
for (i = 2, len = menuOpts.length; i < len; ++i) {
paddedIdx = String(i - 1);
missingPadLen = paddedIdx.length - totalPadLen;
if (missingPadLen < 0)
paddedIdx = '0000000000'.slice(missingPadLen)+paddedIdx;
menuOpts[i].menuText = paddedIdx + ': ' + menuOpts[i].menuText;
}
return {
presetCache: presetCache,
menuOpts: menuOpts
};
};
menu.setTitle('Colorbox Fast-look Presets');
var presetCache = {};
var rebuild = function(reload) {
if (reload) { // TODO: This is a hacky solution... Make it better?
var newConfig = new Options.advanced_options({presets:[]});
userConfig.options.presets = newConfig.getValue('presets');
}
var built = buildMenuOptions(userConfig.getValue('presets'));
presetCache = built.presetCache;
menu.setOptions(built.menuOpts, 2);
if (reload && menu.isMenuActive())
menu.renderMenu(); // Update and clear prefix.
};
rebuild();
var applyLook = function(values) {
if (values === 'reset')
values = { contrast:0, brightness: 0, gamma: 0,
saturation: 0, hue: 0, sharpen: 0.0 };
if (!values || typeof values !== 'object')
return false; // Nothing to apply.
for (var prop in values) {
if (values.hasOwnProperty(prop))
mp.set_property(prop, values[prop]);
}
return true; // Successfully applied object properties.
};
var applyLookWithFeedback = function(title, values) {
var success = applyLook(values);
mp.osd_message(
Ass.startSeq()+Ass.size(14)+
'Colorbox: '+(!success ? 'Failed to apply ' : '')+'"'+title+'".'
);
};
var handleMenuAction = function(action) {
var selection = menu.getSelectedItem();
if (selection.preset === 'reload') {
rebuild(true);
return;
}
switch (action) {
case 'Menu-Open':
menu.hideMenu();
if (selection.menuText && selection.preset) // Avoids invalid presets.
applyLookWithFeedback(selection.menuText, selection.preset);
break;
case 'Menu-Right':
var success = applyLook(selection.preset);
menu.renderMenu(success ? '*' : '-');
break;
}
};
menu.setCallbackMenuOpen(handleMenuAction);
menu.setCallbackMenuRight(handleMenuAction);
mp.add_key_binding(null, 'Colorbox', function() {
if (!menu.isMenuActive())
menu.renderMenu();
else
menu.hideMenu();
});
var applyLookByName = function(lookName) {
var title, values;
if (lookName === 'reset') {
title = resetTitle;
values = 'reset';
} else if (presetCache.hasOwnProperty(lookName)) {
title = lookName;
values = presetCache[lookName];
} else {
var err = 'Colorbox: Cannot find preset "'+lookName+'".';
mp.osd_message(Ass.startSeq()+Ass.size(14)+err);
mp.msg.warn(err);
return;
}
applyLookWithFeedback(title, values);
};
mp.register_script_message('Colorbox_ApplyLook', function(lookName) {
applyLookByName(lookName);
});
var startupPreset = userConfig.getValue('startup_preset');
if (startupPreset.length)
applyLookByName(startupPreset);
})();
================================================
FILE: scripts/Gallerizer.js
================================================
/*
* GALLERIZER.JS
*
* Description: Image gallery autoloader for mpv.
* Version: 1.1.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
// Read the bottom of this file for configuration and script setup instructions.
/* jshint -W097 */
/* global mp, require, setTimeout */
'use strict';
var Options = require('Options'),
PathIndex = require('PathIndex'),
PathTools = require('PathTools'),
Utils = require('MicroUtils');
(function() {
var userConfig = new Options.advanced_options({
find_extensions: 'jpg,jpeg,png,bmp,gif'
});
var debug = false;
var wantedExts = userConfig.getValue('find_extensions').split(',');
var autoQueue = function(filename)
{
var currentFile = PathTools.getBasename(filename),
path = PathTools.getPathname(PathTools.makePathAbsolute(filename));
if (!path || !currentFile)
return;
var i, len, file, ext,
foundAt = -1,
files = [],
index = new PathIndex(path, {
skipDotfiles: true,
includeDirs: false
});
for (i = 0, len = index.files.length; i < len; ++i) {
file = index.files[i];
ext = PathTools.getExtension(file);
if (wantedExts.indexOf(ext) < 0)
continue;
if (file === currentFile)
foundAt = files.length;
files.push(PathTools.getSubPath(path, file));
}
if (foundAt === -1 || files.length <= 1)
return; // We didn't find target file, or ONLY found that file.
// Immediately ensure playback is paused, since mpv's playback is async.
// NOTE: This works even before the player has fully initialized.
mp.set_property_bool('pause', true);
// Append all files, including the one we've already started with. This
// means that it will exist as a duplicate at both offset 0 and X. We
// cannot trigger any playlist-pos or playlist-move commands to take
// care of that now, since mpv may not have fully initialized yet, and
// would just insist on always starting at pos 0.
for (i = 0, len = files.length; i < len; ++i)
mp.commandv('loadfile', files[i], 'append');
// Swap position to the "duplicate" at the real offset, which is the
// same file and therefore a flicker-free switch. Then delete original.
mp.set_property('playlist-pos', foundAt + 1);
mp.commandv('playlist-remove', 0);
};
var resolvingDir = false;
mp.observe_property('playlist/0/playing', 'bool', function(name, isPlaying) {
var filename, info, ext;
if (isPlaying && mp.get_property_number('playlist-count') === 1) {
// There's only a single entry and it started playing. Analyze it.
filename = mp.get_property('playlist/0/filename');
if (!filename || PathTools.isWebURL(filename)) {
resolvingDir = false;
return;
}
info = PathTools.getPathInfo(filename);
switch (info) {
case 'dir':
// We know the next playlist-modification will be loaded
// folder contents. And since there was only a single
// playlist entry, we know whole list will be replaced.
resolvingDir = true;
if (debug)
Utils.dump('resolving dir:'+filename);
break;
case 'file':
resolvingDir = false;
// The playlist contains a single, local file. Determine
// if it's from a filetype that we should be autoloading.
ext = PathTools.getExtension(filename);
if (wantedExts.indexOf(ext) >= 0) {
if (debug)
Utils.dump('autoload:'+filename);
autoQueue(filename);
}
break;
default: // "missing".
resolvingDir = false;
}
}
});
mp.observe_property('playlist/0/filename', 'string', function(name, filename) {
if (!resolvingDir)
return;
// When we load a dir, the whole playlist changes. If the 1st queued
// file from the dir is a wanted type, we should now pause the playback.
var ext = PathTools.getExtension(filename);
if (wantedExts.indexOf(ext) >= 0) {
if (debug)
Utils.dump('dir contained autoload filetype, pausing:'+filename);
mp.set_property_bool('pause', true);
}
resolvingDir = false;
});
})();
================================================
FILE: scripts/Leapfrog.js
================================================
/*
* LEAPFROG.JS
*
* Description: Effortlessly jump through your playlist, with your own custom
* jump size and direction, including the ability to jump randomly.
* Excellent when queuing lots of images and using mpv as an image
* viewer.
* Version: 1.7.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
// Read the bottom of this file for configuration and script setup instructions.
/* jshint -W097 */
/* global mp, require */
'use strict';
var Options = require('Options'),
Utils = require('MicroUtils'),
Ass = require('AssFormat'),
Stack = require('Stack'),
RandomCycle = require('RandomCycle');
var Leapfrog = function(globalOpts)
{
this.fontSize = globalOpts.fontSize;
this.fontAlpha = Ass.convertPercentToHex( // Throws if invalid input.
(typeof globalOpts.fontAlpha === 'number' &&
globalOpts.fontAlpha >= 0 && globalOpts.fontAlpha <= 1 ?
globalOpts.fontAlpha : 1),
true // Invert input range so "1.0" is visible and "0.0" is invisible.
);
this.throttleTime = 0;
this.history = new Stack(200);
this.randomOrder = {
rebuild: true,
cycle: new RandomCycle()
};
var self = this;
this.playlistPos = mp.get_property_number('playlist-pos');
mp.observe_property('playlist-pos', 'number', function(name, value) {
self.playlistPos = value;
});
this.playlistCount = mp.get_property_number('playlist-count');
mp.observe_property('playlist-count', 'number', function(name, value) {
self.playlistCount = value;
// New count means new/changed playlist. Clear unreliable history.
// NOTE: When people queue folders, count changes to "1" (the folder)
// and then the real file count, so re-queuing same/diff folders with
// the same amount of files is accurately detected and cleared too.
self.history.clearStack();
// We must also mark the playlist cycle for rebuilding, so that every
// playlist modification always begins a new randomly ordered sequence.
self.randomOrder.rebuild = true;
});
// Only use menu text colors while mpv is rendering in GUI mode (non-CLI).
this.useTextColors = mp.get_property_bool('vo-configured');
mp.observe_property('vo-configured', 'bool', function(name, value) {
self.useTextColors = value;
});
};
Leapfrog.prototype._formatMsg = function(msg, useTextColors)
{
if (useTextColors === false)
return msg;
var out = Ass.startSeq();
if (this.fontSize > 0)
out += Ass.size(this.fontSize);
out += Ass.alpha(this.fontAlpha);
out += Ass.esc(msg)+Ass.stopSeq();
return out;
};
Leapfrog.prototype.jump = function(offset, rawOptions)
{
if (!this.playlistCount)
return; // Nothing in playlist.
// Parse options.
var i,
options = {},
parts = rawOptions ? rawOptions.split(',') : [];
for (i = 0; i < parts.length; ++i) {
options[parts[i]] = true;
}
// Handle throttling.
if (options.throttle) {
var now = mp.get_time_ms();
if (now - this.throttleTime < 250) // 0.25s
return;
this.throttleTime = now;
}
// Calculate new playlist position.
var newPosition, msgPrefix, historyEntry,
c = this.useTextColors;
switch (offset) {
case 'undo-random':
var previous = this.history.pop();
if (typeof previous !== 'object' || typeof previous.pos === 'undefined') {
if (!options.silent && !options.silenterr)
mp.osd_message(this._formatMsg('Undo: No history.', c));
return;
}
newPosition = previous.pos;
msgPrefix = 'Undo:';
break;
case 'random':
if (this.randomOrder.rebuild) { // Generate deterministic jump-order.
this.randomOrder.cycle.setCount(this.playlistCount);
this.randomOrder.rebuild = false;
}
historyEntry = {pos: this.playlistPos};
try {
newPosition = this.randomOrder.cycle.getNext(this.playlistPos); // Throws.
} catch (e) { // Safeguard against pos somehow being larger than cycle.
this.randomOrder.rebuild = true;
return; // Silently fail and let the user press again to retry.
}
msgPrefix = 'Random:';
break;
case 'first':
newPosition = 0;
msgPrefix = 'First:';
break;
case 'last':
newPosition = this.playlistCount - 1;
msgPrefix = 'Last:';
break;
default:
offset = parseInt(offset, 10);
if (isNaN(offset) || offset === 0) {
mp.msg.error('Leapfrog: Invalid offset number.');
if (!options.silent && !options.silenterr)
mp.osd_message(this._formatMsg('Jump: Invalid offset number.', c));
return;
}
newPosition = this.playlistPos + offset;
msgPrefix = 'Jump: '+(offset > 0 ? '+' : '')+offset;
}
// Clamp position value to edges of playlist.
if (newPosition < 0)
newPosition = 0;
else if (newPosition >= this.playlistCount)
newPosition = this.playlistCount - 1;
// Save old position to history, but only if different.
if (historyEntry && newPosition !== this.playlistPos)
this.history.push(historyEntry);
// Update position.
mp.set_property('playlist-pos', newPosition);
// Display on-screen feedback.
if (!options.silent)
mp.osd_message(
this._formatMsg(
msgPrefix+' ('+(newPosition + 1)+' / '+this.playlistCount+')',
c
),
1.5
);
};
(function() {
// Read user configuration (uses defaults for any unconfigured options).
// * You can override these values via the configuration system, as follows:
// - Via permanent file: `<mpv config dir>/script-settings/Leapfrog.conf`
// - Command override: `mpv --script-opts=Leapfrog-font_size=16`
// - Or by editing this file directly (not recommended, makes your updates harder).
var userConfig = new Options.advanced_options({
// What font size to use for the Leapfrog status messages.
// * NOTE: Final size can vary in non-fullscreen due to mpv's scaling.
// * (int) Ex: `-1` (use same size as regular OSD), `16` (size 16).
font_size: -1,
// How transparent the status text should be (from 0.0 to 1.0).
// * (float) Ex: `1.0` (fully visible) to `0.0` (fully transparent).
font_alpha: 1.0
});
// Provide the bindable mpv command which performs the playlist jump.
// * Bind this via input.conf: `ctrl+x script-message Leapfrog -10`.
// - Jumps can be either positive (ie. `100`) or negative (ie. `-3`).
// - You can use the word `first` to jump directly to the first entry, or
// `last` for the last playlist entry: `script-message Leapfrog first`.
// - Use the word `random` to perform completely random jumps, as follows:
// `script-message Leapfrog random`.
// - To undo your random jumps, use `script-message Leapfrog undo-random`.
// This function always takes you back to the position you were at before
// your last random jump. Pressing it multiple times traverses backwards
// through the history of random jump locations.
// - You can silence the on-screen messages by adding the option "silent"
// at the end: `script-message Leapfrog 5 silent`.
// - To only silence error messages, use "silenterr" instead (this is useful
// together with "undo-random" to hide the error when history is empty).
// - If you want to be able to hold down the key, you should bind it with
// the "repeatable" flag and the "throttle" option, as follows:
// `repeatable script-message Leapfrog 1 throttle`. The throttling
// ensures playlist progression at a sane pace when the key is held down.
// - Lastly, you can combine multiple options by separating them with
// commas, such as: `Leapfrog random throttle,silent`.
// - The randomizer uses an intelligent "random cycle" algorithm which
// traverses all playlist entries in a random order and never visits the
// same item twice (until it has wrapped around through all entries). The
// main reason for this is to achieve a deterministic order for the
// "random" and "undo-random" functions, so that you can go back and forth
// through the results (and achieving that without using any "forwards
// history stack" when going forwards again, since that would have locked
// you to being forced to re-watch all history entries you had previously
// randomized through, whenever you want to resume going forwards). It
// also avoids the annoyance of seeing randomly repeated entries (which
// is what you'd naturally see if each keypress was truly randomizing the
// selection independently of each other; for example, a truly random
// algorithm which randomizes at every press may select an order such as
// "3 -> 2 -> 3 -> 2 -> 3 -> 1 -> 4", so true randomness is very bad).
// Under the hood, our algorithm instead uses the current playlist
// position number to determine what position to visit next (and the
// algorithm generates a new, unique order for this every time the
// playlist changes). So if you've pressed "random" five times (and
// traversed "1 -> 5 -> 3 -> 4 -> 2 -> 6"), and then "undo-random" four
// times (which took you back to "5"), and you would prefer to not have to
// press "random" four times to travel forwards through the sequence
// you've already seen ("3, 4, 2, 6"), then all you have to do is manually
// change your current position by going to another playlist entry before
// pressing "random" again. Doing so will cause your "random" request to
// travel the sequence starting at that playlist entry's position instead.
// Try it out and you'll get the hang of the technique! ;-)
var frog = new Leapfrog({
fontSize: userConfig.getValue('font_size'),
fontAlpha: userConfig.getValue('font_alpha')
});
mp.register_script_message('Leapfrog', function(offset, rawOptions) {
frog.jump(offset, rawOptions);
});
})();
================================================
FILE: scripts/auto-keep-gui-open.lua
================================================
-- -----------------------------------------------------------
--
-- AUTO-KEEP-GUI-OPEN.LUA
-- Version: 1.0.1
-- Author: VideoPlayerCode
-- URL: https://github.com/VideoPlayerCode/mpv-tools
--
-- Description:
--
-- Intelligently switches mpv's "keep-open" behavior based on
-- whether you are running in video-mode or audio-only mode.
--
-- -----------------------------------------------------------
--
-- Recommended configuration:
-- AKGO_WINDOW_KEEP_OPEN_VALUE="yes" (or "always")
-- AKGO_NOWINDOW_KEEP_OPEN_VALUE="no" (or "original")
-- AKGO_USE_WINDOW_FOR_AUDIO_AFTER_VIDEO=true
--
-- Result:
-- * If you manually queue up a bunch of audio-only files via your
-- command line, then you'll never see a GUI, so you'll always be in
-- the "NOWINDOW" state, which means that the player automatically
-- advances to the next audio file and quits when it reaches the end.
-- * It's only when you play a *video* file that the "WINDOW" state
-- happens, and thanks to the "USE_WINDOW" option mpv will then
-- automatically *stay* in the video/window state and use the
-- "AKGO_WINDOW_KEEP_OPEN_VALUE" for the rest of the playlist.
-- * This script therefore automatically gives you the best of both
-- worlds. Audio-only playlists started from the command line will
-- stay in CLI mode if started from CLI, but Video / Audio+Video
-- playlists (regardless of whether they were started from the
-- desktop via double-clicking or via the command line) will *stay*
-- in GUI mode and use the WINDOW keep-open value, which makes mpv
-- behave even better as a GUI application, without hurting CLI mode.
--
-- -----------------------------------------------------------
--
-- ### START OF USER CONFIGURATION:
--
-------
--
-- AKGO_WINDOW_KEEP_OPEN_VALUE:
-- Possible values are: "no", "yes", "always", "original".
-- Reference: https://mpv.io/manual/master/#options-keep-open
--
-- The "keep-open" option will be set to this value every time the video
-- output is created. Most video output modules create a GUI, so in
-- other words, this is the "keep-open" value that mpv will use when in
-- GUI mode with a video window on screen.
--
-- Note: The special value "original" means whatever was in your user
-- config before this script was loaded.
--
--
-- I recommend setting this option to "yes", so that mpv's GUI stays
-- open after you've reached the end of your playlist or scrubbed the
-- playback position to the end of the last file. This means that you
-- can safely scrub the playback without worrying that you'll hover near
-- the end and make mpv "insta-terminate" when you were just trying to
-- scrub the video position. It makes mpv behave much better as a GUI
-- application.
--
-- You may even want to set it to "always", to always pause after the
-- end of every playlist item (instead of just the last one), to give
-- you manual control over advancing your playlists. But to most people,
-- that isn't as important as simply ensuring that the player stays alive
-- after the end of the final file.
--
-- The benefit of using this script instead of setting the option
-- globally, is that it makes it easy to have one "keep-open" setting
-- for music and another for videos, without having to fiddle with
-- manual per-extension settings. This script is smarter than extension
-- filters, since we detect when video output is used, as opposed to
-- blindly guessing based on file extension. The dynamic switching of
-- this script means that you preserve the ability to use mpv from the
-- command line to play your music files without worrying about having
-- to manually advance every music playlist step by step if you had used
-- a global "keep-open" setting. And your mpv GUI will be much more
-- reliable for video playback as well, since you will prevent mpv from
-- accidentally quitting its GUI at the slightest accidental touch. ;-)
--
AKGO_WINDOW_KEEP_OPEN_VALUE="yes"
--
-------
--
-- AKGO_NOWINDOW_KEEP_OPEN_VALUE:
-- Possible values are: "no", "yes", "always", "original".
-- Reference: https://mpv.io/manual/master/#options-keep-open
--
-- The "keep-open" option will be set to this value every time the video
-- output is destroyed. If you want non-video files in your playlist
-- to be treated differently, then this is for you.
--
-- Note: The special value "original" means whatever was in your user
-- config before this script was loaded.
--
--
-- However, if you set AKGO_USE_WINDOW_FOR_AUDIO_AFTER_VIDEO to true,
-- then all audio files in the playlist (*after* a *video* has played)
-- will continue using the GUI, and this "NOWINDOW" state won't happen.
--
-- I recommend keeping this at "no" or "original" (which means "no" if
-- you haven't set any custom config value for "keep-open").
--
AKGO_NOWINDOW_KEEP_OPEN_VALUE="original"
--
-------
--
-- AKGO_USE_WINDOW_FOR_AUDIO_AFTER_VIDEO:
-- Possible values are: true, false.
-- Reference: https://mpv.io/manual/master/#options-force-window
--
-- If true, we will automatically enable "force-window" after mpv has
-- used video output at least once during the current playlist session.
--
--
-- For most people (who only play a single file) this won't do anything.
-- Nor for people who start mpv with "--player-operation-mode pseudo-gui"
-- since that already enables the video output for audio files too.
--
-- Instead, this option is for when you're *manually* queuing up
-- multiple files from the command line and some of them are video files
-- and some are audio files. Without this option, mpv would switch back
-- and forth between terminal output (CLI) for the playlist's audio
-- files, and video output (GUI) for its video files.
--
-- Automatically switching to force-window means we will use the GUI
-- even for the audio files.
--
-- I recommend leaving this option enabled.
--
AKGO_USE_WINDOW_FOR_AUDIO_AFTER_VIDEO=true
--
-------
--
-- ### END OF USER CONFIGURATION.
-- -----------------------------------------------------------
-- Only proceed if user's personal/system mpv config was loaded.
-- NOTE: This is just a safeguard to protect programs that use
-- mpv as their backend via "--no-config" mode. For now, that
-- actually prevents all user scripts from loading, but there's
-- no guarantee it will always be that way. Better safe than sorry.
if (mp.get_property("config") ~= "no") then
local originalKeepOpenValue = mp.get_property("keep-open")
-- This runs with "false" at mpv initialization (before the playback
-- of the first file), regardless of whether that file will use
-- video output or not. After that, it runs whenever the video
-- output (usually a GUI window) is created or destroyed. So if the
-- first file is a video, it runs twice at startup (false -> true).
--
-- Implementation details:
-- vo-configured == video output created && its configuration went ok.
-- What that means depends on the platform-specific video output module,
-- but usually it means there is a GUI on screen to display video.
mp.observe_property(
"vo-configured",
"bool",
function (name, value)
if (value and mp.get_property("vo") ~= "image") then
-- Video output (usually a GUI) has been created.
-- NOTE: We ignore the "image" vo driver, which isn't a "real"
-- window since it has no GUI and just writes images to disk.
if (AKGO_WINDOW_KEEP_OPEN_VALUE == "original") then
mp.set_property("keep-open", originalKeepOpenValue)
else
mp.set_property("keep-open", AKGO_WINDOW_KEEP_OPEN_VALUE)
end
if (AKGO_USE_WINDOW_FOR_AUDIO_AFTER_VIDEO) then
mp.set_property("force-window", "yes")
end
else
-- Video output (usually a GUI) has been destroyed.
if (AKGO_NOWINDOW_KEEP_OPEN_VALUE == "original") then
mp.set_property("keep-open", originalKeepOpenValue)
else
mp.set_property("keep-open", AKGO_NOWINDOW_KEEP_OPEN_VALUE)
end
end
end)
end
================================================
FILE: scripts/cycle-video-rotate.lua
================================================
-- -----------------------------------------------------------
--
-- CYCLE-VIDEO-ROTATE.LUA
-- Version: 1.0
-- Author: VideoPlayerCode
-- URL: https://github.com/VideoPlayerCode/mpv-tools
--
-- Description:
--
-- Allows you to perform video rotation which perfectly
-- cycles through all 360 degrees without any glitches.
--
-- -----------------------------------------------------------
function cycle_video_rotate(amt)
-- Ensure that amount is a base 10 integer.
amt = tonumber(amt, 10)
if amt == nil then
mp.osd_message("Rotate: Invalid rotation amount")
return nil -- abort
end
-- Calculate what the next rotation value should be,
-- and wrap value to correct range (0 (aka 360) to 359).
local newrotate = mp.get_property_number("video-rotate")
newrotate = ( newrotate + amt ) % 360
-- Change rotation and tell the user.
mp.set_property_number("video-rotate", newrotate)
mp.osd_message("Rotate: " .. newrotate)
end
-- Bind this via input.conf. Example:
-- Alt+LEFT script-message Cycle_Video_Rotate -90
-- Alt+RIGHT script-message Cycle_Video_Rotate 90
mp.register_script_message("Cycle_Video_Rotate", cycle_video_rotate)
================================================
FILE: scripts/modules.js/AssFormat.js
================================================
/*
* ASSFORMAT.JS (MODULE)
*
* Version: 1.2.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
/* jshint -W097 */
/* global mp, module, require */
'use strict';
var Utils = require('MicroUtils');
var Ass = {};
Ass._startSeq = mp.get_property_osd('osd-ass-cc/0');
Ass._stopSeq = mp.get_property_osd('osd-ass-cc/1');
Ass.startSeq = function(output)
{
return output === false ? '' : Ass._startSeq;
};
Ass.stopSeq = function(output)
{
return output === false ? '' : Ass._stopSeq;
};
Ass.esc = function(str, escape)
{
if (escape === false) // Conveniently disable escaping via the same call.
return str;
// Uses the same technique as mangle_ass() in mpv's osd_libass.c:
// - Treat backslashes as literal by inserting a U+2060 WORD JOINER after
// them so libass can't interpret the next char as an escape sequence.
// - Replace `{` with `\{` to avoid opening an ASS override block. There is
// no need to escape the `}` since it's printed literally when orphaned.
// - See: https://github.com/libass/libass/issues/194#issuecomment-351902555
return str.replace(/\\/g, '\\\u2060').replace(/\{/g, '\\{');
};
Ass.size = function(fontSize, output)
{
return output === false ? '' : '{\\fs'+fontSize+'}';
};
Ass.scale = function(scalePercent, output)
{
return output === false ? '' : '{\\fscx'+scalePercent+'\\fscy'+scalePercent+'}';
};
Ass.convertPercentToHex = function(percent, invertValue)
{
// Tip: Use with "invertValue" to convert input range 0.0 (invisible) - 1.0
// (fully visible) to hex range '00' (fully visible) - 'FF' (invisible), for
// use with the alpha() function in a logical manner for end-users.
if (typeof percent !== 'number' || percent < 0 || percent > 1)
throw 'Invalid percentage value (must be 0.0 - 1.0)';
return Utils.toHex(
Math.floor( // Invert range (optionally), and make into a 0-255 value.
255 * (invertValue ? 1 - percent : percent)
),
2 // Fixed-size: 2 bytes (00-FF), as needed for hex in ASS subtitles.
);
};
Ass.alpha = function(transparencyHex, output)
{
return output === false ? '' : '{\\alpha&H'+transparencyHex+'&}'; // 00-FF.
};
Ass.color = function(rgbHex, output)
{
return output === false ? '' : '{\\1c&H'+rgbHex.substring(4, 6)+rgbHex.substring(2, 4)+rgbHex.substring(0, 2)+'&}';
};
Ass.white = function(output)
{
return Ass.color('FFFFFF', output);
};
Ass.gray = function(output)
{
return Ass.color('909090', output);
};
Ass.yellow = function(output)
{
return Ass.color('FFFF90', output);
};
Ass.green = function(output)
{
return Ass.color('90FF90', output);
};
module.exports = Ass;
================================================
FILE: scripts/modules.js/MicroUtils.js
================================================
/*
* MICROUTILS.US (MODULE)
*
* Version: 1.3.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
/* jshint -W097 */
/* global mp, module, require */
'use strict';
var Utils = {};
// NOTE: This is an implementation of a non-recursive quicksort, which doesn't
// risk any stack overflows. This function is necessary because of a MuJS <=
// 1.0.1 bug which causes a stack overflow when running its built-in sort() on
// any large array. See: https://github.com/ccxvii/mujs/issues/55
// Furthermore, this performs optimized case-insensitive sorting.
Utils.quickSort = function(arr, options)
{
options = options || {};
var i, sortRef,
caseInsensitive = !!options.caseInsensitive;
if (caseInsensitive) {
sortRef = arr.slice(0);
for (i = sortRef.length - 1; i >= 0; --i)
if (typeof sortRef[i] === 'string')
sortRef[i] = sortRef[i].toLowerCase();
return Utils.quickSort_Run(arr, sortRef);
}
return Utils.quickSort_Run(arr);
};
Utils.quickSort_Run = function(arr, sortRef)
{
if (arr.length <= 1)
return arr;
var hasSortRef = !!sortRef;
if (!hasSortRef)
sortRef = arr; // Use arr instead. Makes a direct reference (no copy).
if (arr.length !== sortRef.length)
throw 'Array and sort-reference length must be identical';
// Adapted from a great, public-domain C algorithm by Darel Rex Finley.
// Original implementation: http://alienryderflex.com/quicksort/
// Ported by VideoPlayerCode and extended to sort via a 2nd reference array,
// to allow sorting the main array by _any_ criteria via the 2nd array.
var refPiv, arrPiv, beg = [], end = [], stackMax = -1, stackPtr = 0, L, R;
beg.push(0); end.push(sortRef.length);
++stackMax; // Tracks highest available stack index.
while (stackPtr >= 0) {
L = beg[stackPtr]; R = end[stackPtr] - 1;
if (L < R) {
if (hasSortRef) // If we have a SEPARATE sort-ref, mirror actions!
arrPiv = arr[L];
refPiv = sortRef[L]; // Left-pivot is fastest, no MuJS math needed!
while (L < R) {
while (sortRef[R] >= refPiv && L < R) R--;
if (L < R) {
if (hasSortRef)
arr[L] = arr[R];
sortRef[L++] = sortRef[R];
}
while (sortRef[L] <= refPiv && L < R) L++;
if (L < R) {
if (hasSortRef)
arr[R] = arr[L];
sortRef[R--] = sortRef[L];
}
}
if (hasSortRef)
arr[L] = arrPiv;
sortRef[L] = refPiv;
if (stackPtr === stackMax) {
beg.push(0); end.push(0); // Grow stacks to fit next elem.
++stackMax;
}
beg[stackPtr + 1] = L + 1;
end[stackPtr + 1] = end[stackPtr];
end[stackPtr++] = L;
} else {
stackPtr--;
// NOTE: No need to shrink stack here. Size-reqs GROW until sorted!
// (Anyway, MuJS is slow at splice() and wastes time if we shrink.)
}
}
return arr;
};
Utils.isInt = function(value)
{
// Verify that the input is an integer (whole number).
return (typeof value !== 'number' || isNaN(value)) ?
false :
(value | 0) === value;
};
Utils._hexSymbols = [
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
];
Utils.toHex = function(num, outputLength)
{
// Generates a fixed-length output, and handles negative numbers properly.
var result = '';
while (outputLength--) {
result = Utils._hexSymbols[num & 0xF] + result;
num >>= 4;
}
return result;
};
Utils.shuffle = function(arr)
{
var m = arr.length, tmp, i;
while (m) { // While items remain to shuffle...
// Pick a remaining element...
i = Math.floor(Math.random() * m--);
// And swap it with the current element.
tmp = arr[m];
arr[m] = arr[i];
arr[i] = tmp;
}
return arr;
};
Utils.trim = function(str)
{
return str.replace(/(?:^\s+|\s+$)/g, ''); // Trim left and right whitespace.
};
Utils.ltrim = function(str)
{
return str.replace(/^\s+/, ''); // Trim left whitespace.
};
Utils.rtrim = function(str)
{
return str.replace(/\s+$/, ''); // Trim right whitespace.
};
Utils.dump = function(value)
{
mp.msg.error(JSON.stringify(value));
};
Utils.benchmarkStart = function(textLabel)
{
Utils.benchmarkTimestamp = mp.get_time();
Utils.benchmarkTextLabel = textLabel;
};
Utils.benchmarkEnd = function()
{
var now = mp.get_time(),
start = Utils.benchmarkTimestamp ? Utils.benchmarkTimestamp : now,
elapsed = now - start,
label = typeof Utils.benchmarkTextLabel === 'string' ? Utils.benchmarkTextLabel : '';
mp.msg.info('Time Elapsed (Benchmark'+(label.length ? ': '+label : '')+'): '+elapsed+' seconds.');
};
module.exports = Utils;
================================================
FILE: scripts/modules.js/Options.js
================================================
/*
* OPTIONS.JS (MODULE)
*
* Description: JavaScript implementation of mpv's Lua API's config file system,
* via "mp.options.read_options()". See official Lua docs for help.
* https://github.com/mpv-player/mpv/blob/master/DOCS/man/lua.rst#mpoptions-functions
* Version: 2.1.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
/* jshint -W097 */
/* global mp, exports, require */
'use strict';
var ScriptConfig = function(options, identifier)
{
if (!options)
throw 'Options table parameter is missing.';
this.options = options;
this.scriptName = typeof identifier === 'string' ? identifier : mp.get_script_name();
this.configFile = null;
// Converts string "val" to same primitive type as "destTypeVal".
var typeConv = function(destTypeVal, val)
{
switch (typeof destTypeVal) {
case 'object':
if (!Array.isArray(destTypeVal))
val = undefined; // Unknown "object" target variable.
else if (typeof val !== 'string')
val = String(val); // Target is array, so use string values.
break;
case 'string':
if (typeof val !== 'string')
val = String(val);
break;
case 'boolean':
if (val === 'yes')
val = true;
else if (val === 'no')
val = false;
else {
mp.msg.error('Error: Can\'t convert '+JSON.stringify(val)+' to boolean!');
val = undefined;
}
break;
case 'number':
var num = parseFloat(val);
if (!isNaN(num))
val = num;
else {
mp.msg.error('Error: Can\'t convert '+JSON.stringify(val)+' to number!');
val = undefined;
}
break;
default:
val = undefined;
}
return val;
};
// Find config file.
if (this.scriptName && this.scriptName.length) {
mp.msg.debug('Reading options for '+this.scriptName+'.');
this.configFile = mp.find_config_file('script-settings/'+this.scriptName+'.conf');
if (!this.configFile) // Try legacy settings location as fallback.
this.configFile = mp.find_config_file('lua-settings/'+this.scriptName+'.conf');
}
// Read and parse configuration if found.
var i, len, pos, key, val, isArrayVal, convVal;
if (this.configFile && this.configFile.length) {
try {
var line, configLines = mp.utils.read_file(this.configFile).split(/[\r\n]+/);
for (i = 0, len = configLines.length; i < len; ++i) {
line = configLines[i].replace(/^\s+/, '');
if (!line.length || line.charAt(0) === '#')
continue;
pos = line.indexOf('=');
if (pos < 0) {
mp.msg.warn('"'+this.configFile+'": Ignoring malformatted config line "'+line.replace(/\s+$/, '')+'".');
continue;
}
key = line.substring(0, pos);
val = line.substring(pos + 1);
isArrayVal = false;
if ('[]' === line.substring(pos - 2, pos)) {
key = key.substring(0, key.length - 2);
isArrayVal = true;
}
if (this.options.hasOwnProperty(key)) {
convVal = typeConv(this.options[key], val);
if (typeof convVal !== 'undefined') {
if (Array.isArray(this.options[key])) {
if (isArrayVal)
this.options[key].push(convVal);
else
mp.msg.error('"'+this.configFile+'": Ignoring non-array value for array-based option key "'+key+'".');
}
else
this.options[key] = convVal;
}
else
mp.msg.error('"'+this.configFile+'": Unable to convert value "'+val+'" for key "'+key+'".');
}
else
mp.msg.warn('"'+this.configFile+'": Ignoring unknown key "'+key+'".');
}
} catch (e) {
mp.msg.error('Unable to read configuration file "'+this.configFile+'".');
}
}
else
mp.msg.verbose('Unable to find configuration file for '+this.scriptName+'.');
// Parse command-line options.
if (this.scriptName && this.scriptName.length) {
var cmdOpts = mp.get_property_native('options/script-opts'), rawOpt,
prefix = this.scriptName+'-', keyLen;
len = prefix.length;
for (rawOpt in cmdOpts) {
if (!cmdOpts.hasOwnProperty(rawOpt))
continue;
pos = rawOpt.indexOf(prefix);
if (pos !== 0)
continue;
key = rawOpt.substring(len);
keyLen = key.length;
isArrayVal = false;
if ('[]' === key.substring(keyLen - 2)) {
key = key.substring(0, keyLen - 2);
isArrayVal = true;
}
if (key.length && this.options.hasOwnProperty(key)) {
val = cmdOpts[rawOpt];
convVal = typeConv(this.options[key], val);
if (typeof convVal !== 'undefined') {
if (Array.isArray(this.options[key])) {
if (isArrayVal)
this.options[key].push(convVal);
else
mp.msg.error('script-opts: Ignoring non-array value for array-based option key "'+key+'".');
}
else
this.options[key] = convVal;
}
else
mp.msg.error('script-opts: Unable to convert value "'+val+'" for key "'+key+'".');
}
else
mp.msg.warn('script-opts: Ignoring unknown key "'+key+'".');
}
}
};
ScriptConfig.prototype.getValue = function(key)
{
if (!this.options.hasOwnProperty(key))
throw 'Invalid option "'+key+'"';
return this.options[key];
};
ScriptConfig.prototype.getMultiValue = function(key)
{
// Multi-value format: `{one}+{two}+{three}`.
var i, len,
val = this.getValue(key), // Throws.
result = [];
if (typeof val !== 'string')
throw 'Invalid non-string value in multi-value option "'+key+'"';
len = val.length;
if (len) {
if (val.charAt(0) !== '{' || val.charAt(len - 1) !== '}')
throw 'Missing surrounding "{}" brackets in multi-value option "'+key+'"';
val = val.substring(1, len - 1).split('}+{');
len = val.length;
for (i = 0; i < len; ++i) {
result.push(val[i]);
}
}
return result;
};
// Class `advanced_options()`: Offers extended features such as multi-values.
exports.advanced_options = ScriptConfig;
// Function `read_options()`: Behaves like Lua API (returns plain list of opts).
exports.read_options = function(table, identifier) {
// NOTE: "table" will be modified by reference, just as the Lua version.
var config = new ScriptConfig(table, identifier);
return config.options; // This is the same object as "table".
};
================================================
FILE: scripts/modules.js/PathIndex.js
================================================
/*
* PATHINDEX.JS (MODULE)
* Version: 1.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
/* jshint -W097 */
/* global mp, module, require */
'use strict';
var Utils = require('MicroUtils');
var PathIndex = function(path, settings)
{
this.path = path;
this.dirs = [];
this.files = [];
this.skipDotfiles = false;
this.includeDirs = true;
this.includeFiles = true;
this.dirFilterRgx = null;
this.fileFilterRgx = null;
this.changeSettings(settings);
this.update();
};
PathIndex.prototype.changeSettings = function(settings)
{
settings = settings || {};
if (typeof settings.skipDotfiles !== 'undefined')
this.skipDotfiles = !!settings.skipDotfiles;
if (typeof settings.includeDirs !== 'undefined')
this.includeDirs = !!settings.includeDirs;
if (typeof settings.includeFiles !== 'undefined')
this.includeFiles = !!settings.includeFiles;
if (typeof settings.dirFilterRgx !== 'undefined')
this.dirFilterRgx = settings.dirFilterRgx;
if (typeof settings.fileFilterRgx !== 'undefined')
this.fileFilterRgx = settings.fileFilterRgx;
};
PathIndex.prototype._readdir = function(path, type)
{
if (typeof path !== 'string')
throw '_readdir: No path provided';
// NOTE: Items are listed in "filesystem order", which MAY not be sorted.
var result = mp.utils.readdir(path, type);
if (result === undefined)
throw '_readdir: '+mp.last_error()+' ("'+path+'")';
// If filtering is enabled, we'll ONLY keep files MATCHING the filter!
var filterRgx = type === 'dirs' ? this.dirFilterRgx : this.fileFilterRgx;
if (filterRgx || this.skipDotfiles) {
for (var i = result.length - 1; i >= 0; --i) {
if (
(this.skipDotfiles && result[i].charAt(0) === '.') ||
(filterRgx && !filterRgx.exec(result[i]))
) {
result.splice(i, 1);
}
}
}
// Sort all items in case-insensitive alphabetical order.
Utils.quickSort(result, {caseInsensitive: true});
return result;
};
PathIndex.prototype.update = function(newPath, newSettings)
{
// Change the path and/or settings if requested.
if (typeof newPath === 'string')
this.path = newPath;
if (typeof newSettings !== 'undefined')
this.changeSettings(newSettings);
// Attempt to load the directory contents.
try {
// NOTE: Blocks whole JS engine until done! Throws if bad path!
var dirs = this.includeDirs ? this._readdir(this.path, 'dirs') : [],
files = this.includeFiles ? this._readdir(this.path, 'files') : [];
this.dirs = dirs;
this.files = files;
} catch (e) {
this.dirs = [];
this.files = [];
throw e;
}
};
module.exports = PathIndex;
================================================
FILE: scripts/modules.js/PathTools.js
================================================
/*
* PATHTOOLS.JS (MODULE)
* Version: 1.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
/* jshint -W097 */
/* global mp, module, require */
'use strict';
var PathIndex = require('PathIndex');
var PathTools = {};
PathTools.getCwd = function(strictErrors)
{
var cwdPath = mp.utils.getcwd();
if (cwdPath)
return cwdPath;
if (strictErrors)
throw mp.last_error();
return '';
};
PathTools._isUnix = null;
PathTools._isMac = null;
PathTools._pathSep = null;
PathTools._detectOS = function()
{
var cwdPath = PathTools.getCwd(true); // Throws.
// Detect Unix/Linux/macOS if the path starts with a forward slash.
PathTools._isUnix = cwdPath.charAt(0) === '/';
PathTools._isMac = false; // Mac is also Unix, but we'll detect separately.
PathTools._pathSep = PathTools._isUnix ? '/' : '\\';
// Differentiate macOS from other Unix-like systems.
if (PathTools._isUnix) {
var unameResult = mp.utils.subprocess({
args: ['uname', '-s'], // "Linux" or "Darwin" (Mac) or "BSD", etc.
cancellable: false // Cannot be interrupted by user playback.
});
if (typeof unameResult.stdout === 'string' && unameResult.stdout.match(/^\s*Darwin\s*$/))
PathTools._isMac = true;
}
};
PathTools.isUnix = function()
{
if (PathTools._isUnix === null)
PathTools._detectOS();
return PathTools._isUnix;
};
PathTools.isMac = function()
{
if (PathTools._isMac === null)
PathTools._detectOS();
return PathTools._isMac;
};
PathTools.pathSep = function()
{
if (PathTools._pathSep === null)
PathTools._detectOS();
return PathTools._pathSep;
};
PathTools.getPathInfo = function(path)
{
// Use the modern file_info() API if the user's mpv is built with it!
if (mp.utils.file_info) {
var fileInfo = mp.utils.file_info(path);
return fileInfo ? (fileInfo.is_dir ? 'dir' : 'file') : 'missing';
}
// Fallback: Check if it's a dir by attempting to list directories in it.
// NOTE: Misdetects on permission issues, but best we can do for old mpv.
try {
var dirContents = new PathIndex(path, { // Throws.
// Skips file query, and just asks for directories (filtered out).
includeFiles: false,
dirFilterRgx: /^$/
});
return 'dir';
} catch (e) {}
// It's either an unreadable directory, or a file, or missing. We'll use
// a trick (reading 1 byte) to check if it's a (readable) file.
try {
// NOTE: We must read at least 1 byte (0 doesn't work). And the docs
// claim that the function "allows text content only". Seems to only
// affect the write function, since reading binary actually works!
// NOTE: This properly works on (and detects) 0-byte files too.
var data = mp.utils.read_file(path, 1); // Throws.
return 'file';
} catch (e) {
return 'missing';
}
};
PathTools.getParentPath = function(path)
{
if (PathTools._isUnix === null || PathTools._pathSep === null)
PathTools._detectOS();
var pathParts = path.split(PathTools._pathSep),
previousDir = null;
if (pathParts.length > 1) // Refuse to remove last remaining (drive root).
previousDir = pathParts.pop();
var newPath = pathParts.join(PathTools._pathSep);
if (PathTools._isUnix && !newPath.length) // Preserve unix drive root.
newPath = '/';
if (!newPath.length) // Safeguard against empty parent path result.
newPath = path;
return {
path: path, // Original input.
newPath: newPath, // May still be empty (or "/") if path was empty.
previousDir: previousDir // May be null.
};
};
PathTools.getSubPath = function(path, file)
{
if (PathTools._isUnix === null || PathTools._pathSep === null)
PathTools._detectOS();
return (PathTools._isUnix && path === '/' ? '/' : path+PathTools._pathSep)+file;
};
PathTools.getPathname = function(path)
{
if (PathTools._pathSep === null)
PathTools._detectOS();
// If there is no path separator, we assume there is no path (empty string).
var filenameSep = path.lastIndexOf(PathTools._pathSep);
return filenameSep >= 0 ? path.substring(0, filenameSep) : '';
};
PathTools.getBasename = function(path)
{
if (PathTools._pathSep === null)
PathTools._detectOS();
// If there is no path separator, we assume the whole path is a filename.
var filenameSep = path.lastIndexOf(PathTools._pathSep);
return filenameSep >= 0 ? path.substring(filenameSep + 1) : path;
};
PathTools.getExtension = function(path, includeDotfiles, noLowerCase) {
var filename = PathTools.getBasename(path);
var match = includeDotfiles ?
filename.match(/\.([^.]+)$/) :
filename.match(/[^.]\.([^.]+)$/);
return match ? (noLowerCase ? match[1] : match[1].toLowerCase()) : null;
};
PathTools.isPathAbsolute = function(path)
{
if (PathTools._isUnix === null)
PathTools._detectOS();
return (
// Unix paths always start from "/" (even network paths).
(PathTools._isUnix && path.charAt(0) === '/') ||
// Windows paths are "C:" (disk) or "\\XYZ" (network).
(!PathTools._isUnix && path.match(/^(?:[a-z]:|\\\\[a-z])/i))
);
};
PathTools.makePathAbsolute = function(path)
{
return PathTools.isPathAbsolute(path) ? path :
PathTools.getSubPath(PathTools.getCwd(), path);
};
PathTools.isWebURL = function(path)
{
return path && path.match(/^[^:]+:\/\//);
};
module.exports = PathTools;
================================================
FILE: scripts/modules.js/PlaylistManager.js
================================================
/*
* PLAYLISTMANAGER.JS (MODULE)
* Version: 1.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
/* jshint -W097 */
/* global mp, module, require */
'use strict';
var PathTools = require('PathTools');
var PlaylistManager = {};
PlaylistManager.getPlaylist = function(itemPos)
{
var playlist = mp.get_property_native('playlist');
if (playlist === undefined)
return null;
if (typeof itemPos !== 'undefined') { // Single item is desired.
if (!playlist.length)
return null;
if (itemPos === -1) // Get last item.
return playlist[playlist.length - 1];
else if (itemPos >= 0 && itemPos < playlist.length) // Specific item.
return playlist[itemPos];
else // Invalid position.
return null;
}
return playlist; // Array of all playlist items.
};
PlaylistManager.getCurrentlyPlaying = function(makeAbsolute)
{
// Attempt to detect the currently playing file (or the first playlist
// file in case the playlist hasn't been started yet). Will be empty if
// no playlist exists (such as in mpv's "idle with forced GUI" mode).
var playlist = PlaylistManager.getPlaylist(),
playlistItem = playlist.length ? playlist[0] : null,
fullPath = null;
for (var i = 0; i < playlist.length; ++i) {
if (playlist[i] && playlist[i].current) {
playlistItem = playlist[i];
break;
}
}
if (playlistItem) {
fullPath = playlistItem.filename;
if (makeAbsolute && !PathTools.isWebURL(fullPath))
// Append the relative path to mpv's working dir (which is
// what relative playlist files must be resolved against).
fullPath = PathTools.makePathAbsolute(fullPath);
}
return fullPath;
};
PlaylistManager.getCurrentlyPlayingLocal = function(makeAbsolute)
{
var info = {
// Grab the current playlist file (if any) and make its path absolute.
fullPath: PlaylistManager.getCurrentlyPlaying(makeAbsolute),
pathName: null,
baseName: null
};
// If nothing is queued or it's a web URL, erase it and return.
if (!info.fullPath || PathTools.isWebURL(info.fullPath)) {
info.fullPath = null;
return info;
}
// NOTE: Pathname will be empty if this is a relative file that has no dirs.
info.pathName = PathTools.getPathname(info.fullPath);
info.baseName = PathTools.getBasename(info.fullPath);
return info;
};
module.exports = PlaylistManager;
================================================
FILE: scripts/modules.js/PseudoRandom.js
================================================
/*
* PSEUDORANDOM.JS (MODULE)
*
* Version: 1.0.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
/* jshint -W097 */
/* global mp, module, require, setInterval, clearInterval, setTimeout, clearTimeout */
'use strict';
var Utils = require('MicroUtils');
/**
* Pseudo-random number generator.
*
* Always generates the same output sequence based on the given input seed.
*/
var PseudoRandom = function(initialSeed)
{
// Based on Park-Miller-Carta PRNG (http://www.firstpr.com.au/dsp/rand31/).
this.setSeed(initialSeed);
};
/**
* Get the current seed.
*/
PseudoRandom.prototype.getSeed = function()
{
return this._seed;
};
/**
* Set the current seed.
*
* This is useful for returning the PRNG to an earlier state.
*/
PseudoRandom.prototype.setSeed = function(seed)
{
if (!Utils.isInt(seed) || seed === 0)
throw 'The seed must be a positive integer';
seed = seed % 2147483647;
if (seed <= 0)
seed += 2147483646;
this._seed = seed;
};
/**
* Returns a pseudo-random value between 1 and 2^32 - 2.
*/
PseudoRandom.prototype.nextSeed = function()
{
// Generate the next seed based on current seed. Result will always be an
// integer and can never become 0 (since only a float could lead to that).
this._seed = this._seed * 16807 % 2147483647;
return this._seed;
};
/**
* Returns a pseudo-random floating point number in range [0, 1] (exclusive).
*/
PseudoRandom.prototype.next = function()
{
// We know that `_nextSeed()` will be 1 to 2147483646 (inclusive), so simply
// subtract one to turn the result into a float from 0 to 1 (exclusive).
return (this.nextSeed() - 1) / 2147483646;
};
module.exports = PseudoRandom;
================================================
FILE: scripts/modules.js/RandomCycle.js
================================================
/*
* RANDOMCYCLE.JS (MODULE)
*
* Version: 1.0.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
/* jshint -W097 */
/* global mp, module, require, setInterval, clearInterval, setTimeout, clearTimeout */
'use strict';
var Utils = require('MicroUtils');
/**
* Generates a randomly ordered set and lets you traverse it in any direction.
*
* When it comes to randomizing data, this class is vastly superior to a pure
* "pick a random entry", since this guarantees that you'll never encounter the
* same entry twice. It also guarantees that the user can traverse forwards as
* much as they want, and suddenly decide that they want to go back a few steps
* to land on an entry they just passed through. Since the order is linked, they
* just have to travel a few steps backwards and they'll reach the entry again.
*
* For example, for a set size of 6, you'd have the data `[0,1,2,3,4,5]`.
* Shuffled, it may look like `[4,5,3,0,1,2]`. If you traverse it forwards
* and query it about what comes after "3", it would answer "0". If you ask
* what comes before "3", it would answer "5". Whenever you reach an edge,
* it wraps around and thereby gives you a perfect cycle/chain which always
* leads back to where you started, and covers every value along the way.
*
* Expressed in "next" order: `3 -> 0 -> 1 -> 2 -> 4 -> 5 -> 3 -> 0 -> 1 -> 2`.
* And in "previous" order: `4 -> 2 -> 1 -> 0 -> 3 -> 5 -> 4 -> 2 -> 1 -> 0`.
*/
var RandomCycle = function()
{
this._count = 0;
this._shuffled = [];
};
/**
* Change the size of the set and shuffle the data.
*
* This must be done before you can query about any number, and it must be
* called any time the set size changes.
*/
RandomCycle.prototype.setCount = function(count)
{
if (!Utils.isInt(count))
throw 'The count must be a positive integer';
this._count = count;
this._shuffled = [];
for (var i = 0; i < count; ++i) {
this._shuffled.push(i);
}
Utils.shuffle(this._shuffled);
};
/**
* Shuffle the current dataset again.
*
* Can be used anytime you want to re-organize the cycle pattern into a
* different order.
*/
RandomCycle.prototype.shuffleCycle = function()
{
Utils.shuffle(this._shuffled);
};
/**
* Get the next index after the given index.
*
* Throws if the given index is out of range of the dataset's count.
*/
RandomCycle.prototype.getNext = function(fromIdx)
{
var current = this._findIdx(fromIdx), // Throws.
next = current + 1;
if (next >= this._count) // Wrap.
next = 0;
return this._shuffled[next];
};
/**
* Get the previous index before the given index.
*
* Throws if the given index is out of range of the dataset's count.
*/
RandomCycle.prototype.getPrevious = function(fromIdx)
{
var current = this._findIdx(fromIdx), // Throws.
previous = current - 1;
if (previous < 0) // Wrap.
previous = this._count - 1;
return this._shuffled[previous];
};
/**
* (Internal) Locate an index value in the shuffled dataset.
*
*
* Throws if the given index is out of range of the dataset's count.
*/
RandomCycle.prototype._findIdx = function(idx)
{
if (!Utils.isInt(idx) || idx < 0 || idx >= this._count)
throw 'The index must be an integer within the current count-range';
var foundAt = this._shuffled.indexOf(idx);
if (foundAt < 0) // Just a safeguard.
throw 'Unable to find index in shuffled dataset';
return foundAt;
};
module.exports = RandomCycle;
================================================
FILE: scripts/modules.js/SelectionMenu.js
================================================
/*
* SELECTIONMENU.JS (MODULE)
*
* Version: 1.3.1
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
/* jshint -W097 */
/* global mp, module, require, setInterval, clearInterval, setTimeout, clearTimeout */
'use strict';
var Ass = require('AssFormat'),
Utils = require('MicroUtils');
var SelectionMenu = function(settings)
{
settings = settings || {};
this.uniqueId = 'M'+String(mp.get_time_ms()).replace(/\./g, '').substring(3)+
Math.floor((100+(Math.random()*899)));
this.metadata = null;
this.title = 'No title';
this.options = [];
this.selectionIdx = 0;
this.cbMenuShow = typeof settings.cbMenuShow === 'function' ? settings.cbMenuShow : null;
this.cbMenuHide = typeof settings.cbMenuHide === 'function' ? settings.cbMenuHide : null;
this.cbMenuLeft = typeof settings.cbMenuLeft === 'function' ? settings.cbMenuLeft : null;
this.cbMenuRight = typeof settings.cbMenuRight === 'function' ? settings.cbMenuRight : null;
this.cbMenuOpen = typeof settings.cbMenuOpen === 'function' ? settings.cbMenuOpen : null;
this.cbMenuUndo = typeof settings.cbMenuUndo === 'function' ? settings.cbMenuUndo : null;
this.maxLines = typeof settings.maxLines === 'number' &&
settings.maxLines >= 3 ? Math.floor(settings.maxLines) : 10;
this.menuFontAlpha = Ass.convertPercentToHex( // Throws if invalid input.
(typeof settings.menuFontAlpha === 'number' &&
settings.menuFontAlpha >= 0 && settings.menuFontAlpha <= 1 ?
settings.menuFontAlpha : 1),
true // Invert input range so "1.0" is visible and "0.0" is invisible.
);
this.menuFontSize = typeof settings.menuFontSize === 'number' &&
settings.menuFontSize >= 1 ? Math.floor(settings.menuFontSize) : 40;
this.originalFontSize = null;
this.hasRegisteredKeys = false; // Also means that menu is active/open.
this.useTextColors = true;
this.currentMenuText = '';
this.isShowingMessage = false;
this.currentMessageText = '';
this.menuInterval = null;
this.stopMessageTimeout = null;
this.autoCloseDelay = typeof settings.autoCloseDelay === 'number' &&
settings.autoCloseDelay >= 0 ? settings.autoCloseDelay : 5; // 0 = Off.
this.autoCloseActiveAt = 0;
this.keyBindings = { // Default keybindings.
'Menu-Up':{repeatable:true, keys:['up']},
'Menu-Down':{repeatable:true, keys:['down']},
'Menu-Up-Fast':{repeatable:true, keys:['shift+up']},
'Menu-Down-Fast':{repeatable:true, keys:['shift+down']},
'Menu-Left':{repeatable:true, keys:['left']},
'Menu-Right':{repeatable:false, keys:['right']},
'Menu-Open':{repeatable:false, keys:['enter']},
'Menu-Undo':{repeatable:false, keys:['bs']},
'Menu-Help':{repeatable:false, keys:['h']},
'Menu-Close':{repeatable:false, keys:['esc']}
};
// Apply custom rebinding overrides if provided.
// Format: `{'Menu-Open':['a','shift+b']}`
// Note that all "shift variants" MUST be specified as "shift+<key>".
var i, action, key, allKeys, erasedDefaults,
rebinds = settings.keyRebindings;
if (rebinds) {
for (action in rebinds) {
if (!rebinds.hasOwnProperty(action))
continue;
if (!this.keyBindings.hasOwnProperty(action))
throw 'Invalid menu action "'+action+'" in rebindings';
erasedDefaults = false;
allKeys = rebinds[action];
for (i = 0; i < allKeys.length; ++i) {
key = allKeys[i];
if (typeof key !== 'string')
throw 'Invalid non-string key ('+JSON.stringify(key)+') in custom rebindings';
key = key.toLowerCase(); // Unify case of all keys for de-dupe.
key = Utils.trim(key); // Trim whitespace.
if (!key.length)
continue;
if (!erasedDefaults) { // Erase default keys for this action.
erasedDefaults = true;
this.keyBindings[action].keys = [];
}
this.keyBindings[action].keys.push(key);
}
}
}
// Verify that no duplicate bindings exist for the same key.
var boundKeys = {};
for (action in this.keyBindings) {
if (!this.keyBindings.hasOwnProperty(action))
continue;
allKeys = this.keyBindings[action].keys;
for (i = 0; i < allKeys.length; ++i) {
key = allKeys[i];
if (boundKeys.hasOwnProperty(key))
throw 'Invalid duplicate menu bindings for key "'+key+'" (detected in action "'+action+'")';
boundKeys[key] = true;
}
}
};
SelectionMenu.prototype.setMetadata = function(metadata)
{
this.metadata = metadata;
};
SelectionMenu.prototype.getMetadata = function()
{
return this.metadata;
};
SelectionMenu.prototype.setTitle = function(newTitle)
{
if (typeof newTitle !== 'string')
throw 'setTitle: No title value provided';
this.title = newTitle;
};
SelectionMenu.prototype.setOptions = function(newOptions, initialSelectionIdx)
{
if (typeof newOptions === 'undefined')
throw 'setOptions: No options value provided';
this.options = newOptions;
this.selectionIdx = typeof initialSelectionIdx === 'number' &&
initialSelectionIdx >= 0 && initialSelectionIdx < newOptions.length ?
initialSelectionIdx : 0;
};
SelectionMenu.prototype.setCallbackMenuShow = function(newCbMenuShow)
{
this.cbMenuShow = typeof newCbMenuShow === 'function' ? newCbMenuShow : null;
};
SelectionMenu.prototype.setCallbackMenuHide = function(newCbMenuHide)
{
this.cbMenuHide = typeof newCbMenuHide === 'function' ? newCbMenuHide : null;
};
SelectionMenu.prototype.setCallbackMenuLeft = function(newCbMenuLeft)
{
this.cbMenuLeft = typeof newCbMenuLeft === 'function' ? newCbMenuLeft : null;
};
SelectionMenu.prototype.setCallbackMenuRight = function(newCbMenuRight)
{
this.cbMenuRight = typeof newCbMenuRight === 'function' ? newCbMenuRight : null;
};
SelectionMenu.prototype.setCallbackMenuOpen = function(newCbMenuOpen)
{
this.cbMenuOpen = typeof newCbMenuOpen === 'function' ? newCbMenuOpen : null;
};
SelectionMenu.prototype.setCallbackMenuUndo = function(newCbMenuUndo)
{
this.cbMenuUndo = typeof newCbMenuUndo === 'function' ? newCbMenuUndo : null;
};
SelectionMenu.prototype.setUseTextColors = function(value)
{
var hasChanged = this.useTextColors !== value;
this.useTextColors = !!value;
// Update text cache, and redraw menu if visible (otherwise don't show it).
if (hasChanged)
this.renderMenu(null, 1); // 1 = Only redraw if menu is onscreen.
};
SelectionMenu.prototype.isMenuActive = function()
{
return this.hasRegisteredKeys; // If keys are registered, menu is active.
};
SelectionMenu.prototype.getSelectedItem = function()
{
if (this.selectionIdx < 0 || this.selectionIdx >= this.options.length)
return '';
else
return this.options[this.selectionIdx];
};
SelectionMenu.prototype._processBindings = function(fnCb)
{
if (typeof fnCb !== 'function')
throw 'Missing callback for _processBindings';
var i, key, allKeys, action, identifier,
bindings = this.keyBindings;
for (action in bindings) {
if (!bindings.hasOwnProperty(action))
continue;
allKeys = bindings[action].keys;
for (i = 0; i < allKeys.length; ++i) {
key = allKeys[i];
identifier = this.uniqueId+'_'+action+'_'+key;
fnCb(
identifier, // Unique identifier for this binding.
action, // What action the key is assigned to trigger.
key, // What key.
bindings[action] // Details about this binding.
);
}
}
};
SelectionMenu.prototype._registerMenuKeys = function()
{
if (this.hasRegisteredKeys)
return;
// Necessary in order to preserve "this" in the called function, since mpv's
// callbacks don't receive "this" if the object's func is keybound directly.
var createFn = function(obj, fn) {
return function() {
obj._menuAction(fn);
};
};
var self = this;
this._processBindings(function(identifier, action, key, details) {
mp.add_forced_key_binding(
key, // What key.
identifier, // Unique identifier for the binding.
createFn(self, action), // Generate anonymous func to execute.
{repeatable:details.repeatable} // Extra options.
);
});
this.hasRegisteredKeys = true;
};
SelectionMenu.prototype._unregisterMenuKeys = function()
{
if (!this.hasRegisteredKeys)
return;
var self = this;
this._processBindings(function(identifier, action, key, details) {
mp.remove_key_binding(
identifier // Remove binding by its unique identifier.
);
});
this.hasRegisteredKeys = false;
};
SelectionMenu.prototype._menuAction = function(action)
{
if (this.isShowingMessage && action !== 'Menu-Close')
return; // Block everything except "close" while showing a message.
switch (action) {
case 'Menu-Up':
case 'Menu-Down':
case 'Menu-Up-Fast':
case 'Menu-Down-Fast':
var maxIdx = this.options.length - 1;
if (action === 'Menu-Up' || action === 'Menu-Up-Fast')
this.selectionIdx -= (action === 'Menu-Up-Fast' ? 10 : 1);
else
this.selectionIdx += (action === 'Menu-Down-Fast' ? 10 : 1);
// Handle wraparound in single-move mode, or clamp in fast-move mode.
if (this.selectionIdx < 0)
this.selectionIdx = (action === 'Menu-Up-Fast' ? 0 : maxIdx);
else if (this.selectionIdx > maxIdx)
this.selectionIdx = (action === 'Menu-Down-Fast' ? maxIdx : 0);
this.renderMenu();
break;
case 'Menu-Left':
case 'Menu-Right':
case 'Menu-Open':
case 'Menu-Undo':
var cbName = 'cb'+action.replace(/-/g, '');
if (typeof this[cbName] === 'function') {
// We don't know what the callback will do, and it may be slow, so
// we'll disable the menu's auto-close timeout while it runs.
this._disableAutoCloseTimeout(); // Soft-disable.
this[cbName](action);
}
break;
case 'Menu-Help':
// List all keybindings to help the user remember them.
var entry, entryTitle, allKeys,
c = this.useTextColors,
helpLines = 0,
helpString = Ass.startSeq(c)+Ass.alpha(this.menuFontAlpha, c),
bindings = this.keyBindings;
for (entry in bindings) {
if (!bindings.hasOwnProperty(entry))
continue;
allKeys = bindings[entry].keys;
if (!entry.match(/^Menu-/) || !allKeys || !allKeys.length)
continue;
entryTitle = entry.substring(5);
if (!entryTitle.length)
continue;
Utils.quickSort(allKeys, {caseInsensitive: true});
++helpLines;
helpString += Ass.yellow(c)+Ass.esc(entryTitle, c)+': '+
Ass.white(c)+Ass.esc('{'+allKeys.join('}, {')+'}', c)+'\n';
}
helpString += Ass.stopSeq(c);
if (!helpLines)
helpString = 'No help available.';
this.showMessage(helpString, 5000);
break;
case 'Menu-Close':
this.hideMenu();
break;
default:
mp.msg.error('Unknown menu action "'+action+'"');
return;
}
this._updateAutoCloseTimeout(); // Soft-update.
};
SelectionMenu.prototype._disableAutoCloseTimeout = function(forceLock)
{
this.autoCloseActiveAt = forceLock ? -2 : -1; // -2 = hard, -1 = soft.
};
SelectionMenu.prototype._updateAutoCloseTimeout = function(forceUnlock)
{
if (!forceUnlock && this.autoCloseActiveAt === -2)
return; // Do nothing while autoclose is locked in "disabled" mode.
this.autoCloseActiveAt = mp.get_time();
};
SelectionMenu.prototype._handleAutoClose = function()
{
if (this.autoCloseDelay <= 0 || this.autoCloseActiveAt <= -1) // -2 = hard, -1 = soft.
return; // Do nothing while autoclose is disabled (0) or locked (< 0).
var now = mp.get_time();
if (this.autoCloseActiveAt <= (now - this.autoCloseDelay))
this.hideMenu();
};
SelectionMenu.prototype._renderActiveText = function()
{
if (!this.isMenuActive())
return;
// Determine which text to render (critical messages take precedence).
var msg = this.isShowingMessage ? this.currentMessageText : this.currentMenuText;
if (typeof msg !== 'string')
msg = '';
// Tell mpv's OSD to show the text. It will automatically be replaced and
// refreshed every second while the menu remains open, to ensure that
// nothing else is able to overwrite our menu text.
// NOTE: The long display duration is important, because the JS engine lacks
// real threading, so any slow mpv API calls or slow JS functions will delay
// our redraw timer! Without a long display duration, the menu would vanish.
// NOTE: If a timer misses multiple intended ticks, it will only tick ONCE
// when catching up. So there can thankfully never be any large "backlog"!
mp.osd_message(msg, 1000);
};
SelectionMenu.prototype.renderMenu = function(selectionPrefix, renderMode)
{
var c = this.useTextColors,
finalString;
// Title.
finalString = Ass.startSeq(c)+Ass.alpha(this.menuFontAlpha, c)+
Ass.gray(c)+Ass.scale(75, c)+Ass.esc(this.title, c)+':'+
Ass.scale(100, c)+Ass.white(c)+'\n\n';
// Options.
if (this.options.length > 0) {
// Calculate start/end offsets around focal point.
var startIdx = this.selectionIdx - Math.floor(this.maxLines / 2);
if (startIdx < 0)
startIdx = 0;
var endIdx = startIdx + this.maxLines - 1,
maxIdx = this.options.length - 1;
if (endIdx > maxIdx)
endIdx = maxIdx;
// Increase number of leading lines if we've reached end of list.
var lineCount = (endIdx - startIdx) + 1, // "+1" to count start line too.
lineDiff = this.maxLines - lineCount;
startIdx -= lineDiff;
if (startIdx < 0)
startIdx = 0;
// Format and add all output lines.
var opt;
for (var i = startIdx; i <= endIdx; ++i) {
opt = this.options[i];
if (i === this.selectionIdx)
// NOTE: Prefix stays on screen until cursor-move or re-render.
finalString += Ass.yellow(c)+'> '+(typeof selectionPrefix === 'string' ?
Ass.esc(selectionPrefix, c)+' ' : '');
finalString += (
i === startIdx && startIdx > 0 ? '...' :
(
i === endIdx && endIdx < maxIdx ? '...' : Ass.esc(
typeof opt === 'object' ? opt.menuText : opt,
c
)
)
);
if (i === this.selectionIdx)
finalString += Ass.white(c);
if (i !== endIdx)
finalString += '\n';
}
}
// End the Advanced SubStation command sequence.
finalString += Ass.stopSeq(c);
// Update cached menu text. But only open/redraw the menu if it's already
// active OR if we're NOT being prevented from going out of "hidden" state.
this.currentMenuText = finalString;
// Handle render mode:
// 1 = Only redraw if menu is onscreen (doesn't trigger open/redrawing if
// the menu is closed or busy showing a text message); 2 = Don't show/redraw
// at all (good for just updating the text cache silently); any other value
// (incl. undefined, aka default) = show/redraw the menu.
if ((renderMode === 1 && (!this.isMenuActive() || this.isShowingMessage)) || renderMode === 2)
return;
this._showMenu();
};
SelectionMenu.prototype._showMenu = function()
{
var justOpened = false;
if (!this.isMenuActive()) {
justOpened = true;
this.originalFontSize = mp.get_property_number('osd-font-size');
mp.set_property('osd-font-size', this.menuFontSize);
this._registerMenuKeys();
// Redraw the currently active text every second and do periodic tasks.
// NOTE: This prevents other OSD scripts from removing our menu text.
var self = this;
if (this.menuInterval !== null)
clearInterval(this.menuInterval);
this.menuInterval = setInterval(function() {
self._renderActiveText();
self._handleAutoClose();
}, 1000);
// Get rid of any lingering "stop message" timeout and message.
this.stopMessage(true);
}
// Display the currently active text instantly.
this._renderActiveText();
if (justOpened) {
// Run "menu show" callback if registered.
if (typeof this.cbMenuShow === 'function') {
this._disableAutoCloseTimeout(); // Soft-disable while CB runs.
this.cbMenuShow('Menu-Show');
}
// Force an update/unlock of the activity timeout when menu opens.
this._updateAutoCloseTimeout(true); // Hard-update.
}
};
SelectionMenu.prototype.hideMenu = function()
{
if (!this.isMenuActive())
return;
mp.osd_message('');
if (this.originalFontSize !== null)
mp.set_property('osd-font-size', this.originalFontSize);
this._unregisterMenuKeys();
if (this.menuInterval !== null) {
clearInterval(this.menuInterval);
this.menuInterval = null;
}
// Get rid of any lingering "stop message" timeout and message.
this.stopMessage(true);
// Run "menu hide" callback if registered.
if (typeof this.cbMenuHide === 'function')
this.cbMenuHide('Menu-Hide');
};
SelectionMenu.prototype.showMessage = function(msg, durationMs, clearSelectionPrefix)
{
if (!this.isMenuActive())
return;
if (typeof msg !== 'string')
msg = 'showMessage: Invalid message value.';
if (typeof durationMs !== 'number')
durationMs = 800;
if (clearSelectionPrefix)
this.renderMenu(null, 2); // 2 = Only update text cache (no redraw).
this.isShowingMessage = true;
this.currentMessageText = msg;
this._renderActiveText();
this._disableAutoCloseTimeout(true); // Hard-disable (ignore msg idle time).
var self = this;
if (this.stopMessageTimeout !== null)
clearTimeout(this.stopMessageTimeout);
this.stopMessageTimeout = setTimeout(function() {
self.stopMessage();
}, durationMs);
};
SelectionMenu.prototype.stopMessage = function(preventRender)
{
if (this.stopMessageTimeout !== null) {
clearTimeout(this.stopMessageTimeout);
this.stopMessageTimeout = null;
}
this.isShowingMessage = false;
this.currentMessageText = '';
if (!preventRender)
this._renderActiveText();
this._updateAutoCloseTimeout(true); // Hard-update (last user activity).
};
module.exports = SelectionMenu;
================================================
FILE: scripts/modules.js/Stack.js
================================================
/*
* STACK.JS (MODULE)
*
* Version: 1.0.0
* Author: VideoPlayerCode
* URL: https://github.com/VideoPlayerCode/mpv-tools
* License: Apache License, Version 2.0
*/
/* jshint -W097 */
/* global mp, module, require, setInterval, clearInterval, setTimeout, clearTimeout */
'use strict';
var Utils = require('MicroUtils');
var Stack = function(maxSize)
{
if (!Utils.isInt(maxSize) || maxSize === 0)
throw 'Max stack size must be either -1 (unlimited), or 1 or higher';
this.stack = [];
this.position = -1;
this.maxSize = maxSize;
};
Stack.prototype.push = function(elem)
{
// Add to end of stack.
this.stack.push(elem);
if (this.maxSize !== -1)
while (this.stack.length > this.maxSize) // Normally only triggers once.
this.stack.shift(); // Remove 1st and reindex.
this.position = this.stack.length - 1;
};
Stack.prototype.pop = function()
{
// Pop from end of stack.
if (this.position < 0)
return undefined; // Stack is empty.
var popped = this.stack.pop();
this.position = this.stack.length - 1;
return popped;
};
Stack.prototype.clearStack = function()
{
// NOTE: We use splice rather than `= []` to ensure old references retrieved
// via `getStack()` will still point to the active stack after clearing it.
this.stack.splice(0, this.stack.length);
this.position = -1;
};
Stack.prototype.getStack = function()
{
return this.stack;
};
Stack.prototype.getLast = function()
{
return this.position >= 0 ?
this.stack[this.position] :
undefined;
};
Stack.prototype.getCount = function()
{
return this.position + 1;
};
Stack.prototype.isEmpty = function()
{
return this.position < 0;
};
module.exports = Stack;
================================================
FILE: scripts/multi-command-if.lua
================================================
-- -----------------------------------------------------------
--
-- MULTI-COMMAND-IF.LUA
-- Short Name: MCIF (Multi-Command If)
-- Version: 1.1
-- Author: VideoPlayerCode
-- URL: https://github.com/VideoPlayerCode/mpv-tools
--
-- Description:
--
-- Very powerful conditional logic and multiple
-- action engine for your keybindings, without
-- having to write a single line of code!
--
-- See the bottom of this file for usage examples.
--
-- History:
--
-- 1.1: + Support for multiple empty command arguments in a row.
-- + Added Multi_Command, to easily run without conditions.
--
-- -----------------------------------------------------------
--
-- A FAIR BUT MINOR USAGE WARNING:
-- It's *your* job to carefully type each argument
-- string perfectly. Any misformatted arguments will
-- lead to that condition or action being SKIPPED.
-- But you'll quickly get used to the MCIF syntax!
-- Examples of bad formatting to watch out for:
-- * Condition:
-- Bad: "((fullscreen=='yes))"
-- (missing the final ' apostrophe after yes)
-- Good: "((fullscreen=='yes'))"
-- * Action:
-- Bad: "{{=ontop:yes}}" (missing the final
-- colon : value separator after yes)
-- Good: "{{=ontop:yes:}}"
-- For those curious:
-- - Dual separators were needed to avoid clashing with mpv's
-- nested property expansion string format.
-- - The condition format is intentionally different from
-- actions to avoid confusion about which section is which
-- when you are reading long lines in your input.conf.
--
-- -----------------------------------------------------------
--
-- Parameters:
-- * conditions = Determines which set of actions will be performed.
-- * if_actions = Action string performed if ALL conditions are TRUE.
-- * else_actions = Action string performed if ANY conditions are FALSE.
--
-- See in-code documentation below for proper "conditions"
-- and "actions" string formats and possibilitions
--
-- And see the bottom of this file for usage examples to get you started.
--
-- -----------------------------------------------------------
--
-- [Internal string splitter used for perfect argument separation:]
function mcif_string_split(theString, inSplitPattern, outResults)
if (not outResults) then
outResults = {}
end
if (theString ~= nil) then -- avoid missing strings
local theStart = 1
local theSplitStart, theSplitEnd = string.find(theString, inSplitPattern, theStart)
while theSplitStart do
table.insert(outResults, string.sub(theString, theStart, theSplitStart-1))
theStart = theSplitEnd + 1
theSplitStart, theSplitEnd = string.find(theString, inSplitPattern, theStart)
end
table.insert(outResults, string.sub(theString, theStart))
end
return outResults
end
function multi_command_if(conditions, if_actions, else_actions)
--
-- Check all conditions and choose the if_actions if ALL conditions
-- are TRUE, or choose the else_actions if ANY of them are FALSE.
-- This lets you decide whether or not your actions should run.
-- You can have an unlimited amount of conditions.
--
-- Can be left as empty string (or one simply lacking conditions,
-- such as "(())" which looks nicer), to completely avoid having
-- any conditions! In that case, the "if_actions" will be chosen!
-- That feature can be useful if you just want to enjoy the
-- powerful action-sequencing capabilities of this script,
-- and the various nice shorthand notations it gives you!
-- There is a "multi_command()" wrapper which does this for you.
--
-- * "conditions" parameter string format example:
-- "((fullscreen=='no'))((ontop~='yes'))((window-scale<<'1'))"
--
-- Each property is any property name as defined in mpv.
--
-- There is no need to worry about special characters such as ' apostrophe
-- inside your value sections: "((someproperty=='It's raining'))".
-- The "condition" pattern is "((<condition property name, which is
-- everything up until 2 characters before the first ' apostrophe>
-- <2-character comparison operator>'<value to compare against,
-- which can contain apostrophes>'))". The only special sequence in the
-- <comparison value> is "'))" which ends the condition pattern. So as
-- long as you avoid that in your strings, you will be happy.
--
-- Comparison operators (each operator is 2 characters long):
-- == equals (Lua equivalent: "==")
-- ~= not equal (Lua equivalent: "~=")
-- << less than (Lua equivalent: "<") [ONLY FOR NUMBERS]
-- <= less than or equals (Lua equivalent: "<=") [ONLY FOR NUMBERS]
-- >> greater than (Lua equivalent: ">") [ONLY FOR NUMBERS]
-- >= greater than or equals (Lua equivalent: ">=") [ONLY FOR NUMBERS]
--
local actions = nil
if (conditions == nil or conditions == "" or conditions == "(())") then
-- No conditions: Choose if-actions immediately.
actions = if_actions
else -- Determine which actions to use.
-- The parameter string format example would split into:
-- fullscreen == no
-- ontop ~= yes
-- window-scale << 1
local conditionFailed = false
for propName,propComparisonMethod,propCompareValue in string.gmatch(conditions, "%(%(([^']-)(..)'(.-)'%)%)") do
-- Retrieve the current mpv property value as string for comparison.
local propCurrentValue,err = mp.get_property(propName, nil)
if (propCurrentValue == nil) then
mp.msg.log("info", "No such conditional property '"..propName.."': "..tostring(err))
mp.osd_message("No such conditional property '"..propName.."': "..tostring(err))
return nil -- abort
end
-- Perform the requested method of comparison.
-- NOTE: We cannot compare strings with numbers or vice versa, and
-- we cannot check greater/less than for numbers if we don't treat
-- them as numbers. So we need to determine the common value type
-- and do either a numeric or string comparison. As for booleans
-- "true" and "false", we will compare those as strings. And nil
-- will be compared as the string "nil". If the values weren't both
-- convertible to numbers or both to strings, then we consider the
-- values to be of mixed types, which cannot be numerically compared
-- in Lua. But ANY value (even tables and function references) CAN
-- be converted to a string so the "mixed" scenario should never be
-- able to happen. It is just there as a safeguard against exceptions.
local aN = tonumber(propCurrentValue)
local bN = tonumber(propCompareValue)
local aS = tostring(propCurrentValue) -- these handle bool and nil too.
local bS = tostring(propCompareValue) -- in fact, they handle ANY value.
local areNumbers = ((aN ~= nil and bN ~= nil) and true or false)
local areStrings = ((not areNumbers and aS ~= nil and bS ~= nil) and true or false)
local areMixed = ((not areNumbers and not areStrings) and true or false)
conditionFailed = true
if (propComparisonMethod == "==") then -- equals
if ((areNumbers and aN == bN) or (areStrings and aS == bS)) then
conditionFailed = false
end
elseif (propComparisonMethod == "~=") then -- not equal
if ((areNumbers and aN ~= bN) or (areStrings and aS ~= bS) or (areMixed)) then
conditionFailed = false
end
elseif (propComparisonMethod == "<<") then -- less than
if (areNumbers and aN < bN) then -- numeric-only operator
conditionFailed = false
end
elseif (propComparisonMethod == "<=") then -- less than or equals
if (areNumbers and aN <= bN) then -- numeric-only operator
conditionFailed = false
end
elseif (propComparisonMethod == ">>") then -- greater than
if (areNumbers and aN > bN) then -- numeric-only operator
conditionFailed = false
end
elseif (propComparisonMethod == ">=") then -- greater than or equals
if (areNumbers and aN >= bN) then -- numeric-only operator
conditionFailed = false
end
else
mp.msg.log("info", "Invalid conditional operator '"..propComparisonMethod.."'")
mp.osd_message("Invalid conditional operator '"..propComparisonMethod.."'")
return nil -- abort
end
-- Skip further scanning and choose the else_actions if the condition failed.
if (conditionFailed) then
actions = else_actions
break -- no need to check further conditions
end
end
-- End of loop: If the LAST condition succeeded then ALL of them succeeded,
-- since we would have quit above as soon as any of them failed. So in this
-- case, choose the if_actions since ALL conditions succeeded.
if (not conditionFailed) then
actions = if_actions
end
end
--
-- Perform all actions, but abort instantly if ANY of the actions fail.
-- You can have an unlimited amount of actions.
--
-- Can be left as empty string to avoid having any actions (useful if you
-- don't want any actions in either the "if" or "else" action-strings).
--
-- * "actions" parameter string format example:
-- "{{=ontop:yes:}}{{!multiply:speed|1.25:}}{{$show-text:Speed? It's now: $${speed}.:}}{{@Quick_Scale:1680|1050|0.9|1:}}"
--
-- As you can see from the show-text example, there is no need to worry
-- about special characters such as colon inside your value sections:
-- "{{$show-text:Speed? It's now: $${speed}.:}}". The "action" pattern is
-- "{{<1-character action type><action target name, which is everything up
-- until the first colon>:<a target value which can contain colons>:}}".
-- The only special sequences in the <target value> are "|" which separates
-- multiple arguments, and ":}}" which ends the action pattern. So as long
-- as you avoid those two in your strings, you will be happy.
--
-- Note the double $$ next to $${speed} in the example. That's to prevent
-- mpv's property expansion from taking place in the keybinding. Otherwise,
-- all ${...} sequences would be expanded AT the MOMENT you press the key,
-- instead of during the processing of the action string, so you would see
-- outdated values for the property (which you MAY not want). Adding an extra
-- $$ sign makes the keybinding expand it to "$" so that WE can do the expansion
-- of the most recent "${speed}" value during OUR action processing. Another
-- alternative way to avoid early expansion is to globally turn it off for
-- that whole keybinding by prefixing the binding with the word "raw", as in:
-- "Alt+d raw script-message Multi_Command_If "Now you can ${...} expand later without needing $$.""
--
-- Action type operators (each operator is 1 character long):
-- = set a property
-- ! execute a command (without doing property expansion)
-- $ execute a command (with ${property} expansion, see note above for tips)
-- @ execute a script-message command with property expansion (it's an
-- alias for "{{$script-message:Target_Name|Arg1|Arg2...:}}")
--
-- To call a command which takes no arguments, simply leave the value
-- between the two colons blank, such as "{{@ArglessCommand::}}".
--
-- And to skip arguments (and just send empty strings for those arguments),
-- simply leave that part totally blank between the separators:
-- "{{!empty-example:|foo:}}" (sends arg1="", arg2="foo")
-- "{{!empty-example:foo|:}}" (sends arg1="foo", arg2="")
-- "{{!empty-example:foo|||bar:}}" (sends arg1="foo", arg2="", arg3="", arg4="bar")
--
-- The command or script message arguments are always separated by |.
-- There is no way to escape that character or make it more unique, because
-- Lua sucks at splitting strings by anything more than a single character,
-- BUT this character is non-existent in all commands I've ever seen!
--
-- Also be aware that the ":}}" character sequence marks the end of an action,
-- but there should be no reason for you to ever have that within a string.
-- And for those curious: We end with ":}}" to support mpv's nested expansions,
-- which may contain multiple brackets, so the ":}}" sequence makes ours unique.
-- PS: It also looks like a very happy guy. :}}
--
if (actions ~= nil and actions ~= "") then
-- The parameter string format example would split into:
-- = ontop yes
-- ! multiply speed|1.25
-- $ show-text Speed? It's now ${speed}.
-- @ Quick_Scale 1680|1050|0.9|1
for actionType,targetName,targetValue in string.gmatch(actions, "{{(.)([^:]-):(.-):}}") do
-- Pre-processing to translate the "@" ("script-message") action shorthand.
if (actionType == "@") then
actionType = "$" -- "execute command with property expansion"
targetValue = targetName.."|"..targetValue
targetName = "script-message"
end
-- Pre-processing to translate the "$" action type to expand-properties.
if (actionType == "$") then
actionType = "!" -- "execute mpv command"
targetValue = targetName.."|"..targetValue
targetName = "expand-properties"
end
-- Process the user's action.
if (actionType == "=") then
-- Set mpv property value.
local result,err = mp.set_property(targetName, targetValue)
if (result == nil) then
mp.msg.log("info", "Error while setting property '"..targetName.."': "..tostring(err))
mp.osd_message("Error while setting property '"..targetName.."': "..tostring(err))
return nil -- abort
end
elseif (actionType == "!") then
-- Execute mpv command (list: https://github.com/mpv-player/mpv/blob/master/DOCS/man/input.rst).
-- We must first build the command arguments in the expected table format.
local allArgs = {}
allArgs[1] = targetName
local currentArgNum = 2
for k,token in pairs(mcif_string_split(targetValue, "|")) do
allArgs[currentArgNum] = token
currentArgNum = currentArgNum + 1
end
-- Dispatch the command.
-- NOTE: In the case of "script-message" there is NO way to check the
-- return code of the function(s) that may have handled the message!
local result,err = mp.command_native(allArgs, nil)
if (err ~= nil) then
mp.msg.log("info", "Error while calling '"..targetName.."': "..tostring(err))
mp.osd_message("Error while calling '"..targetName.."': "..tostring(err))
return nil -- abort
end
else
mp.msg.log("info", "Invalid action type operator '"..actionType.."'")
mp.osd_message("Invalid action type operator '"..actionType.."'")
return nil -- abort
end
end
end
end
-- Wrapper for those who just want to run actions and don't care about conditions.
function multi_command(actions)
multi_command_if(nil, actions, nil)
end
--
-- Bind this via input.conf.
--
-- Examples:
--
-- * Very simple "Hello world" example which shows different messages depending
-- on whether you are in fullscreen mode or not:
--
-- Alt+d script-message Multi_Command_If "((fullscreen=='yes'))" "{{!show-text:Hello World in Fullscreen!:}}" "{{!show-text:Not in Fullscreen!:}}"
--
-- * Showing that you can use numeric comparison operators, and that you don't
-- have to provide any "else"-actions. This only scales the video to 100% if
-- the scale is less than 100%. Does nothing if already at 100% or greater:
--
-- Alt+d script-message Multi_Command_If "((window-scale<<'1'))" "{{=window-scale:1:}}{{!show-text:Resetting tiny window to 100% scale.:}}"
--
-- * Shows "Enhance!" when the actions are executed. But if the condition fails
-- it simply shows "Can't resize in fullscreen!":
--
-- Alt+d script-message Multi_Command_If "((fullscreen~='yes'))" "{{=ontop:yes:}}{{!multiply:window-scale|1.1:}}{{!show-text:Enhance!:}}" "{{!show-text:Can't resize in fullscreen!:}}"
--
-- * Lastly, an example of requiring multiple conditions:
--
-- Alt+d script-message Multi_Command_If "((ontop=='yes'))((fullscreen=='no'))" "{{!show-text:Always on top, and not in fullscreen.:}}"
--
mp.register_script_message("Multi_Command_If", multi_command_if)
--
-- * And a small bonus for people who just want to run actions using the nice,
-- compact syntax of MCIF, without checking any conditions:
--
-- Alt+d script-message Multi_Command "{{=ontop:yes:}}{{!multiply:window-scale|1.1:}}{{!show-text:Enhance!:}}"
--
mp.register_script_message("Multi_Command", multi_command)
================================================
FILE: scripts/quick-scale.lua
================================================
-- -----------------------------------------------------------
--
-- QUICK-SCALE.LUA
-- Version: 1.1
-- Author: VideoPlayerCode
-- URL: https://github.com/VideoPlayerCode/mpv-tools
--
-- Description:
--
-- Quickly scale the video player to a target size,
-- with full control over target scale and max scale.
-- Helps you effortlessly resize a video to fit on your
-- desktop, or any other video dimensions you need!
--
-- History:
--
-- 1.0: Initial release.
-- 1.1: Do nothing if mpv is in fullscreen mode.
--
-- -----------------------------------------------------------
--
-- Parameters:
-- targetwidth = How wide you want the target area to be.
-- targetheight = How tall you want the target area to be.
-- targetscale = If this is 1, we use your target width/height
-- as-is, but if it's another value then we scale your provided
-- target size by that amount. This parameter is great if you want
-- a video to be a certain percentage of your desktop resolution.
-- In that case, just set targetwidth/targetheight to your
-- desktop resolution, and set this targetscale to the percentage
-- of your desktop that you want to use for the video, such as
-- "0.25" to resize the video to 25% of your desktop resolution.
-- maxvideoscale = If this is a positive number (anything above 0),
-- then the final video scale cannot exceed this number.
-- This is useful if you for example set the target to 25%
-- of your desktop resolution. If the video is smaller than that,
-- then it would be scaled up (enlarged) to the size of the target.
-- To control that behavior, simply set this parameter.
-- Here are some examples:
-- -1, 0, or any other non-positive number: We'll enlarge
-- too-small videos and shrink too-large videos. Small videos
-- will be enlarged as much as needed to match target size.
-- 1: Video will only be allowed to enlarge to 100% of its natural size.
-- This means that small videos won't become big and blurry.
-- 1.5: Video will only be allowed to enlarge to 150% of its natural size.
function quick_scale(targetwidth, targetheight, targetscale, maxvideoscale)
-- Don't attempt to scale the fullscreen window.
if (mp.get_property_bool("fullscreen", false)) then
return nil -- abort
end
-- Check parameter existence.
if (targetwidth == nil or targetheight == nil
or targetscale == nil or maxvideoscale == nil)
then
mp.osd_message("Quick_Scale: Missing parameters")
return nil -- abort
end
-- Ensure that the incoming strings are valid numbers.
targetwidth = tonumber(targetwidth)
targetheight = tonumber(targetheight)
targetscale = tonumber(targetscale)
maxvideoscale = tonumber(maxvideoscale)
if (targetwidth == nil or targetheight == nil
or targetscale == nil or maxvideoscale == nil)
then
mp.osd_message("Quick_Scale: Non-numeric parameters")
return nil -- abort
end
-- If the target scale isn't 1 (100%), we'll re-calculate target size.
if (targetscale ~= 1) then
targetwidth = targetwidth * targetscale
targetheight = targetheight * targetscale
end
-- Find smallest video scale that fits target size in both width and height.
-- This only looks at video and doesn't take window borders into account!
widthscale = targetwidth / mp.get_property("width")
heightscale = targetheight / mp.get_property("height")
local scale = (widthscale < heightscale and widthscale or heightscale)
-- If we arrived at a target width/height that is larger than the video's
-- natural "100%" scale, then we may want to limit it to a maximum amount.
if (maxvideoscale > 0 and scale > maxvideoscale) then
scale = maxvideoscale
end
-- Apply the new video scale.
mp.set_property_number("window-scale", scale)
end
-- Bind this via input.conf. Examples:
-- To fit a video to 100% of a 1680x1050 desktop size, with unlimited video enlarging:
-- Alt+9 script-message Quick_Scale "1680" "1050" "1" "-1"
-- To fit a video to 80% of a 1680x1050 desktop size, but disallowing the
-- video from becoming larger than 150% of its natural size:
-- Alt+9 script-message Quick_Scale "1680" "1050" "0.8" "1.5"
-- To fit a video to a 200x200 box, with unlimited video enlarging:
-- Alt+9 script-message Quick_Scale "200" "200" "1" "-1"
mp.register_script_message("Quick_Scale", quick_scale)
gitextract_7aqr3828/
├── .gitignore
├── README.md
├── script-settings/
│ ├── Blackbox.conf.example
│ ├── Colorbox.conf.example
│ ├── Leapfrog.conf.example
│ └── README.md
└── scripts/
├── Blackbox.js
├── Colorbox.js
├── Gallerizer.js
├── Leapfrog.js
├── auto-keep-gui-open.lua
├── cycle-video-rotate.lua
├── modules.js/
│ ├── AssFormat.js
│ ├── MicroUtils.js
│ ├── Options.js
│ ├── PathIndex.js
│ ├── PathTools.js
│ ├── PlaylistManager.js
│ ├── PseudoRandom.js
│ ├── RandomCycle.js
│ ├── SelectionMenu.js
│ └── Stack.js
├── multi-command-if.lua
└── quick-scale.lua
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (158K chars).
[
{
"path": ".gitignore",
"chars": 144,
"preview": "# temporary editor files\n*.swp\n.\\#*\n*.sublime-project\n*.sublime-workspace\n.idea\n\n# operating system cache files\n.DS_Stor"
},
{
"path": "README.md",
"chars": 3150,
"preview": "## VideoPlayerCode's MPV Utilities\n\n* `[ Lua ]` **[auto-keep-gui-open.lua](https://github.com/VideoPlayerCode/mpv-tools/"
},
{
"path": "script-settings/Blackbox.conf.example",
"chars": 762,
"preview": "# Menu appearance.\nauto_close=0\nmax_lines=12\nfont_size=40\nhelp_hint=no\n\n# Favorite paths.\nfavorites={/home/example/Movie"
},
{
"path": "script-settings/Colorbox.conf.example",
"chars": 1066,
"preview": "# Presets.\n# Format: v1; contrast; brightness; gamma; saturation; hue; sharpen; title\npresets[]=v1; 6; -2; 0; -4; 0;"
},
{
"path": "script-settings/Leapfrog.conf.example",
"chars": 163,
"preview": "# Tells Leapfrog to use a small fontsize and adds some transparency.\n# Read the Leapfrog.js code for more details about "
},
{
"path": "script-settings/README.md",
"chars": 613,
"preview": "## Script Configurations\n\nThis folder contains example configuration files. To enable them on your own\nsystem, simply cr"
},
{
"path": "scripts/Blackbox.js",
"chars": 38397,
"preview": "/*\n * BLACKBOX.JS\n *\n * Description: Advanced, modular media browser, file manager and playlist\n * manager "
},
{
"path": "scripts/Colorbox.js",
"chars": 7488,
"preview": "/*\n * COLORBOX.JS\n *\n * Description: Apply color correction presets.\n * Version: 1.0.0\n * Author: VideoPlayerCo"
},
{
"path": "scripts/Gallerizer.js",
"chars": 4795,
"preview": "/*\n * GALLERIZER.JS\n *\n * Description: Image gallery autoloader for mpv.\n * Version: 1.1.0\n * Author: VideoPlay"
},
{
"path": "scripts/Leapfrog.js",
"chars": 10504,
"preview": "/*\n * LEAPFROG.JS\n *\n * Description: Effortlessly jump through your playlist, with your own custom\n * jump "
},
{
"path": "scripts/auto-keep-gui-open.lua",
"chars": 8191,
"preview": "-- -----------------------------------------------------------\n--\n-- AUTO-KEEP-GUI-OPEN.LUA\n-- Version: 1.0.1\n-- Author:"
},
{
"path": "scripts/cycle-video-rotate.lua",
"chars": 1196,
"preview": "-- -----------------------------------------------------------\n--\n-- CYCLE-VIDEO-ROTATE.LUA\n-- Version: 1.0\n-- Author: V"
},
{
"path": "scripts/modules.js/AssFormat.js",
"chars": 2793,
"preview": "/*\n * ASSFORMAT.JS (MODULE)\n *\n * Version: 1.2.0\n * Author: VideoPlayerCode\n * URL: https://github.com/"
},
{
"path": "scripts/modules.js/MicroUtils.js",
"chars": 5175,
"preview": "/*\n * MICROUTILS.US (MODULE)\n *\n * Version: 1.3.0\n * Author: VideoPlayerCode\n * URL: https://github.com"
},
{
"path": "scripts/modules.js/Options.js",
"chars": 7544,
"preview": "/*\n * OPTIONS.JS (MODULE)\n *\n * Description: JavaScript implementation of mpv's Lua API's config file system,\n * "
},
{
"path": "scripts/modules.js/PathIndex.js",
"chars": 2915,
"preview": "/*\n * PATHINDEX.JS (MODULE)\n * Version: 1.0\n * Author: VideoPlayerCode\n * URL: https://github.com/VideoPlayerCode/mpv-to"
},
{
"path": "scripts/modules.js/PathTools.js",
"chars": 5681,
"preview": "/*\n * PATHTOOLS.JS (MODULE)\n * Version: 1.0\n * Author: VideoPlayerCode\n * URL: https://github.com/VideoPlayerCode/mpv-to"
},
{
"path": "scripts/modules.js/PlaylistManager.js",
"chars": 2616,
"preview": "/*\n * PLAYLISTMANAGER.JS (MODULE)\n * Version: 1.0\n * Author: VideoPlayerCode\n * URL: https://github.com/VideoPlayerCode/"
},
{
"path": "scripts/modules.js/PseudoRandom.js",
"chars": 1805,
"preview": "/*\n * PSEUDORANDOM.JS (MODULE)\n *\n * Version: 1.0.0\n * Author: VideoPlayerCode\n * URL: https://github.c"
},
{
"path": "scripts/modules.js/RandomCycle.js",
"chars": 3592,
"preview": "/*\n * RANDOMCYCLE.JS (MODULE)\n *\n * Version: 1.0.0\n * Author: VideoPlayerCode\n * URL: https://github.co"
},
{
"path": "scripts/modules.js/SelectionMenu.js",
"chars": 19433,
"preview": "/*\n * SELECTIONMENU.JS (MODULE)\n *\n * Version: 1.3.1\n * Author: VideoPlayerCode\n * URL: https://github."
},
{
"path": "scripts/modules.js/Stack.js",
"chars": 1778,
"preview": "/*\n * STACK.JS (MODULE)\n *\n * Version: 1.0.0\n * Author: VideoPlayerCode\n * URL: https://github.com/Vide"
},
{
"path": "scripts/multi-command-if.lua",
"chars": 17924,
"preview": "-- -----------------------------------------------------------\n--\n-- MULTI-COMMAND-IF.LUA\n-- Short Name: MCIF (Multi-Com"
},
{
"path": "scripts/quick-scale.lua",
"chars": 4470,
"preview": "-- -----------------------------------------------------------\n--\n-- QUICK-SCALE.LUA\n-- Version: 1.1\n-- Author: VideoPla"
}
]
About this extraction
This page contains the full source code of the VideoPlayerCode/mpv-tools GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (148.6 KB), approximately 36.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.