Skip to content

Instantly share code, notes, and snippets.

@sanjarcode
Last active November 7, 2023 04:36
Show Gist options
  • Save sanjarcode/c8d88c86b90db060a8448cf913c7bf8c to your computer and use it in GitHub Desktop.
Save sanjarcode/c8d88c86b90db060a8448cf913c7bf8c to your computer and use it in GitHub Desktop.
VLC Playlist (XSPF) generator from file tree

Why

I had some videos (actually a folder that one more level of folders and finally video files).

I wanted to generate .xspf files for the whole folder, where each subfolder became a playlist. i.e I wanted to generate multiple files (one corresponding to each folder) XSPF files are used by VLC to store a string of videos. You can open the file with VLC and it will play them as a playlist, allowing prev/next video.

How to use

  • tree.js is used for generating the tree structure of the root folder, as an object.
    • All leaves are .mp4 videos, values being the duration in ms.
    • Set the destination in the function it exports (as an arg).
    • Run the file node tree.js, then copy the output to a JSON file and run a formatter.
  • file-gen
    • Set the JSON file destination in the function it exports (as an arg).
    • Run the file node tree.js. XSPF files are generated.

Useful for

Video courses, especially downloaded ones, like Coursera etc.

I start a server on the download device via npm package http-server, i.e. http-server .. Now I can see the video on all devices on my home network. Hope VLC is sandboxed.

const path = require("path");
const fs = require("fs");
function generateXSPF({
title = "Getting started",
children = [
{
pre: "http://192.168.0.103:8080",
path: "1.%20Getting%20Started",
file: "2-%20Prerequisites.mp4",
duration: 38615,
},
],
}) {
const playListStart = `<?xml version="1.0" encoding="UTF-8"?>
<playlist xmlns="http://xspf.org/ns/0/" xmlns:vlc="http://www.videolan.org/vlc/playlist/ns/0/" version="1">
`;
const playListEnd = `</playlist>`;
const _title = () => `<title>${title}</title>`;
const _track = ({
pre = "http://192.168.0.103:8080",
path = "1.%20Getting%20Started",
file = "2-%20Prerequisites.mp4",
duration = 38615,
index = 0,
}) =>
`<track>
<location>${[pre, path, file].join("/")}</location>` +
// `<duration>${duration}</duration>` +
`
<extension application="http://www.videolan.org/vlc/playlist/0">
<vlc:id>${index}</vlc:id>
</extension>
</track>`;
const trackList = (arr = []) =>
`<trackList>${arr
.map((obj, index) => _track({ ...obj, index }))
.join("")}</trackList>`;
const extensionTags = (
n
) => `<extension application="http://www.videolan.org/vlc/playlist/0">
${Array(n)
.fill(null)
.map((_, i) => `<vlc:item tid="${i}" />`)
.join("")}
</extension>`;
const final = [
playListStart,
_title(title),
trackList(children),
extensionTags(children.length),
playListEnd,
].join("");
return final;
}
/**
*
* @returns {[{title, fileContent}]}
*/
function getFileObjects(obj, pre = "http://192.168.0.103:8080") {
const op = [];
Object.entries(obj)
.sort(([a], [b]) =>
a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })
)
.forEach(([key, value]) => {
const title = key;
const children = Object.entries(value)
.sort(([a], [b]) =>
a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" })
)
.reduce((accum, [key, value], index) => {
accum.push({
pre: pre,
path: encodeURIComponent(title),
file: encodeURIComponent(key),
duration: value,
});
return accum;
}, []);
// if (op.length === 0)
op.push({ title, fileContent: generateXSPF({ title, children }) });
});
return op;
}
function generateFilesFromFileObjects(pathToJSONFile = {}) {
let obj = pathToJSONFile;
if (typeof pathToJSONFile === typeof "") {
obj = require(pathToJSONFile);
}
const fileObjects = getFileObjects(obj);
const HIDDEN_CHARACTER = ` `;
fileObjects.forEach(({ title, fileContent }) => {
const friendlyTitle =
title
.replaceAll("'", "")
.replaceAll('"', "")
.replaceAll(".", "")
.replaceAll(HIDDEN_CHARACTER, "")
.replaceAll(" ", "-") + ".xspf";
const filePath = path.join(__dirname, friendlyTitle);
fs.writeFileSync(filePath, fileContent);
});
}
module.exports = { generateFilesFromFileObjects };
// generateFilesFromFileObjects("./one.json");
var fs = require("fs");
var path = require("path");
var filetree = {};
const util = require("util");
const runInTerminal = util.promisify(require("child_process").exec);
const limitDecimalsWithRounding = (value, maxDecimals = 2) => {
const amount = parseFloat(value);
const power = 10 ** maxDecimals;
return Math.round(amount * power) / power;
};
var walkDirectory = async function (location, obj = {}, untilNow = __dirname) {
var dir = fs.readdirSync(location);
for (var i = 0; i < dir.length; i++) {
var name = dir[i];
var target = location + "/" + name;
var stats = fs.statSync(target);
if (stats.isFile()) {
if (name.slice(-5).includes(".")) {
// obj[name.slice(0, -3)] = require(target);
const currentFilePath = path.join(untilNow, name);
const isVideo = currentFilePath.endsWith(".mp4");
if (!isVideo) false;
const op = !isVideo
? await runInTerminal(`du -h "${currentFilePath}"`)
: await runInTerminal(
`ffprobe "${currentFilePath}" -show_entries format=duration -v quiet -of csv="p=0";`
);
const { stdout, stderr } = op;
// console.log({ stdout, stderr });
const usefulOp = isVideo
? Math.round(
1e3 * limitDecimalsWithRounding(stdout.split("\n").at(0), 4)
)
: limitDecimalsWithRounding(stdout.split("\t").at(0), 4);
obj[name] = stderr ? usefulOp : null;
}
} else if (stats.isDirectory()) {
obj[name] = {};
await walkDirectory(target, obj[name], path.join(untilNow, name));
}
}
};
// walkDirectory(".", filetree).then(() => console.log(filetree));
module.exports = {
/**
* @returns {Object}
*/
async getVideoFilesTreeAsObject(location = ".") {
const retVal = await walkDirectory(location, {}, location);
return retVal;
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment