Documentation

How Kinode Media Server works, how to run it, and what to do when it misbehaves.

What this is

Kinode Media Server serves the video and music already on your own disks to the devices you already own. It runs on one machine in your house. Everything else, meaning phones, TVs, browsers and consoles, connects to that machine.

There is no subscription, and no feature is held back for a paid tier. Remote streaming, hardware transcoding, subtitle handling and library sharing are all included because there is nothing to sell you.

The pieces

PieceRuns whereWhat it does
The serverYour machineReads your library, serves the web app, streams and converts video and audio.
The hubusekinode.appAn account system and a directory. It helps apps find your server when you are away from home. It never touches your video.
AppsPhones, TVsAndroid and Roku clients. They talk to your server directly whenever they can.
The Discord botYour machine, optionalAnnouncements, library search, and music in voice channels.
The hub never sees your library

The hub stores your account, a list of your servers, and their addresses. It does not store what you watch, who your managed users are, or anything about your files. When an app streams video, it connects to your server directly and the hub is not in the path. If the hub is offline, playback keeps working.

Two ways it can be set up

The installer produces a bundled install. A complete ffmpeg with every codec compiled in ships inside it, so nothing else has to be present for playback to work.

There is also an attached mode, where the server reads an existing media library database and uses external binaries. That is configured through data/engine.json or the ENGINE_* environment variables. Bundled binaries win when both are present, because the bundled ffmpeg carries codecs that vendor builds leave out.

Read this before you install

The scanner that indexes your media folders is part of the attached mode. A bundled install ships with playback and everything around it, and it has no scanner of its own yet. If you install fresh and nothing appears after you add a folder, that is why. See Scan finds nothing for what to do about it today.

Installing

What is inside the installer

Everything the server needs is in the file you download. Nothing is fetched during setup, so a slow or missing internet connection does not break the install.

BundledWhy
Node runtimeThe server is written in JavaScript. The runtime ships alongside it so you never install one yourself, and a system Node upgrade cannot break the server.
ffmpeg, full buildEvery common codec is compiled in. This is what lets odd files play instead of failing. Vendor ffmpeg builds often ship without the video codecs for licensing reasons, which is exactly the problem this avoids.
Service wrapperTurns the server into a Windows service so it starts with the machine and restarts if it stops.
The Discord botOptional. You choose during setup whether to include it.

The unknown publisher warning

Windows will show a blue SmartScreen box saying it protected your PC and naming an unknown publisher. Choose More info, then Run anyway.

That warning is about a code signing certificate, which costs a few hundred dollars a year from a certificate authority. Until one is bought, every download shows this. It is not a statement about what the installer does. If that bothers you, the source is public and you can build it yourself.

Ports

PortProtocolUsed for
5466TCPThe web app and the API. This is the one you open in a browser.
5467UDPLocal discovery, so apps on your network find the server without anyone typing an address.
5468TCPThe Discord bot's local API. Only installed if you chose the bot.

5466 spells KINO on a phone keypad. The block was chosen to stay clear of Plex, which uses 32400 for its web interface and 32410 through 32414 for discovery. An earlier build of this server used 32402 and 32410, and 32410 collided with Plex discovery on any machine running both. That is fixed, and it is the reason the ports look arbitrary.

You can change the main port during setup. The installer validates it is between 1024 and 65535, writes it into the service definition, and uses it for the shortcuts and the firewall rule.

The firewall rule

If you leave the firewall task ticked, setup runs two netsh commands and creates two inbound rules:

netsh advfirewall firewall add rule name="Kinode Media Server"
  dir=in action=allow protocol=TCP localport=5466 profile=private,domain

netsh advfirewall firewall add rule name="Kinode Media Server discovery"
  dir=in action=allow protocol=UDP localport=5467 profile=private,domain

Both are scoped to the private and domain profiles. They do not apply to public networks, which is what Windows calls a coffee shop or an untrusted connection.

This does not expose you to the internet

A firewall rule lets devices on your own network reach the server. Reaching it from outside your house additionally needs a port forward on your router, which is a separate thing you have to do deliberately. See Remote access.

Where your data lives

The program goes in C:\Program Files\Kinode Media Server. Your data goes somewhere else entirely:

C:\ProgramData\Kinode Media Server\
  data\
    accounts.db      accounts, profiles, PINs, watch history, playlists
    secret.key       signs sign-in tokens
    plugins\         installed plugins
    trickplay\       seek preview images
    subtitles\       downloaded subtitle files
  logs\              service logs, rolled at 10 MB, four kept

This split is deliberate. A service account cannot write into Program Files, and keeping data outside the program directory means an upgrade or a reinstall never touches your accounts or your watch history.

The server finds this directory through the KINODE_DATA environment variable, which the installer sets in the service definition. Running from a source checkout with no such variable, the data sits beside the code in data\, which is what you want while developing.

Service or window

Ticking the service task during setup installs a Windows service called Kinode Media Server. It starts automatically after boot, restarts itself ten seconds after a crash, and writes rolling logs.

sc query KinodeMediaServer
net stop KinodeMediaServer
net start KinodeMediaServer

If you skip the service, use the Start Kinode Media Server.cmd shortcut that setup writes into the install folder. It opens a console window and the server runs until you close it. That is useful for testing because you see errors immediately.

Registry entries

Setup writes five values under HKLM\Software\Kinode\MediaServer: InstallPath, DataPath, Version, Port, and Architecture. Uninstalling removes the key. Nothing else in the registry is touched, and no file associations or shell extensions are registered.

Uninstalling

