kodilist
Version:
CLI tool for generating Kodi playlists based on filename-embedded tags.
185 lines (174 loc) • 5.58 kB
text/typescript
import fs from "fs";
import path from "path";
import os from "os";
import chalk from "chalk";
export interface SearchTerms {
[index: string]: string[];
sources: string[];
tags: string[];
people: string[];
}
export interface ConfigFile {
kodi: string;
configPath: string;
save: () => void;
directories: string[];
ignoreList: string[];
searchTerms: SearchTerms;
}
/**
* Turns a full file path like "~/foo/bar/Rainbow.Lorikeet.2017.mp4"
* and returns a simplified name like "rainbow lorikeet 2017 mp4" by
* lowercasing it, swapping dots for spaces, and removing path information.
*/
export function simplifyName(name: string): string {
return name
.split(path.sep)
.pop()!
.toLowerCase()
.replace(/\./g, " ");
}
/**
* jA list of the default video playlist folders for each platform, relative to
* the user's home directory.
*/
const possiblePlaylistFolders: string[][] = [
["AppData", "Roaming", "Kodi", "userdata", "playlists", "video"], // Win32
[
"AppData",
"Local",
"Packages",
"XBMCFoundation.Kodi_4n2hpmxwrvr6p",
"LocalCache",
"Roaming",
"Kodi",
"userdata",
"playlists",
"video"
], // UWP
[".kodi", "userdata", "playlists", "video"],
["Library", "Application Support", "Kodi", "userdata"]
];
/**
* Returns the path to Kodi's video playlists folder if it exists; aborts the
* entire program if it doesn't. The path is discovered by checking for the
* default path on various platforms, so if the user has installed Kodi in a
* non-standard location, this can fail.
*
* TODO: Allow the user to manually specify where they installed Kodi without
* having to edit `kodilist.json`.
*/
export function getKodiFolder(): string {
const home = os.homedir();
const kodiFolder = possiblePlaylistFolders.find(folder => {
const absolutePath = path.resolve(path.join(home, ...folder));
return fs.existsSync(absolutePath);
});
if (kodiFolder === undefined) {
console.error("Could not locate Kodi folder!");
return process.exit(1);
}
return path.resolve(path.join(home, ...kodiFolder));
}
/**
* Creates a folder at the supplied path if it doesn't already exist.
*/
export function guaranteeFolder(root: string, name: string): void {
const target = path.join(root, name);
if (!fs.existsSync(target)) fs.mkdirSync(target);
}
/**
* Writes M3U playlists for each of the user's tracked people, tags, and
* sources in folders named 'people', 'tags', and 'sources' respectively.
*/
export function writePlaylistsSearchTerms(
config: ConfigFile,
files: any[]
): void {
Object.keys(config.searchTerms).forEach((folderName: string) => {
process.stdout.write(" " + folderName + "...");
guaranteeFolder(config.kodi, folderName);
config.searchTerms[folderName].forEach((tagName: string) => {
const matches = files
.filter(name => name.simpleName.includes(tagName))
.map(file => file.filename)
.join("\n");
const p = path.join(config.kodi, folderName, tagName + ".m3u");
fs.writeFileSync(p, matches);
});
process.stdout.write(`\r${chalk.green("✔️")} ${folderName} \n`);
});
}
/**
* Writes a playlist `new.m3u` with files created within the last week,
* measured using `fs.Stats.birthTimeMs`. For large libraries this is the
* slowest part of the program after generating the initial file list. This
* could be improved performance-wise by caching data from the last run, at the
* potential cost of reliability -- what if a file is replaced by a new but
* identically-named file -- and we'd still need to check that the files still
* exist so it'd still involve touching the disk again.
*/
export function writePlaylistsNew(config: ConfigFile, files: any[]): void {
process.stdout.write(" this week's files...");
const now = new Date().getTime();
const lastWeek = now - 604800000;
const newFiles = files
.filter(file => file.mtime > lastWeek)
.sort((a: any, b: any) => (a.mtime > b.mtime ? -1 : 1))
.map(file => file.filename)
.join("\n");
fs.writeFileSync(path.join(config.kodi, "new.m3u"), newFiles);
process.stdout.write(`\r${chalk.green("✔️")} this week's files \n`);
}
const alphabet: { [index: string]: string[] } = {
a: [],
b: [],
c: [],
d: [],
e: [],
f: [],
g: [],
h: [],
i: [],
j: [],
k: [],
l: [],
m: [],
n: [],
o: [],
p: [],
q: [],
r: [],
s: [],
t: [],
u: [],
v: [],
w: [],
x: [],
y: [],
z: [],
"!": []
};
/**
* Writes a playlist `a.m3u` with all files beginning with A or a, `b.m3u`
* with all files beginning with B or b, and so on. If the filename begins with
* any non-alphabetical character it falls under `!.m3u`.
*/
export function writePlaylistsAlphabet(config: ConfigFile, files: any[]): void {
guaranteeFolder(config.kodi, "alphabetical");
process.stdout.write(" alphabetical files...");
files.forEach(file => {
if (alphabet.hasOwnProperty(file.simpleName[0])) {
alphabet[file.simpleName[0]].push(file.filename);
} else {
alphabet["!"].push(file.filename);
}
});
Object.keys(alphabet).forEach(letter => {
fs.writeFileSync(
path.join(config.kodi, "alphabetical", letter + ".m3u"),
alphabet[letter].join("\n")
);
});
process.stdout.write(`\r${chalk.green("✔️")} alphabetical files \n`);
}