vidup 1.0.17 → 1.0.18

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/dist/bin/cli.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import e from"node:path";import t from"pretty-ms";import n from"untildify";import r from"yargs";import{hideBin as i}from"yargs/helpers";import{oraPromise as a}from"ora";import o from"picocolors";import{execa as s}from"execa";import c from"ffmpeg-static";import l from"node:fs/promises";import{BunnyCdnStream as u}from"bunnycdn-stream";import{hash as d}from"hasha";import{JSONFilePreset as f}from"lowdb/node";import{createReadStream as p}from"node:fs";var m=`1.0.17`;const h=process.versions.node!==void 0,g={verbose:!1,log(...e){if(!this.verbose)return;let t=o.gray(`[Log]`);h?console.warn(t,...e):console.log(t,...e)},logPrefixed(e,...t){this.info(o.blue(`[${e}]`),...t)},info(...e){if(!this.verbose)return;let t=o.green(`[Info]`);h?console.warn(t,...e):console.info(t,...e)},infoPrefixed(e,...t){this.info(o.blue(`[${e}]`),...t)},async infoSpin(e,t){return a(e,{prefixText:o.green(`[Info]`),stream:process.stderr,text:t})},warn(...e){console.warn(o.yellow(`[Warning]`),...e)},warnPrefixed(e,...t){this.warn(o.blue(`[${e}]`),...t)},error(...e){console.error(o.red(`[Error]`),...e)},errorPrefixed(e,...t){this.error(o.blue(`[${e}]`),...t)}};async function _(t){return(await l.readdir(t)).filter(t=>{let n=e.extname(t);return[`.mp4`,`.mov`,`.avi`,`.mkv`].includes(n)}).map(n=>e.join(t,n))}async function v(e){if(c===null)throw Error(`ffmpeg-static failed to locate ffmpeg binary`);try{let{stderr:t,stdout:n}=await s(c,[`-i`,e,`-f`,`ffmetadata`,`-`]);return n.includes(`creation_time`)||t.includes(`creation_time`)}catch(t){throw Error(`Failed to check metadata for: ${e}:\n${String(t)}`)}}async function y(t){if(c===null)throw Error(`ffmpeg-static failed to locate ffmpeg binary`);try{let n=e.extname(t).slice(1),r=`${t.slice(0,Math.max(0,t.lastIndexOf(`.`)))}.temp.${n}`;await s(c,[`-i`,t,`-map_metadata`,`-1`,`-c:v`,`copy`,`-c:a`,`copy`,r]),await l.rename(r,t)}catch(e){throw Error(`Failed to strip metadata from: ${t}:\n${String(e)}`)}}async function b(e,t={}){let{dryRun:n=!1,verbose:r=!1}=t;return x(await _(e),{dryRun:n,verbose:r})}async function x(e,t={}){let{dryRun:n=!1,verbose:r=!1}=t,i=g.verbose;g.verbose=r;let a=[];for(let t of e)await v(t)&&a.push(t);if(!n){g.info(`Found ${a.length} videos with metadata to strip`);for(let e of a)console.log(`Stripping metadata from: ${e}`),await y(e)}return g.verbose=i,a}async function S(t,n){let{credentials:r,dryRun:i=!1,service:a,verbose:o=!1}=n,s=g.verbose;g.verbose=o;let c=[],l=await _(t);g.info(`Found ${l.length} video files in directory "${t}"`);let m=await f(e.join(t,`.vidup-state.json`),{syncState:[]});m.data.syncState=m.data.syncState.filter(n=>l.includes(e.join(t,n.filename)));for(let t of l){let n=e.basename(t),r=await d(t,{algorithm:`sha1`}),i=m.data.syncState.find(e=>e.filename===n);i?i.localHash=r:m.data.syncState.push({filename:n,localHash:r,remoteHash:{}})}if(i||await m.write(),a!==`bunny`)throw Error(`Streaming service not yet implemented: ${a}`);let h=new u({apiKey:r.key,videoLibrary:r.library}),v=await h.listAllVideos();g.info(`Found ${v.length} video files on ${a} remote streaming service`);let y=v.filter(e=>{let t=m.data.syncState.find(t=>t.filename===e.title);return t!==void 0&&t.localHash===t.remoteHash[a]});for(let e of y)c.push({action:`None`,localFile:e.title,remoteId:e.guid});let b=m.data.syncState.filter(e=>!v.some(t=>t.title===e.filename));for(let e of b)c.push({action:`Create`,localFile:e.filename,remoteId:`Not yet uploaded (Dry run)`});let x=v.filter(e=>{let t=m.data.syncState.find(t=>t.filename===e.title);return t!==void 0&&t.localHash!==t.remoteHash[a]});for(let e of x)c.push({action:`Update`,localFile:e.title,remoteId:`Not yet uploaded (Dry run)`});let S=v.filter(e=>!m.data.syncState.some(t=>t.filename===e.title));for(let e of S)c.push({action:`Delete`,localFile:e.title,remoteId:e.guid});if(!i){m.data.lastRun=new Date,await m.write(),g.info(`Synchronizing...`);for(let[e,t]of S.entries()){if(e===0&&g.info(`Deleting ${S.length} remote videos...`),!(await g.infoSpin(h.deleteVideo(t.guid),`Deleting remote video ${e+1}/${S.length}: ${t.title}`)).success)throw Error(`Failed to delete remote video: ${t.title}`);m.data.lastUpdate=new Date,await m.write()}for(let[n,r]of b.entries()){n===0&&g.info(`Uploading ${b.length} new local videos...`);let i=p(e.join(t,r.filename)),o=await g.infoSpin(h.createAndUploadVideo(i,{title:r.filename}),`Uploading new video ${n+1}/${b.length}: ${r.filename}`);g.info(`Remote video ${n+1}/${b.length} created with GUID: ${o.guid}`);let s=m.data.syncState.find(e=>e.filename===r.filename);if(s===void 0)throw Error(`Failed to find state entry for: ${r.filename}`);s.remoteHash[a]=r.localHash,m.data.lastUpdate=new Date,await m.write();let l=c.find(e=>e.localFile===r.filename);if(l===void 0)throw Error(`Failed to find report entry for: ${r.filename}`);l.remoteId=o.guid}for(let[n,r]of x.entries()){n===0&&(g.info(`Updating ${x.length} remote videos...`),g.warn(`Bunny does not currently support updating videos in-place, so vidup will delete and re-uploading instead... this will change the video's GUID!`));let i=m.data.syncState.find(e=>e.filename===r.title);if(i===void 0)throw Error(`Failed to find state entry for: ${r.title}`);if(!(await h.deleteVideo(r.guid)).success)throw Error(`Failed to delete remote video before updating: ${r.title}`);let o=p(e.join(t,i.filename)),s=await g.infoSpin(h.createAndUploadVideo(o,{title:i.filename}),`Updating remote video ${n+1}/${x.length}: ${r.title}`);g.info(`Updated remote video ${n+1}/${x.length} created with GUID: ${s.guid}`),i.remoteHash[a]=i.localHash,m.data.lastUpdate=new Date,await m.write();let l=c.find(e=>e.localFile===i.filename);if(l===void 0)throw Error(`Failed to find report entry for: ${i.filename}`);l.remoteId=s.guid}g.info(`All ${l.length} videos are now in sync!`)}return g.verbose=s,c}const C=performance.now(),w=r(i(process.argv));await w.scriptName(`vidup`).usage(`$0 [command]`,`Run a vidup command.`).command([`$0 <directory> [options]`,`sync <directory> [options]`],`Synchronize a remote video streaming service to mirror the contents of a local directory. Warning: This command will irrevocably delete remote videos that are not present in the local directory.`,e=>e.positional(`directory`,{default:void 0,demandOption:!0,describe:`The path to the local directory of video files to sync.`,type:`string`}).option(`service`,{alias:`s`,choices:[`bunny`,`mux`,`cloudflare`],demandOption:!0,describe:`Streaming service / platform to sync local video files to. Only the Bunny.net streaming CDN is supported at this time.`,type:`string`}).option(`key`,{default:void 0,demandOption:!0,describe:`Streaming service API access key.`,type:`string`}).option(`library`,{default:void 0,demandOption:!0,describe:`Streaming service library ID.`,type:`string`}).option(`strip-metadata`,{default:!1,describe:`Remove all metadata from the video files before uploading them to the streaming service. Warning: This will modify local videos in-place.`,type:`boolean`}).option(`dry-run`,{alias:`d`,default:!1,describe:"Perform a dry run without making any changes. Useful for testing and debugging. Pairs well with the `--json` command.",type:`boolean`}).option(`json`,{default:!1,describe:`Output the sync report as JSON.`,type:`boolean`}).option(`verbose`,{default:!1,describe:"Enable verbose logging. All verbose logs and prefixed with their log level and are printed to `stderr` for ease of redirection.",type:`boolean`}).check(e=>{if(e.service===`bunny`)return e;throw Error(`Sorry, vidup doesn't yet implement synchronization support the ${e.service} streaming service. Only Bunny.net is supported at the moment. If you'd like to see support for another service, please open an issue or send a PR on GitHub: https://github.com/kitschpatrol/vidup/issues`)}),async({directory:r,dryRun:i,json:a,key:o,library:s,service:c,stripMetadata:l,verbose:u})=>{g.verbose=u;let d=e.resolve(n(r));g.info(`Starting video synchronization...`),i&&g.warn(`Dry run enabled, not making any changes`);let f=l?await b(d,{dryRun:i,verbose:u}):[],p=await S(d,{credentials:{key:o,library:s},dryRun:i,service:c,verbose:u});if(a){for(let e of p)e.stripMetadata=e.localFile!==void 0&&f.includes(e.localFile);process.stdout.write(JSON.stringify(p,void 0,2)),process.stdout.write(`
2
+ import e from"node:path";import t from"pretty-ms";import n from"untildify";import r from"yargs";import{hideBin as i}from"yargs/helpers";import{oraPromise as a}from"ora";import o from"picocolors";import{execa as s}from"execa";import c from"ffmpeg-static";import l from"node:fs/promises";import{BunnyCdnStream as u}from"bunnycdn-stream";import{hash as d}from"hasha";import{JSONFilePreset as f}from"lowdb/node";import{createReadStream as p}from"node:fs";var m=`1.0.18`;const h=process.versions.node!==void 0,g={verbose:!1,log(...e){if(!this.verbose)return;let t=o.gray(`[Log]`);h?console.warn(t,...e):console.log(t,...e)},logPrefixed(e,...t){this.info(o.blue(`[${e}]`),...t)},info(...e){if(!this.verbose)return;let t=o.green(`[Info]`);h?console.warn(t,...e):console.info(t,...e)},infoPrefixed(e,...t){this.info(o.blue(`[${e}]`),...t)},async infoSpin(e,t){return a(e,{prefixText:o.green(`[Info]`),stream:process.stderr,text:t})},warn(...e){console.warn(o.yellow(`[Warning]`),...e)},warnPrefixed(e,...t){this.warn(o.blue(`[${e}]`),...t)},error(...e){console.error(o.red(`[Error]`),...e)},errorPrefixed(e,...t){this.error(o.blue(`[${e}]`),...t)}};async function _(t){return(await l.readdir(t)).filter(t=>{let n=e.extname(t);return[`.mp4`,`.mov`,`.avi`,`.mkv`].includes(n)}).map(n=>e.join(t,n))}async function v(e){if(c===null)throw Error(`ffmpeg-static failed to locate ffmpeg binary`);try{let{stderr:t,stdout:n}=await s(c,[`-i`,e,`-f`,`ffmetadata`,`-`]);return n.includes(`creation_time`)||t.includes(`creation_time`)}catch(t){throw Error(`Failed to check metadata for: ${e}:\n${String(t)}`)}}async function y(t){if(c===null)throw Error(`ffmpeg-static failed to locate ffmpeg binary`);try{let n=e.extname(t).slice(1),r=`${t.slice(0,Math.max(0,t.lastIndexOf(`.`)))}.temp.${n}`;await s(c,[`-i`,t,`-map_metadata`,`-1`,`-c:v`,`copy`,`-c:a`,`copy`,r]),await l.rename(r,t)}catch(e){throw Error(`Failed to strip metadata from: ${t}:\n${String(e)}`)}}async function b(e,t={}){let{dryRun:n=!1,verbose:r=!1}=t;return x(await _(e),{dryRun:n,verbose:r})}async function x(e,t={}){let{dryRun:n=!1,verbose:r=!1}=t,i=g.verbose;g.verbose=r;let a=[];for(let t of e)await v(t)&&a.push(t);if(!n){g.info(`Found ${a.length} videos with metadata to strip`);for(let e of a)console.log(`Stripping metadata from: ${e}`),await y(e)}return g.verbose=i,a}async function S(t,n){let{credentials:r,dryRun:i=!1,service:a,verbose:o=!1}=n,s=g.verbose;g.verbose=o;let c=[],l=await _(t);g.info(`Found ${l.length} video files in directory "${t}"`);let m=await f(e.join(t,`.vidup-state.json`),{syncState:[]});m.data.syncState=m.data.syncState.filter(n=>l.includes(e.join(t,n.filename)));for(let t of l){let n=e.basename(t),r=await d(t,{algorithm:`sha1`}),i=m.data.syncState.find(e=>e.filename===n);i?i.localHash=r:m.data.syncState.push({filename:n,localHash:r,remoteHash:{}})}if(i||await m.write(),a!==`bunny`)throw Error(`Streaming service not yet implemented: ${a}`);let h=new u({apiKey:r.key,videoLibrary:r.library}),v=await h.listAllVideos();g.info(`Found ${v.length} video files on ${a} remote streaming service`);let y=v.filter(e=>{let t=m.data.syncState.find(t=>t.filename===e.title);return t!==void 0&&t.localHash===t.remoteHash[a]});for(let e of y)c.push({action:`None`,localFile:e.title,remoteId:e.guid});let b=m.data.syncState.filter(e=>!v.some(t=>t.title===e.filename));for(let e of b)c.push({action:`Create`,localFile:e.filename,remoteId:`Not yet uploaded (Dry run)`});let x=v.filter(e=>{let t=m.data.syncState.find(t=>t.filename===e.title);return t!==void 0&&t.localHash!==t.remoteHash[a]});for(let e of x)c.push({action:`Update`,localFile:e.title,remoteId:`Not yet uploaded (Dry run)`});let S=v.filter(e=>!m.data.syncState.some(t=>t.filename===e.title));for(let e of S)c.push({action:`Delete`,localFile:e.title,remoteId:e.guid});if(!i){m.data.lastRun=new Date,await m.write(),g.info(`Synchronizing...`);for(let[e,t]of S.entries()){if(e===0&&g.info(`Deleting ${S.length} remote videos...`),!(await g.infoSpin(h.deleteVideo(t.guid),`Deleting remote video ${e+1}/${S.length}: ${t.title}`)).success)throw Error(`Failed to delete remote video: ${t.title}`);m.data.lastUpdate=new Date,await m.write()}for(let[n,r]of b.entries()){n===0&&g.info(`Uploading ${b.length} new local videos...`);let i=p(e.join(t,r.filename)),o=await g.infoSpin(h.createAndUploadVideo(i,{title:r.filename}),`Uploading new video ${n+1}/${b.length}: ${r.filename}`);g.info(`Remote video ${n+1}/${b.length} created with GUID: ${o.guid}`);let s=m.data.syncState.find(e=>e.filename===r.filename);if(s===void 0)throw Error(`Failed to find state entry for: ${r.filename}`);s.remoteHash[a]=r.localHash,m.data.lastUpdate=new Date,await m.write();let l=c.find(e=>e.localFile===r.filename);if(l===void 0)throw Error(`Failed to find report entry for: ${r.filename}`);l.remoteId=o.guid}for(let[n,r]of x.entries()){n===0&&(g.info(`Updating ${x.length} remote videos...`),g.warn(`Bunny does not currently support updating videos in-place, so vidup will delete and re-uploading instead... this will change the video's GUID!`));let i=m.data.syncState.find(e=>e.filename===r.title);if(i===void 0)throw Error(`Failed to find state entry for: ${r.title}`);if(!(await h.deleteVideo(r.guid)).success)throw Error(`Failed to delete remote video before updating: ${r.title}`);let o=p(e.join(t,i.filename)),s=await g.infoSpin(h.createAndUploadVideo(o,{title:i.filename}),`Updating remote video ${n+1}/${x.length}: ${r.title}`);g.info(`Updated remote video ${n+1}/${x.length} created with GUID: ${s.guid}`),i.remoteHash[a]=i.localHash,m.data.lastUpdate=new Date,await m.write();let l=c.find(e=>e.localFile===i.filename);if(l===void 0)throw Error(`Failed to find report entry for: ${i.filename}`);l.remoteId=s.guid}g.info(`All ${l.length} videos are now in sync!`)}return g.verbose=s,c}const C=performance.now(),w=r(i(process.argv));await w.scriptName(`vidup`).usage(`$0 [command]`,`Run a vidup command.`).command([`$0 <directory> [options]`,`sync <directory> [options]`],`Synchronize a remote video streaming service to mirror the contents of a local directory. Warning: This command will irrevocably delete remote videos that are not present in the local directory.`,e=>e.positional(`directory`,{default:void 0,demandOption:!0,describe:`The path to the local directory of video files to sync.`,type:`string`}).option(`service`,{alias:`s`,choices:[`bunny`,`mux`,`cloudflare`],demandOption:!0,describe:`Streaming service / platform to sync local video files to. Only the Bunny.net streaming CDN is supported at this time.`,type:`string`}).option(`key`,{default:void 0,demandOption:!0,describe:`Streaming service API access key.`,type:`string`}).option(`library`,{default:void 0,demandOption:!0,describe:`Streaming service library ID.`,type:`string`}).option(`strip-metadata`,{default:!1,describe:`Remove all metadata from the video files before uploading them to the streaming service. Warning: This will modify local videos in-place.`,type:`boolean`}).option(`dry-run`,{alias:`d`,default:!1,describe:"Perform a dry run without making any changes. Useful for testing and debugging. Pairs well with the `--json` command.",type:`boolean`}).option(`json`,{default:!1,describe:`Output the sync report as JSON.`,type:`boolean`}).option(`verbose`,{default:!1,describe:"Enable verbose logging. All verbose logs and prefixed with their log level and are printed to `stderr` for ease of redirection.",type:`boolean`}).check(e=>{if(e.service===`bunny`)return e;throw Error(`Sorry, vidup doesn't yet implement synchronization support the ${e.service} streaming service. Only Bunny.net is supported at the moment. If you'd like to see support for another service, please open an issue or send a PR on GitHub: https://github.com/kitschpatrol/vidup/issues`)}),async({directory:r,dryRun:i,json:a,key:o,library:s,service:c,stripMetadata:l,verbose:u})=>{g.verbose=u;let d=e.resolve(n(r));g.info(`Starting video synchronization...`),i&&g.warn(`Dry run enabled, not making any changes`);let f=l?await b(d,{dryRun:i,verbose:u}):[],p=await S(d,{credentials:{key:o,library:s},dryRun:i,service:c,verbose:u});if(a){for(let e of p)e.stripMetadata=e.localFile!==void 0&&f.includes(e.localFile);process.stdout.write(JSON.stringify(p,void 0,2)),process.stdout.write(`
3
3
  `)}else{for(let e of f)process.stdout.write(`${i?`Found`:`Stripped`} metadata: "${e}"\n`);for(let e of p)process.stdout.write(`${e.action}: "${e.localFile}"\t Remote GUID: "${e.remoteId}"\n`)}g.info(`Synchronized video in ${t(performance.now()-C)}`),process.exitCode=0}).demandCommand(1).alias(`h`,`help`).version(m).alias(`v`,`version`).help().wrap(process.stdout.isTTY?Math.min(120,w.terminalWidth()):0).parse();export{};
@@ -5,6 +5,7 @@ type StripOptions = {
5
5
  };
6
6
  /**
7
7
  * Strip metadata from video files in a directory
8
+ *
8
9
  * @returns List of file paths with metadata that were stripped
9
10
  */
10
11
  declare function stripVideoMetadataInDirectory(directory: string, options?: StripOptions): Promise<string[]>;
@@ -27,8 +28,11 @@ type SyncOptions = {
27
28
  verbose?: boolean;
28
29
  };
29
30
  /**
30
- * Synchronize a remote streaming service to mirror the contents of a local directory
31
- * @returns Array of sync report entries describing what was changed (or will change if a dry run)
31
+ * Synchronize a remote streaming service to mirror the contents of a local
32
+ * directory
33
+ *
34
+ * @returns Array of sync report entries describing what was changed (or will
35
+ * change if a dry run)
32
36
  */
33
37
  declare function syncVideoInDirectory(directory: string, options: SyncOptions): Promise<SyncReport>;
34
38
  //#endregion
package/dist/lib/index.js CHANGED
@@ -112,6 +112,7 @@ async function stripVideoMetadata(filename) {
112
112
  //#region src/lib/strip.ts
113
113
  /**
114
114
  * Strip metadata from video files in a directory
115
+ *
115
116
  * @returns List of file paths with metadata that were stripped
116
117
  */
117
118
  async function stripVideoMetadataInDirectory(directory, options = {}) {
@@ -123,6 +124,7 @@ async function stripVideoMetadataInDirectory(directory, options = {}) {
123
124
  }
124
125
  /**
125
126
  * Strip metadata from multiple video files
127
+ *
126
128
  * @returns List of file paths with metadata that were stripped
127
129
  */
128
130
  async function stripVideoMetadataInFiles(files, options = {}) {
@@ -144,8 +146,11 @@ async function stripVideoMetadataInFiles(files, options = {}) {
144
146
  //#endregion
145
147
  //#region src/lib/sync.ts
146
148
  /**
147
- * Synchronize a remote streaming service to mirror the contents of a local directory
148
- * @returns Array of sync report entries describing what was changed (or will change if a dry run)
149
+ * Synchronize a remote streaming service to mirror the contents of a local
150
+ * directory
151
+ *
152
+ * @returns Array of sync report entries describing what was changed (or will
153
+ * change if a dry run)
149
154
  */
150
155
  async function syncVideoInDirectory(directory, options) {
151
156
  const { credentials, dryRun = false, service, verbose = false } = options;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidup",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "Synchronize a local directory of video files to remote streaming services.",
5
5
  "keywords": [
6
6
  "bunnycdn-stream",
@@ -47,7 +47,7 @@
47
47
  "ffmpeg-static": "^5.3.0",
48
48
  "hasha": "^7.0.0",
49
49
  "lowdb": "^7.0.1",
50
- "ora": "^9.3.0",
50
+ "ora": "^9.4.0",
51
51
  "picocolors": "^1.1.1",
52
52
  "pretty-ms": "^9.3.0",
53
53
  "untildify": "^6.0.0",
@@ -55,14 +55,15 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "@arethetypeswrong/core": "^0.18.2",
58
- "@kitschpatrol/shared-config": "^7.1.0",
59
- "@types/node": "^20.19.39",
60
- "bumpp": "^11.0.1",
61
- "mdat-plugin-cli-help": "^2.0.2",
62
- "publint": "^0.3.18",
63
- "tsdown": "^0.21.7",
58
+ "@kitschpatrol/shared-config": "^7.6.0",
59
+ "@types/node": "^20.19.41",
60
+ "bumpp": "^11.1.0",
61
+ "mdat-plugin-cli-help": "^3.0.0",
62
+ "publint": "^0.3.21",
63
+ "shx": "^0.4.0",
64
+ "tsdown": "^0.22.0",
64
65
  "typescript": "^5.9.3",
65
- "vitest": "^4.1.2"
66
+ "vitest": "^4.1.6"
66
67
  },
67
68
  "engines": {
68
69
  "node": ">=20.19.0"
@@ -75,10 +76,10 @@
75
76
  },
76
77
  "scripts": {
77
78
  "build": "tsdown",
78
- "clean": "git rm -f pnpm-lock.yaml ; git clean -fdX",
79
+ "clean": "shx rm -f pnpm-lock.yaml && git clean -fdX -e !.claude/",
79
80
  "fix": "ksc fix",
80
81
  "lint": "ksc lint",
81
- "release": "bumpp --commit 'Release: %s' && pnpm run build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') && pnpm publish",
82
+ "release": "bumpp --commit 'Release: %s' && pnpm build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') pnpm publish",
82
83
  "test": "vitest run"
83
84
  }
84
85
  }
package/readme.md CHANGED
@@ -7,7 +7,7 @@
7
7
  <!-- badges -->
8
8
 
9
9
  [![NPM Package vidup](https://img.shields.io/npm/v/vidup.svg)](https://npmjs.com/package/vidup)
10
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/license/mit/)
11
11
  [![CI](https://github.com/kitschpatrol/vidup/actions/workflows/ci.yml/badge.svg)](https://github.com/kitschpatrol/vidup/actions/workflows/ci.yml)
12
12
 
13
13
  <!-- /badges -->
@@ -216,7 +216,7 @@ Video streaming providers have slow and clunky web UIs. I'd rather not maintain
216
216
 
217
217
  ## Maintainers
218
218
 
219
- [@kitschpatrol](https://github.com/kitschpatrol)
219
+ [kitschpatrol](https://github.com/kitschpatrol)
220
220
 
221
221
  ## Acknowledgments
222
222
 
@@ -226,10 +226,14 @@ Thanks to [@dan-online](https://github.com/dan-online) for his super-helpful [bu
226
226
 
227
227
  ## Contributing
228
228
 
229
- [Issues](https://github.com/kitschpatrol/vidup/issues) and pull requests are welcome.
229
+ [Issues](https://github.com/kitschpatrol/vidup/issues) are welcome and appreciated.
230
+
231
+ Please open an issue to discuss changes before submitting a pull request. Unsolicited PRs (especially AI-generated ones) are unlikely to be merged.
232
+
233
+ This repository uses [@kitschpatrol/shared-config](https://github.com/kitschpatrol/shared-config) (via its `ksc` CLI) for linting and formatting, plus [MDAT](https://github.com/kitschpatrol/mdat) for readme placeholder expansion.
230
234
 
231
235
  ## License
232
236
 
233
- [MIT](license.txt) © Eric Mika
237
+ [MIT](license.txt) © [Eric Mika](https://ericmika.com)
234
238
 
235
239
  <!-- /footer -->