Uninstall from Settings, Apps, or from the Start menu shortcut. It stops the service, removes it, deletes both firewall rules, and removes the program files.

It then asks whether to remove your accounts, watch history and settings. Choosing No keeps C:\ProgramData\Kinode Media Server intact, which is what you want if you are reinstalling or moving to a new version. Your media files are never touched either way, because the server only ever reads them.

Architectures

There are two builds: x64 for Intel and AMD machines, and arm64 for Windows on ARM laptops. The download page detects which one you need and preselects it, and you can override that.

There is no 32 bit build. Windows 11 has no 32 bit edition, a 32 bit process cannot address more than 4 GB of memory, and video transcoding wants more than that. If you have a real need for one, say so on the forum.

First run

Creating the first account

Open http://localhost:5466. The first account you create becomes the owner of that server, with administrator rights on it. Nothing is emailed and no account is created anywhere else. This account exists only on your machine.

A note on words

People who run their own server are called owners. The word admin is reserved for staff on this website, meaning the people who moderate the forum and review plugins. The two are unrelated, and owning a server gives you no rights here.

Adding libraries

Go to Settings, then Library, and add a folder. Point it at the directory that holds your media. Give it the right type, because it changes how things are grouped and displayed:

  • Movies expects one film per folder or per file.
  • TV Shows expects show, then season, then episode.
  • Music expects artist, then album, then track.

Naming that scans cleanly

Scanners match on filenames. These layouts work:

Movies\
  Blade Runner (1982)\Blade Runner (1982).mkv
  Heat (1995)\Heat (1995) Bluray-1080p.mkv

TV Shows\
  The Wire\Season 01\The Wire - S01E01 - The Target.mkv

Music\
  Radiohead\OK Computer\01 Airbag.flac

A year in parentheses removes almost all ambiguity for films, especially remakes. S01E01 is the most reliably parsed episode format.

The scan

A scan walks the folder, works out what each file is, and writes it into the library database. Artwork and descriptions are fetched afterwards, so titles usually appear before their posters do.

Large libraries take a while. Ten thousand files is normal at tens of minutes, and it is disk bound rather than processor bound.

Libraries also rescan themselves once a day at 3 AM, so files you drop in overnight show up without anyone pressing anything.

If a scan does nothing at all

A bundled install has no scanner binary configured, and a scan will fail immediately rather than slowly. This is a real gap in the current build. The troubleshooting entry explains the workaround.

Artwork and descriptions

Posters, backgrounds and summaries are fetched after the files are indexed. If a film has the wrong poster it is almost always because the filename matched the wrong title. Renaming the file to include the year and rescanning fixes it.

Profiles and family

Managed users

A managed user is a person on your server who has a display name and nothing else. They have no password and no email address. After you sign in, a screen asks who is watching, and everyone picks their name.

What each managed user gets:

  • Their own watch history and Continue Watching row.
  • Their own ratings, watchlist, playlists and recommendations.
  • Access to exactly the libraries you granted them.
  • An optional PIN, between four and eight digits, stored hashed.

Create them from the who-is-watching screen using Manage users. The account owner is always the first tile and cannot be deleted.

Managed users are local to your server

They are stored only in your server's database. The hub has no concept of them, no table for them, and no way to learn they exist. Nothing about a managed user ever leaves your machine.

PINs

A PIN gates entering a profile. It is not a password and it does not protect the account, because anyone who is already signed in is inside your server. It stops a child from picking your profile and it stops someone seeing your Continue Watching row.

PINs are stored as bcrypt hashes. There is no way to read one back, including for you. Clear it and set a new one from the same edit screen.

Per-profile library access

Editing a managed user offers either everything on the server, or a specific list of libraries. Access is enforced on the server for every request, so a hidden library cannot be reached by guessing a URL.

A profile can only ever be narrowed, never widened past what the account itself may see. If someone shared a subset of their libraries with you, your own managed users can never see more than that subset.

Kid mode and rating limits

Kid mode gives that profile a simplified interface with larger tiles, and hides settings and statistics.

A rating ceiling blocks anything above it. Ratings are ranked, and unrated content is blocked whenever a ceiling is set, because unrated is where the surprises live.

RankRatings treated as equivalent
1G, TV-Y, TV-G
2PG, TV-Y7
3TV-PG
4PG-13, TV-14
5R, TV-MA
6NC-17, X

Daily limits and bedtimes

A managed profile can have a daily watch limit in minutes and a bedtime window. Both are enforced by the server on every playback request, so switching device or app does not get around them.

The daily limit counts real time watched, taken from the same ledger that feeds the statistics page. It is not the sum of runtimes, so abandoning a film after ten minutes costs ten minutes.

How bedtime handles midnight

A bedtime window is a start time and an end time. When the start is earlier than the end, the window is the obvious range. When the start is later than the end, the window wraps past midnight.

bedtime 20:00 to 22:00
  blocked between 8 PM and 10 PM, same evening

bedtime 21:00 to 07:00
  start is after end, so the window wraps:
  blocked from 9 PM through midnight and on until 7 AM

In code that is a single comparison. When start is less than or equal to end, the current time must fall inside the pair. Otherwise the current time must be at or after the start, or before the end. The second case is what makes an overnight window work.

Bonus time

The account owner can grant extra minutes on top of today's limit from the profile edit screen. Bonus minutes are recorded against today's date and are ignored tomorrow, so there is nothing to remember to undo.

What happens when a limit is reached

Playback that has already started is stopped at the next progress report, which is within about ten seconds, and the reason appears on screen. Starting something new is refused immediately with the same message. The watch time is still recorded, because the ledger is written before the check runs.

Playback

Direct play, direct stream, and converting

Every time you press play the server decides how to deliver the file. The player shows which one it chose, and Settings shows why in plain words.

ModeWhat happensCost
Direct playThe original file is sent untouched.Almost none. This is the goal.
Direct streamVideo is copied as-is, audio and the container are rebuilt.Small. Video is the expensive part and it is not being re-encoded.
ConvertingVideo is decoded and re-encoded.Large. This is what makes a processor work hard.

What qualifies for direct play

All three of these have to be true:

PartAccepted
Video codech264, vp8, vp9, av1. Also hevc when the browser proved it can decode it.
Audio codecaac, mp3, opus, vorbis, flac
Containermp4, mkv, webm, mov, m4v

If the video codec is fine but something else is not, the result is direct stream. If the video codec itself is unsupported, the result is converting.

Why DTS and TrueHD are not on the list

Chrome plays a file with DTS or E-AC3 audio silently. The video runs, the audio track is simply missing, and it looks like a broken speaker rather than a codec problem. Treating those as unsupported forces an audio rebuild, which costs very little and means sound always works.

Picking a quality forces conversion

Choosing 1080p, 720p or 480p in the player always converts, because resizing a picture requires decoding it. The presets are 1920 wide at 8 Mbps, 1280 at 4 Mbps, and 854 at 2 Mbps.

HEVC and 4K

The browser tests whether it can hardware decode HEVC and tells the server. If it can, a 4K HEVC file is stream copied instead of re-encoded. On an 80 GB remux that is the difference between a modest network transfer and a processor at full load for two hours.

Copied HEVC is tagged hvc1 inside the output container, because browsers reject the alternative tag.

The fallback that makes odd files work

If a direct or direct stream playback errors part way through, the player restarts the same moment as a full conversion and shows a short message. This catches 10 bit H.264, damaged containers, and files whose metadata claims a codec they do not actually contain.

The practical effect is that if ffmpeg can decode it, it plays. Conversion also forces 8 bit output, deinterlaces frames that are flagged as interlaced, and allows a large probe window so unusual Matroska files are read correctly.

Audio tracks

Files with more than one audio track show a track list in the player's settings menu. Picking a track that is not the first one forces at least a direct stream, because browsers cannot switch audio tracks by themselves.

Subtitles

Three sources are used, in this order:

  1. Embedded text tracks inside the file. Recognised formats are SRT, SubRip, ASS, SSA, mov_text, WebVTT and VTT.
  2. Sidecar files next to the media file, with extensions .srt, .ass, .ssa, .vtt or .sub.
  3. Downloaded from OpenSubtitles, if you set an API key in Settings.
Image based subtitles are skipped

PGS and VobSub are pictures rather than text. They cannot be turned into browser subtitles without optical character recognition, so they are not listed. Use a sidecar file or generate one.

Generated subtitles

Movies and episodes have a Generate subtitles button. It extracts a small mono audio file, sends it to a Kinode Transcripts server, and writes the result back as an .srt beside the media file. Configure the engine URL and key under Settings, Maintenance.

With nightly captioning enabled, up to three recent additions that have no subtitles are captioned at 4 AM.

Remembered language

The subtitle track you pick is remembered per show and per profile. The next episode selects the same language without asking.

Seek preview images

Hovering the seek bar shows a frame from that point. A background job builds one image strip per file using keyframes only, one file at a time, and caches it permanently. The first time you hover a file it queues the job, so previews appear on the next play rather than immediately.

Equalizer and normalization

Both live behind the equalizer button in the player and the music bar, and both apply live. Dragging a slider changes the sound immediately with no restart.

  • Ten band equalizer from 31 Hz to 16 kHz, plus or minus 12 dB. Presets are Flat, Dialogue, Bass boost, Treble boost and Less bass. You can save your own named curves.
  • Audio normalization is a compressor that evens out films where dialogue whispers and explosions do not. One toggle.

Settings are saved per profile. Custom equalizer presets are shared across the profiles on one account.

Accessibility and TV mode

The accessibility panel is reachable from the top bar and from the who-is-watching screen. It offers interface size, subtitle size, a solid subtitle background, high contrast, and reduced motion. The system setting for reduced motion is respected whether or not the toggle is on.

TV mode makes everything larger, adds strong focus outlines, and turns on arrow key navigation across the whole interface with Enter to select. That makes any browser on a television or a console usable from a sofa with a keyboard or a remote.

Music

Music libraries browse as artists, then albums, then tracks. Playback uses a bar along the bottom of the window, so browsing does not interrupt what is playing. Starting a video pauses the music and the reverse is also true.

Files stream in their original format. Browsers handle MP3, FLAC, M4A, OGG and WAV without conversion. Operating system media keys work through the Media Session API, so the play button on a keyboard or a lock screen does what you expect.

Playlists

Playlists are per profile and appear in the sidebar. Add a track from any album with the plus button that appears when you hover a row. A playlist page can play, shuffle, reorder and remove.

Lyrics

Put an .lrc file next to the audio file and the lyrics button scrolls it in time with the music, highlighting the current line. A plain .txt file with the same base name is shown as static text. Nothing is fetched from the internet.

Audiobooks and long audio

Anything longer than fifteen minutes resumes where that profile stopped rather than starting over. The music bar has a speed control that cycles through 1x, 1.25x, 1.5x, 2x and 0.75x.

Volume leveling

Each track gets a loudness measurement the first time it is played, taken in the background. Later plays use it to bring tracks to a common level, so a quiet rip and a loud master do not make you reach for the volume. The toggle is in the accessibility panel.

Radio

