vidup 1.0.3 → 1.0.4
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/bin/cli.js +7 -7
- package/package.json +10 -11
- package/readme.md +1 -1
package/bin/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var te=(t=0)=>e=>`\x1B[${e+t}m`,
|
|
2
|
+
var te="1.0.4";var re=(t=0)=>e=>`\x1B[${e+t}m`,oe=(t=0)=>e=>`\x1B[${38+t};5;${e}m`,ne=(t=0)=>(e,r,o)=>`\x1B[${38+t};2;${e};${r};${o}m`,u={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],overline:[53,55],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],gray:[90,39],grey:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgGray:[100,49],bgGrey:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}},pt=Object.keys(u.modifier),Ee=Object.keys(u.color),Fe=Object.keys(u.bgColor),mt=[...Ee,...Fe];function Re(){let t=new Map;for(let[e,r]of Object.entries(u)){for(let[o,n]of Object.entries(r))u[o]={open:`\x1B[${n[0]}m`,close:`\x1B[${n[1]}m`},r[o]=u[o],t.set(n[0],n[1]);Object.defineProperty(u,e,{value:r,enumerable:!1})}return Object.defineProperty(u,"codes",{value:t,enumerable:!1}),u.color.close="\x1B[39m",u.bgColor.close="\x1B[49m",u.color.ansi=re(),u.color.ansi256=oe(),u.color.ansi16m=ne(),u.bgColor.ansi=re(10),u.bgColor.ansi256=oe(10),u.bgColor.ansi16m=ne(10),Object.defineProperties(u,{rgbToAnsi256:{value(e,r,o){return e===r&&r===o?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(r/255*5)+Math.round(o/255*5)},enumerable:!1},hexToRgb:{value(e){let r=/[a-f\d]{6}|[a-f\d]{3}/i.exec(e.toString(16));if(!r)return[0,0,0];let[o]=r;o.length===3&&(o=[...o].map(i=>i+i).join(""));let n=Number.parseInt(o,16);return[n>>16&255,n>>8&255,n&255]},enumerable:!1},hexToAnsi256:{value:e=>u.rgbToAnsi256(...u.hexToRgb(e)),enumerable:!1},ansi256ToAnsi:{value(e){if(e<8)return 30+e;if(e<16)return 90+(e-8);let r,o,n;if(e>=232)r=((e-232)*10+8)/255,o=r,n=r;else{e-=16;let y=e%36;r=Math.floor(e/36)/5,o=Math.floor(y/6)/5,n=y%6/5}let i=Math.max(r,o,n)*2;if(i===0)return 30;let a=30+(Math.round(n)<<2|Math.round(o)<<1|Math.round(r));return i===2&&(a+=60),a},enumerable:!1},rgbToAnsi:{value:(e,r,o)=>u.ansi256ToAnsi(u.rgbToAnsi256(e,r,o)),enumerable:!1},hexToAnsi:{value:e=>u.ansi256ToAnsi(u.hexToAnsi256(e)),enumerable:!1}}),u}var Te=Re(),v=Te;import Y from"node:process";import Ie from"node:os";import ie from"node:tty";function w(t,e=globalThis.Deno?globalThis.Deno.args:Y.argv){let r=t.startsWith("-")?"":t.length===1?"-":"--",o=e.indexOf(r+t),n=e.indexOf("--");return o!==-1&&(n===-1||o<n)}var{env:p}=Y,A;w("no-color")||w("no-colors")||w("color=false")||w("color=never")?A=0:(w("color")||w("colors")||w("color=true")||w("color=always"))&&(A=1);function Me(){if("FORCE_COLOR"in p)return p.FORCE_COLOR==="true"?1:p.FORCE_COLOR==="false"?0:p.FORCE_COLOR.length===0?1:Math.min(Number.parseInt(p.FORCE_COLOR,10),3)}function Ce(t){return t===0?!1:{level:t,hasBasic:!0,has256:t>=2,has16m:t>=3}}function $e(t,{streamIsTTY:e,sniffFlags:r=!0}={}){let o=Me();o!==void 0&&(A=o);let n=r?A:o;if(n===0)return 0;if(r){if(w("color=16m")||w("color=full")||w("color=truecolor"))return 3;if(w("color=256"))return 2}if("TF_BUILD"in p&&"AGENT_NAME"in p)return 1;if(t&&!e&&n===void 0)return 0;let i=n||0;if(p.TERM==="dumb")return i;if(Y.platform==="win32"){let a=Ie.release().split(".");return Number(a[0])>=10&&Number(a[2])>=10586?Number(a[2])>=14931?3:2:1}if("CI"in p)return"GITHUB_ACTIONS"in p||"GITEA_ACTIONS"in p?3:["TRAVIS","CIRCLECI","APPVEYOR","GITLAB_CI","BUILDKITE","DRONE"].some(a=>a in p)||p.CI_NAME==="codeship"?1:i;if("TEAMCITY_VERSION"in p)return/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(p.TEAMCITY_VERSION)?1:0;if(p.COLORTERM==="truecolor"||p.TERM==="xterm-kitty")return 3;if("TERM_PROGRAM"in p){let a=Number.parseInt((p.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(p.TERM_PROGRAM){case"iTerm.app":return a>=3?3:2;case"Apple_Terminal":return 2}}return/-256(color)?$/i.test(p.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(p.TERM)||"COLORTERM"in p?1:i}function se(t,e={}){let r=$e(t,{streamIsTTY:t&&t.isTTY,...e});return Ce(r)}var De={stdout:se({isTTY:ie.isatty(1)}),stderr:se({isTTY:ie.isatty(2)})},ae=De;function le(t,e,r){let o=t.indexOf(e);if(o===-1)return t;let n=e.length,i=0,a="";do a+=t.slice(i,o)+e+r,i=o+n,o=t.indexOf(e,i);while(o!==-1);return a+=t.slice(i),a}function ce(t,e,r,o){let n=0,i="";do{let a=t[o-1]==="\r";i+=t.slice(n,a?o-1:o)+e+(a?`\r
|
|
3
3
|
`:`
|
|
4
4
|
`)+r,n=o+1,o=t.indexOf(`
|
|
5
|
-
`,n)}while(o!==-1);return i+=t.slice(n),i}var{stdout:
|
|
6
|
-
`);return i!==-1&&(e=
|
|
7
|
-
${String(e)}`)}}async function
|
|
8
|
-
${String(e)}`)}}async function Z(t,e={}){let{dryRun:r=!1,verbose:o=!1}=e,n=await _(t);return je(n,{dryRun:r,verbose:o})}async function je(t,e={}){let{dryRun:r=!1,verbose:o=!1}=e,n=d.verbose;d.verbose=o;let i=[];for(let a of t)await ye(a)&&i.push(a);if(!r){d.info(`Found ${i.length} videos with metadata to strip`);for(let a of i)console.log(`Stripping metadata from: ${a}`),await ge(a)}return d.verbose=n,i}import{BunnyCdnStream as qe}from"bunnycdn-stream";import{hash as Qe}from"hasha";import{readFile as ze}from"node:fs/promises";import{rename as Ue,writeFile as Le}from"node:fs/promises";import{basename as Ge,dirname as He,join as Ye}from"node:path";import{fileURLToPath as Je}from"node:url";function We(t){let e=t instanceof URL?Je(t):t.toString();return Ye(He(e),`.${Ge(e)}.tmp`)}async function Ke(t,e,r){for(let o=0;o<e;o++)try{return await t()}catch(n){if(o<e-1)await new Promise(i=>setTimeout(i,r));else throw n}}var k=class{#e;#t;#r=!1;#n=null;#i=null;#s=null;#o=null;#a(e){return this.#o=e,this.#s||=new Promise((r,o)=>{this.#i=[r,o]}),new Promise((r,o)=>{var n;(n=this.#s)==null||n.then(r).catch(o)})}async#l(e){var r,o;this.#r=!0;try{await Le(this.#t,e,"utf-8"),await Ke(async()=>{await Ue(this.#t,this.#e)},10,100),(r=this.#n)==null||r[0]()}catch(n){throw n instanceof Error&&((o=this.#n)==null||o[1](n)),n}finally{if(this.#r=!1,this.#n=this.#i,this.#i=this.#s=null,this.#o!==null){let n=this.#o;this.#o=null,await this.write(n)}}}constructor(e){this.#e=e,this.#t=We(e)}async write(e){return this.#r?this.#a(e):this.#l(e)}};var V=class{#e;#t;constructor(e){this.#e=e,this.#t=new k(e)}async read(){let e;try{e=await ze(this.#e,"utf-8")}catch(r){if(r.code==="ENOENT")return null;throw r}return e}write(e){return this.#t.write(e)}};var j=class{#e;#t;#r;constructor(e,{parse:r,stringify:o}){this.#e=new V(e),this.#t=r,this.#r=o}async read(){let e=await this.#e.read();return e===null?null:this.#t(e)}write(e){return this.#e.write(this.#r(e))}};var U=class extends j{constructor(e){super(e,{parse:JSON.parse,stringify:r=>JSON.stringify(r,null,2)})}};var L=class{#e=null;read(){return Promise.resolve(this.#e)}write(e){return this.#e=e,Promise.resolve()}};function Ze(t,e){if(t===void 0)throw new Error("lowdb: missing adapter");if(e===void 0)throw new Error("lowdb: missing default data")}var G=class{adapter;data;constructor(e,r){Ze(e,r),this.adapter=e,this.data=r}async read(){let e=await this.adapter.read();e&&(this.data=e)}async write(){this.data&&await this.adapter.write(this.data)}async update(e){e(this.data),await this.write()}};async function be(t,e){let r=process.env.NODE_ENV==="test"?new L:new U(t),o=new G(r,e);return await o.read(),o}import{createReadStream as we}from"node:fs";import D from"node:path";async function q(t,e){let{credentials:r,dryRun:o=!1,service:n,verbose:i=!1}=e,a=d.verbose;d.verbose=i;let y=[],S=await _(t);d.info(`Found ${S.length} video files in directory "${t}"`);let c=await be(D.join(t,".vidup-state.json"),{syncState:[]});c.data.syncState=c.data.syncState.filter(s=>S.includes(D.join(t,s.filename)));for(let s of S){let l=D.basename(s),g=await Qe(s,{algorithm:"sha1"}),E=c.data.syncState.find(F=>F.filename===l);E?E.localHash=g:c.data.syncState.push({filename:l,localHash:g,remoteHash:{}})}if(o||await c.write(),n!=="bunny")throw new Error(`Streaming service not yet implemented: ${n}`);let p=new qe({apiKey:r.key,videoLibrary:r.library}),f=await p.listAllVideos();d.info(`Found ${f.length} video files on ${n} remote streaming service`);let h=f.filter(s=>{let l=c.data.syncState.find(g=>g.filename===s.title);return l!==void 0&&l.localHash===l.remoteHash[n]});for(let s of h)y.push({action:"None",localFile:s.title,remoteId:s.guid});let b=c.data.syncState.filter(s=>!f.some(l=>l.title===s.filename));for(let s of b)y.push({action:"Create",localFile:s.filename,remoteId:"Not yet uploaded (Dry run)"});let x=f.filter(s=>{let l=c.data.syncState.find(g=>g.filename===s.title);return l!==void 0&&l.localHash!==l.remoteHash[n]});for(let s of x)y.push({action:"Update",localFile:s.title,remoteId:"Not yet uploaded (Dry run)"});let N=f.filter(s=>!c.data.syncState.some(l=>l.filename===s.title));for(let s of N)y.push({action:"Delete",localFile:s.title,remoteId:s.guid});if(!o){c.data.lastRun=new Date,await c.write(),d.info("Synchronizing...");for(let[s,l]of N.entries()){if(s===0&&d.info(`Deleting ${N.length} remote videos...`),!(await d.infoSpin(p.deleteVideo(l.guid),`Deleting remote video ${s+1}/${N.length}: ${l.title}`)).success)throw new Error(`Failed to delete remote video: ${l.title}`);c.data.lastUpdate=new Date,await c.write()}for(let[s,l]of b.entries()){s===0&&d.info(`Uploading ${b.length} new local videos...`);let g=we(D.join(t,l.filename)),E=await d.infoSpin(p.createAndUploadVideo(g,{title:l.filename}),`Uploading new video ${s+1}/${b.length}: ${l.filename}`);d.info(`Remote video ${s+1}/${b.length} created with GUID: ${E.guid}`);let F=c.data.syncState.find(R=>R.filename===l.filename);if(F===void 0)throw new Error(`Failed to find state entry for: ${l.filename}`);F.remoteHash[n]=l.localHash,c.data.lastUpdate=new Date,await c.write();let M=y.find(R=>R.localFile===l.filename);if(M===void 0)throw new Error(`Failed to find report entry for: ${l.filename}`);M.remoteId=E.guid}for(let[s,l]of x.entries()){s===0&&(d.info(`Updating ${x.length} remote videos...`),d.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 g=c.data.syncState.find(H=>H.filename===l.title);if(g===void 0)throw new Error(`Failed to find state entry for: ${l.title}`);if(!(await p.deleteVideo(l.guid)).success)throw new Error(`Failed to delete remote video before updating: ${l.title}`);let F=we(D.join(t,g.filename)),M=await d.infoSpin(p.createAndUploadVideo(F,{title:g.filename}),`Updating remote video ${s+1}/${x.length}: ${l.title}`);d.info(`Updated remote video ${s+1}/${x.length} created with GUID: ${M.guid}`),g.remoteHash[n]=g.localHash,c.data.lastUpdate=new Date,await c.write();let R=y.find(H=>H.localFile===g.filename);if(R===void 0)throw new Error(`Failed to find report entry for: ${g.filename}`);R.remoteId=M.guid}d.info(`All ${S.length} videos are now in sync!`)}return d.verbose=a,y}import st from"node:path";var ve=t=>Number.isFinite(t)?t:0;function Xe(t){return{days:Math.trunc(t/864e5),hours:Math.trunc(t/36e5%24),minutes:Math.trunc(t/6e4%60),seconds:Math.trunc(t/1e3%60),milliseconds:Math.trunc(t%1e3),microseconds:Math.trunc(ve(t*1e3)%1e3),nanoseconds:Math.trunc(ve(t*1e6)%1e3)}}function et(t){return{days:t/86400000n,hours:t/3600000n%24n,minutes:t/60000n%60n,seconds:t/1000n%60n,milliseconds:t%1000n,microseconds:0n,nanoseconds:0n}}function Q(t){switch(typeof t){case"number":{if(Number.isFinite(t))return Xe(t);break}case"bigint":return et(t)}throw new TypeError("Expected a finite number or bigint")}var tt=t=>t===0||t===0n,rt=(t,e)=>e===1||e===1n?t:`${t}s`,ot=1e-7,nt=24n*60n*60n*1000n;function X(t,e){let r=typeof t=="bigint";if(!r&&!Number.isFinite(t))throw new TypeError("Expected a finite number or bigint");e={...e},e.colonNotation&&(e.compact=!1,e.formatSubMilliseconds=!1,e.separateMilliseconds=!1,e.verbose=!1),e.compact&&(e.unitCount=1,e.secondsDecimalDigits=0,e.millisecondsDecimalDigits=0);let o=[],n=(c,p)=>{let f=Math.floor(c*10**p+ot);return(Math.round(f)/10**p).toFixed(p)},i=(c,p,f,h)=>{if(!((o.length===0||!e.colonNotation)&&tt(c)&&!(e.colonNotation&&f==="m"))){if(h=h??String(c),e.colonNotation){let b=h.includes(".")?h.split(".")[0].length:h.length,x=o.length>0?2:1;h="0".repeat(Math.max(0,x-b))+h}else h+=e.verbose?" "+rt(p,c):f;o.push(h)}},a=Q(t),y=BigInt(a.days);if(i(y/365n,"year","y"),i(y%365n,"day","d"),i(Number(a.hours),"hour","h"),i(Number(a.minutes),"minute","m"),e.separateMilliseconds||e.formatSubMilliseconds||!e.colonNotation&&t<1e3){let c=Number(a.seconds),p=Number(a.milliseconds),f=Number(a.microseconds),h=Number(a.nanoseconds);if(i(c,"second","s"),e.formatSubMilliseconds)i(p,"millisecond","ms"),i(f,"microsecond","\xB5s"),i(h,"nanosecond","ns");else{let b=p+f/1e3+h/1e6,x=typeof e.millisecondsDecimalDigits=="number"?e.millisecondsDecimalDigits:0,N=b>=1?Math.round(b):Math.ceil(b),s=x?b.toFixed(x):N;i(Number.parseFloat(s),"millisecond","ms",s)}}else{let c=(r?Number(t%nt):t)/1e3%60,p=typeof e.secondsDecimalDigits=="number"?e.secondsDecimalDigits:1,f=n(c,p),h=e.keepDecimalsOnWholeSeconds?f:f.replace(/\.0+$/,"");i(Number.parseFloat(h),"second","s",h)}if(o.length===0)return"0"+(e.verbose?" milliseconds":"ms");let S=e.colonNotation?":":" ";return typeof e.unitCount=="number"&&(o=o.slice(0,Math.max(e.unitCount,1))),o.join(S)}import it from"node:os";var xe=it.homedir();function ee(t){if(typeof t!="string")throw new TypeError(`Expected a string, got ${typeof t}`);return xe?t.replace(/^~(?=$|\/|\\)/,xe):t}import at from"yargs";import{hideBin as lt}from"yargs/helpers";var ct=performance.now(),Oe=at(lt(process.argv));await Oe.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.",t=>t.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 new 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:t,dryRun:e,json:r,key:o,library:n,service:i,stripMetadata:a,verbose:y})=>{d.verbose=y;let S=st.resolve(ee(t));d.info("Starting video synchronization..."),e&&d.warn("Dry run enabled, not making any changes");let c=a?await Z(S,{dryRun:e,verbose:y}):[],p=await q(S,{credentials:{key:o,library:n},dryRun:e,service:i,verbose:y});if(r){for(let f of p)f.stripMetadata=f.localFile!==void 0&&c.includes(f.localFile);process.stdout.write(JSON.stringify(p,void 0,2)),process.stdout.write(`
|
|
5
|
+
`,n)}while(o!==-1);return i+=t.slice(n),i}var{stdout:de,stderr:fe}=ae,J=Symbol("GENERATOR"),T=Symbol("STYLER"),C=Symbol("IS_EMPTY"),ue=["ansi","ansi","ansi256","ansi16m"],I=Object.create(null),Ae=(t,e={})=>{if(e.level&&!(Number.isInteger(e.level)&&e.level>=0&&e.level<=3))throw new Error("The `level` option should be an integer from 0 to 3");let r=de?de.level:0;t.level=e.level===void 0?r:e.level};var ke=t=>{let e=(...r)=>r.join(" ");return Ae(e,t),Object.setPrototypeOf(e,$.prototype),e};function $(t){return ke(t)}Object.setPrototypeOf($.prototype,Function.prototype);for(let[t,e]of Object.entries(v))I[t]={get(){let r=k(this,z(e.open,e.close,this[T]),this[C]);return Object.defineProperty(this,t,{value:r}),r}};I.visible={get(){let t=k(this,this[T],!0);return Object.defineProperty(this,"visible",{value:t}),t}};var W=(t,e,r,...o)=>t==="rgb"?e==="ansi16m"?v[r].ansi16m(...o):e==="ansi256"?v[r].ansi256(v.rgbToAnsi256(...o)):v[r].ansi(v.rgbToAnsi(...o)):t==="hex"?W("rgb",e,r,...v.hexToRgb(...o)):v[r][t](...o),Pe=["rgb","hex","ansi256"];for(let t of Pe){I[t]={get(){let{level:r}=this;return function(...o){let n=z(W(t,ue[r],"color",...o),v.color.close,this[T]);return k(this,n,this[C])}}};let e="bg"+t[0].toUpperCase()+t.slice(1);I[e]={get(){let{level:r}=this;return function(...o){let n=z(W(t,ue[r],"bgColor",...o),v.bgColor.close,this[T]);return k(this,n,this[C])}}}}var Be=Object.defineProperties(()=>{},{...I,level:{enumerable:!0,get(){return this[J].level},set(t){this[J].level=t}}}),z=(t,e,r)=>{let o,n;return r===void 0?(o=t,n=e):(o=r.openAll+t,n=e+r.closeAll),{open:t,close:e,openAll:o,closeAll:n,parent:r}},k=(t,e,r)=>{let o=(...n)=>_e(o,n.length===1?""+n[0]:n.join(" "));return Object.setPrototypeOf(o,Be),o[J]=t,o[T]=e,o[C]=r,o},_e=(t,e)=>{if(t.level<=0||!e)return t[C]?"":e;let r=t[T];if(r===void 0)return e;let{openAll:o,closeAll:n}=r;if(e.includes("\x1B"))for(;r!==void 0;)e=le(e,r.close,r.open),r=r.parent;let i=e.indexOf(`
|
|
6
|
+
`);return i!==-1&&(e=ce(e,n,o,i)),o+e+n};Object.defineProperties($.prototype,I);var Ve=$(),Nt=$({level:fe?fe.level:0});var O=Ve;import{oraPromise as je}from"ora";var me,pe=((me=process==null?void 0:process.versions)==null?void 0:me.node)!==void 0,Ue={verbose:!1,log(...t){if(!this.verbose)return;let e=O.gray("[Log]");pe?console.warn(e,...t):console.log(e,...t)},logPrefixed(t,...e){this.info(O.blue(`[${t}]`),...e)},info(...t){if(!this.verbose)return;let e=O.green("[Info]");pe?console.warn(e,...t):console.info(e,...t)},infoPrefixed(t,...e){this.info(O.blue(`[${t}]`),...e)},async infoSpin(t,e){return je(t,{prefixText:O.green("[Info]"),stream:process.stderr,text:e})},warn(...t){console.warn(O.yellow("[Warning]"),...t)},warnPrefixed(t,...e){this.warn(O.blue(`[${t}]`),...e)},error(...t){console.error(O.red("[Error]"),...t)},errorPrefixed(t,...e){this.error(O.blue(`[${t}]`),...e)}},d=Ue;import{execa as he}from"execa";import P from"ffmpeg-static";import ye from"node:fs/promises";import K from"node:path";async function B(t){return(await ye.readdir(t)).filter(o=>{let n=K.extname(o);return[".mp4",".mov",".avi",".mkv"].includes(n)}).map(o=>K.join(t,o))}async function ge(t){if(P===null)throw new Error("ffmpeg-static failed to locate ffmpeg binary");try{let{stderr:e,stdout:r}=await he(P,["-i",t,"-f","ffmetadata","-"]);return r.includes("creation_time")||e.includes("creation_time")}catch(e){throw new Error(`Failed to check metadata for: ${t}:
|
|
7
|
+
${String(e)}`)}}async function be(t){if(P===null)throw new Error("ffmpeg-static failed to locate ffmpeg binary");try{let e=K.extname(t).slice(1),r=`${t.slice(0,Math.max(0,t.lastIndexOf(".")))}.temp.${e}`;await he(P,["-i",t,"-map_metadata","-1","-c:v","copy","-c:a","copy",r]),await ye.rename(r,t)}catch(e){throw new Error(`Failed to strip metadata from: ${t}:
|
|
8
|
+
${String(e)}`)}}async function Z(t,e={}){let{dryRun:r=!1,verbose:o=!1}=e,n=await B(t);return Le(n,{dryRun:r,verbose:o})}async function Le(t,e={}){let{dryRun:r=!1,verbose:o=!1}=e,n=d.verbose;d.verbose=o;let i=[];for(let a of t)await ge(a)&&i.push(a);if(!r){d.info(`Found ${i.length} videos with metadata to strip`);for(let a of i)console.log(`Stripping metadata from: ${a}`),await be(a)}return d.verbose=n,i}import{BunnyCdnStream as Qe}from"bunnycdn-stream";import{hash as et}from"hasha";import{readFile as Xe}from"node:fs/promises";import{rename as Ge,writeFile as He}from"node:fs/promises";import{basename as Ye,dirname as Je,join as We}from"node:path";import{fileURLToPath as ze}from"node:url";function Ke(t){let e=t instanceof URL?ze(t):t.toString();return We(Je(e),`.${Ye(e)}.tmp`)}async function Ze(t,e,r){for(let o=0;o<e;o++)try{return await t()}catch(n){if(o<e-1)await new Promise(i=>setTimeout(i,r));else throw n}}var _=class{#e;#t;#r=!1;#n=null;#i=null;#s=null;#o=null;#a(e){return this.#o=e,this.#s||=new Promise((r,o)=>{this.#i=[r,o]}),new Promise((r,o)=>{var n;(n=this.#s)==null||n.then(r).catch(o)})}async#l(e){var r,o;this.#r=!0;try{await He(this.#t,e,"utf-8"),await Ze(async()=>{await Ge(this.#t,this.#e)},10,100),(r=this.#n)==null||r[0]()}catch(n){throw n instanceof Error&&((o=this.#n)==null||o[1](n)),n}finally{if(this.#r=!1,this.#n=this.#i,this.#i=this.#s=null,this.#o!==null){let n=this.#o;this.#o=null,await this.write(n)}}}constructor(e){this.#e=e,this.#t=Ke(e)}async write(e){return this.#r?this.#a(e):this.#l(e)}};var V=class{#e;#t;constructor(e){this.#e=e,this.#t=new _(e)}async read(){let e;try{e=await Xe(this.#e,"utf-8")}catch(r){if(r.code==="ENOENT")return null;throw r}return e}write(e){return this.#t.write(e)}};var j=class{#e;#t;#r;constructor(e,{parse:r,stringify:o}){this.#e=new V(e),this.#t=r,this.#r=o}async read(){let e=await this.#e.read();return e===null?null:this.#t(e)}write(e){return this.#e.write(this.#r(e))}};var U=class extends j{constructor(e){super(e,{parse:JSON.parse,stringify:r=>JSON.stringify(r,null,2)})}};var L=class{#e=null;read(){return Promise.resolve(this.#e)}write(e){return this.#e=e,Promise.resolve()}};function qe(t,e){if(t===void 0)throw new Error("lowdb: missing adapter");if(e===void 0)throw new Error("lowdb: missing default data")}var G=class{adapter;data;constructor(e,r){qe(e,r),this.adapter=e,this.data=r}async read(){let e=await this.adapter.read();e&&(this.data=e)}async write(){this.data&&await this.adapter.write(this.data)}async update(e){e(this.data),await this.write()}};async function we(t,e){let r=process.env.NODE_ENV==="test"?new L:new U(t),o=new G(r,e);return await o.read(),o}import{createReadStream as ve}from"node:fs";import D from"node:path";async function X(t,e){let{credentials:r,dryRun:o=!1,service:n,verbose:i=!1}=e,a=d.verbose;d.verbose=i;let y=[],S=await B(t);d.info(`Found ${S.length} video files in directory "${t}"`);let c=await we(D.join(t,".vidup-state.json"),{syncState:[]});c.data.syncState=c.data.syncState.filter(s=>S.includes(D.join(t,s.filename)));for(let s of S){let l=D.basename(s),g=await et(s,{algorithm:"sha1"}),E=c.data.syncState.find(F=>F.filename===l);E?E.localHash=g:c.data.syncState.push({filename:l,localHash:g,remoteHash:{}})}if(o||await c.write(),n!=="bunny")throw new Error(`Streaming service not yet implemented: ${n}`);let m=new Qe({apiKey:r.key,videoLibrary:r.library}),f=await m.listAllVideos();d.info(`Found ${f.length} video files on ${n} remote streaming service`);let h=f.filter(s=>{let l=c.data.syncState.find(g=>g.filename===s.title);return l!==void 0&&l.localHash===l.remoteHash[n]});for(let s of h)y.push({action:"None",localFile:s.title,remoteId:s.guid});let b=c.data.syncState.filter(s=>!f.some(l=>l.title===s.filename));for(let s of b)y.push({action:"Create",localFile:s.filename,remoteId:"Not yet uploaded (Dry run)"});let x=f.filter(s=>{let l=c.data.syncState.find(g=>g.filename===s.title);return l!==void 0&&l.localHash!==l.remoteHash[n]});for(let s of x)y.push({action:"Update",localFile:s.title,remoteId:"Not yet uploaded (Dry run)"});let N=f.filter(s=>!c.data.syncState.some(l=>l.filename===s.title));for(let s of N)y.push({action:"Delete",localFile:s.title,remoteId:s.guid});if(!o){c.data.lastRun=new Date,await c.write(),d.info("Synchronizing...");for(let[s,l]of N.entries()){if(s===0&&d.info(`Deleting ${N.length} remote videos...`),!(await d.infoSpin(m.deleteVideo(l.guid),`Deleting remote video ${s+1}/${N.length}: ${l.title}`)).success)throw new Error(`Failed to delete remote video: ${l.title}`);c.data.lastUpdate=new Date,await c.write()}for(let[s,l]of b.entries()){s===0&&d.info(`Uploading ${b.length} new local videos...`);let g=ve(D.join(t,l.filename)),E=await d.infoSpin(m.createAndUploadVideo(g,{title:l.filename}),`Uploading new video ${s+1}/${b.length}: ${l.filename}`);d.info(`Remote video ${s+1}/${b.length} created with GUID: ${E.guid}`);let F=c.data.syncState.find(R=>R.filename===l.filename);if(F===void 0)throw new Error(`Failed to find state entry for: ${l.filename}`);F.remoteHash[n]=l.localHash,c.data.lastUpdate=new Date,await c.write();let M=y.find(R=>R.localFile===l.filename);if(M===void 0)throw new Error(`Failed to find report entry for: ${l.filename}`);M.remoteId=E.guid}for(let[s,l]of x.entries()){s===0&&(d.info(`Updating ${x.length} remote videos...`),d.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 g=c.data.syncState.find(H=>H.filename===l.title);if(g===void 0)throw new Error(`Failed to find state entry for: ${l.title}`);if(!(await m.deleteVideo(l.guid)).success)throw new Error(`Failed to delete remote video before updating: ${l.title}`);let F=ve(D.join(t,g.filename)),M=await d.infoSpin(m.createAndUploadVideo(F,{title:g.filename}),`Updating remote video ${s+1}/${x.length}: ${l.title}`);d.info(`Updated remote video ${s+1}/${x.length} created with GUID: ${M.guid}`),g.remoteHash[n]=g.localHash,c.data.lastUpdate=new Date,await c.write();let R=y.find(H=>H.localFile===g.filename);if(R===void 0)throw new Error(`Failed to find report entry for: ${g.filename}`);R.remoteId=M.guid}d.info(`All ${S.length} videos are now in sync!`)}return d.verbose=a,y}import lt from"node:path";var xe=t=>Number.isFinite(t)?t:0;function tt(t){return{days:Math.trunc(t/864e5),hours:Math.trunc(t/36e5%24),minutes:Math.trunc(t/6e4%60),seconds:Math.trunc(t/1e3%60),milliseconds:Math.trunc(t%1e3),microseconds:Math.trunc(xe(t*1e3)%1e3),nanoseconds:Math.trunc(xe(t*1e6)%1e3)}}function rt(t){return{days:t/86400000n,hours:t/3600000n%24n,minutes:t/60000n%60n,seconds:t/1000n%60n,milliseconds:t%1000n,microseconds:0n,nanoseconds:0n}}function q(t){switch(typeof t){case"number":{if(Number.isFinite(t))return tt(t);break}case"bigint":return rt(t)}throw new TypeError("Expected a finite number or bigint")}var ot=t=>t===0||t===0n,nt=(t,e)=>e===1||e===1n?t:`${t}s`,it=1e-7,st=24n*60n*60n*1000n;function Q(t,e){let r=typeof t=="bigint";if(!r&&!Number.isFinite(t))throw new TypeError("Expected a finite number or bigint");e={...e},e.colonNotation&&(e.compact=!1,e.formatSubMilliseconds=!1,e.separateMilliseconds=!1,e.verbose=!1),e.compact&&(e.unitCount=1,e.secondsDecimalDigits=0,e.millisecondsDecimalDigits=0);let o=[],n=(c,m)=>{let f=Math.floor(c*10**m+it);return(Math.round(f)/10**m).toFixed(m)},i=(c,m,f,h)=>{if(!((o.length===0||!e.colonNotation)&&ot(c)&&!(e.colonNotation&&f==="m"))){if(h=h??String(c),e.colonNotation){let b=h.includes(".")?h.split(".")[0].length:h.length,x=o.length>0?2:1;h="0".repeat(Math.max(0,x-b))+h}else h+=e.verbose?" "+nt(m,c):f;o.push(h)}},a=q(t),y=BigInt(a.days);if(i(y/365n,"year","y"),i(y%365n,"day","d"),i(Number(a.hours),"hour","h"),i(Number(a.minutes),"minute","m"),e.separateMilliseconds||e.formatSubMilliseconds||!e.colonNotation&&t<1e3){let c=Number(a.seconds),m=Number(a.milliseconds),f=Number(a.microseconds),h=Number(a.nanoseconds);if(i(c,"second","s"),e.formatSubMilliseconds)i(m,"millisecond","ms"),i(f,"microsecond","\xB5s"),i(h,"nanosecond","ns");else{let b=m+f/1e3+h/1e6,x=typeof e.millisecondsDecimalDigits=="number"?e.millisecondsDecimalDigits:0,N=b>=1?Math.round(b):Math.ceil(b),s=x?b.toFixed(x):N;i(Number.parseFloat(s),"millisecond","ms",s)}}else{let c=(r?Number(t%st):t)/1e3%60,m=typeof e.secondsDecimalDigits=="number"?e.secondsDecimalDigits:1,f=n(c,m),h=e.keepDecimalsOnWholeSeconds?f:f.replace(/\.0+$/,"");i(Number.parseFloat(h),"second","s",h)}if(o.length===0)return"0"+(e.verbose?" milliseconds":"ms");let S=e.colonNotation?":":" ";return typeof e.unitCount=="number"&&(o=o.slice(0,Math.max(e.unitCount,1))),o.join(S)}import at from"node:os";var Oe=at.homedir();function ee(t){if(typeof t!="string")throw new TypeError(`Expected a string, got ${typeof t}`);return Oe?t.replace(/^~(?=$|\/|\\)/,Oe):t}import ct from"yargs";import{hideBin as dt}from"yargs/helpers";var ft=performance.now(),Se=ct(dt(process.argv));await Se.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.",t=>t.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 new 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:t,dryRun:e,json:r,key:o,library:n,service:i,stripMetadata:a,verbose:y})=>{d.verbose=y;let S=lt.resolve(ee(t));d.info("Starting video synchronization..."),e&&d.warn("Dry run enabled, not making any changes");let c=a?await Z(S,{dryRun:e,verbose:y}):[],m=await X(S,{credentials:{key:o,library:n},dryRun:e,service:i,verbose:y});if(r){for(let f of m)f.stripMetadata=f.localFile!==void 0&&c.includes(f.localFile);process.stdout.write(JSON.stringify(m,void 0,2)),process.stdout.write(`
|
|
9
9
|
`)}else{for(let f of c)process.stdout.write(`${e?"Found":"Stripped"} metadata: "${f}"
|
|
10
|
-
`);for(let f of
|
|
11
|
-
`)}d.info(`Synchronized video in ${
|
|
10
|
+
`);for(let f of m)process.stdout.write(`${f.action}: "${f.localFile}" Remote GUID: "${f.remoteId}"
|
|
11
|
+
`)}d.info(`Synchronized video in ${Q(performance.now()-ft)}`),process.exitCode=0}).demandCommand(1).alias("h","help").version(te).alias("v","version").help().wrap(process.stdout.isTTY?Math.min(120,Se.terminalWidth()):0).parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vidup",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Synchronize a local directory of video files to remote streaming services.",
|
|
6
6
|
"repository": {
|
|
@@ -18,10 +18,9 @@
|
|
|
18
18
|
"url": "https://ericmika.com"
|
|
19
19
|
},
|
|
20
20
|
"license": "MIT",
|
|
21
|
-
"packageManager": "pnpm@8.15.6+sha256.01c01eeb990e379b31ef19c03e9d06a14afa5250b82e81303f88721c99ff2e6f",
|
|
22
21
|
"engines": {
|
|
23
22
|
"node": ">=18.0.0",
|
|
24
|
-
"pnpm": ">=
|
|
23
|
+
"pnpm": ">=9.0.0"
|
|
25
24
|
},
|
|
26
25
|
"bin": {
|
|
27
26
|
"vidup": "bin/cli.js"
|
|
@@ -43,7 +42,7 @@
|
|
|
43
42
|
"video"
|
|
44
43
|
],
|
|
45
44
|
"dependencies": {
|
|
46
|
-
"bunnycdn-stream": "^2.
|
|
45
|
+
"bunnycdn-stream": "^2.4.0",
|
|
47
46
|
"execa": "^8.0.1",
|
|
48
47
|
"ffmpeg-static": "^5.2.0",
|
|
49
48
|
"fs-extra": "^11.2.0",
|
|
@@ -52,16 +51,16 @@
|
|
|
52
51
|
"yargs": "^17.7.2"
|
|
53
52
|
},
|
|
54
53
|
"devDependencies": {
|
|
55
|
-
"@kitschpatrol/shared-config": "^4.
|
|
54
|
+
"@kitschpatrol/shared-config": "^4.7.0",
|
|
56
55
|
"@types/fs-extra": "^11.0.4",
|
|
57
|
-
"@types/node": "^20.
|
|
56
|
+
"@types/node": "^20.14.2",
|
|
58
57
|
"@types/yargs": "^17.0.32",
|
|
59
|
-
"bumpp": "^9.4.
|
|
58
|
+
"bumpp": "^9.4.1",
|
|
60
59
|
"chalk": "^5.3.0",
|
|
61
60
|
"lowdb": "^7.0.1",
|
|
62
61
|
"pretty-ms": "^9.0.0",
|
|
63
|
-
"tsup": "^8.0
|
|
64
|
-
"typescript": "^5.4.
|
|
62
|
+
"tsup": "^8.1.0",
|
|
63
|
+
"typescript": "^5.4.5",
|
|
65
64
|
"untildify": "^5.0.0"
|
|
66
65
|
},
|
|
67
66
|
"publishConfig": {
|
|
@@ -69,10 +68,10 @@
|
|
|
69
68
|
},
|
|
70
69
|
"scripts": {
|
|
71
70
|
"build": "tsup && tsc -p tsconfig.build.json",
|
|
72
|
-
"clean": "git clean -fdX",
|
|
71
|
+
"clean": "git clean -fdX && rm -f pnpm-lock.yaml",
|
|
73
72
|
"fix": "pnpm shared-config --fix",
|
|
74
73
|
"lint": "pnpm shared-config --lint",
|
|
75
|
-
"release": "pnpm bumpp -r --commit 'Release: %s' --tag 'v%s' && pnpm
|
|
74
|
+
"release": "pnpm bumpp -r --commit 'Release: %s' --tag 'v%s' && pnpm build && pnpm publish --otp $(op read 'op://Personal/Npmjs/one-time password?attribute=otp')",
|
|
76
75
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
77
76
|
}
|
|
78
77
|
}
|
package/readme.md
CHANGED
|
@@ -34,7 +34,7 @@ Currently, only Bunny.net's [Bunny Stream](https://bunny.net/stream/) service is
|
|
|
34
34
|
|
|
35
35
|
### Dependencies
|
|
36
36
|
|
|
37
|
-
The vidup CLI tool requires Node 18+. The exported APIs
|
|
37
|
+
The vidup CLI tool requires Node 18+. The exported APIs are ESM-only and share the Node 18+ requirement. Vidup is implemented in TypeScript and bundles type definitions.
|
|
38
38
|
|
|
39
39
|
A [Bunny Stream](https://bunny.net/stream/) account, API key, and library ID are required.
|
|
40
40
|
|