Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
Something went wrong. Try again.
42 kB · 123 lines
HTML
at gh-pages
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123<!doctype html><html lang="en" dir="ltr" class="docs-wrapper plugin-docs plugin-id-default docs-version-current docs-doc-page docs-doc-id-development/dev-common" data-has-hydrated="false"><head><meta charset="UTF-8"><meta name="generator" content="Docusaurus v3.10.2"><title data-rh="true">Common Development | Multi-Scrobbler</title><meta data-rh="true" name="viewport" content="width=device-width,initial-scale=1"><meta data-rh="true" name="twitter:card" content="summary_large_image"><meta data-rh="true" property="og:image" content="https://foxxmd.github.io/preview-images/f718b5cefb2aeaab85ad0d2443326ee4b6ad2530.png"><meta data-rh="true" name="twitter:image" content="https://foxxmd.github.io/preview-images/f718b5cefb2aeaab85ad0d2443326ee4b6ad2530.png"><meta data-rh="true" property="og:url" content="https://foxxmd.github.io/multi-scrobbler/development/dev-common/"><meta data-rh="true" property="og:locale" content="en"><meta data-rh="true" name="docusaurus_locale" content="en"><meta data-rh="true" name="docsearch:language" content="en"><meta data-rh="true" name="docusaurus_version" content="current"><meta data-rh="true" name="docusaurus_tag" content="docs-default-current"><meta data-rh="true" name="docsearch:version" content="current"><meta data-rh="true" name="docsearch:docusaurus_tag" content="docs-default-current"><meta data-rh="true" property="og:title" content="Common Development | Multi-Scrobbler"><meta data-rh="true" name="description" content="Common Development Knowledge"><meta data-rh="true" property="og:description" content="Common Development Knowledge"><link data-rh="true" rel="icon" href="/multi-scrobbler/img/favicon.ico"><link data-rh="true" rel="canonical" href="https://docs.multi-scrobbler.app/development/dev-common/"><link data-rh="true" rel="alternate" href="https://docs.multi-scrobbler.app/development/dev-common/" hreflang="en"><link data-rh="true" rel="alternate" href="https://docs.multi-scrobbler.app/development/dev-common/" hreflang="x-default"><script data-rh="true" type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Development","item":"https://foxxmd.github.io/multi-scrobbler/category/development"},{"@type":"ListItem","position":2,"name":"Common Development","item":"https://foxxmd.github.io/multi-scrobbler/development/dev-common"}]}</script><meta name="google-site-verification" content="8hqrPhw4yuoTBM6c3S10aC8L4tu4aIBe8Q-oSP3OES0"><script src="https://ry.foxxmd.dev/api/script.js" defer="defer" data-site-id="3"></script><link rel="stylesheet" href="/multi-scrobbler/assets/css/styles.4c018de4.css"><script src="/multi-scrobbler/assets/js/runtime~main.16272664.js" defer="defer"></script><script src="/multi-scrobbler/assets/js/main.82c32de0.js" defer="defer"></script><meta name="image" content="https://foxxmd.github.io/preview-images/f718b5cefb2aeaab85ad0d2443326ee4b6ad2530.png"></head><body><svg style="display: none;"><defs><symbol id="theme-svg-external-link" viewBox="0 0 24 24"><path fill="currentColor" d="M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"></path></symbol></defs></svg><script>!function(){var t=function(){try{return new URLSearchParams(window.location.search).get("docusaurus-theme")}catch(t){}}()||function(){try{return window.localStorage.getItem("theme")}catch(t){}}();document.documentElement.setAttribute("data-theme",t||"dark"),document.documentElement.setAttribute("data-theme-choice",t||"dark")}(),function(){try{const c=new URLSearchParams(window.location.search).entries();for(var[t,e]of c)if(t.startsWith("docusaurus-data-")){var a=t.replace("docusaurus-data-","data-");document.documentElement.setAttribute(a,e)}}catch(t){}}()</script><div id="__docusaurus"><link rel="preload" as="image" href="/multi-scrobbler/img/icon.svg"><div role="region" aria-label="Skip to main content"><a class="skipToContent_fXgn" href="#__docusaurus_skipToContent_fallback">Skip to main content</a></div><nav aria-label="Main" class="theme-layout-navbar navbar navbar--fixed-top navbarHideable_m1mJ"><div class="navbar__inner"><div class="theme-layout-navbar-left navbar__items"><button aria-label="Toggle navigation bar" aria-expanded="false" class="navbar__toggle clean-btn" type="button"><svg width="30" height="30" viewBox="0 0 30 30" aria-hidden="true"><path stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2" d="M4 7h22M4 15h22M4 23h22"></path></svg></button><a class="navbar__brand" href="/multi-scrobbler/"><div class="navbar__logo"><img src="/multi-scrobbler/img/icon.svg" alt="Logo" class="themedComponent_mlkZ themedComponent--light_NVdE" height="40"><img src="/multi-scrobbler/img/icon.svg" alt="Logo" class="themedComponent_mlkZ themedComponent--dark_xIcU" height="40"></div><b class="navbar__title text--truncate">Multi-Scrobbler</b></a><a aria-current="page" class="navbar__item navbar__link navbar__link--active" href="/multi-scrobbler/">Docs</a><a class="navbar__item navbar__link" href="/multi-scrobbler/playground/">Config Playground</a><a class="navbar__item navbar__link" href="/multi-scrobbler/api/multi-scrobbler-api/">API</a></div><div class="theme-layout-navbar-right navbar__items navbar__items--right"><a href="https://github.com/foxxmd/multi-scrobbler" target="_blank" rel="noopener noreferrer" class="navbar__item navbar__link">GitHub<svg width="13.5" height="13.5" aria-label="(opens in new tab)" class="iconExternalLink_nPIU"><use href="#theme-svg-external-link"></use></svg></a><a href="https://multi-scrobbler.app" target="_blank" rel="noopener noreferrer" class="navbar__item navbar__link">Website<svg width="13.5" height="13.5" aria-label="(opens in new tab)" class="iconExternalLink_nPIU"><use href="#theme-svg-external-link"></use></svg></a><div class="toggle_vylO colorModeToggle_DEke"><button class="clean-btn toggleButton_gllP toggleButtonDisabled_aARS" type="button" disabled="" title="system mode" aria-label="Switch between dark and light mode (currently system mode)"><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" class="toggleIcon_g3eP lightToggleIcon_pyhR"><path fill="currentColor" d="M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"></path></svg><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" class="toggleIcon_g3eP darkToggleIcon_wfgR"><path fill="currentColor" d="M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"></path></svg><svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" class="toggleIcon_g3eP systemToggleIcon_QzmC"><path fill="currentColor" d="m12 21c4.971 0 9-4.029 9-9s-4.029-9-9-9-9 4.029-9 9 4.029 9 9 9zm4.95-13.95c1.313 1.313 2.05 3.093 2.05 4.95s-0.738 3.637-2.05 4.95c-1.313 1.313-3.093 2.05-4.95 2.05v-14c1.857 0 3.637 0.737 4.95 2.05z"></path></svg></button></div><div class="navbarSearchContainer_Bca1"><div class="navbar__search searchBarContainer_NW3z" dir="ltr"><input placeholder="Search" aria-label="Search" class="navbar__search-input searchInput_YFbd" value=""><div class="loadingRing_RJI3 searchBarLoadingRing_YnHq"><div></div><div></div><div></div><div></div></div></div></div></div></div><div role="presentation" class="navbar-sidebar__backdrop"></div></nav><div id="__docusaurus_skipToContent_fallback" class="theme-layout-main main-wrapper mainWrapper_z2l0"><div class="docsWrapper_hBAB"><button aria-label="Scroll back to top" class="clean-btn theme-back-to-top-button backToTopButton_sjWU" type="button"></button><div class="docRoot_UBD9"><aside class="theme-doc-sidebar-container docSidebarContainer_YfHR"><div class="sidebarViewport_aRkj"><div class="sidebar_njMd sidebarWithHideableNavbar_wUlq"><a tabindex="-1" class="sidebarLogo_isFc" href="/multi-scrobbler/"><img src="/multi-scrobbler/img/icon.svg" alt="Logo" class="themedComponent_mlkZ themedComponent--light_NVdE" height="40"><img src="/multi-scrobbler/img/icon.svg" alt="Logo" class="themedComponent_mlkZ themedComponent--dark_xIcU" height="40"><b>Multi-Scrobbler</b></a><nav aria-label="Docs sidebar" class="menu thin-scrollbar menu_SIkG"><ul class="theme-doc-sidebar-menu menu__list"><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-1 menu__list-item"><a class="menu__link" href="/multi-scrobbler/"><span class="linkLabel_WmDU">Overview</span></a></li><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-1 menu__list-item"><a class="menu__link" href="/multi-scrobbler/quickstart/"><span class="linkLabel_WmDU">Quickstart</span></a></li><li class="theme-doc-sidebar-item-category theme-doc-sidebar-item-category-level-1 menu__list-item menu__list-item--collapsed"><div class="menu__list-item-collapsible"><a class="categoryLink_byQd menu__link menu__link--sublist" href="/multi-scrobbler/installation/"><span class="categoryLinkLabel_W154">Installation</span></a><button aria-label="Expand sidebar category 'Installation'" aria-expanded="false" type="button" class="clean-btn menu__caret"></button></div></li><li class="theme-doc-sidebar-item-category theme-doc-sidebar-item-category-level-1 menu__list-item menu__list-item--collapsed"><div class="menu__list-item-collapsible"><a class="categoryLink_byQd menu__link menu__link--sublist" href="/multi-scrobbler/updating/"><span class="categoryLinkLabel_W154">Updating</span></a><button aria-label="Expand sidebar category 'Updating'" aria-expanded="false" type="button" class="clean-btn menu__caret"></button></div></li><li class="theme-doc-sidebar-item-category theme-doc-sidebar-item-category-level-1 menu__list-item menu__list-item--collapsed"><div class="menu__list-item-collapsible"><a class="categoryLink_byQd menu__link menu__link--sublist" href="/multi-scrobbler/configuration/"><span class="categoryLinkLabel_W154">Configuration</span></a><button aria-label="Expand sidebar category 'Configuration'" aria-expanded="false" type="button" class="clean-btn menu__caret"></button></div></li><li class="theme-doc-sidebar-item-category theme-doc-sidebar-item-category-level-1 menu__list-item"><div class="menu__list-item-collapsible"><a class="categoryLink_byQd menu__link menu__link--sublist menu__link--active" href="/multi-scrobbler/category/development/"><span class="categoryLinkLabel_W154">Development</span></a><button aria-label="Collapse sidebar category 'Development'" aria-expanded="true" type="button" class="clean-btn menu__caret"></button></div><ul class="menu__list"><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-2 menu__list-item"><a class="menu__link menu__link--active" aria-current="page" tabindex="0" href="/multi-scrobbler/development/dev-common/"><span class="linkLabel_WmDU">Common Development</span></a></li><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-2 menu__list-item"><a class="menu__link" tabindex="0" href="/multi-scrobbler/development/dev-source/"><span class="linkLabel_WmDU">Source Development/Tutorial</span></a></li><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-2 menu__list-item"><a class="menu__link" tabindex="0" href="/multi-scrobbler/development/flatpak/"><span class="linkLabel_WmDU">Flatpak</span></a></li></ul></li><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-1 menu__list-item"><a class="menu__link" href="/multi-scrobbler/FAQ/"><span class="linkLabel_WmDU">FAQ</span></a></li><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-1 menu__list-item"><a class="menu__link" href="/multi-scrobbler/help/"><span class="linkLabel_WmDU">Help</span></a></li><li class="theme-doc-sidebar-item-link theme-doc-sidebar-item-link-level-1 menu__list-item"><a class="menu__link" href="/multi-scrobbler/playground/"><span class="linkLabel_WmDU">Config Playground</span></a></li><li class="theme-doc-sidebar-item-category theme-doc-sidebar-item-category-level-1 menu__list-item menu__list-item--collapsed"><div class="menu__list-item-collapsible"><a class="categoryLink_byQd menu__link menu__link--sublist menu__link--sublist-caret" role="button" aria-expanded="false" href="/multi-scrobbler/api/multi-scrobbler-api/"><span class="categoryLinkLabel_W154">Api</span></a></div></li></ul></nav></div></div></aside><main class="docMainContainer_TBSr"><div class="container padding-top--md padding-bottom--lg"><div class="row"><div class="col docItemCol_xLCN"><div class="docItemContainer_jfFK"><article><nav class="theme-doc-breadcrumbs breadcrumbsContainer_Z_bl" aria-label="Breadcrumbs"><ul class="breadcrumbs"><li class="breadcrumbs__item"><a aria-label="Home page" class="breadcrumbs__link" href="/multi-scrobbler/"><svg viewBox="0 0 24 24" class="breadcrumbHomeIcon_YNFT"><path d="M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1z" fill="currentColor"></path></svg></a></li><li class="breadcrumbs__item"><a class="breadcrumbs__link" href="/multi-scrobbler/category/development/"><span>Development</span></a></li><li class="breadcrumbs__item breadcrumbs__item--active"><span class="breadcrumbs__link">Common Development</span></li></ul></nav><div class="tocCollapsible_ETCw theme-doc-toc-mobile tocMobile_ITEo"><button type="button" class="clean-btn tocCollapsibleButton_TO0P">On this page</button></div><div class="theme-doc-markdown markdown"><div class="row"><div class="col col--12 markdown"><header><h1>Development</h1></header><h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="architecture">Architecture<a href="#architecture" class="hash-link" aria-label="Direct link to Architecture" title="Direct link to Architecture" translate="no"></a></h2><p>Multi-scrobbler is written entirely in <a href="https://www.typescriptlang.org/" target="_blank" rel="noopener noreferrer" class="">Typescript</a>. It consists of a backend and frontend.</p><p>The backend handles all Source/Client (Component) logic, mounts web server endpoints that listen for Auth callbacks and Source ingress using <a href="https://expressjs.com/" target="_blank" rel="noopener noreferrer" class="">expressjs</a>, and serves the frontend. It creates and uses a sqlite database to persist Plays/Scrobbles, manage queue functionality, and keep track of lifecycle events for Plays/Scrobbles.</p><p>The frontend is a standalone <a href="https://vitejs.dev/" target="_blank" rel="noopener noreferrer" class="">Vitejs</a> app that communicates via API to the backend.</p><h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="project-setup">Project Setup<a href="#project-setup" class="hash-link" aria-label="Direct link to Project Setup" title="Direct link to Project Setup" translate="no"></a></h2><p>Development requires <a href="https://nodejs.org/en" target="_blank" rel="noopener noreferrer" class="">Node v24.14.0</a> or higher is installed on your system.</p><div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>tip</div><div class="admonitionContent_BuS1"><p>When running locally (not with a devcontainer) you can use <a href="https://github.com/nvm-sh/nvm" target="_blank" rel="noopener noreferrer" class="">nvm</a> to manage the installed node version.</p></div></div><p>Clone this repository somewhere and then install from the working directory</p><div class="language-shell codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-shell codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token function" style="color:rgb(80, 250, 123)">git</span><span class="token plain"> clone https://github.com/FoxxMD/multi-scrobbler.git </span><span class="token builtin class-name" style="color:rgb(189, 147, 249)">.</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token builtin class-name" style="color:rgb(189, 147, 249)">cd</span><span class="token plain"> multi-scrobbler</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">nvm use </span><span class="token comment" style="color:rgb(98, 114, 164)"># optional, sets correct node version when running without devcontainer</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token function" style="color:rgb(80, 250, 123)">npm</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">install</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token function" style="color:rgb(80, 250, 123)">npm</span><span class="token plain"> run docs:install </span><span class="token operator">&&</span><span class="token plain"> </span><span class="token function" style="color:rgb(80, 250, 123)">npm</span><span class="token plain"> run build</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token function" style="color:rgb(80, 250, 123)">npm</span><span class="token plain"> run start</span><br></div></code></pre></div></div><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="vscode">VSCode<a href="#vscode" class="hash-link" aria-label="Direct link to VSCode" title="Direct link to VSCode" translate="no"></a></h3><p>This repository contains <a href="https://github.com/FoxxMD/multi-scrobbler/blob/master/.devcontainer" target="_blank" rel="noopener noreferrer" class="">workspace settings</a> for development with VSCode. These include:</p><ul><li class="">Run/Debug <a href="https://code.visualstudio.com/Docs/editor/debugging#_launch-configurations" target="_blank" rel="noopener noreferrer" class="">Launch configurations</a> for the application and tests</li><li class=""><a href="https://code.visualstudio.com/docs/devcontainers/containers" target="_blank" rel="noopener noreferrer" class="">Devcontainer</a> for development with all dependencies already installed</li><li class="">Useful extensions for linting and running tests</li></ul><p>To use the Devcontainer simple open the repository in VSCode and "Use Devcontainer" when the notification is presented. <code>npm install</code> will be run when a new container is created.</p><h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="common-development">Common Development<a href="#common-development" class="hash-link" aria-label="Direct link to Common Development" title="Direct link to Common Development" translate="no"></a></h2><div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_BuS1"><p>In this document, when referring to aspects of Sources and Clients that are shared between both, the Source/Client will be referred to as a <strong>Component.</strong></p></div></div><p>A Component is composed of two parts:</p><ul><li class="">Typescript interfaces describing structure of configuration for that Component</li><li class="">A concrete class inheriting from a common "startup" abstract class that enforces how the Component is built and operates</li></ul><p>In both parts Source/Clients share some common properties/behavior before diverging in how they operate.</p><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="config">Config<a href="#config" class="hash-link" aria-label="Direct link to Config" title="Direct link to Config" translate="no"></a></h3><p>The configuration for a Component should always have this minimum shape, enforced respectively by the interfaces <code>CommonSourceConfig</code> and <code>CommonClientConfig</code>:</p><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">interface</span><span class="token plain"> </span><span class="token class-name maybe-class-name">MyConfig</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> id</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">string</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> name</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">string</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> data</span><span class="token operator">?</span><span class="token operator">:</span><span class="token plain"> object</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> options</span><span class="token operator">?</span><span class="token operator">:</span><span class="token plain"> object</span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div><ul><li class=""><code>data</code> contains data that is required for a Component to operate such as credentials, callback urls, api keys, endpoints, etc...</li><li class=""><code>options</code> are <strong>optional</strong> settings that can be used to fine-tune the usage of the Component but are not required or do not majorly affect behavior. EX additional logging toggles</li></ul><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="concrete-class">Concrete Class<a href="#concrete-class" class="hash-link" aria-label="Direct link to Concrete Class" title="Direct link to Concrete Class" translate="no"></a></h3><p>Components inherit from an abstract base class, <a href="https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/AbstractComponent.ts" target="_blank" rel="noopener noreferrer" class=""><code>AbstractComponent</code></a>, that defines different "stages" of how a Component is built and initialized when MS first starts as well as when restarting the Component in the event it stops due to an error/network failure/etc...</p><h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="stages">Stages<a href="#stages" class="hash-link" aria-label="Direct link to Stages" title="Direct link to Stages" translate="no"></a></h4><p>Stages below are invoked in the order listed. All stages are asynchronous to allow fetching network requests or reading files.</p><p>The stage function (described in each stage below) should return a value or throw:</p><ul><li class="">return <code>null</code> if the stage is not required</li><li class="">return <code>true</code> if the stage succeeded</li><li class="">return a <code>string</code> if the stage succeeded and you wish to append a result to the log output for this stage</li><li class="">throw an <code>Exception</code> if the stage failed for any reason and the Component should not continue to run/start up</li></ul><h5 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="stage-build-data">Stage: Build Data<a href="#stage-build-data" class="hash-link" aria-label="Direct link to Stage: Build Data" title="Direct link to Stage: Build Data" translate="no"></a></h5><p>This stage should be used to validate user configuration, parse any additional data from async sources (file, network), and finalize the shape of any configuration/data needed for the Component to operate.</p><div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_BuS1"><p>Implement <code>doBuildInitData</code> in your concrete class to invoke this stage.</p></div></div><details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Examples</summary><div><div class="collapsibleContent_i85q"><ul><li class="">Parse a full URL like <code>http://SOME_IP:7000/subfolder/api</code> from user config containing a base url like <code>data.baseUrl: 'SOME_IP'</code> and then store this in the class config</li><li class="">Validate that config <code>data</code> contains required properties <code>user</code> <code>password</code> <code>salt</code></li><li class="">Read stored credentials from <code>${this.configDir}/currentCreds-MySource-${name}.json</code>;</li></ul></div></div></details><h5 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="stage-check-connection">Stage: Check Connection<a href="#stage-check-connection" class="hash-link" aria-label="Direct link to Stage: Check Connection" title="Direct link to Stage: Check Connection" translate="no"></a></h5><p>This stage is used to validate that MS can communicate with the service the Component is interacting with. This stage is invoked on MS startup as well as any time the Component tries to restart after a failure.</p><p>If the Component depends on <strong>ingress</strong> (like Jellyfin/Plex webhook) this stage is not necessary.</p><div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_BuS1"><p>Implement <code>doCheckConnection</code> in your concrete class to invoke this stage.</p></div></div><details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Examples</summary><div><div class="collapsibleContent_i85q"><ul><li class="">Make a <a href="https://nodejs.org/docs/latest-v18.x/api/http.html#httprequesturl-options-callback" target="_blank" rel="noopener noreferrer" class=""><code>request</code></a> to the service's server to ensure it is accessible</li><li class="">Open a websocket connection and check for a ping-pong</li></ul></div></div></details><h5 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="stage-test-auth">Stage: Test Auth<a href="#stage-test-auth" class="hash-link" aria-label="Direct link to Stage: Test Auth" title="Direct link to Stage: Test Auth" translate="no"></a></h5><p>MS determines if Auth is required for a Component based on two class properties. You should set these properties during <code>constructor</code> initialization for your Component class:</p><ul><li class=""><code>requiresAuth</code> - (default <code>false</code>) Set to <code>true</code> if MS should check/test Auth for this Component</li><li class=""><code>requiresAuthInteraction</code> - (default <code>false</code>) Set to <code>true</code> if user interaction is required to complete auth IE user needs to visit a callback URL</li></ul><p>If the Component requires authentication in order to communicate with a service then any required data should be built in this stage and a request made to the service to ensure the authentication data is valid.</p><p>This stage should return:</p><ul><li class=""><code>true</code> if auth succeeded</li><li class=""><code>false</code> if auth failed without unexpected errors<ul><li class="">IE the authentication data is not valid and requires user interaction to resolve the failure</li></ul></li><li class="">throw an exception if network failure or unexpected error occurred</li></ul><p>You <em>should</em> attempt to re-authenticate, if possible. Only throw an exception or return <code>false</code> if there is no way to recover from an authentication failure.</p><div class="theme-admonition theme-admonition-info admonition_xJq3 alert alert--info"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_BuS1"><p>Implement <code>doAuthentication</code> in your child class to invoke this stage.</p></div></div><details class="details_lb9f alert alert--info details_b_Ee" data-collapsed="true"><summary>Examples</summary><div><div class="collapsibleContent_i85q"><ul><li class="">Generate a Bearer Token for Basic Auth from user/password given in config and store in class properties</li><li class="">Make a request to a known endpoint with Authorization token from read credentials file to see if succeeds or returns 403</li><li class="">Catch a 403 and attempt to reauthenticate at an auth endpoint with user/password given in config</li></ul></div></div></details><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="play-object">Play Object<a href="#play-object" class="hash-link" aria-label="Direct link to Play Object" title="Direct link to Play Object" translate="no"></a></h3><p>The <strong>PlayObject</strong> is the standard data structure MS uses to store listen (track) information and data required for scrobbling. It consists of:</p><ul><li class="">Track Data -- a standard format for storing track, artists, album, track duration, the date the track was played at, etc...</li><li class="">Listen Metadata -- Optional but useful data related to the specific play or specifics about the Source/Client context for this play such as<ul><li class="">Platform specific ID, web URL to track, device/user ID that played this track, etc...</li></ul></li></ul><p>Both Sources and Clients use the <strong>PlayObject</strong> interface. When a Component receives track info from its corresponding service it must transform this data into a PlayObject before it can be interacted with.</p><p>For more refer to the TS documentation for <code>PlayObject</code> or <code>AmbPlayObject</code> in your project</p><h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="creating-clients-and-sources">Creating Clients and Sources<a href="#creating-clients-and-sources" class="hash-link" aria-label="Direct link to Creating Clients and Sources" title="Direct link to Creating Clients and Sources" translate="no"></a></h2><ul><li class=""><a class="" href="/multi-scrobbler/development/dev-source/">Source Development and Tutorial</a></li><li class="">Client Development and Tutorial (TBD)</li></ul><h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="profiling">Profiling<a href="#profiling" class="hash-link" aria-label="Direct link to Profiling" title="Direct link to Profiling" translate="no"></a></h2><p>Run node with inspect args</p><div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">NODE_ENV=production node --inspect --heap-prof src/backend/index.ts</span><br></div></code></pre></div></div><p>Use <code>chrome://inspect</code> from a chromium-based browser and attach to the running process, usually <code>localhost:9229</code></p><p>From the opened DevTools window use Performance or Memory to profile the running process.</p><div class="theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 12 16"><path fill-rule="evenodd" d="M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"></path></svg></span>tip</div><div class="admonitionContent_BuS1"><p>To profile/inspect the production docker image use <code>NODE_ARGS</code> env:</p><div class="language-yaml codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-yaml codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token key atrule">services</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token key atrule">multi-scrobbler</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token comment" style="color:rgb(98, 114, 164)"># ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token key atrule">environment</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token comment" style="color:rgb(98, 114, 164)"># ...</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain"> NODE_ARGS=</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">inspect=0.0.0.0</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain">9229 </span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">heap</span><span class="token punctuation" style="color:rgb(248, 248, 242)">-</span><span class="token plain">prof</span><br></div></code></pre></div></div></div></div></div></div></div><div class="row"><div class="col col--12"><footer class="theme-doc-footer docusaurus-mt-lg"><div class="row margin-top--sm theme-doc-footer-edit-meta-row"><div class="col noPrint_WFHX"><a href="https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/development/dev-common.md?plain=1" target="_blank" rel="noopener noreferrer" class="theme-edit-this-page"><svg fill="currentColor" height="20" width="20" viewBox="0 0 40 40" class="iconEdit_Z9Sw" aria-hidden="true"><g><path d="m34.5 11.7l-3 3.1-6.3-6.3 3.1-3q0.5-0.5 1.2-0.5t1.1 0.5l3.9 3.9q0.5 0.4 0.5 1.1t-0.5 1.2z m-29.5 17.1l18.4-18.5 6.3 6.3-18.4 18.4h-6.3v-6.2z"></path></g></svg>Edit this page</a></div><div class="col lastUpdated_JAkA"></div></div></footer></div></div></article><div class="row"><div class="col col--12"><nav class="docusaurus-mt-lg pagination-nav" aria-label="Docs pages"><a class="pagination-nav__link pagination-nav__link--prev" href="/multi-scrobbler/category/development/"><div class="pagination-nav__sublabel">Previous</div><div class="pagination-nav__label">Development</div></a><a class="pagination-nav__link pagination-nav__link--next" href="/multi-scrobbler/development/dev-source/"><div class="pagination-nav__sublabel">Next</div><div class="pagination-nav__label">Source Development/Tutorial</div></a></nav></div></div></div></div><div class="col col--3"><div class="tableOfContents_bqdL thin-scrollbar theme-doc-toc-desktop"><ul class="table-of-contents table-of-contents__left-border"><li><a href="#architecture" class="table-of-contents__link toc-highlight">Architecture</a></li><li><a href="#project-setup" class="table-of-contents__link toc-highlight">Project Setup</a><ul><li><a href="#vscode" class="table-of-contents__link toc-highlight">VSCode</a></li></ul></li><li><a href="#common-development" class="table-of-contents__link toc-highlight">Common Development</a><ul><li><a href="#config" class="table-of-contents__link toc-highlight">Config</a></li><li><a href="#concrete-class" class="table-of-contents__link toc-highlight">Concrete Class</a><ul><li><a href="#stages" class="table-of-contents__link toc-highlight">Stages</a><ul><li><a href="#stage-build-data" class="table-of-contents__link toc-highlight">Stage: Build Data</a></li><li><a href="#stage-check-connection" class="table-of-contents__link toc-highlight">Stage: Check Connection</a></li><li><a href="#stage-test-auth" class="table-of-contents__link toc-highlight">Stage: Test Auth</a></li></ul></li></ul></li><li><a href="#play-object" class="table-of-contents__link toc-highlight">Play Object</a></li></ul></li><li><a href="#creating-clients-and-sources" class="table-of-contents__link toc-highlight">Creating Clients and Sources</a></li><li><a href="#profiling" class="table-of-contents__link toc-highlight">Profiling</a></li></ul></div></div></div></div></main></div></div></div><footer class="theme-layout-footer footer"><div class="container container-fluid"><div class="row footer__links"><div class="theme-layout-footer-column col footer__col"><div class="footer__title">Docs</div><ul class="footer__items clean-list"><li class="footer__item"><a class="footer__link-item" href="/multi-scrobbler/">Overview</a></li><li class="footer__item"><a class="footer__link-item" href="/multi-scrobbler/installation/">Installation</a></li><li class="footer__item"><a class="footer__link-item" href="/multi-scrobbler/configuration/">Configuration</a></li><li class="footer__item"><a class="footer__link-item" href="/multi-scrobbler/development/dev-common/">Development</a></li></ul></div><div class="theme-layout-footer-column col footer__col"><div class="footer__title">More</div><ul class="footer__items clean-list"><li class="footer__item"><a href="https://github.com/foxxmd/multi-scrobbler" target="_blank" rel="noopener noreferrer" class="footer__link-item">GitHub<svg width="13.5" height="13.5" aria-label="(opens in new tab)" class="iconExternalLink_nPIU"><use href="#theme-svg-external-link"></use></svg></a></li><li class="footer__item"><a href="https://multi-scrobbler.app" target="_blank" rel="noopener noreferrer" class="footer__link-item">Website<svg width="13.5" height="13.5" aria-label="(opens in new tab)" class="iconExternalLink_nPIU"><use href="#theme-svg-external-link"></use></svg></a></li></ul></div></div><div class="footer__bottom text--center"><div class="footer__copyright">Copyright © 2026 Multi-Scrobbler. Built with Docusaurus.</div></div></div></footer></div></body></html>