The radio button builds an endless queue from any track, seeded by the artist first and then filled from the rest of that library. It uses your own files, and no external service is involved.

Remote access

How the directory works

Your server sends the hub a heartbeat every five minutes. That heartbeat carries four things: a machine identifier, a shared secret, the friendly name, and a list of addresses where the server can be reached.

When an app signs in, the hub returns each server the account owns or has been shared, with a list of candidate addresses and an access token. The app tries the addresses in order and uses whichever answers.

Access tokens are signed with your server's own secret, which the hub holds a copy of from registration. Your server verifies them itself, without asking the hub anything. That is why playback continues to work when the hub is down.

Why local addresses are tried first

The candidate list puts private network addresses ahead of the public one. When your phone is on your own wifi it talks to the server directly across the local network, which is faster and does not touch your internet connection at all. The public address is used only when the local ones do not answer.

Making it reachable from outside

Two things are needed. The firewall rule, which the installer created, and a port forward on your router, which you have to do yourself.

  1. Give the machine running the server a fixed local address, either a static address or a DHCP reservation on the router.
  2. In the router's admin page, forward external TCP port 5466 to that machine's port 5466.
  3. Wait for the next heartbeat, which is within five minutes, or restart the server to send one immediately.

Your server cannot see its own public address from behind a router, so the hub fills it in. The address a heartbeat arrives from is the public address, and the hub records it and hands it back as a candidate.

Setting your own public address

If you have a domain, dynamic DNS, or a reverse proxy with a certificate, put the full address into Settings, Maintenance, Public address. For example https://media.example.com. That takes priority over the address the hub infers, and it is the only way to get HTTPS on remote connections today.

Remote connections are plain HTTP by default

Without your own domain and certificate, remote streaming happens over unencrypted HTTP. That is acceptable inside a household and it is not acceptable for anything sensitive. A reverse proxy with a free certificate fixes it, and setting the public address above is how you tell the server about it.

Apps

The web app

The browser interface is installable. Chrome and Edge offer an install button in the address bar, which gives it a window without browser furniture and its own icon. A small service worker caches the interface so it opens even if the server is briefly unavailable, though playback obviously needs the server.

Android

  • Casting. A cast button appears when a Chromecast is on the network. Local playback pauses, the television picks up at the same position, and stopping the cast hands the position back to the phone.
  • Downloads. Long press any movie or episode and choose Original, 1080p or 720p. Downloads are stored in app private storage with their artwork, and the Downloads screen plays them with no server involved. Watch progress is queued locally and sent to the server the next time it is reachable.
  • Remote. Lists players across every server on the account and drives them with play, pause, seek, next, previous and stop.

Casting uses the converted stream rather than the original file. Chromecast support for Matroska and DTS is inconsistent, so the reliable path is used deliberately.

Roku

The Roku channel is sideloaded rather than published. Enable developer mode on the Roku, browse to its address, and upload the zip.

It has the who-is-watching screen with PIN entry, sends the profile with every request so watch history is correct, and can be driven from the Remote screen in the Android app. Next and previous are ignored on Roku because it has no episode queue yet.

Discord bot

The bot is optional and runs next to the server. It needs a Discord application of your own, because a bot token identifies your bot and cannot be shared.

Commands

CommandWhat it does
/nowplayingWhat is streaming right now, with who is watching and whether it is converting.
/searchSearch the library from Discord.
/statsWatch time across the household, library size, data served.
/serverHealth checks and drive space.
/requestAsk for something to be added. Goes into the request queue and triggers automation if it is set up.
/bugFile a bug on the tracker.
/bugsList bugs, optionally filtered by status.
/playPlay a track, an album or an artist in your voice channel.
/playlistPlay a playlist from the server.
/nowqueueWhat is lined up.
/skip /pause /stopTransport controls.
/watchStart a watch party and post the join code.
/queue /triage /reviewStaff commands for reports, bug status and plugin review.

Channels it creates

Calling POST /bot/setup once creates seven channels and returns their identifiers to paste into the configuration:

ChannelCarries
kinode-announcementsNew episodes, new arrivals, the weekly recap
kinode-requestsRequests, and a message when one is ready
kinode-alertsDisk space warnings
kinode-activityWhat is playing
kinode-bugsBug intake
kinode-bug-logNew bug notifications for whoever is fixing them
kinode-moderationReports and plugin reviews

Bug intake threads

Anything said in the bugs channel becomes a bug on the tracker. The bot replies with the number and opens a thread underneath. From then on that thread and the bug are the same conversation: messages in the thread become comments on the bug, and staff replies on the website are pushed back into the thread.

Severity is guessed from the wording. Words like crash, data loss and cannot sign in read as critical. Broken, error and hangs read as high. Typo, cosmetic and occasionally read as low. You can always override it.

Threads do not survive a bot restart

The link between a Discord thread and a bug is held in memory. Restarting the bot means older threads stop mirroring. That is deliberate, because persisting the mapping risks a stale identifier routing a comment onto the wrong bug. New threads work normally.

Music in voice channels

The bot streams from your library into a voice channel. It does not use Lavalink or any other external audio service. The server converts the track to Ogg Opus, which Discord voice plays directly, so there is no Java process and no native audio module to compile.

The player message has album art, a progress bar, and buttons for back, pause, skip, stop, shuffle, loop and queue, plus a radio button that extends the queue with similar music.

Rich Presence

Showing what you are watching on your own Discord profile is a separate program called presence.js, and it has to be. Discord only allows an application to set its own status, so a bot can never write to your profile. Rich Presence works over a local socket that the Discord desktop client opens on your machine, which means the program has to run on your machine next to Discord.

