scriptName = "Folder Tunes"; scriptVersion = "1.0.0"; scriptDate = "April 6th, 2023"; scriptCopyright = "©2023 DeLaRoka"; scriptMinVersion = "12.21"; scriptDesc = "Associate audio files with specific folders, " + "so that the audio plays automatically when you " + "navigate into that folder"; scriptUrl = "https://resource.dopus.com/t/folder-tunes-automatic-audio-playback-upon-folder-entry/44133"; var configProps = { entries: "entries", file: "file", playOnFolderChange: "playOnFolderChange", playOnTabChange: "playOnTabChange", playOnPaneChange: "playOnPaneChange", playOnListerChange: "playOnListerChange", stopOnFolderChange: "stopOnFolderChange", stopOnTabChange: "stopOnTabChange", stopOnPaneChange: "stopOnPaneChange", stopOnListerChange: "stopOnListerChange", initialDelay: "initialDelay", debug: "debug", }; var scriptVars = { entries: "entries", map: "map", layoutMap: "layoutMap", }; var dialogNames = { entriesList: "dlgFolderTunesEntriesList", entry: "dlgFolderTunesEntry", }; // HOOKS /** * * @param initData {DOpusScriptInitData} * @returns {boolean} */ function OnInit(initData) { initData.name = scriptName; initData.desc = scriptDesc; initData.version = scriptVersion; initData.copyright = scriptCopyright; initData.default_enable = true; initData.min_version = scriptMinVersion; initData.url = scriptUrl; initData.config_desc = DOpus.create().map(); initData.config_groups = DOpus.create().map(); // Associations initData.config[configProps.entries] = DOpus.create().vector(); initData.config_groups.set(configProps.entries, "Associations"); initData.config_desc.set( configProps.entries, "Associations between folders and audio files. " + "Basic format: [folder path], [audio file path]\r\n" + "Example: C:\\Program Files, C:\\Users\\John\\Desktop\\sound.wav" ); initData.config[configProps.file] = ""; initData.config_groups.set(configProps.file, "Associations"); initData.config_desc.set( configProps.file, "Optional. File containing associations between folders and audio files. \r\n" + "Basic format: [folder path], [audio file path]" ); // Activation initData.config[configProps.playOnFolderChange] = true; initData.config_groups.set( configProps.playOnFolderChange, "Trigger: Folder navigation" ); initData.config_desc.set( configProps.playOnFolderChange, "Play audio when opening folder" ); initData.config[configProps.stopOnFolderChange] = true; initData.config_groups.set( configProps.stopOnFolderChange, "Trigger: Folder navigation" ); initData.config_desc.set( configProps.stopOnFolderChange, "Silence audio when leaving folder" ); initData.config[configProps.playOnTabChange] = true; initData.config_groups.set( configProps.playOnTabChange, "Trigger: Tab activation" ); initData.config_desc.set( configProps.playOnTabChange, "Play audio on tab switch" ); initData.config[configProps.stopOnTabChange] = true; initData.config_groups.set( configProps.stopOnTabChange, "Trigger: Tab activation" ); initData.config_desc.set( configProps.stopOnTabChange, "Silence audio on tab switch" ); initData.config[configProps.playOnPaneChange] = false; initData.config_groups.set( configProps.playOnPaneChange, "Trigger: Pane focus" ); initData.config_desc.set( configProps.playOnPaneChange, "Play audio on pane switch" ); initData.config[configProps.stopOnPaneChange] = false; initData.config_groups.set( configProps.stopOnPaneChange, "Trigger: Pane focus" ); initData.config_desc.set( configProps.stopOnPaneChange, "Silence audio on pane switch" ); initData.config[configProps.playOnListerChange] = false; initData.config_groups.set( configProps.playOnListerChange, "Trigger: Lister focus" ); initData.config_desc.set( configProps.playOnListerChange, "Play audio on lister switch" ); initData.config[configProps.stopOnListerChange] = false; initData.config_groups.set( configProps.stopOnListerChange, "Trigger: Lister focus" ); initData.config_desc.set( configProps.stopOnListerChange, "Silence audio on lister switch" ); // Other initData.config.DEBUG = false; initData.config_groups.set(configProps.debug, "Other"); initData.config_desc.set( configProps.debug, "Set DEBUG flag to true in order to enable logging messages to the Opus Output Window" ); initData.vars.set(scriptVars.map, DOpus.create.map()); initData.vars.set(scriptVars.layoutMap, DOpus.create.map()); // init entries if (!initData.vars.exists(scriptVars.entries)) { initData.vars.set(scriptVars.entries, DOpus.create.vector()); initData.vars(scriptVars.entries).persist = true; } } /** * @param cmdData {DOpusAddCmdData} * @constructor */ function OnAddCommands(cmdData) { saveEntries(DOpus.create().vector(Script.vars.get(scriptVars.entries))); var cmd = cmdData.addCommand(); cmd.name = "FolderTunes"; cmd.method = "OnFolderTunesCommand"; cmd.label = scriptName; cmd.template = "PLAY/O,STOP/O,SHOWLIST/O,ASSIGN/O[dialog]," + "FOLDER/O/K,AUDIO/O/K,LAYOUT/O/K,STATE/O[1,0,toggle]"; } function OnScriptConfigChange() { Script.vars.set( scriptVars.entries, DOpus.create.vector(Script.config[configProps.entries]) ); loadAssociations(); } /** * @param e {DOpusActivateListerData} */ function OnActivateLister(e) { if (!e.active || !e.lister.activeTab) return; DOpus.delay(300); if (Script.config[configProps.stopOnListerChange]) stopAllSounds(); if (Script.config[configProps.playOnListerChange]) { playForDir(e.lister.activeTab.path, e.lister.layout); } } /** * @param e {DOpusSourceDestData} * @constructor */ function OnSourceDestChange(e) { if (!e.source) return; if (Script.config[configProps.stopOnPaneChange]) stopAllSounds(); if (Script.config[configProps.playOnPaneChange]) { playForDir(e.tab.path, e.tab.lister.layout); } } /** * @param e {DOpusActivateTabData} */ function OnActivateTab(e) { if (Script.config[configProps.stopOnTabChange]) stopAllSounds(); if (Script.config[configProps.playOnTabChange]) { playForDir(e.newTab.path, e.newTab.lister.layout); } } /** * @param e {DOpusBeforeFolderChangeData} * @constructor */ function OnBeforeFolderChange(e) { if (Script.config[configProps.stopOnFolderChange]) stopAllSounds(); if (Script.config[configProps.playOnFolderChange]) { playForDir(e.path, e.tab.lister.layout); } } /** * @param cmdData {DOpusScriptCommandData} */ function OnFolderTunesCommand(cmdData) { var argsMap = cmdData.func.argsMap; var window = cmdData.func.sourceTab.lister; if (argsMap.exists("SHOWLIST")) { return showListDialog(cmdData.func.sourceTab.lister); } if (argsMap.exists("STOP")) { return stopAllSounds(); } var entry = { folder: argsMap.exists("FOLDER") ? normalizeEntryPropValue(argsMap.get("FOLDER")) : "", audio: argsMap.exists("AUDIO") ? normalizeEntryPropValue(argsMap.get("AUDIO")) : "", enabled: argsMap.exists("STATE") ? !!+argsMap.get("STATE") : true, layout: argsMap.exists("LAYOUT") ? normalizeEntryPropValue(argsMap.get("LAYOUT")) : null, }; if (argsMap.exists("PLAY")) { if (!entry.folder && !entry.audio) { entry.folder = cmdData.func.sourceTab.path; } return playEntry(entry); } if (argsMap.exists("ASSIGN")) { var existingEntry = findExistingEntryInConfig(entry); if (existingEntry && argsMap.exists("STATE")) { entry.enabled = argsMap.get("STATE") === "toggle" ? !existingEntry.enabled : entry.enabled; entry.layout = entry.layout || existingEntry.layout; entry.audio = entry.audio || existingEntry.audio; } return assignEntry(entry, window, argsMap.get("ASSIGN") === "dialog"); } } // METHODS /** * @param folderPath {DOpusPath | string} * @param layout {string?} */ function playForDir(folderPath, layout) { var path = resolvePath(folderPath); LogMessage("playForDir: " + path + ", Layout: " + layout); if (layout) { /** @type {DOpusMap} */ var layoutMap = Script.vars.get(scriptVars.layoutMap); if (layoutMap.exists(layout)) { var layoutEntriesMap = layoutMap.get(layout); if (layoutEntriesMap.exists(path)) { if (playAudio(layoutEntriesMap.get(path))) return; } } } if (layout) LogMessage("Layout '" + layout + "' was not found."); /** @type {DOpusMap} */ var map = Script.vars.get(scriptVars.map); if (map.exists(path)) playAudio(map.get(path)); else LogMessage("No entry found for path " + path); } /** * @param audioFilePath {string | DOpusPath} * @returns {boolean} bool whether the audio file was found or not */ function playAudio(audioFilePath) { if (audioFilePath && DOpus.fsUtil().exists(audioFilePath)) { DOpus.create() .command() .runCommand('Play "' + audioFilePath + '" QUIET'); return true; } return false; } /** * Play STOPALL */ function stopAllSounds() { DOpus.create().command().runCommand("Play STOPALL"); } /** * Load the combined associations from both the file * and the entries vector. Store them in the map. */ function loadAssociations() { var map = DOpus.create.map(); var layoutMap = DOpus.create.map(); /** @type {DOpusVector} */ var entries = Script.vars.get(scriptVars.entries); var file = Script.config[configProps.file]; var list = DOpus.create().vector(entries); if (file) { try { var fileContents = readFile(file).split("\r\n"); for (var i = 0; i < fileContents.length; i++) { list.push_back(fileContents[i]); } } catch (e) { LogMessage("Error reading file: " + e.toString()); } } for (var i = 0; i < list.length; i++) { var entry = parseEntry(list[i]); if (!entry.enabled || !entry.folder || !entry.audio) { continue; } var folderResolved = resolvePath(entry.folder); if (entry.layout) { var layoutMapEntries = layoutMap.exists(entry.layout) ? layoutMap.get(entry.layout) : DOpus.create.map(); layoutMapEntries.set(folderResolved, entry.audio); layoutMap.set(entry.layout, layoutMapEntries); } else { map.set(folderResolved, entry.audio); } } Script.vars.set(scriptVars.map, map); Script.vars.set(scriptVars.layoutMap, layoutMap); } /** * @param str {string} * @returns {object} */ function parseEntry(str) { var arr = str.split(", "); return { folder: arr[0], audio: arr[1], enabled: arr.length > 2 ? !!+arr[2] : true, layout: arr.length > 3 ? arr[3] : null, }; } /** * @param entry {object} * @returns {string} */ function stringifyEntry(entry) { var arr = [entry.folder, entry.audio]; if (!entry.enabled || entry.layout !== null) { arr.push(+entry.enabled); } if (entry.layout !== null) arr.push(entry.layout); return arr.join(", "); } /** * Save the entries to the config and script vars, * then reload the associations. * @param entries {DOpusVector} * @param entries */ function saveEntries(entries) { /** @type {DOpusVector} */ var list = Script.config[configProps.entries]; list.clear(); for (var i = 0; i < entries.length; i++) { try { var entry = typeof entries[i] !== "string" ? stringifyEntry(entries[i]) : entries[i]; list.push_back(entry); } catch (e) { LogMessage(e.toString() + ": " + JSON.stringify(entries[i])); } } Script.vars.set(scriptVars.entries, list); loadAssociations(); } /** * @param entry * @param entries * @returns {number | boolean} index of the entry or true when added to the end */ function putEntry(entry, entries) { for (var i = 0; i < entries.length; i++) { var isString = typeof entries[i] === "string"; var item = isString ? parseEntry(entries[i]) : entries[i]; if ( DOpus.fsUtil().comparePath(entry.folder, item.folder) && entry.layout === item.layout ) { entries[i] = isString ? stringifyEntry(entry) : entry; return i; } } entries.push_back(entry); return true; } /** * @param folder {DOpusPath | string} * @param layout {string | null} * @param entries {DOpusVector} * @returns {*} */ function findEntry(folder, layout, entries) { for (var i = 0; i < entries.length; i++) { var isString = typeof entries[i] === "string"; var item = isString ? parseEntry(entries[i]) : entries[i]; if ( DOpus.fsUtil().comparePath(item.folder, folder) && item.layout === layout ) { return item; } } } /** * @param entry {object} * @returns {entry|undefined} */ function findExistingEntryInConfig(entry) { var existingEntry; var entries = DOpus.create().vector(Script.vars.get(scriptVars.entries)); if (entry.folder) { existingEntry = findEntry(entry.folder, entry.layout, entries); if (!existingEntry && entry.layout) { LogMessage( "No entry found for layout: " + entry.layout + ", trying default layout..." ); existingEntry = findEntry(entry.folder, null, entries); } LogMessage( existingEntry ? "Found existing entry: " + JSON.stringify(existingEntry) : "No existing entry found." ); } return existingEntry; } /** * Play the audio file for the given entry. * @param entry {object} * @returns {boolean} */ function playEntry(entry) { if (entry.audio) return playAudio(entry.audio); if (entry.folder) playForDir(entry.folder, entry.layout); } /** * @param entry * @param window {DOpusLister | DOpusTab} * @param entryDialog {boolean} */ function assignEntry(entry, window, entryDialog) { if (entryDialog) { entry = showEntryDialog(entry, window); } else { var dlg = DOpus.dlg(); dlg.window = window; if (!entry.folder) { var folderPath = dlg.folder("", entry.folder); if (!folderPath.result) return; else entry.folder = folderPath; } if (!entry.audio) { var audioPath = dlg.open("Select audio file", entry.audio); if (!audioPath.result) return; else entry.audio = audioPath.realpath; } } if (!entry || !entry.folder || !entry.audio) return; var entries = DOpus.create().vector(Script.vars.get(scriptVars.entries)); putEntry(entry, entries); saveEntries(entries); } // DIALOGS /** * Dialog window with the list of entries. * @param window {DOpusLister | DOpusTab} */ function showListDialog(window) { var dlg = DOpus.dlg(); dlg.window = window; dlg.title = scriptName; dlg.template = dialogNames.entriesList; dlg.detach = true; dlg.want_close = true; dlg.want_resize = true; dlg.create(); dlg.loadPosition(dialogNames.entriesList); var fsUtil = DOpus.fsUtil(); var controlNames = { list: "list", addBtn: "addBtn", editBtn: "editBtn", deleteBtn: "deleteBtn", filter: "filter", clearBtn: "clearBtn", closeBtn: "closeBtn", exportBtn: "exportBtn", }; var listView = dlg.control(controlNames.list); var filterControl = dlg.control(controlNames.filter); /** @type {DOpusVector} */ var entriesStrings = Script.vars.get(scriptVars.entries); var entries = DOpus.create().vector(); var entriesFiltered = DOpus.create().vector(); listView.columns.addColumn("Folder"); listView.columns.addColumn("Audio"); var groups = DOpus.create().map(); listView.addGroup("All lister layouts", 0, "c"); listView.removeItem(-1); for (var i = 0; i < entriesStrings.length; i++) { var entry = parseEntry(entriesStrings[i]); entries.push_back(entry); entriesFiltered.push_back(entry); addListViewItem(entry); } dlg.control(controlNames.exportBtn).enabled = entries.length > 0; listView.enableGroupView(true); listView.columns.autoSize(); dlg.show(); var selectedIndex = -1; while (true) { var msg = dlg.getMsg(); if (!msg.result) break; if ( msg.event === "close" || (msg.event === "click" && msg.control === controlNames.closeBtn) ) { saveEntries(entries); dlg.endDlg(); return; } if (msg.event === "resize") { dlg.savePosition(dialogNames.entriesList); } if (msg.event === "checked" && msg.control === controlNames.list) { entriesFiltered[msg.index].enabled = msg.checked; } if (msg.event === "selchange" && msg.control === controlNames.list) { selectedIndex = +listView.value; dlg.control(controlNames.editBtn).enabled = selectedIndex > -1; dlg.control(controlNames.deleteBtn).enabled = selectedIndex > -1; } if (msg.event === "click" && msg.control === controlNames.exportBtn) { exportEntriesDialog(entriesFiltered, window); } if (msg.event === "click" && msg.control === controlNames.addBtn) { var newEntry = showEntryDialog(null, window); if (newEntry) { putEntry(newEntry, entries); filterControl.value = filterControl.value; } } if ( (msg.event === "click" && msg.control === controlNames.editBtn) || (msg.event === "dblclk" && msg.control === controlNames.list) ) { var entryToEdit = entriesFiltered[selectedIndex]; var editedEntry = showEntryDialog(entryToEdit, window); if ( editedEntry && stringifyEntry(entryToEdit) !== stringifyEntry(editedEntry) ) { entries[findGlobalEntryIndex(entryToEdit)] = editedEntry; filterControl.value = filterControl.value; } } if (msg.event === "click" && msg.control === controlNames.deleteBtn) { var entryToDelete = entriesFiltered[selectedIndex]; entries.erase(findGlobalEntryIndex(entryToDelete)); entriesFiltered.erase(selectedIndex); listView.removeItem(selectedIndex); listView.columns.autoSize(); } if (msg.event === "editchange" && msg.control === controlNames.filter) { var filter = msg.value.toLowerCase(); entriesFiltered.clear(); listView.removeItem(-1); for (var i = 0; i < entries.length; i++) { var folder = entries[i].folder.toLowerCase(); var audio = entries[i].audio.toLowerCase(); var isVisible = folder.search(filter) > -1 || audio.search(filter) > -1; if (!isVisible) continue; entriesFiltered.push_back(entries[i]); addListViewItem(entries[i]); } listView.columns.autoSize(); dlg.control(controlNames.exportBtn).enabled = entriesFiltered.length > 0; } if (msg.event === "click" && msg.control === controlNames.clearBtn) { clearFilter(); } } function getGroupIndex(layoutName) { return layoutName !== null ? groups.exists(layoutName) ? groups.get(layoutName) : null : 0; } function ensureListViewGroup(layoutName) { var groupIndex = getGroupIndex(layoutName); if (groupIndex !== null) return; groups.set(layoutName, groups.length + 1); listView.addGroup(layoutName, groups.length, "c"); } function addListViewItem(entry, index) { if (index) listView.insertItemAt(index, entry.folder); else index = listView.addItem(entry.folder); var item = listView.getItemAt(index); item.checked = +entry.enabled; item.subitems(0, entry.audio); ensureListViewGroup(entry.layout); item.group = getGroupIndex(entry.layout); styleListViewItem(item); } function findGlobalEntryIndex(entry) { for (var i = 0; i < entries.length; i++) { if (entry === entries[i]) return i; } return null; } function clearFilter() { if (filterControl.value !== "") { entriesFiltered.clear(); filterControl.value = ""; } } /** * @param item {DOpusDialogListItem} */ function styleListViewItem(item) { var isValid = fsUtil.exists(item.name) && fsUtil.exists(item.subitems(0)); item.fg = isValid ? "#000000" : "#e01212"; item.style = isValid ? "" : "b"; } } /** * @param entry * @param window {DOpusLister | DOpusTab} * @returns {object | null} */ function showEntryDialog(entry, window) { var dlg = DOpus.dlg(); dlg.window = window; dlg.title = scriptName + ": Entry"; dlg.template = dialogNames.entry; dlg.detach = true; dlg.create(); var fsUtil = DOpus.fsUtil(); var controlNames = { folder: "folder", folderBrowseBtn: "folderBrowseBtn", audio: "audio", audioBrowseBtn: "audioBrowseBtn", state: "state", layout: "layout", ok: "okBtn", }; var folderControl = dlg.control(controlNames.folder); var audioControl = dlg.control(controlNames.audio); var stateControl = dlg.control(controlNames.state); var layoutControl = dlg.control(controlNames.layout); var filePath = resolvePath( "%AppData%\\GPSoftware\\Directory Opus\\Layouts\\order.xml" ); var all = "-- All --"; try { var layouts = extractLayoutNames(filePath); } catch (e) { layouts = []; } var layoutIndex = 0; layoutControl.addItem(all); for (var i = 0; i < layouts.length; i++) { layoutControl.addItem(layouts[i]); if (entry && entry.layout === layouts[i]) { layoutIndex = i + 1; } } if (!entry) entry = { enabled: true }; folderControl.value = entry.folder || ""; audioControl.value = entry.audio || ""; layoutControl.value = layoutIndex; stateControl.value = entry.enabled; function updateOkBtn() { dlg.control(controlNames.ok).enabled = isFilledAndCommaless(folderControl.value) && isFilledAndCommaless(audioControl.value); } dlg.show(); while (true) { var msg = dlg.getMsg(); if (!msg.result) return null; LogMessage(msg.event + " : " + msg.control); if (msg.event === "drop") { var object = new Enumerator(msg.object).item(); var isFolder = fsUtil.getItem(object).is_dir; if (isFolder) folderControl.value = object; else audioControl.value = object; } else if ( msg.event === "click" && msg.control === controlNames.folderBrowseBtn ) { var folder = dlg.folder("", folderControl.value); if (folder.result) folderControl.value = folder; } else if ( msg.event === "click" && msg.control === controlNames.audioBrowseBtn ) { var audio = dlg.open("Select audio file", audioControl.value); if (audio.result) audioControl.value = audio; updateOkBtn(); } else if ( msg.event === "editchange" && (msg.control === controlNames.folder || msg.control === controlNames.audio) ) { updateOkBtn(); } else if (msg.event === "click" && msg.control === controlNames.ok) { dlg.endDlg(0); return { enabled: stateControl.value, folder: folderControl.value, audio: audioControl.value, layout: layoutControl.value > 0 ? normalizeEntryPropValue(layouts[layoutControl.value - 1]) : null, }; } } } /** * @param entries {object[]} * @param window {DOpusLister | DOpusTab} */ function exportEntriesDialog(entries, window) { var filepath = DOpus.dlg().save( "Export FolderTunes list to file", "FolderTunes.txt", window, "#Text Files!*.txt" ); if (!filepath.result) return; var fso = new ActiveXObject("Scripting.FilesystemObject"); var file = fso.OpenTextFile(filepath, 2, true); for (var i = 0; i < entries.length; i++) { file.WriteLine(stringifyEntry(entries[i])); } file.Close(); } // UTILS function extractLayoutNames(filePath) { var xmlDoc = getXmlDoc(filePath); var layoutNodes = xmlDoc.getElementsByTagName("layout"); var layoutNames = []; for (var i = 0; i < layoutNodes.length; i++) { layoutNames.push(layoutNodes[i].getAttribute("name")); } return layoutNames; } function getXmlDoc(filePath) { var fileContents = readFile(filePath); var result = fileContents .replace("\u043f", "") .replace("\u00bb", "") .replace("\u0457", ""); var msxml = new ActiveXObject("MSXML2.DOMDocument.6.0"); msxml.loadXML(result); if (msxml.parseError.errorCode !== 0) { LogMessage("XML load error code: " + msxml.parseError.errorCode); return null; } return msxml; } function readFile(dir) { var fs = new ActiveXObject("Scripting.FileSystemObject"); var frd = fs.OpenTextFile(dir, 1); var i = 1; if (frd.AtEndOfStream) throw Error("The file is empty"); while (i) { if (!frd.AtEndOfStream) { var rdd = frd.ReadAll(); frd.Close(); i = 0; return rdd; } } } function resolvePath(path) { return typeof path === "string" ? DOpus.fsUtil.resolve(path, "j") : path; } function isFilledAndCommaless(str) { return typeof str === "string" && str.length > 0 && str.search(",") === -1; } function normalizeEntryPropValue(value) { return typeof value === "string" ? value ? value.split(",")[0] : null : value; } function IsDebugEnabled() { return Script.config.DEBUG; } function LogMessage(message, force) { if (force || IsDebugEnabled()) DOpus.OutputString(message); } ==SCRIPT RESOURCES