Skip to content

Instantly share code, notes, and snippets.

@sanjarcode
Last active November 7, 2023 04:36
Show Gist options
  • Select an option

  • Save sanjarcode/c8d88c86b90db060a8448cf913c7bf8c to your computer and use it in GitHub Desktop.

Select an option

Save sanjarcode/c8d88c86b90db060a8448cf913c7bf8c to your computer and use it in GitHub Desktop.

Revisions

  1. sanjarcode revised this gist Nov 7, 2023. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -19,4 +19,5 @@ XSPF files are used by VLC to store a string of videos. You can open the file wi

    Video courses, especially downloaded ones, like Coursera etc.

    I start a server on the download device via npm package [http-server](https://www.npmjs.com/package/http-server), i.e. `http-server .`. Now I can see the video on all devices on my home network.
    I start a server on the download device via npm package [http-server](https://www.npmjs.com/package/http-server), i.e. `http-server .`.
    Now I can see the video on all devices on my home network. Hope VLC is [sandboxed](https://en.wikipedia.org/wiki/Sandbox_(computer_security)).
  2. sanjarcode revised this gist Nov 7, 2023. No changes.
  3. sanjarcode revised this gist Nov 7, 2023. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -18,3 +18,5 @@ XSPF files are used by VLC to store a string of videos. You can open the file wi
    ## Useful for

    Video courses, especially downloaded ones, like Coursera etc.

    I start a server on the download device via npm package [http-server](https://www.npmjs.com/package/http-server), i.e. `http-server .`. Now I can see the video on all devices on my home network.
  4. sanjarcode created this gist Oct 25, 2023.
    20 changes: 20 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,20 @@
    ## 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.
    120 changes: 120 additions & 0 deletions file-gen.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,120 @@
    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");
    60 changes: 60 additions & 0 deletions tree.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,60 @@
    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;
    },
    };