It reads what you are playing from your server, and writes it to your profile with artwork, a live progress bar and a paused state. There is a private mode that keeps it running without showing anything. Nothing leaves your network.

The bot itself shows a server wide status instead, naming what is playing when one thing is, or a count when several are.

Plugins

The shape of a plugin

A plugin is a folder holding a manifest and up to two scripts. Both scripts are optional, so a plugin can add only a home row, only a panel on an item page, only an endpoint, or any combination.

on-this-day\
  plugin.json     required
  server.js       optional, runs in the server process
  client.js       optional, runs in the web app
{
  "id": "on-this-day",
  "name": "On This Day",
  "version": "1.0.0",
  "author": "you",
  "description": "One or two sentences.",
  "homepage": "https://github.com/you/on-this-day",
  "permissions": ["library:read", "ui:home", "storage"],
  "server": "server.js",
  "client": "client.js",
  "settings": [
    { "key": "minYears", "label": "Only films this old", "type": "number", "default": 5 }
  ]
}

The identifier must be lowercase letters, numbers and dashes, and it must match the folder name. Anything listed under settings becomes a form on the owner's plugin page, and whatever they save arrives as plugin.config.

Permissions

A plugin asks for what it needs. The owner sees the list in plain words before enabling it, and a permission that was not requested is simply absent from the object the plugin receives.

PermissionGrants
library:readLook up libraries, items, search, random picks, recently added, genres.
playback:eventsHear when something is played or finished.
ui:homeContribute a row to the home page.
ui:detailContribute a panel to movie and show pages.
http:routesExpose endpoints under /api/plugins/<id>/.
scheduleRun a function on an interval, never faster than once a minute.
notifySend a message through the server's notification channel.
networkReach the internet.
storageKey and value storage. Always granted, listed so the owner sees it.
The trust model, stated plainly

A server plugin is JavaScript running inside the media server process, the same as any dependency the server installs. The permission list shapes the API a plugin receives and tells the owner what to expect. It is not a sandbox, and it does not stop determined code from doing anything the runtime can do.

That is the same arrangement as Plex channels, Jellyfin plugins and browser extensions. Install plugins you trust, and read the source when you can. Review before listing here is a check on obvious problems, and it is not a security guarantee.

The server half

module.exports = {
  async setup(plugin) {
    // library:read
    plugin.library.libraries();
    plugin.library.item(1234);
    plugin.library.search('blade runner', 20);
    plugin.library.format(item);

    // ui:home, return null and nothing renders
    plugin.homeRow('key', (ctx) => {
      return { title: 'A row title', items: [ /* items */ ] };
    });

    // http:routes, reachable at /api/plugins/<your-id>/thing
    plugin.route('get', 'thing', (req, res) => res.json({ ok: true }));

    // playback:events
    plugin.on('playback:finished', (info) => {});

    // schedule
    plugin.every('30m', async () => {});

    // storage, survives restarts
    plugin.storage.set('cursor', 42);
    plugin.storage.get('cursor', 0);
  },
  async teardown() {}
};

Home rows have a four second budget. A row that takes longer is skipped for that page load, so a slow plugin cannot stall the home page.

The browser half

plugin.onDetail((item, mount) => {
  const el = document.createElement('div');
  el.className = 'panel-card';
  el.textContent = item.title + ' came out in ' + item.year;
  mount.appendChild(el);
});

const data = await (await plugin.api('thing')).json();
plugin.ui.card(item);
plugin.ui.play(itemId);
plugin.toast('done');

Client code is wrapped in a try and catch by the host, so a mistake logs an error rather than breaking the page.

Installing and publishing

While writing one, drop the folder into the server's data\plugins\ directory. It appears in Settings, Plugins, turned off. Turning it on starts it, and saving its settings reloads it, which is the fastest way to pick up code changes without restarting the server.

To share it, zip the folder so that plugin.json sits at the archive root or inside a single folder named for the plugin, publish the zip at an HTTPS address, and submit it on the plugins page.

Every submission is reviewed before it is listed, and a new version of an approved plugin returns to pending. That prevents an approved listing being swapped for different code after review.

Automation

Requests can be wired to Radarr, Sonarr and Lidarr so that asking for something starts the process of getting it.

Request typeServiceAPIMatched on
movieRadarr/api/v3tmdbId
showSonarr/api/v3tvdbId
musicLidarr/api/v1foreignArtistId

Set each service's address and API key under Settings, Maintenance. The first quality profile and the first root folder are used, so make sure the ones you want are first in each service.

What happens to a request

  1. Someone asks for a title and marks it as a movie, show or music.
  2. The matching service is asked to look the title up, and the first result is added as monitored with a search started immediately.
  3. Every five minutes the download queue is checked. The request shows searching, then downloading with a percentage.
  4. When the file lands, the request flips to done and a notification is posted.

An owner can also send any request to a service by hand from the requests inbox. Subtitles are handled by the built in captioning, so Bazarr is not needed.

Owner tools and maintenance

The health panel

Settings, Maintenance shows a list of checks with plain verdicts. It covers the library database, the transcoder binary, whether a hardware encoder was found, the scanner binary, the hub connection with the age of the last heartbeat, remote access, the notification webhook, the captioning key, background queue depths, and free space on every drive holding a library.

Backups

Settings, Maintenance has a one click backup that downloads accounts.db. That single file holds accounts, profiles, PINs, watch history, playlists, ratings, comments and requests.

To restore it, stop the server, put the file back at C:\ProgramData\Kinode Media Server\data\accounts.db, and start the server. Your media files are not in the backup because they are not the server's to hold.

