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.
99 kB · 125 lines
HTML
at gh-pages
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125<!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-source" data-has-hydrated="false"><head><meta charset="UTF-8"><meta name="generator" content="Docusaurus v3.10.2"><title data-rh="true">Source Development/Tutorial | 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/9b6e7226e269ee3319dfdd1444b6a22a0f585b07.png"><meta data-rh="true" name="twitter:image" content="https://foxxmd.github.io/preview-images/9b6e7226e269ee3319dfdd1444b6a22a0f585b07.png"><meta data-rh="true" property="og:url" content="https://foxxmd.github.io/multi-scrobbler/development/dev-source/"><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="Source Development/Tutorial | Multi-Scrobbler"><meta data-rh="true" name="description" content="Developing a new Source"><meta data-rh="true" property="og:description" content="Developing a new Source"><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-source/"><link data-rh="true" rel="alternate" href="https://docs.multi-scrobbler.app/development/dev-source/" hreflang="en"><link data-rh="true" rel="alternate" href="https://docs.multi-scrobbler.app/development/dev-source/" 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":"Source Development/Tutorial","item":"https://foxxmd.github.io/multi-scrobbler/development/dev-source"}]}</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/9b6e7226e269ee3319dfdd1444b6a22a0f585b07.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" 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 menu__link--active" aria-current="page" 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">Source Development/Tutorial</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"><header><h1>Source Development/Tutorial</h1></header><div class="row"><div class="col col--12 markdown"><p>This document will provide a step-by-step guide for creating a (trivial) new Source in MS alongside describing what aspects of the Source need to be implemented based on the service you use. Before using this document you should review <a class="" href="/multi-scrobbler/development/dev-common/#common-development">Common Development</a>.</p><h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="scenario">Scenario<a href="#scenario" class="hash-link" aria-label="Direct link to Scenario" title="Direct link to Scenario" translate="no"></a></h2><p>You are the developer of a fancy, new self-hosted web-based media player called <strong>Cool Player.</strong> Cool Player has a slick interface and many bells and whistles, but most importantly it has an API. The API:</p><ul><li class="">Has an unauthenticated health endpoint at <code>/api/health</code> that returns <code>200</code> if the service is running properly</li><li class="">Has authenticated endpoints that require a user-generated token in the header <code>Authorization MY_TOKEN</code><ul><li class="">Has a <code>/api/recent</code> endpoint that lists recently played tracks with a timestamp</li><li class="">Has a <code>/api/now-playing</code> endpoint that returns information about the state of the player like current track, player position in the track, etc...</li></ul></li><li class="">Cool Player is by default accessed on port <code>6969</code></li><li class="">Your personal instance of Cool Player is hosted at <code>http://192.168.0.100:6969</code> and the api is accessed at <code>http://192.168.0.100:6969/api</code></li></ul><p>Because there is an API that MS can actively read this will be a <strong>polling</strong> Source where MS sends requests to Cool Player to get scrobble information -- as opposed to an <strong>ingress</strong> Source like Webscrobbler that uses webhooks from the service to send data to MS.</p><h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="minimal-implementation">Minimal Implementation<a href="#minimal-implementation" class="hash-link" aria-label="Direct link to Minimal Implementation" title="Direct link to Minimal Implementation" translate="no"></a></h2><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="define-and-implement-config">Define and Implement Config<a href="#define-and-implement-config" class="hash-link" aria-label="Direct link to Define and Implement Config" title="Direct link to Define and Implement Config" translate="no"></a></h3><p>We will create a new config interface for Cool Player using the <a class="" href="/multi-scrobbler/development/dev-common/#config">Common Config</a> and tell MS it is a valid config that can be used.</p><p>MS uses <a href="https://zod.dev/" target="_blank" rel="noopener noreferrer" class="">Zod</a> to define schema objects and the infer the config type from this schema.</p><p>The eaiest way to use this for our new config is focus on implementing only the <strong>data</strong> and <strong>options</strong> schemas, then assigning those to a generic source config schema.</p><p>Create a new file for your config and implement a <strong>data</strong> schema, this is the required config for the Source to operate:</p><div>Loading ...</div><p>Basic options and the complete config shape:</p><div>Loading ...</div><p>If your Source should also be configurable via <a class="" href="/multi-scrobbler/configuration/?configType=env#configuration-types">ENV</a> then implement a separate shape for ENV (reusing shape from data/options) as well as an <code>envSchemas</code> object that is used by MS for validation, generating ENV config docs, and transforming common ENV keys for your Source:</p><div>Loading ...</div><p>Finally, add a shape for use with <a class="" href="/multi-scrobbler/configuration/?configType=aio#configuration-types">AIO</a> config:</p><div>Loading ...</div><h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="add-to-config-map">Add to Config Map<a href="#add-to-config-map" class="hash-link" aria-label="Direct link to Add to Config Map" title="Direct link to Add to Config Map" translate="no"></a></h4><p>Next, we add our newely created config Types and zod schemas to several data structures that MS uses for typing Sources, generating documentation, and parsing/validating config data.</p><p>In <a href="https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/infrastructure/config/source/sources.ts" target="_blank" rel="noopener noreferrer" class=""><code>sources.ts</code></a>, import <code>coolPlaySourceConfigSchema</code> (for File) and <code>coolPlayerSourceAIOConfigSchema</code> (for AIO) and add them to the ends of their respective union types:</p><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">sources.ts</div><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 module" style="color:rgb(189, 147, 249);font-style:italic">export</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> sourceConfigSchema </span><span class="token operator">=</span><span class="token plain"> z</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">union</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"></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"> appleMusicSourceConfigSchema</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> coolPlaySourceConfigSchema</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 punctuation" style="color:rgb(248, 248, 242)">)</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" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">export</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">type</span><span class="token plain"> </span><span class="token class-name maybe-class-name">SourceConfig</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> z</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token property-access">infer</span><span class="token operator"><</span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">typeof</span><span class="token plain"> sourceConfigSchema</span><span class="token operator">></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" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">export</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> sourceAIOConfigSchema </span><span class="token operator">=</span><span class="token plain"> z</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">union</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"></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"> appleMusicSourceAIOConfigSchema</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> coolPlayerSourceAIOConfigSchema</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 punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div><p>In <a href="https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/common/infrastructure/config/source/sourcesMap.ts" target="_blank" rel="noopener noreferrer" class=""><code>sourcesMap.ts</code></a> add <code>coolplayer</code> to <code>SourceTypeConfigMap</code> for type narrowing and <code>sourceConfigSchemaMapAsync</code> to enable dynamic importing for schema validation.</p><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">sourcesMap.ts</div><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 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 keyword module" style="color:rgb(189, 147, 249);font-style:italic">export</span><span class="token plain"> </span><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">SourceTypeConfigMap</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">extends</span><span class="token plain"> </span><span class="token class-name maybe-class-name">Record</span><span class="token class-name operator"><</span><span class="token class-name maybe-class-name">SourceType</span><span class="token class-name punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token class-name"> </span><span class="token class-name punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token class-name maybe-class-name">CommonSourceConfig</span><span class="token class-name punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token class-name"> </span><span class="token class-name maybe-class-name">SourceAIOConfig</span><span class="token class-name punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token class-name"> </span><span class="token class-name maybe-class-name">Partial</span><span class="token class-name operator"><</span><span class="token class-name maybe-class-name">Pick</span><span class="token class-name operator"><</span><span class="token class-name maybe-class-name">CommonSourceConfig</span><span class="token class-name punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token class-name"> </span><span class="token class-name string" style="color:rgb(255, 121, 198)">'data'</span><span class="token class-name"> </span><span class="token class-name operator">|</span><span class="token class-name"> </span><span class="token class-name string" style="color:rgb(255, 121, 198)">'options'</span><span class="token class-name operator">>></span><span class="token class-name punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token class-name operator">></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"> </span><span class="token comment" style="color:rgb(98, 114, 164)">// ...</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> coolplayer</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token maybe-class-name">CoolPlayerSourceConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token maybe-class-name">CoolPlayerSourceAIOConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token maybe-class-name">Partial</span><span class="token operator"><</span><span class="token maybe-class-name">Pick</span><span class="token operator"><</span><span class="token maybe-class-name">CoolPlayerSourceConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'data'</span><span class="token plain"> </span><span class="token operator">|</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'options'</span><span class="token operator">>></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"></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"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></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" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">export</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> sourceConfigSchemaMapAsync</span><span class="token operator">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token constant" style="color:rgb(189, 147, 249)">K</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">in</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">keyof</span><span class="token plain"> </span><span class="token maybe-class-name">SourceTypeConfigMap</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token operator">:</span><span class="token plain"> </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"> </span><span class="token arrow operator">=></span><span class="token plain"> </span><span class="token known-class-name class-name">Promise</span><span class="token operator"><</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token maybe-class-name">ZodType</span><span class="token operator"><</span><span class="token maybe-class-name">SourceTypeConfigMap</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token constant" style="color:rgb(189, 147, 249)">K</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 number">0</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token operator">></span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token maybe-class-name">ZodType</span><span class="token operator"><</span><span class="token maybe-class-name">SourceTypeConfigMap</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token constant" style="color:rgb(189, 147, 249)">K</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 number">1</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token operator">></span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token maybe-class-name">EnvSourceSchema</span><span class="token operator"><</span><span class="token maybe-class-name">ZodObject</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token maybe-class-name">SourceTypeConfigMap</span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token constant" style="color:rgb(189, 147, 249)">K</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 number">0</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token operator">></span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token operator">></span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token operator">=</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"> </span><span class="token comment" style="color:rgb(98, 114, 164)">// ...</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token function-variable function" style="color:rgb(80, 250, 123)">coolplayer</span><span class="token operator">:</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">async</span><span class="token plain"> </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"> </span><span class="token arrow operator">=></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 theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">coolPlayerSourceConfigSchema</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> coolPlayerSourceAIOConfigSchema</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> envSchemas </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">'./coolplayer.ts'</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 punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">[</span><span class="token plain">coolPlayerSourceConfigSchema</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> coolPlayerSourceAIOConfigSchema</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> envSchemas</span><span class="token punctuation" style="color:rgb(248, 248, 242)">]</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> </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"></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><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="create-coolplayer-source">Create CoolPlayer Source<a href="#create-coolplayer-source" class="hash-link" aria-label="Direct link to Create CoolPlayer Source" title="Direct link to Create CoolPlayer Source" translate="no"></a></h3><p>Now we will create a new Source inheriting from <a class="" href="/multi-scrobbler/development/dev-common/#concrete-class"><code>AbstractComponent</code></a> that:</p><ul><li class="">accepts our config interface</li><li class="">implements a function to transform CoolPlayer's track data into a <a class="" href="/multi-scrobbler/development/dev-common/#play-object"><strong>PlayObject</strong></a></li><li class="">implements required <a class="" href="/multi-scrobbler/development/dev-common/#stages">stages</a></li><li class="">implements required methods to get current player state and/or now playing track</li></ul><p>First we create a new Source called <code>CoolPlayerSource</code> and setup our constructor to accept the config as well as setting any behavior <code>overrides</code>.</p><div>Loading ...</div><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="implement-stages">Implement Stages<a href="#implement-stages" class="hash-link" aria-label="Direct link to Implement Stages" title="Direct link to Implement Stages" translate="no"></a></h3><p>Next we will implement the <a class="" href="/multi-scrobbler/development/dev-common/#stages">Stages</a> required to get CoolPlayerSource running.</p><h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="build-data">Build Data<a href="#build-data" class="hash-link" aria-label="Direct link to Build Data" title="Direct link to Build Data" translate="no"></a></h4><p>First we implement the <a class="" href="/multi-scrobbler/development/dev-common/#stage-build-data">Build Data Stage</a>. We will check the <code>baseUrl</code> property includes the necessary prefix and then parse it to a normalized data structure we can use elsewhere.</p><div>Loading ...</div><h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="check-connection">Check Connection<a href="#check-connection" class="hash-link" aria-label="Direct link to Check Connection" title="Direct link to Check Connection" translate="no"></a></h4><p>Second we will implement the <a class="" href="/multi-scrobbler/development/dev-common/#stage-check-connection">Check Connection Stage</a>:</p><div>Loading ...</div><h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="test-auth">Test Auth<a href="#test-auth" class="hash-link" aria-label="Direct link to Test Auth" title="Direct link to Test Auth" translate="no"></a></h4><p>Finally, we will implement <a class="" href="/multi-scrobbler/development/dev-common/#stage-test-auth">Auth Test Stage</a>:</p><div>Loading ...</div><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="implement-play-object-transform">Implement Play Object Transform<a href="#implement-play-object-transform" class="hash-link" aria-label="Direct link to Implement Play Object Transform" title="Direct link to Implement Play Object Transform" translate="no"></a></h3><p>Now that Source initialization is taken care we can move on to implementing how our Source handles activity data from Cool Player.</p><p>First, we create a static function that is used to take the track data returned from Cool Player's API and return a standard <a class="" href="/multi-scrobbler/development/dev-common/#play-object"><code>PlayObject</code>.</a></p><div>Loading ...</div><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="implement-polling">Implement Polling<a href="#implement-polling" class="hash-link" aria-label="Direct link to Implement Polling" title="Direct link to Implement Polling" translate="no"></a></h3><p>Next, we implement the functions needed for MS to get the actual data from Cool Player and do something with it.</p><p>The majority of Sources MS monitors primarily operate as a source of truth for a <strong>music player</strong> rather than a <strong>played music history.</strong></p><p>To this end, MS implements a <a href="https://www.freecodecamp.org/news/state-machines-basics-of-computer-science-d42855debc66/" target="_blank" rel="noopener noreferrer" class="">state machine</a> that emulates the behavior of a music player in order to keep track of when a song you are listening to should be scrobbled. It does this by monitoring the "currently playing" track reported by a Source's service, with varying degrees of accuracy depending on what information is returned from the service. The state machine is implemented in <code>MemorySource</code> which our <code>CoolPlayerSource</code> inherits from.</p><p>For a polling Source to work properly we need to implement a function, <code>getRecentlyPlayed</code>, that returns PlayObjects or PlayObjects + Player data, that are "new". These are then checked against previously "discovered" Plays and their timestamp to determine if they should be surfaced to Clients to scrobble.</p><p>To take advantage of the <code>MemorySource</code> state machine we will additionally use <code>processRecentPlays</code> from <code>MemorySource</code> inside <code>getRecentlyPlayed</code>. We pass track and/or player state returned from the Source service to <code>processRecentPlayers</code>. It then takes care of deriving Source player state based on how this data changes over time. The advantage to using <code>processRecentPlays</code> is that our Source service does not necessarily need to pass any player information -- as long as the track info has a <strong>duration</strong> we can more-or-less determine if it has been played long enough to scrobble.</p><div>Loading ...</div><p>There are other types of Sources that can be implemented based on the granularity of data returned or the type of data returned. See further below, <a href="#other-source-types" class="">Other Source Types</a>.</p><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="initialize-source-from-config">Initialize Source from Config<a href="#initialize-source-from-config" class="hash-link" aria-label="Direct link to Initialize Source from Config" title="Direct link to Initialize Source from Config" translate="no"></a></h3><p>The last step for this implementation is to add your new, concrete <code>CoolPlayerSource</code> class to the Source builder class that handles parsing user config.</p><p>When MS starts it reads all configs and determines which Source to build based on the configs found. We need to tell it to build a <code>CoolPlayerSource</code> when a <code>coolplayer</code> config type is found.</p><p>We modify <a href="https://github.com/FoxxMD/multi-scrobbler/blob/master/src/backend/sources/ScrobbleSources.ts" target="_blank" rel="noopener noreferrer" class=""><code>ScrobbleSources.ts</code></a> to add <code>CoolPlayerSource</code> as a case in the <code>addSource</code> function:</p><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">src/backend/sources/ScrobbleSources.ts</div><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 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 keyword module" style="color:rgb(189, 147, 249);font-style:italic">export</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">default</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name maybe-class-name">ScrobbleSources</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"> </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 function-variable function" style="color:rgb(80, 250, 123)">addSource</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">async</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">sourceType</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">SourceType</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> strongConfigs</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">CommonParsedConfig</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 punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> defaults</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">SourceDefaults</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </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 punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token arrow operator">=></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"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">switch</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">sourceType</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</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"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">case</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'spotify'</span><span class="token operator">:</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 comment" style="color:rgb(98, 114, 164)">//</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token comment" style="color:rgb(98, 114, 164)">// add a dynamic import for CoolPlayersource</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">case</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'coolplayer'</span><span class="token operator">:</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 theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> </span><span class="token maybe-class-name">CoolPlayerSource</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">'./CoolPlayerSource.ts'</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 punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">default</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><span class="token plain"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">this</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">instantiateSources</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">'coolplayer'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> strongConfigs</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> defaults</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token maybe-class-name">CoolPlayerSource</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"></span><br></div><div class="token-line theme-code-block-highlighted-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">break</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 keyword module" style="color:rgb(189, 147, 249);font-style:italic">default</span><span class="token operator">:</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">break</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 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"></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><br></div></code></pre></div></div><p>Congratulations! Your new <strong>Cool Player</strong> Source is implemented and ready to be built by MS with the config your defined.</p><h2 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="further-implementation">Further Implementation<a href="#further-implementation" class="hash-link" aria-label="Direct link to Further Implementation" title="Direct link to Further Implementation" translate="no"></a></h2><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="backlog">Backlog<a href="#backlog" class="hash-link" aria-label="Direct link to Backlog" title="Direct link to Backlog" translate="no"></a></h3><p>To have your Source try to scrobble "missed" tracks when MS starts up the Source's service must be able to provide:</p><ul><li class="">track information</li><li class="">timestamp of when the track was played</li></ul><p>In your Source implement <code>getBackloggedPlays</code> and set setting in constructor indicating it has backlogging capabilities:</p><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">src/backend/sources/CoolPlayerSource.ts</div><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 module" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token imports">request</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">'superagent'</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 keyword module" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token imports punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token imports"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token imports"> </span><span class="token imports maybe-class-name">PlayObject</span><span class="token imports punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token imports"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token imports"></span><span class="token imports punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"../common/infrastructure/Atomic.js"</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 keyword module" style="color:rgb(189, 147, 249);font-style:italic">export</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">default</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name maybe-class-name">CoolPlayerSource</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">extends</span><span class="token plain"> </span><span class="token class-name maybe-class-name">MemorySource</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" style="display:inline-block"></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)">// tell MS it should try to get backlogged tracks on startup</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> override canBacklog</span><span class="token operator">:</span><span class="token plain"> </span><span class="token builtin" style="color:rgb(189, 147, 249)">boolean</span><span class="token plain"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token boolean">true</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" style="display:inline-block"></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)">constructor</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token comment" style="color:rgb(98, 114, 164)">/* ... */</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</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"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">super</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token comment" style="color:rgb(98, 114, 164)">/* ... */</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"></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"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></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" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">protected</span><span class="token plain"> getBackloggedPlays </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">async</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">options</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">RecentlyPlayedOptions</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token operator">:</span><span class="token plain"> </span><span class="token known-class-name class-name">Promise</span><span class="token operator"><</span><span class="token maybe-class-name">PlayObject</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 operator">></span><span class="token plain"> </span><span class="token arrow operator">=></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"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">try</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"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> resp </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">await</span><span class="token plain"> request</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 method function property-access" style="color:rgb(80, 250, 123)">get</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token template-string template-punctuation string" style="color:rgb(255, 121, 198)">`</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(248, 248, 242)">${</span><span class="token template-string interpolation keyword" style="color:rgb(189, 147, 249);font-style:italic">this</span><span class="token template-string interpolation punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token template-string interpolation property-access">urlData</span><span class="token template-string interpolation punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token template-string interpolation property-access">url</span><span class="token template-string interpolation punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token template-string interpolation method function property-access" style="color:rgb(80, 250, 123)">toString</span><span class="token template-string interpolation punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token template-string interpolation punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token template-string string" style="color:rgb(255, 121, 198)">/history</span><span class="token template-string template-punctuation string" style="color:rgb(255, 121, 198)">`</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 punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">set</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">'Authorization'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token template-string template-punctuation string" style="color:rgb(255, 121, 198)">`</span><span class="token template-string string" style="color:rgb(255, 121, 198)">Token </span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(248, 248, 242)">${</span><span class="token template-string interpolation keyword" style="color:rgb(189, 147, 249);font-style:italic">this</span><span class="token template-string interpolation punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token template-string interpolation property-access">config</span><span class="token template-string interpolation punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token template-string interpolation property-access">token</span><span class="token template-string interpolation interpolation-punctuation punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token template-string template-punctuation string" style="color:rgb(255, 121, 198)">`</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"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain" style="display:inline-block"></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)">// assuming list from body looks like track info returned in </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)">// "Implement Play Object Transform" section</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</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"> body </span><span class="token operator">=</span><span class="token plain"> </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"></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"> </span><span class="token operator">=</span><span class="token plain"> resp</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" style="display:inline-block"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">return</span><span class="token plain"> body</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">map</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">x </span><span class="token arrow operator">=></span><span class="token plain"> </span><span class="token maybe-class-name">CoolPlayerSource</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">formatPlayObj</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">x</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 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 punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">catch</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">e</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</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"> </span><span class="token keyword control-flow" style="color:rgb(189, 147, 249);font-style:italic">throw</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">new</span><span class="token plain"> </span><span class="token class-name known-class-name class-name">Error</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token string" style="color:rgb(255, 121, 198)">'Error occurred while getting recently played'</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain">cause</span><span class="token operator">:</span><span class="token plain"> e</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 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 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" style="display:inline-block"></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"></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><h3 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="other-source-types">Other Source Types<a href="#other-source-types" class="hash-link" aria-label="Direct link to Other Source Types" title="Direct link to Other Source Types" translate="no"></a></h3><p>There are some scenarios where polling and/or state machine is not the right tool to handle determining if incoming data should be scrobbled:</p><ul><li class="">The Source service handles scrobble threshold internally, the data being received should always be scrobbled (WebScrobbler, Plex, Tautulli, Listenbrainz, Last.fm)</li><li class="">You prefer to handle the scrobble determination yourself</li></ul><h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="music-history-source">Music History Source<a href="#music-history-source" class="hash-link" aria-label="Direct link to Music History Source" title="Direct link to Music History Source" translate="no"></a></h4><p>If the Source is still polling but the track returned should always be scrobbled if not already seen IE the Source service is a <strong>music history source</strong> (Listenbrainz, Last.fm), rather than a music player, then simply indicate to MS the source of truth type by setting it in the constructor. The state machine will always return a track if it is new and not seen, regardless of how recently it was seen:</p><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">src/backend/sources/CoolPlayerSource.ts</div><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 module" style="color:rgb(189, 147, 249);font-style:italic">import</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"> </span><span class="token constant" style="color:rgb(189, 147, 249)">SOURCE_SOT</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">from</span><span class="token plain"> </span><span class="token string" style="color:rgb(255, 121, 198)">"../../core/Atomic.js"</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 keyword module" style="color:rgb(189, 147, 249);font-style:italic">export</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">default</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name maybe-class-name">CoolPlayerSource</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">extends</span><span class="token plain"> </span><span class="token class-name maybe-class-name">MemorySource</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" style="display:inline-block"></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)">// tell MS it should immediately scrobble any new, unseen tracks from the upstream service</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> override playerSourceofTruth </span><span class="token operator">=</span><span class="token plain"> </span><span class="token constant" style="color:rgb(189, 147, 249)">SOURCE_SOT</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token constant" style="color:rgb(189, 147, 249)">HISTORY</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" style="display:inline-block"></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)">constructor</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token comment" style="color:rgb(98, 114, 164)">/* ... */</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</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"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">super</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token comment" style="color:rgb(98, 114, 164)">/* ... */</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"></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"></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><p>Remember to also set <a href="#implement-play-object-transform" class=""><code>meta.parsedFrom</code></a> for your <code>Play</code> objects.</p><h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="non-polling-source">Non-Polling Source<a href="#non-polling-source" class="hash-link" aria-label="Direct link to Non-Polling Source" title="Direct link to Non-Polling Source" translate="no"></a></h4><p><strong>Ingress</strong> Sources (like Plex, Tautulli, Webscrobbler, Jellyfin) do not having a polling mechanism because the upstream service contacts MS when there is an event, rather than MS contacting the upstream service.</p><p>For these Sources you will need to implement endpoints in <code>src/service/api.ts</code> and corresponding files. See the existing Sources in the project as references for how to do this.</p><p>You may still wish to use the state machine <code>MemorySource</code> (like Jellyfin) if the events received are not "scrobble" events but instead of implementing <code>getRecentlyPlayed</code> you will implement your own function in your Source class, like <code>handle()</code>, that receives data and then uses <code>processRecentPlays</code>.</p><p>After new plays have been determined see the next section for how to scrobble...</p><h4 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="basic-source">Basic Source<a href="#basic-source" class="hash-link" aria-label="Direct link to Basic Source" title="Direct link to Basic Source" translate="no"></a></h4><p>At the core of a Source that implements <code>AbstractSource</code>'s functionality is the ability to <strong>Discover</strong> and <strong>Scrobble</strong> plays.</p><p>These functions are not seen in the MVP <code>CoolPlayerSource</code> because they are automatically done by the polling functionality after being returned from <code>getRecentlyPlayed</code>.</p><h5 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="discovery">Discovery<a href="#discovery" class="hash-link" aria-label="Direct link to Discovery" title="Direct link to Discovery" translate="no"></a></h5><p>A Source keeps track of all the "plays" that are determined to be valid for scrobbling. When a play is valid it is checked to see if it has already been "discovered" by comparing the track info and timestamp of the play against already discovered plays. This prevents duplicate scrobbling by using the Source's own data and simplifies scrobbling for Sources by allowing your implementation to "always" ingest track data without having to worry about whether its new or not -- <code>AbstractSource</code> and <code>discover()</code> will take care of that for you.</p><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">src/backend/sources/MyBasicSource.ts</div><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 module" style="color:rgb(189, 147, 249);font-style:italic">export</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">default</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name maybe-class-name">MyBasicSource</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">extends</span><span class="token plain"> </span><span class="token class-name maybe-class-name">AbstractSource</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"> </span><span class="token function" style="color:rgb(80, 250, 123)">handle</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">somePlay</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">PlayObject</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</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"> </span><span class="token comment" style="color:rgb(98, 114, 164)">// if the track is "new" and not seen before it is returned in the discovered list</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)">// we then know it is OK to be sent to Clients for scrobbling</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> discovered</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">PlayObject</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"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">this</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">discover</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">somePlay</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 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 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 punctuation" style="color:rgb(248, 248, 242)">}</span><br></div></code></pre></div></div><p>This additionally will be surfaced to the user in the Dashboard in the "Tracks Discovered" page.</p><h5 class="anchor anchorTargetHideOnScrollNavbar_vjPI" id="scrobbling">Scrobbling<a href="#scrobbling" class="hash-link" aria-label="Direct link to Scrobbling" title="Direct link to Scrobbling" translate="no"></a></h5><p>After a play is verified to be discovered we can then scrobble it. This will emit the plays to the ScrobbleClients service which then disseminates the play to all Clients that were configured to listen in the Source's config.</p><div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockTitle_OeMC">src/backend/sources/MyBasicSource.ts</div><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 module" style="color:rgb(189, 147, 249);font-style:italic">export</span><span class="token plain"> </span><span class="token keyword module" style="color:rgb(189, 147, 249);font-style:italic">default</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">class</span><span class="token plain"> </span><span class="token class-name maybe-class-name">MyBasicSourceSource</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">extends</span><span class="token plain"> </span><span class="token class-name maybe-class-name">AbstractSource</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"> </span><span class="token function" style="color:rgb(80, 250, 123)">handle</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">somePlay</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">PlayObject</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</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"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">const</span><span class="token plain"> discovered</span><span class="token operator">:</span><span class="token plain"> </span><span class="token maybe-class-name">PlayObject</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"> </span><span class="token operator">=</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">this</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">discover</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">somePlay</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 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)">// emit plays that can be scrobbled by clients</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">this</span><span class="token punctuation" style="color:rgb(248, 248, 242)">.</span><span class="token method function property-access" style="color:rgb(80, 250, 123)">scrobble</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">discovered</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"></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"></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><p>If your service only emits an event when a play is scrobbled you can <em>technically</em> skip using <code>discover()</code> but it is good practice to use it unless you have a very good reason not to.</p><div class="theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary"><div class="admonitionHeading_Gvgb"><span class="admonitionIcon_Rf37"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_BuS1"><p>Using <code>scrobble()</code> does not guarantee a track is actually scrobbled! The Scrobble Clients also check the play against their own "recently scrobbled" list to prevent duplicates.</p></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-source.mdx?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/development/dev-common/"><div class="pagination-nav__sublabel">Previous</div><div class="pagination-nav__label">Common Development</div></a><a class="pagination-nav__link pagination-nav__link--next" href="/multi-scrobbler/development/flatpak/"><div class="pagination-nav__sublabel">Next</div><div class="pagination-nav__label">Flatpak</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="#scenario" class="table-of-contents__link toc-highlight">Scenario</a></li><li><a href="#minimal-implementation" class="table-of-contents__link toc-highlight">Minimal Implementation</a><ul><li><a href="#define-and-implement-config" class="table-of-contents__link toc-highlight">Define and Implement Config</a><ul><li><a href="#add-to-config-map" class="table-of-contents__link toc-highlight">Add to Config Map</a></li></ul></li><li><a href="#create-coolplayer-source" class="table-of-contents__link toc-highlight">Create CoolPlayer Source</a></li><li><a href="#implement-stages" class="table-of-contents__link toc-highlight">Implement Stages</a><ul><li><a href="#build-data" class="table-of-contents__link toc-highlight">Build Data</a></li><li><a href="#check-connection" class="table-of-contents__link toc-highlight">Check Connection</a></li><li><a href="#test-auth" class="table-of-contents__link toc-highlight">Test Auth</a></li></ul></li><li><a href="#implement-play-object-transform" class="table-of-contents__link toc-highlight">Implement Play Object Transform</a></li><li><a href="#implement-polling" class="table-of-contents__link toc-highlight">Implement Polling</a></li><li><a href="#initialize-source-from-config" class="table-of-contents__link toc-highlight">Initialize Source from Config</a></li></ul></li><li><a href="#further-implementation" class="table-of-contents__link toc-highlight">Further Implementation</a><ul><li><a href="#backlog" class="table-of-contents__link toc-highlight">Backlog</a></li><li><a href="#other-source-types" class="table-of-contents__link toc-highlight">Other Source Types</a><ul><li><a href="#music-history-source" class="table-of-contents__link toc-highlight">Music History Source</a></li><li><a href="#non-polling-source" class="table-of-contents__link toc-highlight">Non-Polling Source</a></li><li><a href="#basic-source" class="table-of-contents__link toc-highlight">Basic Source</a><ul><li><a href="#discovery" class="table-of-contents__link toc-highlight">Discovery</a></li><li><a href="#scrobbling" class="table-of-contents__link toc-highlight">Scrobbling</a></li></ul></li></ul></li></ul></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>