vsmeta-to-jpeg 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Charles Aldarondo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # vsmeta-to-jpeg
2
+
3
+ Extract embedded JPEG images (poster and backdrop) from Synology Video Station `.vsmeta` metadata files.
4
+
5
+ Synology has officially deprecated Video Station with DSM 7.2.2. If you are migrating to Jellyfin, Kodi, Plex, or other generic ecosystems, this tool helps you recover the high-quality artwork (posters and backdrops) currently trapped inside your binary `.vsmeta` files and saves them as standard `.jpg` sidecars directly alongside your source media.
6
+
7
+ Looking to generate `.nfo` metadata files as well? Check out the sibling package [vsmeta-to-nfo](https://github.com/aldarondo/vsmeta-to-nfo).
8
+
9
+ This package provides both a **command-line tool** for batch extraction and a **programmatic interface**.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ # Global install for CLI use
15
+ npm install -g vsmeta-to-jpeg
16
+
17
+ # Or install locally
18
+ npm install vsmeta-to-jpeg
19
+ ```
20
+
21
+ ## CLI Usage
22
+
23
+ ### Installed Package
24
+
25
+ ```bash
26
+ # Process a single file
27
+ vsmeta-to-jpeg ./path/to/movie.mp4.vsmeta
28
+
29
+ # Recursively scan a directory and extract images from all discovered .vsmeta files
30
+ vsmeta-to-jpeg ./path/to/library
31
+ ```
32
+
33
+ ### Options
34
+
35
+ * **`-d, --dry-run`**: Simulate the process without writing any actual `.jpg` files.
36
+ * **`-f, --overwrite`**: By default, the script skips extraction if the target `.jpg` files already exist. This option overrides that safeguard.
37
+ * **`-v, --verbose`**: Append a concise list of all skipped or failed files and errors at the end of the console execution run.
38
+
39
+ ### Image Naming Convention
40
+
41
+ Images are extracted using a prefix based on the `.vsmeta` filename:
42
+ - `poster.jpg`: The main artwork / cover.
43
+ - `fanart.jpg`: The backdrop / background (**Movies/Others**).
44
+ - `thumb.jpg`: The backdrop / background (**TV Episodes**).
45
+
46
+ If your file is `MyMovie.mp4.vsmeta`, the tool will generate `MyMovie.mp4-poster.jpg` and `MyMovie.mp4-fanart.jpg`.
47
+ If your file is `S01E01.mp4.vsmeta` (and contains episode metadata), it will generate `S01E01.mp4-poster.jpg` and `S01E01.mp4-thumb.jpg`.
48
+
49
+ ## Programmatic API
50
+
51
+ ```javascript
52
+ import { convertVsMetaToJpeg } from 'vsmeta-to-jpeg';
53
+
54
+ const result = convertVsMetaToJpeg('./libraries/movies/MyMovie (2020).mp4.vsmeta', {
55
+ dryRun: false,
56
+ overwrite: true,
57
+ });
58
+
59
+ if (result.status === 'SUCCESS') {
60
+ console.log(`Extracted images for: ${result.vsmetaPath}`);
61
+ }
62
+ ```
63
+
64
+ ## License
65
+
66
+ MIT License
@@ -0,0 +1,134 @@
1
+ // src/converter.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { parseVsMeta } from "vsmeta-parser";
5
+ var MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mkv", ".avi", ".mov", ".wmv", ".m4v", ".m2ts", ".ts", ".mpg", ".mpeg"]);
6
+ var readdirCache = /* @__PURE__ */ new Map();
7
+ function clearReaddirCache() {
8
+ readdirCache.clear();
9
+ }
10
+ function hasMatchingMediaFile(vsmetaPath) {
11
+ const dir = path.dirname(vsmetaPath);
12
+ const basename = path.basename(vsmetaPath, ".vsmeta");
13
+ if (fs.existsSync(path.join(dir, basename))) {
14
+ return true;
15
+ }
16
+ let files = readdirCache.get(dir);
17
+ if (!files) {
18
+ files = fs.readdirSync(dir);
19
+ readdirCache.set(dir, files);
20
+ }
21
+ for (const file of files) {
22
+ if (file.startsWith(basename) && file !== path.basename(vsmetaPath)) {
23
+ const ext = path.extname(file).toLowerCase();
24
+ if (MEDIA_EXTENSIONS.has(ext)) {
25
+ return true;
26
+ }
27
+ }
28
+ }
29
+ return false;
30
+ }
31
+ function convertVsMetaToJpeg(vsmetaPath, options = {}) {
32
+ try {
33
+ if (!hasMatchingMediaFile(vsmetaPath)) {
34
+ return {
35
+ status: "SKIP",
36
+ vsmetaPath,
37
+ message: `No matching media file found for: ${vsmetaPath}`
38
+ };
39
+ }
40
+ const buffer = fs.readFileSync(vsmetaPath);
41
+ let meta;
42
+ try {
43
+ meta = parseVsMeta(buffer);
44
+ } catch (e) {
45
+ return {
46
+ status: "WARN",
47
+ vsmetaPath,
48
+ message: `Failed to parse ${vsmetaPath}: ${e.message}`
49
+ };
50
+ }
51
+ const isEpisode = meta.contentType === 2 || meta.season != null && meta.episode != null;
52
+ const posterPath = vsmetaPath.replace(/\.vsmeta$/i, "-poster.jpg");
53
+ const fanartPath = vsmetaPath.replace(/\.vsmeta$/i, isEpisode ? "-thumb.jpg" : "-fanart.jpg");
54
+ const posterExists = fs.existsSync(posterPath);
55
+ const fanartExists = fs.existsSync(fanartPath);
56
+ if (!options.overwrite && posterExists && fanartExists) {
57
+ return {
58
+ status: "SKIP",
59
+ vsmetaPath,
60
+ message: `Images already exist (use --overwrite to replace): ${vsmetaPath}`,
61
+ posterPath,
62
+ fanartPath
63
+ };
64
+ }
65
+ const results = [];
66
+ let anyExtracted = false;
67
+ if (meta.posterImage) {
68
+ if (options.overwrite || !posterExists) {
69
+ if (!options.dryRun) {
70
+ fs.writeFileSync(posterPath, meta.posterImage);
71
+ }
72
+ results.push(`poster: ${posterPath}`);
73
+ anyExtracted = true;
74
+ }
75
+ }
76
+ if (meta.backdropImage) {
77
+ if (options.overwrite || !fanartExists) {
78
+ if (!options.dryRun) {
79
+ fs.writeFileSync(fanartPath, meta.backdropImage);
80
+ }
81
+ results.push(`${isEpisode ? "thumb" : "fanart"}: ${fanartPath}`);
82
+ anyExtracted = true;
83
+ }
84
+ }
85
+ if (!anyExtracted) {
86
+ return {
87
+ status: "WARN",
88
+ vsmetaPath,
89
+ message: `No images found in ${vsmetaPath}`
90
+ };
91
+ }
92
+ const prefix = options.dryRun ? "[DRY RUN] " : "";
93
+ return {
94
+ status: "SUCCESS",
95
+ vsmetaPath,
96
+ message: `${prefix}Extracted ${results.join(", ")}`,
97
+ posterPath: meta.posterImage ? posterPath : void 0,
98
+ fanartPath: meta.backdropImage ? fanartPath : void 0
99
+ };
100
+ } catch (err) {
101
+ const e = err;
102
+ let msg = "";
103
+ if (e.code === "EACCES" || e.code === "EPERM") {
104
+ msg = `Permission denied: Cannot read/write files. Please check permissions for ${vsmetaPath}`;
105
+ } else {
106
+ msg = `Processing ${vsmetaPath}: ${e.message || err}`;
107
+ }
108
+ return { status: "ERROR", vsmetaPath, message: msg };
109
+ }
110
+ }
111
+
112
+ // src/logger.ts
113
+ function getResultMessage(result) {
114
+ return `[${result.status}] ${result.message}`;
115
+ }
116
+ function logConvertResult(result) {
117
+ const msg = getResultMessage(result);
118
+ if (result.status === "ERROR") {
119
+ console.error(msg);
120
+ } else if (result.status === "WARN") {
121
+ console.warn(msg);
122
+ } else {
123
+ console.log(msg);
124
+ }
125
+ }
126
+
127
+ export {
128
+ clearReaddirCache,
129
+ hasMatchingMediaFile,
130
+ convertVsMetaToJpeg,
131
+ getResultMessage,
132
+ logConvertResult
133
+ };
134
+ //# sourceMappingURL=chunk-2EEHQI7P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/converter.ts","../src/logger.ts"],"sourcesContent":["import fs from 'fs';\r\nimport path from 'path';\r\nimport { parseVsMeta, VsMetaData } from 'vsmeta-parser';\r\nimport { ConvertOptions, ConvertResult } from './types.js';\r\n\r\n// Common video extensions checked when verifying a matching media file exists.\r\nconst MEDIA_EXTENSIONS = new Set(['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.m4v', '.m2ts', '.ts', '.mpg', '.mpeg']);\r\n\r\n// Per-directory cache for readdirSync results — avoids redundant I/O when\r\n// multiple .vsmeta files share the same folder during a batch conversion.\r\nconst readdirCache = new Map<string, string[]>();\r\n\r\n/** Clear the internal directory listing cache. Call after a batch run completes. */\r\nexport function clearReaddirCache(): void {\r\n readdirCache.clear();\r\n}\r\n\r\nexport function hasMatchingMediaFile(vsmetaPath: string): boolean {\r\n const dir = path.dirname(vsmetaPath);\r\n const basename = path.basename(vsmetaPath, '.vsmeta');\r\n\r\n if (fs.existsSync(path.join(dir, basename))) {\r\n return true;\r\n }\r\n\r\n let files = readdirCache.get(dir);\r\n if (!files) {\r\n files = fs.readdirSync(dir) as string[];\r\n readdirCache.set(dir, files);\r\n }\r\n\r\n for (const file of files) {\r\n if (file.startsWith(basename) && file !== path.basename(vsmetaPath)) {\r\n const ext = path.extname(file).toLowerCase();\r\n if (MEDIA_EXTENSIONS.has(ext)) {\r\n return true;\r\n }\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport function convertVsMetaToJpeg(vsmetaPath: string, options: ConvertOptions = {}): ConvertResult {\r\n try {\r\n if (!hasMatchingMediaFile(vsmetaPath)) {\r\n return {\r\n status: 'SKIP',\r\n vsmetaPath,\r\n message: `No matching media file found for: ${vsmetaPath}`\r\n };\r\n }\r\n\r\n const buffer = fs.readFileSync(vsmetaPath);\r\n let meta: VsMetaData;\r\n try {\r\n meta = parseVsMeta(buffer);\r\n } catch (e) {\r\n return {\r\n status: 'WARN',\r\n vsmetaPath,\r\n message: `Failed to parse ${vsmetaPath}: ${(e as Error).message}`\r\n };\r\n }\r\n\r\n const isEpisode = meta.contentType === 2 || (meta.season != null && meta.episode != null);\r\n const posterPath = vsmetaPath.replace(/\\.vsmeta$/i, '-poster.jpg');\r\n const fanartPath = vsmetaPath.replace(/\\.vsmeta$/i, isEpisode ? '-thumb.jpg' : '-fanart.jpg');\r\n\r\n const posterExists = fs.existsSync(posterPath);\r\n const fanartExists = fs.existsSync(fanartPath);\r\n\r\n if (!options.overwrite && posterExists && fanartExists) {\r\n return {\r\n status: 'SKIP',\r\n vsmetaPath,\r\n message: `Images already exist (use --overwrite to replace): ${vsmetaPath}`,\r\n posterPath,\r\n fanartPath\r\n };\r\n }\r\n\r\n const results: string[] = [];\r\n let anyExtracted = false;\r\n\r\n if (meta.posterImage) {\r\n if (options.overwrite || !posterExists) {\r\n if (!options.dryRun) {\r\n fs.writeFileSync(posterPath, meta.posterImage);\r\n }\r\n results.push(`poster: ${posterPath}`);\r\n anyExtracted = true;\r\n }\r\n }\r\n\r\n if (meta.backdropImage) {\r\n if (options.overwrite || !fanartExists) {\r\n if (!options.dryRun) {\r\n fs.writeFileSync(fanartPath, meta.backdropImage);\r\n }\r\n results.push(`${isEpisode ? 'thumb' : 'fanart'}: ${fanartPath}`);\r\n anyExtracted = true;\r\n }\r\n }\r\n\r\n if (!anyExtracted) {\r\n return {\r\n status: 'WARN',\r\n vsmetaPath,\r\n message: `No images found in ${vsmetaPath}`\r\n };\r\n }\r\n\r\n const prefix = options.dryRun ? '[DRY RUN] ' : '';\r\n return {\r\n status: 'SUCCESS',\r\n vsmetaPath,\r\n message: `${prefix}Extracted ${results.join(', ')}`,\r\n posterPath: meta.posterImage ? posterPath : undefined,\r\n fanartPath: meta.backdropImage ? fanartPath : undefined\r\n };\r\n\r\n } catch (err) {\r\n const e = err as Error & { code?: string };\r\n let msg = '';\r\n if (e.code === 'EACCES' || e.code === 'EPERM') {\r\n msg = `Permission denied: Cannot read/write files. Please check permissions for ${vsmetaPath}`;\r\n } else {\r\n msg = `Processing ${vsmetaPath}: ${e.message || err}`;\r\n }\r\n return { status: 'ERROR', vsmetaPath, message: msg };\r\n }\r\n}\r\n","import { ConvertResult } from './types.js';\r\n\r\nexport function getResultMessage(result: ConvertResult): string {\r\n return `[${result.status}] ${result.message}`;\r\n}\r\n\r\nexport function logConvertResult(result: ConvertResult) {\r\n const msg = getResultMessage(result);\r\n if (result.status === 'ERROR') {\r\n console.error(msg);\r\n } else if (result.status === 'WARN') {\r\n console.warn(msg);\r\n } else {\r\n console.log(msg);\r\n }\r\n}\r\n"],"mappings":";AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,mBAA+B;AAIxC,IAAM,mBAAmB,oBAAI,IAAI,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,SAAS,OAAO,QAAQ,OAAO,CAAC;AAIlH,IAAM,eAAe,oBAAI,IAAsB;AAGxC,SAAS,oBAA0B;AACtC,eAAa,MAAM;AACvB;AAEO,SAAS,qBAAqB,YAA6B;AAC9D,QAAM,MAAM,KAAK,QAAQ,UAAU;AACnC,QAAM,WAAW,KAAK,SAAS,YAAY,SAAS;AAEpD,MAAI,GAAG,WAAW,KAAK,KAAK,KAAK,QAAQ,CAAC,GAAG;AACzC,WAAO;AAAA,EACX;AAEA,MAAI,QAAQ,aAAa,IAAI,GAAG;AAChC,MAAI,CAAC,OAAO;AACR,YAAQ,GAAG,YAAY,GAAG;AAC1B,iBAAa,IAAI,KAAK,KAAK;AAAA,EAC/B;AAEA,aAAW,QAAQ,OAAO;AACtB,QAAI,KAAK,WAAW,QAAQ,KAAK,SAAS,KAAK,SAAS,UAAU,GAAG;AACjE,YAAM,MAAM,KAAK,QAAQ,IAAI,EAAE,YAAY;AAC3C,UAAI,iBAAiB,IAAI,GAAG,GAAG;AAC3B,eAAO;AAAA,MACX;AAAA,IACJ;AAAA,EACJ;AAEA,SAAO;AACX;AAEO,SAAS,oBAAoB,YAAoB,UAA0B,CAAC,GAAkB;AACjG,MAAI;AACA,QAAI,CAAC,qBAAqB,UAAU,GAAG;AACnC,aAAO;AAAA,QACH,QAAQ;AAAA,QACR;AAAA,QACA,SAAS,qCAAqC,UAAU;AAAA,MAC5D;AAAA,IACJ;AAEA,UAAM,SAAS,GAAG,aAAa,UAAU;AACzC,QAAI;AACJ,QAAI;AACA,aAAO,YAAY,MAAM;AAAA,IAC7B,SAAS,GAAG;AACR,aAAO;AAAA,QACH,QAAQ;AAAA,QACR;AAAA,QACA,SAAS,mBAAmB,UAAU,KAAM,EAAY,OAAO;AAAA,MACnE;AAAA,IACJ;AAEA,UAAM,YAAY,KAAK,gBAAgB,KAAM,KAAK,UAAU,QAAQ,KAAK,WAAW;AACpF,UAAM,aAAa,WAAW,QAAQ,cAAc,aAAa;AACjE,UAAM,aAAa,WAAW,QAAQ,cAAc,YAAY,eAAe,aAAa;AAE5F,UAAM,eAAe,GAAG,WAAW,UAAU;AAC7C,UAAM,eAAe,GAAG,WAAW,UAAU;AAE7C,QAAI,CAAC,QAAQ,aAAa,gBAAgB,cAAc;AACpD,aAAO;AAAA,QACH,QAAQ;AAAA,QACR;AAAA,QACA,SAAS,sDAAsD,UAAU;AAAA,QACzE;AAAA,QACA;AAAA,MACJ;AAAA,IACJ;AAEA,UAAM,UAAoB,CAAC;AAC3B,QAAI,eAAe;AAEnB,QAAI,KAAK,aAAa;AAClB,UAAI,QAAQ,aAAa,CAAC,cAAc;AACpC,YAAI,CAAC,QAAQ,QAAQ;AACjB,aAAG,cAAc,YAAY,KAAK,WAAW;AAAA,QACjD;AACA,gBAAQ,KAAK,WAAW,UAAU,EAAE;AACpC,uBAAe;AAAA,MACnB;AAAA,IACJ;AAEA,QAAI,KAAK,eAAe;AACpB,UAAI,QAAQ,aAAa,CAAC,cAAc;AACpC,YAAI,CAAC,QAAQ,QAAQ;AACjB,aAAG,cAAc,YAAY,KAAK,aAAa;AAAA,QACnD;AACA,gBAAQ,KAAK,GAAG,YAAY,UAAU,QAAQ,KAAK,UAAU,EAAE;AAC/D,uBAAe;AAAA,MACnB;AAAA,IACJ;AAEA,QAAI,CAAC,cAAc;AACf,aAAO;AAAA,QACH,QAAQ;AAAA,QACR;AAAA,QACA,SAAS,sBAAsB,UAAU;AAAA,MAC7C;AAAA,IACJ;AAEA,UAAM,SAAS,QAAQ,SAAS,eAAe;AAC/C,WAAO;AAAA,MACH,QAAQ;AAAA,MACR;AAAA,MACA,SAAS,GAAG,MAAM,aAAa,QAAQ,KAAK,IAAI,CAAC;AAAA,MACjD,YAAY,KAAK,cAAc,aAAa;AAAA,MAC5C,YAAY,KAAK,gBAAgB,aAAa;AAAA,IAClD;AAAA,EAEJ,SAAS,KAAK;AACV,UAAM,IAAI;AACV,QAAI,MAAM;AACV,QAAI,EAAE,SAAS,YAAY,EAAE,SAAS,SAAS;AAC3C,YAAM,4EAA4E,UAAU;AAAA,IAChG,OAAO;AACH,YAAM,cAAc,UAAU,KAAK,EAAE,WAAW,GAAG;AAAA,IACvD;AACA,WAAO,EAAE,QAAQ,SAAS,YAAY,SAAS,IAAI;AAAA,EACvD;AACJ;;;AClIO,SAAS,iBAAiB,QAA+B;AAC5D,SAAO,IAAI,OAAO,MAAM,KAAK,OAAO,OAAO;AAC/C;AAEO,SAAS,iBAAiB,QAAuB;AACpD,QAAM,MAAM,iBAAiB,MAAM;AACnC,MAAI,OAAO,WAAW,SAAS;AAC3B,YAAQ,MAAM,GAAG;AAAA,EACrB,WAAW,OAAO,WAAW,QAAQ;AACjC,YAAQ,KAAK,GAAG;AAAA,EACpB,OAAO;AACH,YAAQ,IAAI,GAAG;AAAA,EACnB;AACJ;","names":[]}
package/dist/cli.cjs ADDED
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/cli.ts
32
+ var cli_exports = {};
33
+ __export(cli_exports, {
34
+ processAction: () => processAction,
35
+ runCLI: () => runCLI
36
+ });
37
+ module.exports = __toCommonJS(cli_exports);
38
+ var import_commander = require("commander");
39
+ var import_path2 = __toESM(require("path"), 1);
40
+ var import_fs2 = __toESM(require("fs"), 1);
41
+ var import_fast_glob = __toESM(require("fast-glob"), 1);
42
+
43
+ // src/converter.ts
44
+ var import_fs = __toESM(require("fs"), 1);
45
+ var import_path = __toESM(require("path"), 1);
46
+ var import_vsmeta_parser = require("vsmeta-parser");
47
+ var MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mkv", ".avi", ".mov", ".wmv", ".m4v", ".m2ts", ".ts", ".mpg", ".mpeg"]);
48
+ var readdirCache = /* @__PURE__ */ new Map();
49
+ function hasMatchingMediaFile(vsmetaPath) {
50
+ const dir = import_path.default.dirname(vsmetaPath);
51
+ const basename = import_path.default.basename(vsmetaPath, ".vsmeta");
52
+ if (import_fs.default.existsSync(import_path.default.join(dir, basename))) {
53
+ return true;
54
+ }
55
+ let files = readdirCache.get(dir);
56
+ if (!files) {
57
+ files = import_fs.default.readdirSync(dir);
58
+ readdirCache.set(dir, files);
59
+ }
60
+ for (const file of files) {
61
+ if (file.startsWith(basename) && file !== import_path.default.basename(vsmetaPath)) {
62
+ const ext = import_path.default.extname(file).toLowerCase();
63
+ if (MEDIA_EXTENSIONS.has(ext)) {
64
+ return true;
65
+ }
66
+ }
67
+ }
68
+ return false;
69
+ }
70
+ function convertVsMetaToJpeg(vsmetaPath, options = {}) {
71
+ try {
72
+ if (!hasMatchingMediaFile(vsmetaPath)) {
73
+ return {
74
+ status: "SKIP",
75
+ vsmetaPath,
76
+ message: `No matching media file found for: ${vsmetaPath}`
77
+ };
78
+ }
79
+ const buffer = import_fs.default.readFileSync(vsmetaPath);
80
+ let meta;
81
+ try {
82
+ meta = (0, import_vsmeta_parser.parseVsMeta)(buffer);
83
+ } catch (e) {
84
+ return {
85
+ status: "WARN",
86
+ vsmetaPath,
87
+ message: `Failed to parse ${vsmetaPath}: ${e.message}`
88
+ };
89
+ }
90
+ const isEpisode = meta.contentType === 2 || meta.season != null && meta.episode != null;
91
+ const posterPath = vsmetaPath.replace(/\.vsmeta$/i, "-poster.jpg");
92
+ const fanartPath = vsmetaPath.replace(/\.vsmeta$/i, isEpisode ? "-thumb.jpg" : "-fanart.jpg");
93
+ const posterExists = import_fs.default.existsSync(posterPath);
94
+ const fanartExists = import_fs.default.existsSync(fanartPath);
95
+ if (!options.overwrite && posterExists && fanartExists) {
96
+ return {
97
+ status: "SKIP",
98
+ vsmetaPath,
99
+ message: `Images already exist (use --overwrite to replace): ${vsmetaPath}`,
100
+ posterPath,
101
+ fanartPath
102
+ };
103
+ }
104
+ const results = [];
105
+ let anyExtracted = false;
106
+ if (meta.posterImage) {
107
+ if (options.overwrite || !posterExists) {
108
+ if (!options.dryRun) {
109
+ import_fs.default.writeFileSync(posterPath, meta.posterImage);
110
+ }
111
+ results.push(`poster: ${posterPath}`);
112
+ anyExtracted = true;
113
+ }
114
+ }
115
+ if (meta.backdropImage) {
116
+ if (options.overwrite || !fanartExists) {
117
+ if (!options.dryRun) {
118
+ import_fs.default.writeFileSync(fanartPath, meta.backdropImage);
119
+ }
120
+ results.push(`${isEpisode ? "thumb" : "fanart"}: ${fanartPath}`);
121
+ anyExtracted = true;
122
+ }
123
+ }
124
+ if (!anyExtracted) {
125
+ return {
126
+ status: "WARN",
127
+ vsmetaPath,
128
+ message: `No images found in ${vsmetaPath}`
129
+ };
130
+ }
131
+ const prefix = options.dryRun ? "[DRY RUN] " : "";
132
+ return {
133
+ status: "SUCCESS",
134
+ vsmetaPath,
135
+ message: `${prefix}Extracted ${results.join(", ")}`,
136
+ posterPath: meta.posterImage ? posterPath : void 0,
137
+ fanartPath: meta.backdropImage ? fanartPath : void 0
138
+ };
139
+ } catch (err) {
140
+ const e = err;
141
+ let msg = "";
142
+ if (e.code === "EACCES" || e.code === "EPERM") {
143
+ msg = `Permission denied: Cannot read/write files. Please check permissions for ${vsmetaPath}`;
144
+ } else {
145
+ msg = `Processing ${vsmetaPath}: ${e.message || err}`;
146
+ }
147
+ return { status: "ERROR", vsmetaPath, message: msg };
148
+ }
149
+ }
150
+
151
+ // src/logger.ts
152
+ function getResultMessage(result) {
153
+ return `[${result.status}] ${result.message}`;
154
+ }
155
+ function logConvertResult(result) {
156
+ const msg = getResultMessage(result);
157
+ if (result.status === "ERROR") {
158
+ console.error(msg);
159
+ } else if (result.status === "WARN") {
160
+ console.warn(msg);
161
+ } else {
162
+ console.log(msg);
163
+ }
164
+ }
165
+
166
+ // src/cli.ts
167
+ async function processAction(targetPath, options) {
168
+ const fullPath = import_path2.default.resolve(targetPath);
169
+ if (!import_fs2.default.existsSync(fullPath)) {
170
+ console.error(`[ERROR] Path not found: ${fullPath}`);
171
+ process.exit(1);
172
+ }
173
+ const stat = import_fs2.default.statSync(fullPath);
174
+ if (stat.isFile()) {
175
+ if (!fullPath.toLowerCase().endsWith(".vsmeta")) {
176
+ console.error(`[ERROR] File is not a .vsmeta: ${fullPath}`);
177
+ process.exit(1);
178
+ }
179
+ const result = convertVsMetaToJpeg(fullPath, options);
180
+ logConvertResult(result);
181
+ } else {
182
+ console.log(`Scanning directory: ${fullPath}`);
183
+ const files = await (0, import_fast_glob.default)("**/*.vsmeta", { cwd: fullPath, absolute: true, caseSensitiveMatch: false });
184
+ if (files.length === 0) {
185
+ console.log("No .vsmeta files found.");
186
+ return;
187
+ }
188
+ console.log(`Found ${files.length} .vsmeta files.`);
189
+ const counts = { SUCCESS: 0, ERROR: 0, SKIP: 0, WARN: 0 };
190
+ const nonSuccesses = [];
191
+ for (const file of files) {
192
+ const result = convertVsMetaToJpeg(file, options);
193
+ logConvertResult(result);
194
+ counts[result.status]++;
195
+ if (result.status !== "SUCCESS") {
196
+ nonSuccesses.push(getResultMessage(result));
197
+ }
198
+ }
199
+ console.log("\n--- Final Summary ---");
200
+ console.log(`[SUCCESS] : ${counts.SUCCESS}`);
201
+ console.log(`[SKIP] : ${counts.SKIP}`);
202
+ console.log(`[WARN] : ${counts.WARN}`);
203
+ console.log(`[ERROR] : ${counts.ERROR}`);
204
+ if (options.verbose && nonSuccesses.length > 0) {
205
+ console.log("\n--- Verbose (Non-Success items) ---");
206
+ nonSuccesses.forEach((msg) => console.log(msg));
207
+ }
208
+ }
209
+ }
210
+ function runCLI(argv) {
211
+ const program = new import_commander.Command();
212
+ program.name("vsmeta-to-jpeg").description("Extract embedded JPEG images from Synology .vsmeta files.").argument("<path>", "File or folder path to process. If a folder, it traverses recursively.").option("-d, --dry-run", "Dry run without outputting files.").option("-f, --overwrite", "Overwrite existing JPEG files.").option("-v, --verbose", "Print a summary of all non-successful operations at the end.").action(processAction);
213
+ program.parse(argv);
214
+ }
215
+ if (process.env.NODE_ENV !== "test") {
216
+ runCLI(process.argv);
217
+ }
218
+ // Annotate the CommonJS export names for ESM import in node:
219
+ 0 && (module.exports = {
220
+ processAction,
221
+ runCLI
222
+ });
223
+ //# sourceMappingURL=cli.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/converter.ts","../src/logger.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { Command } from 'commander';\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport glob from 'fast-glob';\r\nimport { convertVsMetaToJpeg } from './converter.js';\r\nimport { logConvertResult, getResultMessage } from './logger.js';\r\nimport { ConvertOptions } from './types.js';\r\n\r\nexport async function processAction(targetPath: string, options: ConvertOptions) {\r\n const fullPath = path.resolve(targetPath);\r\n\r\n if (!fs.existsSync(fullPath)) {\r\n console.error(`[ERROR] Path not found: ${fullPath}`);\r\n process.exit(1);\r\n }\r\n\r\n const stat = fs.statSync(fullPath);\r\n\r\n if (stat.isFile()) {\r\n if (!fullPath.toLowerCase().endsWith('.vsmeta')) {\r\n console.error(`[ERROR] File is not a .vsmeta: ${fullPath}`);\r\n process.exit(1);\r\n }\r\n const result = convertVsMetaToJpeg(fullPath, options);\r\n logConvertResult(result);\r\n } else {\r\n console.log(`Scanning directory: ${fullPath}`);\r\n const files = await glob('**/*.vsmeta', { cwd: fullPath, absolute: true, caseSensitiveMatch: false });\r\n\r\n if (files.length === 0) {\r\n console.log('No .vsmeta files found.');\r\n return;\r\n }\r\n console.log(`Found ${files.length} .vsmeta files.`);\r\n\r\n const counts = { SUCCESS: 0, ERROR: 0, SKIP: 0, WARN: 0 };\r\n const nonSuccesses: string[] = [];\r\n for (const file of files) {\r\n const result = convertVsMetaToJpeg(file, options);\r\n logConvertResult(result);\r\n counts[result.status]++;\r\n if (result.status !== 'SUCCESS') {\r\n nonSuccesses.push(getResultMessage(result));\r\n }\r\n }\r\n console.log('\\n--- Final Summary ---');\r\n console.log(`[SUCCESS] : ${counts.SUCCESS}`);\r\n console.log(`[SKIP] : ${counts.SKIP}`);\r\n console.log(`[WARN] : ${counts.WARN}`);\r\n console.log(`[ERROR] : ${counts.ERROR}`);\r\n\r\n if (options.verbose && nonSuccesses.length > 0) {\r\n console.log('\\n--- Verbose (Non-Success items) ---');\r\n nonSuccesses.forEach(msg => console.log(msg));\r\n }\r\n }\r\n}\r\n\r\nexport function runCLI(argv: string[]) {\r\n const program = new Command();\r\n program\r\n .name('vsmeta-to-jpeg')\r\n .description('Extract embedded JPEG images from Synology .vsmeta files.')\r\n .argument('<path>', 'File or folder path to process. If a folder, it traverses recursively.')\r\n .option('-d, --dry-run', 'Dry run without outputting files.')\r\n .option('-f, --overwrite', 'Overwrite existing JPEG files.')\r\n .option('-v, --verbose', 'Print a summary of all non-successful operations at the end.')\r\n .action(processAction);\r\n\r\n program.parse(argv);\r\n}\r\n\r\n/* v8 ignore start */\r\n/* istanbul ignore next -- bootstrap guard */\r\nif (process.env.NODE_ENV !== 'test') {\r\n runCLI(process.argv);\r\n}\r\n/* v8 ignore end */\r\n","import fs from 'fs';\r\nimport path from 'path';\r\nimport { parseVsMeta, VsMetaData } from 'vsmeta-parser';\r\nimport { ConvertOptions, ConvertResult } from './types.js';\r\n\r\n// Common video extensions checked when verifying a matching media file exists.\r\nconst MEDIA_EXTENSIONS = new Set(['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.m4v', '.m2ts', '.ts', '.mpg', '.mpeg']);\r\n\r\n// Per-directory cache for readdirSync results — avoids redundant I/O when\r\n// multiple .vsmeta files share the same folder during a batch conversion.\r\nconst readdirCache = new Map<string, string[]>();\r\n\r\n/** Clear the internal directory listing cache. Call after a batch run completes. */\r\nexport function clearReaddirCache(): void {\r\n readdirCache.clear();\r\n}\r\n\r\nexport function hasMatchingMediaFile(vsmetaPath: string): boolean {\r\n const dir = path.dirname(vsmetaPath);\r\n const basename = path.basename(vsmetaPath, '.vsmeta');\r\n\r\n if (fs.existsSync(path.join(dir, basename))) {\r\n return true;\r\n }\r\n\r\n let files = readdirCache.get(dir);\r\n if (!files) {\r\n files = fs.readdirSync(dir) as string[];\r\n readdirCache.set(dir, files);\r\n }\r\n\r\n for (const file of files) {\r\n if (file.startsWith(basename) && file !== path.basename(vsmetaPath)) {\r\n const ext = path.extname(file).toLowerCase();\r\n if (MEDIA_EXTENSIONS.has(ext)) {\r\n return true;\r\n }\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport function convertVsMetaToJpeg(vsmetaPath: string, options: ConvertOptions = {}): ConvertResult {\r\n try {\r\n if (!hasMatchingMediaFile(vsmetaPath)) {\r\n return {\r\n status: 'SKIP',\r\n vsmetaPath,\r\n message: `No matching media file found for: ${vsmetaPath}`\r\n };\r\n }\r\n\r\n const buffer = fs.readFileSync(vsmetaPath);\r\n let meta: VsMetaData;\r\n try {\r\n meta = parseVsMeta(buffer);\r\n } catch (e) {\r\n return {\r\n status: 'WARN',\r\n vsmetaPath,\r\n message: `Failed to parse ${vsmetaPath}: ${(e as Error).message}`\r\n };\r\n }\r\n\r\n const isEpisode = meta.contentType === 2 || (meta.season != null && meta.episode != null);\r\n const posterPath = vsmetaPath.replace(/\\.vsmeta$/i, '-poster.jpg');\r\n const fanartPath = vsmetaPath.replace(/\\.vsmeta$/i, isEpisode ? '-thumb.jpg' : '-fanart.jpg');\r\n\r\n const posterExists = fs.existsSync(posterPath);\r\n const fanartExists = fs.existsSync(fanartPath);\r\n\r\n if (!options.overwrite && posterExists && fanartExists) {\r\n return {\r\n status: 'SKIP',\r\n vsmetaPath,\r\n message: `Images already exist (use --overwrite to replace): ${vsmetaPath}`,\r\n posterPath,\r\n fanartPath\r\n };\r\n }\r\n\r\n const results: string[] = [];\r\n let anyExtracted = false;\r\n\r\n if (meta.posterImage) {\r\n if (options.overwrite || !posterExists) {\r\n if (!options.dryRun) {\r\n fs.writeFileSync(posterPath, meta.posterImage);\r\n }\r\n results.push(`poster: ${posterPath}`);\r\n anyExtracted = true;\r\n }\r\n }\r\n\r\n if (meta.backdropImage) {\r\n if (options.overwrite || !fanartExists) {\r\n if (!options.dryRun) {\r\n fs.writeFileSync(fanartPath, meta.backdropImage);\r\n }\r\n results.push(`${isEpisode ? 'thumb' : 'fanart'}: ${fanartPath}`);\r\n anyExtracted = true;\r\n }\r\n }\r\n\r\n if (!anyExtracted) {\r\n return {\r\n status: 'WARN',\r\n vsmetaPath,\r\n message: `No images found in ${vsmetaPath}`\r\n };\r\n }\r\n\r\n const prefix = options.dryRun ? '[DRY RUN] ' : '';\r\n return {\r\n status: 'SUCCESS',\r\n vsmetaPath,\r\n message: `${prefix}Extracted ${results.join(', ')}`,\r\n posterPath: meta.posterImage ? posterPath : undefined,\r\n fanartPath: meta.backdropImage ? fanartPath : undefined\r\n };\r\n\r\n } catch (err) {\r\n const e = err as Error & { code?: string };\r\n let msg = '';\r\n if (e.code === 'EACCES' || e.code === 'EPERM') {\r\n msg = `Permission denied: Cannot read/write files. Please check permissions for ${vsmetaPath}`;\r\n } else {\r\n msg = `Processing ${vsmetaPath}: ${e.message || err}`;\r\n }\r\n return { status: 'ERROR', vsmetaPath, message: msg };\r\n }\r\n}\r\n","import { ConvertResult } from './types.js';\r\n\r\nexport function getResultMessage(result: ConvertResult): string {\r\n return `[${result.status}] ${result.message}`;\r\n}\r\n\r\nexport function logConvertResult(result: ConvertResult) {\r\n const msg = getResultMessage(result);\r\n if (result.status === 'ERROR') {\r\n console.error(msg);\r\n } else if (result.status === 'WARN') {\r\n console.warn(msg);\r\n } else {\r\n console.log(msg);\r\n }\r\n}\r\n"],"mappings":"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","names":["import_path","import_fs","path","fs","path","fs","glob"]}
package/dist/cli.d.cts ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { C as ConvertOptions } from './types-CSvk8Xx7.cjs';
3
+
4
+ declare function processAction(targetPath: string, options: ConvertOptions): Promise<void>;
5
+ declare function runCLI(argv: string[]): void;
6
+
7
+ export { processAction, runCLI };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { C as ConvertOptions } from './types-CSvk8Xx7.js';
3
+
4
+ declare function processAction(targetPath: string, options: ConvertOptions): Promise<void>;
5
+ declare function runCLI(argv: string[]): void;
6
+
7
+ export { processAction, runCLI };
package/dist/cli.js ADDED
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ convertVsMetaToJpeg,
4
+ getResultMessage,
5
+ logConvertResult
6
+ } from "./chunk-2EEHQI7P.js";
7
+
8
+ // src/cli.ts
9
+ import { Command } from "commander";
10
+ import path from "path";
11
+ import fs from "fs";
12
+ import glob from "fast-glob";
13
+ async function processAction(targetPath, options) {
14
+ const fullPath = path.resolve(targetPath);
15
+ if (!fs.existsSync(fullPath)) {
16
+ console.error(`[ERROR] Path not found: ${fullPath}`);
17
+ process.exit(1);
18
+ }
19
+ const stat = fs.statSync(fullPath);
20
+ if (stat.isFile()) {
21
+ if (!fullPath.toLowerCase().endsWith(".vsmeta")) {
22
+ console.error(`[ERROR] File is not a .vsmeta: ${fullPath}`);
23
+ process.exit(1);
24
+ }
25
+ const result = convertVsMetaToJpeg(fullPath, options);
26
+ logConvertResult(result);
27
+ } else {
28
+ console.log(`Scanning directory: ${fullPath}`);
29
+ const files = await glob("**/*.vsmeta", { cwd: fullPath, absolute: true, caseSensitiveMatch: false });
30
+ if (files.length === 0) {
31
+ console.log("No .vsmeta files found.");
32
+ return;
33
+ }
34
+ console.log(`Found ${files.length} .vsmeta files.`);
35
+ const counts = { SUCCESS: 0, ERROR: 0, SKIP: 0, WARN: 0 };
36
+ const nonSuccesses = [];
37
+ for (const file of files) {
38
+ const result = convertVsMetaToJpeg(file, options);
39
+ logConvertResult(result);
40
+ counts[result.status]++;
41
+ if (result.status !== "SUCCESS") {
42
+ nonSuccesses.push(getResultMessage(result));
43
+ }
44
+ }
45
+ console.log("\n--- Final Summary ---");
46
+ console.log(`[SUCCESS] : ${counts.SUCCESS}`);
47
+ console.log(`[SKIP] : ${counts.SKIP}`);
48
+ console.log(`[WARN] : ${counts.WARN}`);
49
+ console.log(`[ERROR] : ${counts.ERROR}`);
50
+ if (options.verbose && nonSuccesses.length > 0) {
51
+ console.log("\n--- Verbose (Non-Success items) ---");
52
+ nonSuccesses.forEach((msg) => console.log(msg));
53
+ }
54
+ }
55
+ }
56
+ function runCLI(argv) {
57
+ const program = new Command();
58
+ program.name("vsmeta-to-jpeg").description("Extract embedded JPEG images from Synology .vsmeta files.").argument("<path>", "File or folder path to process. If a folder, it traverses recursively.").option("-d, --dry-run", "Dry run without outputting files.").option("-f, --overwrite", "Overwrite existing JPEG files.").option("-v, --verbose", "Print a summary of all non-successful operations at the end.").action(processAction);
59
+ program.parse(argv);
60
+ }
61
+ if (process.env.NODE_ENV !== "test") {
62
+ runCLI(process.argv);
63
+ }
64
+ export {
65
+ processAction,
66
+ runCLI
67
+ };
68
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { Command } from 'commander';\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport glob from 'fast-glob';\r\nimport { convertVsMetaToJpeg } from './converter.js';\r\nimport { logConvertResult, getResultMessage } from './logger.js';\r\nimport { ConvertOptions } from './types.js';\r\n\r\nexport async function processAction(targetPath: string, options: ConvertOptions) {\r\n const fullPath = path.resolve(targetPath);\r\n\r\n if (!fs.existsSync(fullPath)) {\r\n console.error(`[ERROR] Path not found: ${fullPath}`);\r\n process.exit(1);\r\n }\r\n\r\n const stat = fs.statSync(fullPath);\r\n\r\n if (stat.isFile()) {\r\n if (!fullPath.toLowerCase().endsWith('.vsmeta')) {\r\n console.error(`[ERROR] File is not a .vsmeta: ${fullPath}`);\r\n process.exit(1);\r\n }\r\n const result = convertVsMetaToJpeg(fullPath, options);\r\n logConvertResult(result);\r\n } else {\r\n console.log(`Scanning directory: ${fullPath}`);\r\n const files = await glob('**/*.vsmeta', { cwd: fullPath, absolute: true, caseSensitiveMatch: false });\r\n\r\n if (files.length === 0) {\r\n console.log('No .vsmeta files found.');\r\n return;\r\n }\r\n console.log(`Found ${files.length} .vsmeta files.`);\r\n\r\n const counts = { SUCCESS: 0, ERROR: 0, SKIP: 0, WARN: 0 };\r\n const nonSuccesses: string[] = [];\r\n for (const file of files) {\r\n const result = convertVsMetaToJpeg(file, options);\r\n logConvertResult(result);\r\n counts[result.status]++;\r\n if (result.status !== 'SUCCESS') {\r\n nonSuccesses.push(getResultMessage(result));\r\n }\r\n }\r\n console.log('\\n--- Final Summary ---');\r\n console.log(`[SUCCESS] : ${counts.SUCCESS}`);\r\n console.log(`[SKIP] : ${counts.SKIP}`);\r\n console.log(`[WARN] : ${counts.WARN}`);\r\n console.log(`[ERROR] : ${counts.ERROR}`);\r\n\r\n if (options.verbose && nonSuccesses.length > 0) {\r\n console.log('\\n--- Verbose (Non-Success items) ---');\r\n nonSuccesses.forEach(msg => console.log(msg));\r\n }\r\n }\r\n}\r\n\r\nexport function runCLI(argv: string[]) {\r\n const program = new Command();\r\n program\r\n .name('vsmeta-to-jpeg')\r\n .description('Extract embedded JPEG images from Synology .vsmeta files.')\r\n .argument('<path>', 'File or folder path to process. If a folder, it traverses recursively.')\r\n .option('-d, --dry-run', 'Dry run without outputting files.')\r\n .option('-f, --overwrite', 'Overwrite existing JPEG files.')\r\n .option('-v, --verbose', 'Print a summary of all non-successful operations at the end.')\r\n .action(processAction);\r\n\r\n program.parse(argv);\r\n}\r\n\r\n/* v8 ignore start */\r\n/* istanbul ignore next -- bootstrap guard */\r\nif (process.env.NODE_ENV !== 'test') {\r\n runCLI(process.argv);\r\n}\r\n/* v8 ignore end */\r\n"],"mappings":";;;;;;;;AACA,SAAS,eAAe;AACxB,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,OAAO,UAAU;AAKjB,eAAsB,cAAc,YAAoB,SAAyB;AAC7E,QAAM,WAAW,KAAK,QAAQ,UAAU;AAExC,MAAI,CAAC,GAAG,WAAW,QAAQ,GAAG;AAC1B,YAAQ,MAAM,2BAA2B,QAAQ,EAAE;AACnD,YAAQ,KAAK,CAAC;AAAA,EAClB;AAEA,QAAM,OAAO,GAAG,SAAS,QAAQ;AAEjC,MAAI,KAAK,OAAO,GAAG;AACf,QAAI,CAAC,SAAS,YAAY,EAAE,SAAS,SAAS,GAAG;AAC7C,cAAQ,MAAM,kCAAkC,QAAQ,EAAE;AAC1D,cAAQ,KAAK,CAAC;AAAA,IAClB;AACA,UAAM,SAAS,oBAAoB,UAAU,OAAO;AACpD,qBAAiB,MAAM;AAAA,EAC3B,OAAO;AACH,YAAQ,IAAI,uBAAuB,QAAQ,EAAE;AAC7C,UAAM,QAAQ,MAAM,KAAK,eAAe,EAAE,KAAK,UAAU,UAAU,MAAM,oBAAoB,MAAM,CAAC;AAEpG,QAAI,MAAM,WAAW,GAAG;AACpB,cAAQ,IAAI,yBAAyB;AACrC;AAAA,IACJ;AACA,YAAQ,IAAI,SAAS,MAAM,MAAM,iBAAiB;AAElD,UAAM,SAAS,EAAE,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,EAAE;AACxD,UAAM,eAAyB,CAAC;AAChC,eAAW,QAAQ,OAAO;AACtB,YAAM,SAAS,oBAAoB,MAAM,OAAO;AAChD,uBAAiB,MAAM;AACvB,aAAO,OAAO,MAAM;AACpB,UAAI,OAAO,WAAW,WAAW;AAC7B,qBAAa,KAAK,iBAAiB,MAAM,CAAC;AAAA,MAC9C;AAAA,IACJ;AACA,YAAQ,IAAI,yBAAyB;AACrC,YAAQ,IAAI,eAAe,OAAO,OAAO,EAAE;AAC3C,YAAQ,IAAI,eAAe,OAAO,IAAI,EAAE;AACxC,YAAQ,IAAI,eAAe,OAAO,IAAI,EAAE;AACxC,YAAQ,IAAI,eAAe,OAAO,KAAK,EAAE;AAEzC,QAAI,QAAQ,WAAW,aAAa,SAAS,GAAG;AAC5C,cAAQ,IAAI,uCAAuC;AACnD,mBAAa,QAAQ,SAAO,QAAQ,IAAI,GAAG,CAAC;AAAA,IAChD;AAAA,EACJ;AACJ;AAEO,SAAS,OAAO,MAAgB;AACnC,QAAM,UAAU,IAAI,QAAQ;AAC5B,UACK,KAAK,gBAAgB,EACrB,YAAY,2DAA2D,EACvE,SAAS,UAAU,wEAAwE,EAC3F,OAAO,iBAAiB,mCAAmC,EAC3D,OAAO,mBAAmB,gCAAgC,EAC1D,OAAO,iBAAiB,8DAA8D,EACtF,OAAO,aAAa;AAEzB,UAAQ,MAAM,IAAI;AACtB;AAIA,IAAI,QAAQ,IAAI,aAAa,QAAQ;AACjC,SAAO,QAAQ,IAAI;AACvB;","names":[]}
package/dist/index.cjs ADDED
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ clearReaddirCache: () => clearReaddirCache,
34
+ convertVsMetaToJpeg: () => convertVsMetaToJpeg,
35
+ getResultMessage: () => getResultMessage,
36
+ hasMatchingMediaFile: () => hasMatchingMediaFile,
37
+ logConvertResult: () => logConvertResult
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/converter.ts
42
+ var import_fs = __toESM(require("fs"), 1);
43
+ var import_path = __toESM(require("path"), 1);
44
+ var import_vsmeta_parser = require("vsmeta-parser");
45
+ var MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mkv", ".avi", ".mov", ".wmv", ".m4v", ".m2ts", ".ts", ".mpg", ".mpeg"]);
46
+ var readdirCache = /* @__PURE__ */ new Map();
47
+ function clearReaddirCache() {
48
+ readdirCache.clear();
49
+ }
50
+ function hasMatchingMediaFile(vsmetaPath) {
51
+ const dir = import_path.default.dirname(vsmetaPath);
52
+ const basename = import_path.default.basename(vsmetaPath, ".vsmeta");
53
+ if (import_fs.default.existsSync(import_path.default.join(dir, basename))) {
54
+ return true;
55
+ }
56
+ let files = readdirCache.get(dir);
57
+ if (!files) {
58
+ files = import_fs.default.readdirSync(dir);
59
+ readdirCache.set(dir, files);
60
+ }
61
+ for (const file of files) {
62
+ if (file.startsWith(basename) && file !== import_path.default.basename(vsmetaPath)) {
63
+ const ext = import_path.default.extname(file).toLowerCase();
64
+ if (MEDIA_EXTENSIONS.has(ext)) {
65
+ return true;
66
+ }
67
+ }
68
+ }
69
+ return false;
70
+ }
71
+ function convertVsMetaToJpeg(vsmetaPath, options = {}) {
72
+ try {
73
+ if (!hasMatchingMediaFile(vsmetaPath)) {
74
+ return {
75
+ status: "SKIP",
76
+ vsmetaPath,
77
+ message: `No matching media file found for: ${vsmetaPath}`
78
+ };
79
+ }
80
+ const buffer = import_fs.default.readFileSync(vsmetaPath);
81
+ let meta;
82
+ try {
83
+ meta = (0, import_vsmeta_parser.parseVsMeta)(buffer);
84
+ } catch (e) {
85
+ return {
86
+ status: "WARN",
87
+ vsmetaPath,
88
+ message: `Failed to parse ${vsmetaPath}: ${e.message}`
89
+ };
90
+ }
91
+ const isEpisode = meta.contentType === 2 || meta.season != null && meta.episode != null;
92
+ const posterPath = vsmetaPath.replace(/\.vsmeta$/i, "-poster.jpg");
93
+ const fanartPath = vsmetaPath.replace(/\.vsmeta$/i, isEpisode ? "-thumb.jpg" : "-fanart.jpg");
94
+ const posterExists = import_fs.default.existsSync(posterPath);
95
+ const fanartExists = import_fs.default.existsSync(fanartPath);
96
+ if (!options.overwrite && posterExists && fanartExists) {
97
+ return {
98
+ status: "SKIP",
99
+ vsmetaPath,
100
+ message: `Images already exist (use --overwrite to replace): ${vsmetaPath}`,
101
+ posterPath,
102
+ fanartPath
103
+ };
104
+ }
105
+ const results = [];
106
+ let anyExtracted = false;
107
+ if (meta.posterImage) {
108
+ if (options.overwrite || !posterExists) {
109
+ if (!options.dryRun) {
110
+ import_fs.default.writeFileSync(posterPath, meta.posterImage);
111
+ }
112
+ results.push(`poster: ${posterPath}`);
113
+ anyExtracted = true;
114
+ }
115
+ }
116
+ if (meta.backdropImage) {
117
+ if (options.overwrite || !fanartExists) {
118
+ if (!options.dryRun) {
119
+ import_fs.default.writeFileSync(fanartPath, meta.backdropImage);
120
+ }
121
+ results.push(`${isEpisode ? "thumb" : "fanart"}: ${fanartPath}`);
122
+ anyExtracted = true;
123
+ }
124
+ }
125
+ if (!anyExtracted) {
126
+ return {
127
+ status: "WARN",
128
+ vsmetaPath,
129
+ message: `No images found in ${vsmetaPath}`
130
+ };
131
+ }
132
+ const prefix = options.dryRun ? "[DRY RUN] " : "";
133
+ return {
134
+ status: "SUCCESS",
135
+ vsmetaPath,
136
+ message: `${prefix}Extracted ${results.join(", ")}`,
137
+ posterPath: meta.posterImage ? posterPath : void 0,
138
+ fanartPath: meta.backdropImage ? fanartPath : void 0
139
+ };
140
+ } catch (err) {
141
+ const e = err;
142
+ let msg = "";
143
+ if (e.code === "EACCES" || e.code === "EPERM") {
144
+ msg = `Permission denied: Cannot read/write files. Please check permissions for ${vsmetaPath}`;
145
+ } else {
146
+ msg = `Processing ${vsmetaPath}: ${e.message || err}`;
147
+ }
148
+ return { status: "ERROR", vsmetaPath, message: msg };
149
+ }
150
+ }
151
+
152
+ // src/logger.ts
153
+ function getResultMessage(result) {
154
+ return `[${result.status}] ${result.message}`;
155
+ }
156
+ function logConvertResult(result) {
157
+ const msg = getResultMessage(result);
158
+ if (result.status === "ERROR") {
159
+ console.error(msg);
160
+ } else if (result.status === "WARN") {
161
+ console.warn(msg);
162
+ } else {
163
+ console.log(msg);
164
+ }
165
+ }
166
+ // Annotate the CommonJS export names for ESM import in node:
167
+ 0 && (module.exports = {
168
+ clearReaddirCache,
169
+ convertVsMetaToJpeg,
170
+ getResultMessage,
171
+ hasMatchingMediaFile,
172
+ logConvertResult
173
+ });
174
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/converter.ts","../src/logger.ts"],"sourcesContent":["export * from './converter.js';\r\nexport * from './logger.js';\r\nexport * from './types.js';\r\n","import fs from 'fs';\r\nimport path from 'path';\r\nimport { parseVsMeta, VsMetaData } from 'vsmeta-parser';\r\nimport { ConvertOptions, ConvertResult } from './types.js';\r\n\r\n// Common video extensions checked when verifying a matching media file exists.\r\nconst MEDIA_EXTENSIONS = new Set(['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.m4v', '.m2ts', '.ts', '.mpg', '.mpeg']);\r\n\r\n// Per-directory cache for readdirSync results — avoids redundant I/O when\r\n// multiple .vsmeta files share the same folder during a batch conversion.\r\nconst readdirCache = new Map<string, string[]>();\r\n\r\n/** Clear the internal directory listing cache. Call after a batch run completes. */\r\nexport function clearReaddirCache(): void {\r\n readdirCache.clear();\r\n}\r\n\r\nexport function hasMatchingMediaFile(vsmetaPath: string): boolean {\r\n const dir = path.dirname(vsmetaPath);\r\n const basename = path.basename(vsmetaPath, '.vsmeta');\r\n\r\n if (fs.existsSync(path.join(dir, basename))) {\r\n return true;\r\n }\r\n\r\n let files = readdirCache.get(dir);\r\n if (!files) {\r\n files = fs.readdirSync(dir) as string[];\r\n readdirCache.set(dir, files);\r\n }\r\n\r\n for (const file of files) {\r\n if (file.startsWith(basename) && file !== path.basename(vsmetaPath)) {\r\n const ext = path.extname(file).toLowerCase();\r\n if (MEDIA_EXTENSIONS.has(ext)) {\r\n return true;\r\n }\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport function convertVsMetaToJpeg(vsmetaPath: string, options: ConvertOptions = {}): ConvertResult {\r\n try {\r\n if (!hasMatchingMediaFile(vsmetaPath)) {\r\n return {\r\n status: 'SKIP',\r\n vsmetaPath,\r\n message: `No matching media file found for: ${vsmetaPath}`\r\n };\r\n }\r\n\r\n const buffer = fs.readFileSync(vsmetaPath);\r\n let meta: VsMetaData;\r\n try {\r\n meta = parseVsMeta(buffer);\r\n } catch (e) {\r\n return {\r\n status: 'WARN',\r\n vsmetaPath,\r\n message: `Failed to parse ${vsmetaPath}: ${(e as Error).message}`\r\n };\r\n }\r\n\r\n const isEpisode = meta.contentType === 2 || (meta.season != null && meta.episode != null);\r\n const posterPath = vsmetaPath.replace(/\\.vsmeta$/i, '-poster.jpg');\r\n const fanartPath = vsmetaPath.replace(/\\.vsmeta$/i, isEpisode ? '-thumb.jpg' : '-fanart.jpg');\r\n\r\n const posterExists = fs.existsSync(posterPath);\r\n const fanartExists = fs.existsSync(fanartPath);\r\n\r\n if (!options.overwrite && posterExists && fanartExists) {\r\n return {\r\n status: 'SKIP',\r\n vsmetaPath,\r\n message: `Images already exist (use --overwrite to replace): ${vsmetaPath}`,\r\n posterPath,\r\n fanartPath\r\n };\r\n }\r\n\r\n const results: string[] = [];\r\n let anyExtracted = false;\r\n\r\n if (meta.posterImage) {\r\n if (options.overwrite || !posterExists) {\r\n if (!options.dryRun) {\r\n fs.writeFileSync(posterPath, meta.posterImage);\r\n }\r\n results.push(`poster: ${posterPath}`);\r\n anyExtracted = true;\r\n }\r\n }\r\n\r\n if (meta.backdropImage) {\r\n if (options.overwrite || !fanartExists) {\r\n if (!options.dryRun) {\r\n fs.writeFileSync(fanartPath, meta.backdropImage);\r\n }\r\n results.push(`${isEpisode ? 'thumb' : 'fanart'}: ${fanartPath}`);\r\n anyExtracted = true;\r\n }\r\n }\r\n\r\n if (!anyExtracted) {\r\n return {\r\n status: 'WARN',\r\n vsmetaPath,\r\n message: `No images found in ${vsmetaPath}`\r\n };\r\n }\r\n\r\n const prefix = options.dryRun ? '[DRY RUN] ' : '';\r\n return {\r\n status: 'SUCCESS',\r\n vsmetaPath,\r\n message: `${prefix}Extracted ${results.join(', ')}`,\r\n posterPath: meta.posterImage ? posterPath : undefined,\r\n fanartPath: meta.backdropImage ? fanartPath : undefined\r\n };\r\n\r\n } catch (err) {\r\n const e = err as Error & { code?: string };\r\n let msg = '';\r\n if (e.code === 'EACCES' || e.code === 'EPERM') {\r\n msg = `Permission denied: Cannot read/write files. Please check permissions for ${vsmetaPath}`;\r\n } else {\r\n msg = `Processing ${vsmetaPath}: ${e.message || err}`;\r\n }\r\n return { status: 'ERROR', vsmetaPath, message: msg };\r\n }\r\n}\r\n","import { ConvertResult } from './types.js';\r\n\r\nexport function getResultMessage(result: ConvertResult): string {\r\n return `[${result.status}] ${result.message}`;\r\n}\r\n\r\nexport function logConvertResult(result: ConvertResult) {\r\n const msg = getResultMessage(result);\r\n if (result.status === 'ERROR') {\r\n console.error(msg);\r\n } else if (result.status === 'WARN') {\r\n console.warn(msg);\r\n } else {\r\n console.log(msg);\r\n }\r\n}\r\n"],"mappings":"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","names":["path","fs"]}
@@ -0,0 +1,11 @@
1
+ import { C as ConvertOptions, a as ConvertResult } from './types-CSvk8Xx7.cjs';
2
+
3
+ /** Clear the internal directory listing cache. Call after a batch run completes. */
4
+ declare function clearReaddirCache(): void;
5
+ declare function hasMatchingMediaFile(vsmetaPath: string): boolean;
6
+ declare function convertVsMetaToJpeg(vsmetaPath: string, options?: ConvertOptions): ConvertResult;
7
+
8
+ declare function getResultMessage(result: ConvertResult): string;
9
+ declare function logConvertResult(result: ConvertResult): void;
10
+
11
+ export { ConvertOptions, ConvertResult, clearReaddirCache, convertVsMetaToJpeg, getResultMessage, hasMatchingMediaFile, logConvertResult };
@@ -0,0 +1,11 @@
1
+ import { C as ConvertOptions, a as ConvertResult } from './types-CSvk8Xx7.js';
2
+
3
+ /** Clear the internal directory listing cache. Call after a batch run completes. */
4
+ declare function clearReaddirCache(): void;
5
+ declare function hasMatchingMediaFile(vsmetaPath: string): boolean;
6
+ declare function convertVsMetaToJpeg(vsmetaPath: string, options?: ConvertOptions): ConvertResult;
7
+
8
+ declare function getResultMessage(result: ConvertResult): string;
9
+ declare function logConvertResult(result: ConvertResult): void;
10
+
11
+ export { ConvertOptions, ConvertResult, clearReaddirCache, convertVsMetaToJpeg, getResultMessage, hasMatchingMediaFile, logConvertResult };
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ import {
2
+ clearReaddirCache,
3
+ convertVsMetaToJpeg,
4
+ getResultMessage,
5
+ hasMatchingMediaFile,
6
+ logConvertResult
7
+ } from "./chunk-2EEHQI7P.js";
8
+ export {
9
+ clearReaddirCache,
10
+ convertVsMetaToJpeg,
11
+ getResultMessage,
12
+ hasMatchingMediaFile,
13
+ logConvertResult
14
+ };
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,14 @@
1
+ interface ConvertOptions {
2
+ dryRun?: boolean;
3
+ overwrite?: boolean;
4
+ verbose?: boolean;
5
+ }
6
+ interface ConvertResult {
7
+ status: 'SUCCESS' | 'WARN' | 'SKIP' | 'ERROR';
8
+ vsmetaPath: string;
9
+ message: string;
10
+ posterPath?: string;
11
+ fanartPath?: string;
12
+ }
13
+
14
+ export type { ConvertOptions as C, ConvertResult as a };
@@ -0,0 +1,14 @@
1
+ interface ConvertOptions {
2
+ dryRun?: boolean;
3
+ overwrite?: boolean;
4
+ verbose?: boolean;
5
+ }
6
+ interface ConvertResult {
7
+ status: 'SUCCESS' | 'WARN' | 'SKIP' | 'ERROR';
8
+ vsmetaPath: string;
9
+ message: string;
10
+ posterPath?: string;
11
+ fanartPath?: string;
12
+ }
13
+
14
+ export type { ConvertOptions as C, ConvertResult as a };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "vsmeta-to-jpeg",
3
+ "version": "1.0.0",
4
+ "description": "Extract embedded JPEG images from Synology .vsmeta files.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "bin": {
22
+ "vsmeta-to-jpeg": "./dist/cli.js"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "README.md"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "test": "vitest run --coverage",
31
+ "test:watch": "vitest",
32
+ "lint": "eslint src test",
33
+ "format": "prettier --write src test",
34
+ "typecheck": "tsc --noEmit",
35
+ "prepublishOnly": "npm run typecheck && npm run lint && npm run test && npm run build",
36
+ "changeset": "changeset",
37
+ "version-packages": "changeset version",
38
+ "release": "npm run prepublishOnly && changeset publish"
39
+ },
40
+ "author": "Charles Aldarondo <charles.aldarondo@gmail.com>",
41
+ "license": "MIT",
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "devDependencies": {
46
+ "@changesets/cli": "^2.29.8",
47
+ "@types/node": "^25.3.3",
48
+ "@vitest/coverage-v8": "^4.0.18",
49
+ "eslint": "^9.39.3",
50
+ "prettier": "^3.8.1",
51
+ "tsup": "^8.5.1",
52
+ "typescript": "^5.9.3",
53
+ "typescript-eslint": "^8.56.1",
54
+ "vitest": "^4.0.18"
55
+ },
56
+ "dependencies": {
57
+ "commander": "^14.0.3",
58
+ "fast-glob": "^3.3.3",
59
+ "vsmeta-parser": "^1.0.1"
60
+ }
61
+ }