Library audit and reclaim

Settings, Library audit runs four checks and offers a reclaim list:

  • Duplicate versions, meaning one film with more than one file.
  • Below 720p, sorted by height.
  • Season gaps, listing shows that have episode 1 through 4 and then 6.
  • Movies with no subtitles of any kind, embedded or sidecar.
  • Reclaim, listing large files that were watched, rated low, or untouched for a year, ranked by space returned.

Nothing is deleted for you. The list is a starting point and the decisions are yours.

Notifications and the weekly digest

Set a Discord webhook, or the bot, under Settings, Maintenance. The server then sends:

  • New episodes of shows you followed.
  • Newly added films and series.
  • A recap on Sunday evening with hours watched, the most watched title, per person leaders and data served.
  • One warning per day for any library drive above 90 percent full.
  • Movie night reminders an hour before and at the start.

Statistics

Every profile has a statistics page with hours watched, plays, titles, a thirty day chart, a time of day histogram, a playback mode breakdown, most watched, recently watched and a year to date summary.

Hours are real time watched. The server adds the gap between progress reports, capped at sixty seconds so a laptop waking from sleep cannot bank hours it did not play. Runtimes are never summed, so abandoning a film after ten minutes counts as ten minutes.

Owners additionally see server wide totals, a usage calendar, and a live monitor with processor load, memory, streaming bandwidth counted at the byte level, and free space per drive.

Hub accounts, passwords and recovery

Accounts on this website are separate from accounts on your server. There are four ways back into a hub account:

  1. Change your password while signed in.
  2. Recovery codes. Eight single use codes, shown once when you register, stored as hashes. They need no email and no administrator.
  3. A reset link by email, sent to the address on your account.
  4. An administrator generated link, for when the other three fail.

For your own server, a forgotten password is fixed on the machine itself:

cd "C:\Program Files\Kinode Media Server\app"
node tools\reset-password.js               lists the accounts
node tools\reset-password.js owner         sets a generated password
node tools\reset-password.js owner mypass  sets one you choose

Troubleshooting

Symptoms first, then the causes worth checking in the order worth checking them.

The page will not load at all

The server is not running

Check the service and start it if it is stopped.

sc query KinodeMediaServer
net start KinodeMediaServer
Wrong address

On the machine itself use http://localhost:5466. From another device use the machine's local address, for example http://192.168.1.20:5466. Older builds used 32402, so an old bookmark will fail.

The service starts and immediately stops

Read the log, which is the fastest way to a real answer.

type "C:\ProgramData\Kinode Media Server\logs\KinodeMediaServer.err.log"

The service will not start

The port is already taken

See the next entry. This is the most common cause.

The data directory is not writable

The service runs as Local System and needs to write to C:\ProgramData\Kinode Media Server. If that folder was moved or its permissions were changed, the server cannot create its database or its signing key. Recreate the folder and allow Users to modify it.

Run it in a window to see the error

Stop the service and use the Start Kinode Media Server.cmd shortcut. Errors that a service swallows are printed in the console.

Port already in use

Find what is holding it
netstat -ano | findstr :5466
tasklist /fi "pid eq 1234"

The first command prints the process identifier in the last column. The second names it.

Change the port

Edit kinode-service.xml in the install folder, change the PORT value, then reinstall the service and add a firewall rule for the new port.

net stop KinodeMediaServer
"C:\Program Files\Kinode Media Server\kinode-service.exe" uninstall
"C:\Program Files\Kinode Media Server\kinode-service.exe" install
net start KinodeMediaServer

netsh advfirewall firewall add rule name="Kinode Media Server" ^
  dir=in action=allow protocol=TCP localport=5500 profile=private,domain
A previous copy is still running

An older instance started from a console window keeps the port. Close that window, or end the node.exe process holding the port.

It works on the server but not from other devices

The firewall task was not ticked

Add the rules by hand in an administrator command prompt.

netsh advfirewall firewall add rule name="Kinode Media Server" ^
  dir=in action=allow protocol=TCP localport=5466 profile=private,domain
netsh advfirewall firewall add rule name="Kinode Media Server discovery" ^
  dir=in action=allow protocol=UDP localport=5467 profile=private,domain
Windows thinks your network is public

The rules only apply to private and domain networks. Open Settings, Network, and set the connection to Private. This is common after a router change, because Windows treats the reconnected network as new.

Client isolation on the router

Some routers, and most guest networks, stop devices talking to each other. If a phone on wifi cannot reach a server on ethernet, check for a setting called AP isolation or client isolation and turn it off.

Third party security software

Some suites replace the Windows firewall entirely, and netsh rules mean nothing to them. Add the rule in that product's own interface.

Video plays but there is no sound

The audio track is DTS, TrueHD or E-AC3

Browsers cannot decode these. Chrome plays the file with silence rather than reporting an error, which is why it looks like a speaker problem. The server treats these as unsupported so audio is rebuilt automatically, and the mode tag will say Direct Stream rather than Direct Play.

If it is still silent, the file was probably forced to Direct Play. Turn Auto Convert back on in the player's settings menu.

The wrong audio track is selected

Files with several tracks sometimes default to a commentary or a foreign language track. Open the player settings menu and pick another under Audio.

The browser tab is muted

Worth ruling out. Right click the tab and check.

Everything converts when it should direct play

A quality was picked

Choosing 1080p, 720p or 480p always converts, because resizing requires decoding. Set quality back to Original.

The audio codec is not browser friendly

This is the usual answer. The video is fine and the audio forces a rebuild, which shows as Direct Stream. The cost is small and it is not worth avoiding.

