[{"data":1,"prerenderedAt":1647},["ShallowReactive",2],{"docs-nav:docs_en":3,"docs-page:\u002Fen\u002Fdocs\u002Freference\u002Fplugins":158,"docs-surround:\u002Fen\u002Fdocs\u002Freference\u002Fplugins":1644},[4],{"title":5,"path":6,"stem":7,"children":8,"page":74},"En","\u002Fen","en",[9],{"title":10,"path":11,"stem":12,"children":13,"page":74},"Docs","\u002Fen\u002Fdocs","en\u002Fdocs",[14,75,134],{"title":15,"icon":16,"path":17,"stem":18,"children":19,"page":74},"Guides","i-lucide-book-open","\u002Fen\u002Fdocs\u002Fguides","en\u002Fdocs\u002F1.guides",[20,25,30,35,40,44,49,54,59,64,69],{"title":21,"path":22,"stem":23,"icon":24},"Introduction","\u002Fen\u002Fdocs\u002Fguides\u002Fintroduction","en\u002Fdocs\u002F1.guides\u002F01.introduction","i-lucide-info",{"title":26,"path":27,"stem":28,"icon":29},"Quick start","\u002Fen\u002Fdocs\u002Fguides\u002Fquick-start","en\u002Fdocs\u002F1.guides\u002F02.quick-start","i-lucide-play",{"title":31,"path":32,"stem":33,"icon":34},"Add the office computers","\u002Fen\u002Fdocs\u002Fguides\u002Foffice","en\u002Fdocs\u002F1.guides\u002F03.office","i-lucide-building-2",{"title":36,"path":37,"stem":38,"icon":39},"Rendering on GPU Rental Platforms","\u002Fen\u002Fdocs\u002Fguides\u002Fcloud","en\u002Fdocs\u002F1.guides\u002F04.cloud","i-lucide-cloud",{"title":41,"path":42,"stem":43},"Configuring Cloudflare R2 Storage","\u002Fen\u002Fdocs\u002Fguides\u002Fr2","en\u002Fdocs\u002F1.guides\u002F05.r2",{"title":45,"path":46,"stem":47,"icon":48},"Run the host on a NAS","\u002Fen\u002Fdocs\u002Fguides\u002Fnas","en\u002Fdocs\u002F1.guides\u002F06.nas","i-lucide-hard-drive",{"title":50,"path":51,"stem":52,"icon":53},"Devices in different places","\u002Fen\u002Fdocs\u002Fguides\u002Fremote","en\u002Fdocs\u002F1.guides\u002F07.remote","i-lucide-globe",{"title":55,"path":56,"stem":57,"icon":58},"Get the film","\u002Fen\u002Fdocs\u002Fguides\u002Ffilm","en\u002Fdocs\u002F1.guides\u002F08.film","i-lucide-film",{"title":60,"path":61,"stem":62,"icon":63},"When files change","\u002Fen\u002Fdocs\u002Fguides\u002Fupdate","en\u002Fdocs\u002F1.guides\u002F09.update","i-lucide-refresh-cw",{"title":65,"path":66,"stem":67,"icon":68},"When the host fails or moves","\u002Fen\u002Fdocs\u002Fguides\u002Fhost","en\u002Fdocs\u002F1.guides\u002F10.host","i-lucide-server-cog",{"title":70,"path":71,"stem":72,"icon":73},"FAQ","\u002Fen\u002Fdocs\u002Fguides\u002Ffaq","en\u002Fdocs\u002F1.guides\u002F11.faq","i-lucide-circle-help",false,{"title":76,"icon":77,"path":78,"stem":79,"children":80,"page":74},"Concepts","i-lucide-box","\u002Fen\u002Fdocs\u002Fconcepts","en\u002Fdocs\u002F2.concepts",[81,86,90,95,100,105,109,114,119,124,129],{"title":82,"path":83,"stem":84,"icon":85},"Workspace and host","\u002Fen\u002Fdocs\u002Fconcepts\u002Fworkspace","en\u002Fdocs\u002F2.concepts\u002F01.workspace","i-lucide-network",{"title":87,"path":88,"stem":89,"icon":48},"Store","\u002Fen\u002Fdocs\u002Fconcepts\u002Fstorage","en\u002Fdocs\u002F2.concepts\u002F02.storage",{"title":91,"path":92,"stem":93,"icon":94},"Assets, Unpacking & On-Demand Pull","\u002Fen\u002Fdocs\u002Fconcepts\u002Fassets","en\u002Fdocs\u002F2.concepts\u002F03.assets","i-lucide-images",{"title":96,"path":97,"stem":98,"icon":99},"Projects, tasks and versions","\u002Fen\u002Fdocs\u002Fconcepts\u002Ftasks","en\u002Fdocs\u002F2.concepts\u002F04.tasks","i-lucide-list-checks",{"title":101,"path":102,"stem":103,"icon":104},"Shots and chunks","\u002Fen\u002Fdocs\u002Fconcepts\u002Fshots","en\u002Fdocs\u002F2.concepts\u002F05.shots","i-lucide-clapperboard",{"title":106,"path":107,"stem":108,"icon":58},"Films","\u002Fen\u002Fdocs\u002Fconcepts\u002Ffilm","en\u002Fdocs\u002F2.concepts\u002F06.film",{"title":110,"path":111,"stem":112,"icon":113},"Blender and FFmpeg","\u002Fen\u002Fdocs\u002Fconcepts\u002Fruntimes","en\u002Fdocs\u002F2.concepts\u002F07.runtimes","i-lucide-package",{"title":115,"path":116,"stem":117,"icon":118},"Joining and access","\u002Fen\u002Fdocs\u002Fconcepts\u002Fjoining","en\u002Fdocs\u002F2.concepts\u002F08.joining","i-lucide-key-round",{"title":120,"path":121,"stem":122,"icon":123},"Render seats","\u002Fen\u002Fdocs\u002Fconcepts\u002Fseats","en\u002Fdocs\u002F2.concepts\u002F09.seats","i-lucide-gauge",{"title":125,"path":126,"stem":127,"icon":128},"Backup and takeover","\u002Fen\u002Fdocs\u002Fconcepts\u002Fbackup","en\u002Fdocs\u002F2.concepts\u002F10.backup","i-lucide-life-buoy",{"title":130,"path":131,"stem":132,"icon":133},"Plugins","\u002Fen\u002Fdocs\u002Fconcepts\u002Fplugins","en\u002Fdocs\u002F2.concepts\u002F11.plugins","i-lucide-puzzle",{"title":135,"icon":136,"path":137,"stem":138,"children":139,"page":74},"Reference","i-lucide-library","\u002Fen\u002Fdocs\u002Freference","en\u002Fdocs\u002F3.reference",[140,145,150,154],{"title":141,"path":142,"stem":143,"icon":144},"Command line","\u002Fen\u002Fdocs\u002Freference\u002Fcli","en\u002Fdocs\u002F3.reference\u002F1.cli","i-lucide-terminal",{"title":146,"path":147,"stem":148,"icon":149},"Configuration","\u002Fen\u002Fdocs\u002Freference\u002Fconfig","en\u002Fdocs\u002F3.reference\u002F2.config","i-lucide-sliders",{"title":151,"path":152,"stem":153},"HTTP API","\u002Fen\u002Fdocs\u002Freference\u002Fapi","en\u002Fdocs\u002F3.reference\u002F3.api",{"title":155,"path":156,"stem":157},"Plugin development","\u002Fen\u002Fdocs\u002Freference\u002Fplugins","en\u002Fdocs\u002F3.reference\u002F4.plugins",{"id":159,"title":155,"body":160,"description":1636,"extension":1637,"meta":1638,"navigation":609,"path":156,"seo":1639,"stem":157,"__hash__":1643},"docs_en\u002Fen\u002Fdocs\u002F3.reference\u002F4.plugins.md",{"type":161,"value":162,"toc":1621},"minimark",[163,171,174,179,219,223,238,363,511,516,521,574,577,581,588,654,657,668,670,818,824,828,867,884,935,941,1115,1134,1137,1181,1185,1188,1205,1209,1215,1263,1269,1350,1356,1371,1384,1388,1394,1450,1464,1471,1537,1580,1594,1598,1617],[164,165,166,167,170],"p",{},"A plugin is an npm package installed on the host that adds something danio doesn't do itself, such as posting task status to a chat group or uploading films to a cloud drive. This page is about how to write one. For installing and managing plugins, see ",[168,169,130],"a",{"href":131},".",[164,172,173],{},"You write a plugin in JavaScript or TypeScript, and Bun runs it. danio starts one Bun process on the host and loads every enabled plugin into it.",[175,176,178],"h2",{"id":177},"a-few-things-to-know-first","A few things to know first",[180,181,182,190,196,202,213],"ul",{},[183,184,185,189],"li",{},[186,187,188],"strong",{},"It runs only on the host."," Artists' computers and render machines don't install Bun and don't run plugins.",[183,191,192,195],{},[186,193,194],{},"danio manages the life cycle."," Installing, enabling, updating, settings and logs are all in danio; you don't deploy a process yourself.",[183,197,198,201],{},[186,199,200],{},"One plugin's error affects only that plugin."," A plugin that fails to load is marked on its own and the rest carry on. When the Bun process exits, danio restarts it, waiting 1 second at first and at most 5 minutes. Even if every plugin crashes, rendering is not affected.",[183,203,204,207,208,212],{},[186,205,206],{},"Permissions are declared."," A plugin calls the workspace API through ",[209,210,211],"code",{},"ctx.api"," and has only the permissions it declared and an admin confirmed.",[183,214,215,218],{},[186,216,217],{},"It is not a sandbox."," A plugin runs on the host with danio's rights, can use the network and can read and write files. Permissions limit only what it does to the workspace.",[175,220,222],{"id":221},"the-package","The package",[164,224,225,226,229,230,233,234,237],{},"A plugin is an npm package, or a local folder or a ",[209,227,228],{},".tgz"," file. The ",[209,231,232],{},"danio"," field in ",[209,235,236],{},"package.json"," describes the plugin:",[239,240,245],"pre",{"className":241,"code":242,"language":243,"meta":244,"style":244},"language-json shiki shiki-themes material-theme-palenight","{\n  \"name\": \"@my-studio\u002Fdanio-chat-notify\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"main\": \"index.js\",\n  \"engines\": { \"danio\": \">=1\" },\n  \"danio\": {\n    \"name\": \"Chat notifications\",\n    \"summary\": { \"zh\": \"任务完成和出错时发到聊天群\", \"en\": \"Post task results to a chat group\" },\n    \"permissions\": [],\n    \"settings\": {\n      \"type\": \"object\",\n      \"required\": [\"webhookUrl\"],\n      \"properties\": {\n        \"webhookUrl\": { \"type\": \"string\", \"title\": \"Webhook URL\", \"secret\": true }\n      }\n    }\n  }\n}\n","json","",[209,246,247,255,261,267,273,279,285,291,297,303,309,315,321,327,333,339,345,351,357],{"__ignoreMap":244},[248,249,252],"span",{"class":250,"line":251},"line",1,[248,253,254],{},"{\n",[248,256,258],{"class":250,"line":257},2,[248,259,260],{},"  \"name\": \"@my-studio\u002Fdanio-chat-notify\",\n",[248,262,264],{"class":250,"line":263},3,[248,265,266],{},"  \"version\": \"1.0.0\",\n",[248,268,270],{"class":250,"line":269},4,[248,271,272],{},"  \"type\": \"module\",\n",[248,274,276],{"class":250,"line":275},5,[248,277,278],{},"  \"main\": \"index.js\",\n",[248,280,282],{"class":250,"line":281},6,[248,283,284],{},"  \"engines\": { \"danio\": \">=1\" },\n",[248,286,288],{"class":250,"line":287},7,[248,289,290],{},"  \"danio\": {\n",[248,292,294],{"class":250,"line":293},8,[248,295,296],{},"    \"name\": \"Chat notifications\",\n",[248,298,300],{"class":250,"line":299},9,[248,301,302],{},"    \"summary\": { \"zh\": \"任务完成和出错时发到聊天群\", \"en\": \"Post task results to a chat group\" },\n",[248,304,306],{"class":250,"line":305},10,[248,307,308],{},"    \"permissions\": [],\n",[248,310,312],{"class":250,"line":311},11,[248,313,314],{},"    \"settings\": {\n",[248,316,318],{"class":250,"line":317},12,[248,319,320],{},"      \"type\": \"object\",\n",[248,322,324],{"class":250,"line":323},13,[248,325,326],{},"      \"required\": [\"webhookUrl\"],\n",[248,328,330],{"class":250,"line":329},14,[248,331,332],{},"      \"properties\": {\n",[248,334,336],{"class":250,"line":335},15,[248,337,338],{},"        \"webhookUrl\": { \"type\": \"string\", \"title\": \"Webhook URL\", \"secret\": true }\n",[248,340,342],{"class":250,"line":341},16,[248,343,344],{},"      }\n",[248,346,348],{"class":250,"line":347},17,[248,349,350],{},"    }\n",[248,352,354],{"class":250,"line":353},18,[248,355,356],{},"  }\n",[248,358,360],{"class":250,"line":359},19,[248,361,362],{},"}\n",[364,365,366,379],"table",{},[367,368,369],"thead",{},[370,371,372,376],"tr",{},[373,374,375],"th",{},"Field",[373,377,378],{},"Description",[380,381,382,400,431,440,450,464,477,501],"tbody",{},[370,383,384,390],{},[385,386,387],"td",{},[209,388,389],{},"main",[385,391,392,393,396,397],{},"The entry file. When missing, ",[209,394,395],{},"module"," is used, and then ",[209,398,399],{},"index.js",[370,401,402,407],{},[385,403,404],{},[209,405,406],{},"engines.danio",[385,408,409,410,413,414,417,418,417,421,417,423,426,427,430],{},"The major version of the plugin interface it needs, currently ",[209,411,412],{},"1",". You can write ",[209,415,416],{},">=1",", ",[209,419,420],{},"^1",[209,422,412],{},[209,424,425],{},"1.x"," or ",[209,428,429],{},"*",". If it isn't compatible, danio doesn't install it",[370,432,433,437],{},[385,434,435],{},[209,436,232],{},[385,438,439],{},"Required. A package without this field is not a danio plugin",[370,441,442,447],{},[385,443,444],{},[209,445,446],{},"danio.name",[385,448,449],{},"The name shown; the package name when missing",[370,451,452,457],{},[385,453,454],{},[209,455,456],{},"danio.icon",[385,458,459,460,463],{},"The address of an icon, such as a ",[209,461,462],{},"data:"," address",[370,465,466,471],{},[385,467,468],{},[209,469,470],{},"danio.summary",[385,472,473,474],{},"A one-line description per language: ",[209,475,476],{},"{ \"zh\": \"…\", \"en\": \"…\" }",[370,478,479,484],{},[385,480,481],{},[209,482,483],{},"danio.permissions",[385,485,486,487,490,491,493,494,497,498],{},"The workspace permissions it needs, such as ",[209,488,489],{},"workspace.view",". A plugin that doesn't call ",[209,492,211],{}," can write ",[209,495,496],{},"[]",". For the full list of permissions, see ",[168,499,151],{"href":500},"\u002Fen\u002Fdocs\u002Freference\u002Fapi#tokens-and-permissions",[370,502,503,508],{},[385,504,505],{},[209,506,507],{},"danio.settings",[385,509,510],{},"A JSON Schema for the settings",[512,513,515],"h3",{"id":514},"settings","Settings",[164,517,518,520],{},[209,519,507],{}," is a JSON Schema, and the admin fills in the settings in the interface according to it. The interface understands these:",[180,522,523,551,557,564],{},[183,524,525,526,529,530,533,534,417,537,417,540,426,543,546,547,550],{},"The ",[209,527,528],{},"type"," of each item in ",[209,531,532],{},"properties"," can be ",[209,535,536],{},"string",[209,538,539],{},"number",[209,541,542],{},"integer",[209,544,545],{},"boolean","; with an ",[209,548,549],{},"enum",", it is a drop-down.",[183,552,553,556],{},[209,554,555],{},"title"," is the name shown in the interface; the key is shown when it is missing.",[183,558,559,560,563],{},"When an item in ",[209,561,562],{},"required"," isn't filled in, the plugin's row says so.",[183,565,566,567,570,571,170],{},"An item with ",[209,568,569],{},"secret: true"," is write-only: its input is a password field, after saving only \"set\" is shown, and leaving it empty means no change. The plugin itself can read it in ",[209,572,573],{},"ctx.settings",[164,575,576],{},"When the settings change, the plugin restarts.",[175,578,580],{"id":579},"the-entry","The entry",[164,582,583,584,587],{},"The entry file exports a function by default. Its argument is ",[209,585,586],{},"ctx",", and it returns the hooks the plugin implements. The function can be async:",[239,589,593],{"className":590,"code":591,"language":592,"meta":244,"style":244},"language-js shiki shiki-themes material-theme-palenight","export default async (ctx) => {\n  ctx.log('plugin loaded')\n\n  return {\n    events: {\n      types: ['job.done', 'job.attention'],\n      handle: async (event) => {\n        \u002F\u002F handle the event\n      },\n    },\n  }\n}\n","js",[209,594,595,600,605,611,616,621,626,631,636,641,646,650],{"__ignoreMap":244},[248,596,597],{"class":250,"line":251},[248,598,599],{},"export default async (ctx) => {\n",[248,601,602],{"class":250,"line":257},[248,603,604],{},"  ctx.log('plugin loaded')\n",[248,606,607],{"class":250,"line":263},[248,608,610],{"emptyLinePlaceholder":609},true,"\n",[248,612,613],{"class":250,"line":269},[248,614,615],{},"  return {\n",[248,617,618],{"class":250,"line":275},[248,619,620],{},"    events: {\n",[248,622,623],{"class":250,"line":281},[248,624,625],{},"      types: ['job.done', 'job.attention'],\n",[248,627,628],{"class":250,"line":287},[248,629,630],{},"      handle: async (event) => {\n",[248,632,633],{"class":250,"line":293},[248,634,635],{},"        \u002F\u002F handle the event\n",[248,637,638],{"class":250,"line":299},[248,639,640],{},"      },\n",[248,642,643],{"class":250,"line":305},[248,644,645],{},"    },\n",[248,647,648],{"class":250,"line":311},[248,649,356],{},[248,651,652],{"class":250,"line":317},[248,653,362],{},[164,655,656],{},"If it doesn't export a function, or the function throws, the plugin fails to load and the interface shows why.",[164,658,659,660,663,664,667],{},"There are two hooks now: ",[209,661,662],{},"events"," subscribes to events and ",[209,665,666],{},"output"," receives films. Both are optional.",[175,669,586],{"id":586},[364,671,672,681],{},[367,673,674],{},[370,675,676,679],{},[373,677,678],{},"Member",[373,680,378],{},[380,682,683,724,737,765,795,805],{},[370,684,685,689],{},[385,686,687],{},[209,688,211],{},[385,690,691,692,417,695,417,698,417,701,417,704,707,708,711,712,715,716],{},"A client for the workspace API: ",[209,693,694],{},"get(path)",[209,696,697],{},"post(path, body)",[209,699,700],{},"put(path, body)",[209,702,703],{},"patch(path, body)",[209,705,706],{},"delete(path)",". ",[209,709,710],{},"path"," must start with ",[209,713,714],{},"\u002Fapi\u002Fv1\u002F",", and the result is parsed JSON. What it does is recorded under \"Plugin: ",[717,718,719,720,723],"name",{},"\" and has only the permissions an admin confirmed. On an error it throws an error that starts with the status code, such as ",[209,721,722],{},"403 …",". A call takes at most 2 minutes",[370,725,726,730],{},[385,727,728],{},[209,729,573],{},[385,731,732,733,736],{},"The settings the admin filled in, including the ",[209,734,735],{},"secret"," ones",[370,738,739,744],{},[385,740,741],{},[209,742,743],{},"ctx.state",[385,745,746,747,417,750,417,753,756,757,760,761,764],{},"The plugin's own small key-value store: ",[209,748,749],{},"get(key)",[209,751,752],{},"set(key, value)",[209,754,755],{},"delete(key)",". Values are JSON, each at most 64 KB. It is kept in the host's database and goes with backups. ",[209,758,759],{},"get"," returns ",[209,762,763],{},"null"," when the key isn't there",[370,766,767,772],{},[385,768,769],{},[209,770,771],{},"ctx.log",[385,773,774,775,778,779,417,782,417,785,417,788,707,791,794],{},"Write to the log: ",[209,776,777],{},"ctx.log(…)",", and ",[209,780,781],{},"ctx.log.info",[209,783,784],{},"warn",[209,786,787],{},"error",[209,789,790],{},"debug",[209,792,793],{},"console.log"," in a plugin is recorded here too",[370,796,797,802],{},[385,798,799],{},[209,800,801],{},"ctx.notify(title, body)",[385,803,804],{},"Show a notice in the danio interface, marked with the plugin it comes from",[370,806,807,812],{},[385,808,809],{},[209,810,811],{},"ctx.workspace",[385,813,814,817],{},[209,815,816],{},"{ id, name, addresses }",": the workspace ID and name, and the list of the host's addresses",[164,819,820,821,170],{},"For everything else, use what Bun and npm offer, such as ",[209,822,823],{},"fetch",[175,825,827],{"id":826},"events-subscribe-to-events","events: subscribe to events",[239,829,831],{"className":590,"code":830,"language":592,"meta":244,"style":244},"events: {\n  types: ['job.done', 'shot.failed'],\n  handle: async (event) => {\n    const { type, data } = event\n    \u002F\u002F …\n  },\n}\n",[209,832,833,838,843,848,853,858,863],{"__ignoreMap":244},[248,834,835],{"class":250,"line":251},[248,836,837],{},"events: {\n",[248,839,840],{"class":250,"line":257},[248,841,842],{},"  types: ['job.done', 'shot.failed'],\n",[248,844,845],{"class":250,"line":263},[248,846,847],{},"  handle: async (event) => {\n",[248,849,850],{"class":250,"line":269},[248,851,852],{},"    const { type, data } = event\n",[248,854,855],{"class":250,"line":275},[248,856,857],{},"    \u002F\u002F …\n",[248,859,860],{"class":250,"line":281},[248,861,862],{},"  },\n",[248,864,865],{"class":250,"line":287},[248,866,362],{},[164,868,869,872,873,875,876,879,880,883],{},[209,870,871],{},"types"," lists the event types to subscribe to; ",[209,874,429],{}," means all. The ",[209,877,878],{},"event"," that ",[209,881,882],{},"handle"," receives has these fields:",[364,885,886,894],{},[367,887,888],{},[370,889,890,892],{},[373,891,375],{},[373,893,378],{},[380,895,896,906,915,925],{},[370,897,898,903],{},[385,899,900],{},[209,901,902],{},"id",[385,904,905],{},"The event ID, for removing duplicates: the same event may arrive more than once",[370,907,908,912],{},[385,909,910],{},[209,911,528],{},[385,913,914],{},"The event type",[370,916,917,922],{},[385,918,919],{},[209,920,921],{},"at",[385,923,924],{},"When it happened, in ISO 8601",[370,926,927,932],{},[385,928,929],{},[209,930,931],{},"data",[385,933,934],{},"The event's data, see the table below",[164,936,937,938,940],{},"The event types and the fields in ",[209,939,931],{},":",[364,942,943,957],{},[367,944,945],{},[370,946,947,950,953],{},[373,948,949],{},"Type",[373,951,952],{},"When",[373,954,955],{},[209,956,931],{},[380,958,959,980,1006,1024,1044,1062,1079,1099],{},[370,960,961,966,969],{},[385,962,963],{},[209,964,965],{},"job.submitted",[385,967,968],{},"A new task was submitted",[385,970,971,417,974,417,977],{},[209,972,973],{},"jobId",[209,975,976],{},"version",[209,978,979],{},"by",[370,981,982,987,990],{},[385,983,984],{},[209,985,986],{},"shot.done",[385,988,989],{},"A chunk finished rendering",[385,991,992,417,994,417,996,999,1000,417,1003],{},[209,993,973],{},[209,995,976],{},[209,997,998],{},"shot"," (which chunk), ",[209,1001,1002],{},"frames",[209,1004,1005],{},"device",[370,1007,1008,1013,1016],{},[385,1009,1010],{},[209,1011,1012],{},"shot.failed",[385,1014,1015],{},"A chunk failed",[385,1017,1018,1019,417,1021],{},"The same, plus ",[209,1020,787],{},[209,1022,1023],{},"attempts",[370,1025,1026,1031,1034],{},[385,1027,1028],{},[209,1029,1030],{},"job.attention",[385,1032,1033],{},"A task stopped for a person to act: repeated failures, missing files, a failed stitch",[385,1035,1036,417,1038,417,1040,1043],{},[209,1037,973],{},[209,1039,976],{},[209,1041,1042],{},"reason"," (empty when paused by hand)",[370,1045,1046,1051,1054],{},[385,1047,1048],{},[209,1049,1050],{},"output.ready",[385,1052,1053],{},"The film is stitched",[385,1055,1056,417,1058,417,1060],{},[209,1057,973],{},[209,1059,976],{},[209,1061,666],{},[370,1063,1064,1069,1074],{},[385,1065,1066],{},[209,1067,1068],{},"job.done",[385,1070,1071,1072],{},"The task is done, after ",[209,1073,1050],{},[385,1075,1076,1077],{},"The same as ",[209,1078,1050],{},[370,1080,1081,1086,1089],{},[385,1082,1083],{},[209,1084,1085],{},"device.joined",[385,1087,1088],{},"A device joined",[385,1090,1091,417,1094,417,1096],{},[209,1092,1093],{},"deviceId",[209,1095,1005],{},[209,1097,1098],{},"access",[370,1100,1101,1106,1109],{},[385,1102,1103],{},[209,1104,1105],{},"device.removed",[385,1107,1108],{},"A device was removed",[385,1110,1111,417,1113],{},[209,1112,1093],{},[209,1114,1005],{},[164,1116,1117,1118,417,1120,1122,1123,1126,1127,1130,1131,170],{},"For events with a ",[209,1119,973],{},[209,1121,931],{}," also has ",[209,1124,1125],{},"job"," (the task name), ",[209,1128,1129],{},"project"," (the project name) and ",[209,1132,1133],{},"projectId",[164,1135,1136],{},"An example that posts to a chat group when a task is done:",[239,1138,1140],{"className":590,"code":1139,"language":592,"meta":244,"style":244},"handle: async ({ type, data }) => {\n  if (type !== 'job.done') return\n  await fetch(ctx.settings.webhookUrl, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\u002Fjson' },\n    body: JSON.stringify({ text: `${data.project} \u002F ${data.job} v${data.version} finished rendering` }),\n  })\n}\n",[209,1141,1142,1147,1152,1157,1162,1167,1172,1177],{"__ignoreMap":244},[248,1143,1144],{"class":250,"line":251},[248,1145,1146],{},"handle: async ({ type, data }) => {\n",[248,1148,1149],{"class":250,"line":257},[248,1150,1151],{},"  if (type !== 'job.done') return\n",[248,1153,1154],{"class":250,"line":263},[248,1155,1156],{},"  await fetch(ctx.settings.webhookUrl, {\n",[248,1158,1159],{"class":250,"line":269},[248,1160,1161],{},"    method: 'POST',\n",[248,1163,1164],{"class":250,"line":275},[248,1165,1166],{},"    headers: { 'Content-Type': 'application\u002Fjson' },\n",[248,1168,1169],{"class":250,"line":281},[248,1170,1171],{},"    body: JSON.stringify({ text: `${data.project} \u002F ${data.job} v${data.version} finished rendering` }),\n",[248,1173,1174],{"class":250,"line":287},[248,1175,1176],{},"  })\n",[248,1178,1179],{"class":250,"line":293},[248,1180,362],{},[512,1182,1184],{"id":1183},"delivery","Delivery",[164,1186,1187],{},"danio first stores the event in the host's database, then gives it to the plugins that subscribed to its type, so it arrives at least once:",[180,1189,1190,1196,1199,1202],{},[183,1191,1192,1193,1195],{},"If ",[209,1194,882],{}," throws, or doesn't return within 1 minute, danio retries, waiting 2 seconds at first, doubling each time, at most 1 hour.",[183,1197,1198],{},"After the plugin process crashes or the host restarts, events not yet delivered are delivered then.",[183,1200,1201],{},"Events that come while a plugin is disabled are kept and delivered when it is enabled again. Deleting the plugin deletes them too.",[183,1203,1204],{},"An event not delivered within a day is given up, and danio writes a log line.",[175,1206,1208],{"id":1207},"output-deliver-films","output: deliver films",[164,1210,1211,1212,1214],{},"When a film is stitched, danio hands it to every plugin that implements ",[209,1213,666],{},", which uploads it to a cloud drive or another system:",[239,1216,1218],{"className":590,"code":1217,"language":592,"meta":244,"style":244},"output: {\n  handle: async ({ job, project, version, film }) => {\n    const res = await fetch(`https:\u002F\u002Fexample.com\u002Fupload\u002F${film.name}`, {\n      method: 'PUT',\n      body: Bun.file(film.path),\n    })\n    return { url: `https:\u002F\u002Fexample.com\u002Ffilms\u002F${film.name}`, note: `Uploaded to ${project}` }\n  },\n}\n",[209,1219,1220,1225,1230,1235,1240,1245,1250,1255,1259],{"__ignoreMap":244},[248,1221,1222],{"class":250,"line":251},[248,1223,1224],{},"output: {\n",[248,1226,1227],{"class":250,"line":257},[248,1228,1229],{},"  handle: async ({ job, project, version, film }) => {\n",[248,1231,1232],{"class":250,"line":263},[248,1233,1234],{},"    const res = await fetch(`https:\u002F\u002Fexample.com\u002Fupload\u002F${film.name}`, {\n",[248,1236,1237],{"class":250,"line":269},[248,1238,1239],{},"      method: 'PUT',\n",[248,1241,1242],{"class":250,"line":275},[248,1243,1244],{},"      body: Bun.file(film.path),\n",[248,1246,1247],{"class":250,"line":281},[248,1248,1249],{},"    })\n",[248,1251,1252],{"class":250,"line":287},[248,1253,1254],{},"    return { url: `https:\u002F\u002Fexample.com\u002Ffilms\u002F${film.name}`, note: `Uploaded to ${project}` }\n",[248,1256,1257],{"class":250,"line":293},[248,1258,862],{},[248,1260,1261],{"class":250,"line":299},[248,1262,362],{},[164,1264,1265,1266,1268],{},"The argument of ",[209,1267,882],{}," has:",[364,1270,1271,1279],{},[367,1272,1273],{},[370,1274,1275,1277],{},[373,1276,375],{},[373,1278,378],{},[380,1280,1281,1298,1307,1317,1330,1340],{},[370,1282,1283,1295],{},[385,1284,1285,417,1287,417,1289,417,1291,417,1293],{},[209,1286,973],{},[209,1288,1125],{},[209,1290,1129],{},[209,1292,1133],{},[209,1294,976],{},[385,1296,1297],{},"The task and the version",[370,1299,1300,1304],{},[385,1301,1302],{},[209,1303,666],{},[385,1305,1306],{},"Where the film is in the store",[370,1308,1309,1314],{},[385,1310,1311],{},[209,1312,1313],{},"film.path",[385,1315,1316],{},"The path of a temporary file on the host. The film has already been fetched from the store, so the plugin needs no store credentials",[370,1318,1319,1324],{},[385,1320,1321],{},[209,1322,1323],{},"film.name",[385,1325,1326,1327],{},"The file name: the task name, the version number and the extension, such as ",[209,1328,1329],{},"sh010-Main_v2.mp4",[370,1331,1332,1337],{},[385,1333,1334],{},[209,1335,1336],{},"film.size",[385,1338,1339],{},"The size in bytes",[370,1341,1342,1347],{},[385,1343,1344],{},[209,1345,1346],{},"film.open()",[385,1348,1349],{},"Returns a stream that reads the file",[164,1351,1352,1353,1355],{},"When ",[209,1354,882],{}," returns, the temporary file is deleted.",[164,1357,1358,1359,1362,1363,1366,1367,1370],{},"The returned ",[209,1360,1361],{},"url"," and ",[209,1364,1365],{},"note"," are both optional. They show under ",[1368,1369,1184],"ui",{}," in the task details, one row per version and plugin: sending, retrying, sent, failed.",[164,1372,1352,1373,1375,1376,1379,1380,1383],{},[209,1374,882],{}," throws, danio retries as it does for events, waiting at most 1 hour between tries, and one try runs at most 1 hour. If it hasn't succeeded within a day, it is marked failed. After a failure, click ",[1368,1377,1378],{},"Send again",", or call ",[209,1381,1382],{},"POST \u002Fapi\u002Fv1\u002Fjobs\u002F{id}\u002Fhandovers\u002F{plugin}\u002Fretry",", to hand it to the plugin again.",[175,1385,1387],{"id":1386},"install-and-debug-locally","Install and debug locally",[164,1389,1390,1391,1393],{},"While you write a plugin you don't need to publish it to npm. Install a local folder or ",[209,1392,228],{}," on the host:",[239,1395,1399],{"className":1396,"code":1397,"language":1398,"meta":244,"style":244},"language-bash shiki shiki-themes material-theme-palenight","# install a folder\ndanio plugin add .\u002Fmy-plugin\n\n# or pack it first\nnpm pack\ndanio plugin add .\u002Fmy-plugin-1.0.0.tgz\n","bash",[209,1400,1401,1407,1422,1426,1431,1439],{"__ignoreMap":244},[248,1402,1403],{"class":250,"line":251},[248,1404,1406],{"class":1405},"saEQR","# install a folder\n",[248,1408,1409,1412,1416,1419],{"class":250,"line":257},[248,1410,232],{"class":1411},"s5Dmg",[248,1413,1415],{"class":1414},"sfyAc"," plugin",[248,1417,1418],{"class":1414}," add",[248,1420,1421],{"class":1414}," .\u002Fmy-plugin\n",[248,1423,1424],{"class":250,"line":263},[248,1425,610],{"emptyLinePlaceholder":609},[248,1427,1428],{"class":250,"line":269},[248,1429,1430],{"class":1405},"# or pack it first\n",[248,1432,1433,1436],{"class":250,"line":275},[248,1434,1435],{"class":1411},"npm",[248,1437,1438],{"class":1414}," pack\n",[248,1440,1441,1443,1445,1447],{"class":250,"line":281},[248,1442,232],{"class":1411},[248,1444,1415],{"class":1414},[248,1446,1418],{"class":1414},[248,1448,1449],{"class":1414}," .\u002Fmy-plugin-1.0.0.tgz\n",[164,1451,1452,1455,1456,1459,1460,1463],{},[209,1453,1454],{},"danio plugin add"," first shows the permissions the plugin needs and enables it only after you answer ",[209,1457,1458],{},"y",". Add ",[209,1461,1462],{},"--yes"," to skip the question.",[164,1465,1466,1467,1470],{},"After changing the code, run ",[209,1468,1469],{},"danio plugin update",". danio reinstalls every plugin from where it came from, keeping settings and permissions.",[364,1472,1473,1483],{},[367,1474,1475],{},[370,1476,1477,1480],{},[373,1478,1479],{},"Command",[373,1481,1482],{},"What it does",[380,1484,1485,1495,1505,1515,1524],{},[370,1486,1487,1492],{},[385,1488,1489],{},[209,1490,1491],{},"danio plugin [--json]",[385,1493,1494],{},"List plugins: state, version, permissions, subscribed events, the latest error",[370,1496,1497,1502],{},[385,1498,1499],{},[209,1500,1501],{},"danio plugin add \u003Cnpm package | folder | .tgz> [--yes]",[385,1503,1504],{},"Install, and enable after the permissions are confirmed",[370,1506,1507,1512],{},[385,1508,1509],{},[209,1510,1511],{},"danio plugin on|off|rm \u003Cplugin>",[385,1513,1514],{},"Enable, disable, delete",[370,1516,1517,1521],{},[385,1518,1519],{},[209,1520,1469],{},[385,1522,1523],{},"Reinstall every plugin from its own source",[370,1525,1526,1531],{},[385,1527,1528],{},[209,1529,1530],{},"danio plugin config \u003Cplugin> [key=value]…",[385,1532,1533,1534,1536],{},"Change settings; without values it asks one by one. ",[209,1535,735],{}," items can't be written on the command line",[164,1538,1539,1540,1362,1543,417,1546,1549,1550,1362,1553,1556,1557,1560,1561,417,1563,426,1566,1569,1570,1572,1573,1576,1577,170],{},"For ",[209,1541,1542],{},"on",[209,1544,1545],{},"config",[209,1547,1548],{},"\u003Cplugin>"," can be the ID, the display name or the package name; ",[209,1551,1552],{},"off",[209,1554,1555],{},"rm"," accept only the ID. The ID is the package name without the ",[209,1558,1559],{},"@",", with every character that isn't a lowercase letter, digit, ",[209,1562,170],{},[209,1564,1565],{},"_",[209,1567,1568],{},"-"," replaced by ",[209,1571,1568],{},"; for ",[209,1574,1575],{},"@my-studio\u002Fdanio-chat-notify"," it is ",[209,1578,1579],{},"my-studio-danio-chat-notify",[164,1581,1582,1583,1586,1587,1590,1591,1593],{},"These commands run on the host. In the interface, on the ",[1368,1584,1585],{},"Plugins and integrations"," page, each plugin's menu has ",[1368,1588,1589],{},"Log",", which shows the plugin's log, including what ",[209,1592,793],{}," prints.",[512,1595,1597],{"id":1596},"where-bun-comes-from","Where Bun comes from",[164,1599,1600,1601,1604,1605,1608,1609,1612,1613,1616],{},"danio looks in this order: the one named by the environment variable ",[209,1602,1603],{},"DANIO_BUN",", the one on PATH, and the one downloaded earlier into ",[209,1606,1607],{},"runtimes\u002Fbun\u002F"," in the data directory. If there is none, it downloads the latest release from Bun's GitHub releases page, checks it against its ",[209,1610,1611],{},"SHASUMS256.txt"," and puts it there. Plugins are installed in ",[209,1614,1615],{},"data\u002Fplugins\u002F\u003CID>\u002F"," in the data directory.",[1618,1619,1620],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .saEQR, html code.shiki .saEQR{--shiki-default:#676E95;--shiki-default-font-style:italic}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}",{"title":244,"searchDepth":257,"depth":263,"links":1622},[1623,1624,1627,1628,1629,1632,1633],{"id":177,"depth":257,"text":178},{"id":221,"depth":257,"text":222,"children":1625},[1626],{"id":514,"depth":263,"text":515},{"id":579,"depth":257,"text":580},{"id":586,"depth":257,"text":586},{"id":826,"depth":257,"text":827,"children":1630},[1631],{"id":1183,"depth":263,"text":1184},{"id":1207,"depth":257,"text":1208},{"id":1386,"depth":257,"text":1387,"children":1634},[1635],{"id":1596,"depth":263,"text":1597},"[object Object]","md",{},{"title":155,"description":1640},{"How to write a danio plugin":1641,"navigation":1642},"the package structure, permissions and settings, what is in ctx, the events and film-delivery hooks, and how to install and debug.",{"icon":133},"Rs9e1WpYAJfW-Z0xmz6TTN_7U90ylheuD8a2If_alg8",[1645,1646],{"title":151,"path":152,"stem":153,"description":1636,"children":-1},null,1794627696229]