The file is HEVC and the browser did not claim hardware decode

Safari and recent Edge handle HEVC. Firefox generally does not. Chrome depends on the machine. When the browser cannot prove it decodes HEVC, the server converts, which is expensive on 4K.

Play 4K HEVC in a browser that supports it, or use the Android app.

Check the stated reason

The player names the actual cause, for example that the audio is DTS or the container is AVI. Settings, Activity shows the same reason for every running stream.

Buffering or stuttering

Conversion cannot keep up

Settings, Activity shows how many streams are converting. Software conversion of 4K in real time needs a strong processor, and two at once needs a very strong one. Pick a lower quality, or store a version the client can direct play.

No hardware encoder was found

The health panel says whether one was found. The bundled ffmpeg uses the processor, which is slower than a graphics card would be. This is a known limitation of the current build.

Wireless

A 4K remux can exceed 80 Mbps sustained, which is more than many wireless links deliver reliably. Test with a wired device to separate a network problem from a server problem.

The disk is the limit

Several streams from one mechanical drive, or a network share, can starve playback while the processor looks idle.

Seeking is slow on converted video

This is expected, and here is why

A converted stream is a live pipe out of ffmpeg. There is no file to jump around inside. Seeking kills the pipe, restarts ffmpeg at the target second, and resumes, so there is a pause of a second or two.

Direct play seeks instantly, because the browser is reading a real file. Another reason to prefer files that direct play.

Subtitles are missing or out of sync

They are image based

PGS and VobSub are pictures and cannot be displayed as browser subtitles. They are not listed at all. Use a sidecar file or the Generate subtitles button.

The sidecar file is named differently

A sidecar has to share the base name of the media file and sit in the same folder.

Blade Runner (1982).mkv
Blade Runner (1982).srt          works
Blade Runner (1982).en.srt       works
subs\Blade Runner.srt            not found
Out of sync after seeking a converted stream

Subtitle timings are absolute while a restarted conversion resets the clock to zero. The server shifts the subtitle file to compensate on each restart. If they are still wrong, pause and resume to force a refresh.

Consistently early or late by seconds

Then the file itself is mistimed, usually because it was made for a different release. Download another one from the subtitle search in the player.

No artwork, or the wrong artwork

Metadata is still being fetched

Titles appear before posters. Give a large library some time after the first scan.

The filename matched the wrong title

Wrong poster nearly always means wrong match, especially for remakes. Rename to Title (Year).ext and rescan.

Placeholder icons everywhere

If every item shows a generic icon, artwork is failing to load rather than failing to match. Open the browser console and look for failing image requests.

A scan finds nothing

There is no scanner in a bundled install

This is a genuine gap in the current build rather than a misconfiguration. The bundled installer ships playback and everything around it, and the component that indexes folders is part of the attached mode.

Until a built in scanner ships, a bundled install needs to be pointed at an existing library database. Create data\engine.json in the data directory with paths to an existing engine:

{
  "db": "C:\\path\\to\\library.db",
  "scanner": "C:\\path\\to\\scanner.exe"
}

The bundled ffmpeg is still used for playback, because bundled binaries take priority. Check the forum for progress on this.

The folder path is wrong or unreadable

A service runs as Local System, which has no access to mapped network drives created by your user account. Use a UNC path such as \\nas\media, or run the server in a window under your own account to test.

The library type does not match the layout

A TV library pointed at a folder of loose films finds nothing, because it expects show and season directories.

Remote access does not work

No port forward

The firewall rule is not enough. The router has to forward external 5466 to the machine. Verify from a phone on mobile data rather than wifi, because a phone on your own wifi will succeed for the wrong reason.

The local address changed

DHCP hands out a different address after a reboot and the forward now points at nothing. Give the machine a reservation on the router.

The heartbeat has not landed yet

Heartbeats are every five minutes. Restart the server to send one immediately. The health panel shows the age of the last successful one.

Carrier grade NAT

Some internet providers, and most mobile broadband, do not give you a real public address. Port forwarding cannot work at all in that case. Ask your provider about a public address, or use a tunnel service.

Double NAT

A router behind another router means the forward has to exist on both. The symptom is a forward that looks correct and does nothing.

Cannot sign in to a server

Wrong account

Server accounts and hub accounts are separate. The hub account signs in at usekinode.app. The server account signs in at your server's own address.

Forgotten password

Reset it on the machine.

cd "C:\Program Files\Kinode Media Server\app"
node tools\reset-password.js

With no arguments it lists the accounts. With a username it sets a new password and prints it.

Everyone signed out at once

Sign in tokens are signed with a key stored at data\secret.key. If that file is deleted or the data directory moved, every existing token becomes invalid and everyone signs in again. Nothing else is lost.

A managed user sees the wrong libraries

Everything is still ticked

Editing a managed user offers everything on the server, or a specific list. If the first is ticked, the list underneath is ignored.

The account itself is limited

A profile can never see more than the account it belongs to. If your access was shared to you as a subset, your managed users are bounded by that subset regardless of their own list.

The app is stale

Switch profile and back, or reload. Clients remember the profile they last used.

The Discord bot is offline

The token is wrong

Resetting a bot token in the developer portal invalidates the old one immediately. The error is explicit about an invalid token.

Message Content Intent is off

The bug intake channel needs it. Without it, the bot connects and sees empty message bodies. Enable it under Bot, Privileged Gateway Intents.

Commands do not appear

With a guild identifier set they register immediately. Registered globally they take up to an hour. Set DISCORD_GUILD_ID.

The server cannot reach the bot

Use the Test button in Settings, Maintenance. It calls the bot's ping endpoint and reports what answered.

Music will not play

The bot must be able to join and speak in the voice channel, and you have to be in one when you ask.

Email is not arriving

Two real failures that are worth knowing about, because both were hit in practice and neither is obvious.

Messages land in spam with DMARC failing

This happens when the sending address belongs to a domain that has not authorised the sending service. A message claiming to be from a Gmail address, signed by Amazon, is exactly the shape of a phishing attempt and filters treat it that way.

The fix is to send from a domain you control and have verified with the mail provider, so the address in the From line and the signature agree.

Nothing sends and nothing is logged

A systemd unit splits Environment= values on whitespace unless they are quoted. Configuring a sender as Kinode Media Server <noreply@example.com> without quotes delivers the single word Kinode to the program, the mail provider rejects it as an invalid address, and a catch block swallows the error so nothing appears anywhere.

Environment=MAIL_FROM=Kinode Media Server <noreply@example.com>
Environment="MAIL_FROM=Kinode Media Server <noreply@example.com>"

The first line is wrong and the second is right. The lesson generalises: swallowing an error is right for the request and wrong for the operator, so every send is now logged with its identifier or its real error.

The recipient is not verified

A mail provider in sandbox or trial mode delivers only to addresses that have been verified in advance. Everyone else silently receives nothing. Check whether production access has been granted.

A plugin will not load

The folder name does not match the identifier

The folder has to be named exactly as id in the manifest. This is checked and refused.

The manifest is not valid JSON

A trailing comma is the usual culprit. The plugin list shows the reason next to the plugin.

It throws during setup

The plugin is disabled and the message is shown. Read the server log for the stack.

It runs but does nothing

Check the permissions in its manifest. A plugin that did not ask for ui:home has its home rows quietly ignored, and the log says so.

Code changes are not picked up

Saving its settings reloads it. That is quicker than restarting the server.

A drive is full

Find what to remove

Settings, Library audit lists large files that were watched, rated low, or untouched for a year, ranked by space returned.

Caches that grow

Seek preview images live in data\trickplay\ and downloaded subtitles in data\subtitles\. Both are safe to delete, and both rebuild when needed.

Temporary conversion files

Converted streams write segments to the system temporary directory. Sessions nobody touched for five minutes are cleaned up automatically, and a crash can leave some behind. Clearing %TEMP% while the server is stopped is safe.

Warnings

Any library drive above 90 percent full triggers one notification per day, if a webhook or bot is configured.

Glossary

Attached mode
Running against an existing library database and external binaries, configured through engine.json or ENGINE_* variables.
Bundled install
What the installer produces. The runtime and a full ffmpeg ship inside, and nothing else needs to be present.
Container
The file format wrapping the audio and video, for example MKV or MP4. Separate from the codecs inside it.
Direct play
The original file is sent untouched. Cheapest for the server and best for seeking.
Direct stream
Video is copied as-is while audio or the container is rebuilt. Costs very little.
Converting
Video is decoded and re-encoded. The expensive one.
Guest link
A link to one title that works without an account and expires on its own.
Hub
This website. An account system and a directory for finding servers. It never handles video.
Managed user
A person on one server with a display name, an optional PIN, and their own watch history. No password, no email, and unknown to the hub.
Owner
The person who runs a server. The word admin is reserved for staff on this website.
Part
One physical file belonging to an item. A film with a 4K and a 1080p copy has two parts.
Profile
Another word for managed user, and the account owner counts as one.
Remux
Repackaging streams into a different container without re-encoding. What direct stream does.
Sidecar
A file beside the media file sharing its base name, most often a subtitle.
Trickplay
The preview images shown when hovering the seek bar.
Watch together
Synchronised playback in a room joined with a short code.

Questions people ask

Does any of this cost money?

No. There is no paid tier and no feature is held back. Remote streaming, hardware transcoding, sharing and everything else are included.

Do my files get uploaded anywhere?

No. Your media never leaves your machine except when you or someone you shared with streams it, and that connection is direct. The only thing sent to this website is a heartbeat with your server's name and addresses.

Can you see what I watch?

No. Watch history lives in your server's database. The hub has no table for it and never receives it.

Do I need Plex installed?

For playback, no, because a complete ffmpeg is bundled. For indexing a library today, a bundled install still needs to be attached to an existing library database. That gap is described in the scan entry.

Will it run on a Raspberry Pi or a NAS?

Not from these installers, which are Windows only. The server is JavaScript and there is nothing Windows specific about it, so running from source elsewhere is plausible and untested.

How many people can watch at once?

As many as direct play as your network allows, because the server is only reading files. Converting is what limits you, and one 4K conversion can occupy a strong processor entirely.

Why is my processor busy when nobody is watching?

Background jobs. Seek previews, loudness measurement and subtitle generation all run one at a time when the server is idle. They finish and stop.

Can I move the server to a new machine?

Yes. Install it on the new machine, stop both servers, copy C:\ProgramData\Kinode Media Server\data\accounts.db across, and start it. Accounts, profiles and watch history come with it. Library paths have to point somewhere valid on the new machine.

Is my connection encrypted?

Inside your house it is plain HTTP, which is normal for a local service. Remote access is also plain HTTP unless you put your own domain and certificate in front of it and set the public address. Anything sensitive deserves that certificate.

What happens if this website disappears?

Your server keeps working. The hub is a directory. Local access, playback, accounts and profiles are all local, and only finding your server from outside your house would need setting up another way.

Nothing in the documentation matches that. Try a different word, or ask on the forum.