Malachite is a tool to import your Last.fm and Spotify listening history to the AT Protocol network using the fm.teal.alpha.feed.play lexicon.
malachite scrobbles importer atproto music
14
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: basic meta tags

+268 -5
+19 -4
web/src/app.html
··· 5 5 <link rel="icon" href="%sveltekit.assets%/favicon.svg" /> 6 6 <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 7 <meta name="theme-color" content="#090f0c" /> 8 - <meta name="description" content="Import your Last.fm and Spotify listening history into ATProto (Teal / Bluesky)." /> 9 - <meta property="og:title" content="Malachite" /> 10 - <meta property="og:description" content="Import your Last.fm and Spotify listening history into ATProto." /> 8 + 9 + <!-- Primary --> 10 + <title>Malachite</title> 11 + <meta name="description" content="Import your Last.fm and Spotify listening history into Teal on ATProto. Free, open-source, runs in your browser." /> 12 + <meta name="author" content="Ewan Croft" /> 13 + <link rel="canonical" href="https://malachite.ewancroft.uk" /> 14 + 15 + <!-- Open Graph --> 16 + <meta property="og:site_name" content="Malachite" /> 11 17 <meta property="og:type" content="website" /> 12 18 <meta property="og:url" content="https://malachite.ewancroft.uk" /> 13 - <title>Malachite</title> 19 + <meta property="og:title" content="Malachite" /> 20 + <meta property="og:description" content="Import your Last.fm and Spotify listening history into Teal on ATProto. Free, open-source, runs in your browser." /> 21 + <meta property="og:image" content="https://malachite.ewancroft.uk/og-home.svg" /> 22 + 23 + <!-- Twitter / X card --> 24 + <meta name="twitter:card" content="summary_large_image" /> 25 + <meta name="twitter:title" content="Malachite" /> 26 + <meta name="twitter:description" content="Import your Last.fm and Spotify listening history into Teal on ATProto. Free, open-source, runs in your browser." /> 27 + <meta name="twitter:image" content="https://malachite.ewancroft.uk/og-home.svg" /> 28 + 14 29 %sveltekit.head% 15 30 </head> 16 31 <body data-sveltekit-preload-data="hover">
+14
web/src/routes/+page.svelte
··· 5 5 <svelte:head> 6 6 <title>Malachite — Import your music history to Teal</title> 7 7 <meta name="description" content="Import your Last.fm and Spotify listening history into Teal on ATProto. Free, open-source, runs in your browser." /> 8 + <link rel="canonical" href="https://malachite.ewancroft.uk" /> 9 + 10 + <!-- Open Graph --> 11 + <meta property="og:type" content="website" /> 12 + <meta property="og:url" content="https://malachite.ewancroft.uk" /> 13 + <meta property="og:title" content="Malachite — Import your music history to Teal" /> 14 + <meta property="og:description" content="Import your Last.fm and Spotify listening history into Teal on ATProto. Free, open-source, runs in your browser." /> 15 + <meta property="og:image" content="https://malachite.ewancroft.uk/og-home.svg" /> 16 + 17 + <!-- Twitter / X card --> 18 + <meta name="twitter:card" content="summary_large_image" /> 19 + <meta name="twitter:title" content="Malachite — Import your music history to Teal" /> 20 + <meta name="twitter:description" content="Import your Last.fm and Spotify listening history into Teal on ATProto. Free, open-source, runs in your browser." /> 21 + <meta name="twitter:image" content="https://malachite.ewancroft.uk/og-home.svg" /> 8 22 </svelte:head> 9 23 10 24 <main>
+15 -1
web/src/routes/about/+page.svelte
··· 1 1 <svelte:head> 2 2 <title>About — Malachite</title> 3 - <meta name="description" content="Privacy, credits, and licence information for Malachite." /> 3 + <meta name="description" content="Privacy policy, CLI usage, rate limit details, credits, and licence information for Malachite." /> 4 + <link rel="canonical" href="https://malachite.ewancroft.uk/about" /> 5 + 6 + <!-- Open Graph --> 7 + <meta property="og:type" content="website" /> 8 + <meta property="og:url" content="https://malachite.ewancroft.uk/about" /> 9 + <meta property="og:title" content="About — Malachite" /> 10 + <meta property="og:description" content="Privacy policy, CLI usage, rate limit details, credits, and licence information for Malachite." /> 11 + <meta property="og:image" content="https://malachite.ewancroft.uk/og-about.svg" /> 12 + 13 + <!-- Twitter / X card --> 14 + <meta name="twitter:card" content="summary_large_image" /> 15 + <meta name="twitter:title" content="About — Malachite" /> 16 + <meta name="twitter:description" content="Privacy policy, CLI usage, rate limit details, credits, and licence information for Malachite." /> 17 + <meta name="twitter:image" content="https://malachite.ewancroft.uk/og-about.svg" /> 4 18 </svelte:head> 5 19 6 20 <main>
+16
web/src/routes/import/+page.svelte
··· 118 118 119 119 <svelte:head> 120 120 <title>Malachite — Import to Teal</title> 121 + <meta name="description" content="Import your Last.fm and Spotify listening history into Teal on ATProto. Choose a mode, sign in, upload your export, and import." /> 122 + <link rel="canonical" href="https://malachite.ewancroft.uk/import" /> 123 + <meta name="robots" content="noindex" /> 124 + 125 + <!-- Open Graph --> 126 + <meta property="og:type" content="website" /> 127 + <meta property="og:url" content="https://malachite.ewancroft.uk/import" /> 128 + <meta property="og:title" content="Malachite — Import to Teal" /> 129 + <meta property="og:description" content="Import your Last.fm and Spotify listening history into Teal on ATProto. Choose a mode, sign in, upload your export, and import." /> 130 + <meta property="og:image" content="https://malachite.ewancroft.uk/og-import.svg" /> 131 + 132 + <!-- Twitter / X card --> 133 + <meta name="twitter:card" content="summary_large_image" /> 134 + <meta name="twitter:title" content="Malachite — Import to Teal" /> 135 + <meta name="twitter:description" content="Import your Last.fm and Spotify listening history into Teal on ATProto. Choose a mode, sign in, upload your export, and import." /> 136 + <meta name="twitter:image" content="https://malachite.ewancroft.uk/og-import.svg" /> 121 137 </svelte:head> 122 138 123 139 <main>
+51
web/static/og-about.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630"> 2 + <defs> 3 + <pattern id="dots" x="0" y="0" width="32" height="32" patternUnits="userSpaceOnUse"> 4 + <circle cx="1" cy="1" r="1" fill="#1f3328" /> 5 + </pattern> 6 + <filter id="glow" x="-40%" y="-40%" width="180%" height="180%"> 7 + <feGaussianBlur stdDeviation="14" result="blur" /> 8 + <feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge> 9 + </filter> 10 + <radialGradient id="glowGrad" cx="28%" cy="50%" r="40%"> 11 + <stop offset="0%" stop-color="#3fb968" stop-opacity="0.13" /> 12 + <stop offset="100%" stop-color="#090f0c" stop-opacity="0" /> 13 + </radialGradient> 14 + </defs> 15 + 16 + <!-- Background --> 17 + <rect width="1200" height="630" fill="#090f0c" /> 18 + <rect width="1200" height="630" fill="url(#dots)" /> 19 + <rect width="1200" height="630" fill="url(#glowGrad)" /> 20 + 21 + <!-- Left accent bar --> 22 + <rect x="72" y="160" width="3" height="310" rx="2" fill="#3fb968" opacity="0.7" /> 23 + 24 + <!-- Eyebrow --> 25 + <text x="96" y="192" font-family="JetBrains Mono" font-size="18" fill="#6b9e7e" letter-spacing="3">MALACHITE / ABOUT</text> 26 + 27 + <!-- Headline --> 28 + <text x="96" y="280" font-family="Inter" font-size="76" font-weight="700" fill="#d8ede1" letter-spacing="-2">About &amp; privacy.</text> 29 + 30 + <!-- Subline --> 31 + <text x="96" y="334" font-family="Inter" font-size="26" fill="#6b9e7e">How Malachite works, what it does</text> 32 + <text x="96" y="366" font-family="Inter" font-size="26" fill="#6b9e7e">with your data, and how to run it locally.</text> 33 + 34 + <!-- Pill row: privacy promises --> 35 + <g transform="translate(96, 418)"> 36 + <rect x="0" y="0" width="170" height="40" rx="20" fill="#0e1a13" stroke="#1f3328" stroke-width="1.5" /> 37 + <text x="85" y="26" font-family="JetBrains Mono" font-size="14" fill="#3fb968" text-anchor="middle">No tracking</text> 38 + 39 + <rect x="186" y="0" width="170" height="40" rx="20" fill="#0e1a13" stroke="#1f3328" stroke-width="1.5" /> 40 + <text x="271" y="26" font-family="JetBrains Mono" font-size="14" fill="#3fb968" text-anchor="middle">No accounts</text> 41 + 42 + <rect x="372" y="0" width="210" height="40" rx="20" fill="#0e1a13" stroke="#1f3328" stroke-width="1.5" /> 43 + <text x="477" y="26" font-family="JetBrains Mono" font-size="14" fill="#3fb968" text-anchor="middle">No server storage</text> 44 + 45 + <rect x="598" y="0" width="160" height="40" rx="20" fill="#0e1a13" stroke="#1f3328" stroke-width="1.5" /> 46 + <text x="678" y="26" font-family="JetBrains Mono" font-size="14" fill="#3fb968" text-anchor="middle">AGPL-3.0</text> 47 + </g> 48 + 49 + <!-- Bottom accent --> 50 + <rect x="0" y="618" width="1200" height="12" fill="#3fb968" opacity="0.5" /> 51 + </svg>
+70
web/static/og-home.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630"> 2 + <defs> 3 + <pattern id="dots" x="0" y="0" width="32" height="32" patternUnits="userSpaceOnUse"> 4 + <circle cx="1" cy="1" r="1" fill="#1f3328" /> 5 + </pattern> 6 + <filter id="glow" x="-40%" y="-40%" width="180%" height="180%"> 7 + <feGaussianBlur stdDeviation="18" result="blur" /> 8 + <feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge> 9 + </filter> 10 + <radialGradient id="glowGrad" cx="72%" cy="50%" r="42%"> 11 + <stop offset="0%" stop-color="#3fb968" stop-opacity="0.18" /> 12 + <stop offset="100%" stop-color="#090f0c" stop-opacity="0" /> 13 + </radialGradient> 14 + </defs> 15 + 16 + <!-- Background --> 17 + <rect width="1200" height="630" fill="#090f0c" /> 18 + <rect width="1200" height="630" fill="url(#dots)" /> 19 + <rect width="1200" height="630" fill="url(#glowGrad)" /> 20 + 21 + <!-- Left accent bar --> 22 + <rect x="72" y="180" width="3" height="270" rx="2" fill="#3fb968" opacity="0.7" /> 23 + 24 + <!-- Eyebrow --> 25 + <text x="96" y="212" font-family="JetBrains Mono" font-size="18" fill="#6b9e7e" letter-spacing="3">MALACHITE</text> 26 + 27 + <!-- Headline --> 28 + <text x="96" y="300" font-family="Inter" font-size="72" font-weight="700" fill="#d8ede1" letter-spacing="-2">Your listening</text> 29 + <text x="96" y="378" font-family="Inter" font-size="72" font-weight="700" fill="#d8ede1" letter-spacing="-2">history, on <tspan fill="#3fb968">ATProto.</tspan></text> 30 + 31 + <!-- Subline --> 32 + <text x="96" y="434" font-family="Inter" font-size="24" fill="#6b9e7e">Import Last.fm &amp; Spotify scrobbles to Teal — free, open-source,</text> 33 + <text x="96" y="462" font-family="Inter" font-size="24" fill="#6b9e7e">runs entirely in your browser.</text> 34 + 35 + <!-- Pill row --> 36 + <g transform="translate(96, 500)"> 37 + <rect x="0" y="0" width="130" height="40" rx="8" fill="#0e1a13" stroke="#1f3328" stroke-width="1.5" /> 38 + <text x="65" y="26" font-family="JetBrains Mono" font-size="16" fill="#d8ede1" text-anchor="middle">Last.fm</text> 39 + <rect x="148" y="0" width="130" height="40" rx="8" fill="#0e1a13" stroke="#1f3328" stroke-width="1.5" /> 40 + <text x="213" y="26" font-family="JetBrains Mono" font-size="16" fill="#d8ede1" text-anchor="middle">Spotify</text> 41 + <text x="302" y="26" font-family="Inter" font-size="22" fill="#3fb968" text-anchor="middle">→</text> 42 + <rect x="320" y="0" width="100" height="40" rx="8" fill="#152019" stroke="#3fb968" stroke-width="1.5" /> 43 + <text x="370" y="26" font-family="JetBrains Mono" font-size="16" fill="#3fb968" text-anchor="middle">Teal</text> 44 + </g> 45 + 46 + <!-- Right decorative: hexagon cluster --> 47 + <g transform="translate(980, 315)" filter="url(#glow)" opacity="0.85"> 48 + <polygon points="0,-110 95,-55 95,55 0,110 -95,55 -95,-55" fill="none" stroke="#1f3328" stroke-width="1.5" /> 49 + <polygon points="0,-80 69,-40 69,40 0,80 -69,40 -69,-40" fill="none" stroke="#2d8a4e" stroke-width="1.5" opacity="0.6" /> 50 + <polygon points="0,-50 43,-25 43,25 0,50 -43,25 -43,-25" fill="#152019" stroke="#3fb968" stroke-width="2" /> 51 + <polygon points="0,-28 24,-14 24,14 0,28 -24,14 -24,-14" fill="#3fb968" opacity="0.15" /> 52 + <polygon points="0,-28 24,-14 24,14 0,28 -24,14 -24,-14" fill="none" stroke="#3fb968" stroke-width="2" /> 53 + <circle cx="0" cy="0" r="5" fill="#3fb968" /> 54 + <line x1="0" y1="-50" x2="0" y2="-80" stroke="#3fb968" stroke-width="1" opacity="0.5" /> 55 + <line x1="43" y1="-25" x2="69" y2="-40" stroke="#3fb968" stroke-width="1" opacity="0.5" /> 56 + <line x1="43" y1="25" x2="69" y2="40" stroke="#3fb968" stroke-width="1" opacity="0.5" /> 57 + <line x1="0" y1="50" x2="0" y2="80" stroke="#3fb968" stroke-width="1" opacity="0.5" /> 58 + <line x1="-43" y1="25" x2="-69" y2="40" stroke="#3fb968" stroke-width="1" opacity="0.5" /> 59 + <line x1="-43" y1="-25" x2="-69" y2="-40" stroke="#3fb968" stroke-width="1" opacity="0.5" /> 60 + <circle cx="0" cy="-110" r="3" fill="#3fb968" opacity="0.6" /> 61 + <circle cx="95" cy="-55" r="3" fill="#3fb968" opacity="0.6" /> 62 + <circle cx="95" cy="55" r="3" fill="#3fb968" opacity="0.6" /> 63 + <circle cx="0" cy="110" r="3" fill="#3fb968" opacity="0.6" /> 64 + <circle cx="-95" cy="55" r="3" fill="#3fb968" opacity="0.6" /> 65 + <circle cx="-95" cy="-55" r="3" fill="#3fb968" opacity="0.6" /> 66 + </g> 67 + 68 + <!-- Bottom accent --> 69 + <rect x="0" y="618" width="1200" height="12" fill="#3fb968" opacity="0.5" /> 70 + </svg>
+83
web/static/og-import.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630"> 2 + <defs> 3 + <pattern id="dots" x="0" y="0" width="32" height="32" patternUnits="userSpaceOnUse"> 4 + <circle cx="1" cy="1" r="1" fill="#1f3328" /> 5 + </pattern> 6 + <filter id="glow" x="-40%" y="-40%" width="180%" height="180%"> 7 + <feGaussianBlur stdDeviation="14" result="blur" /> 8 + <feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge> 9 + </filter> 10 + <radialGradient id="glowGrad" cx="75%" cy="45%" r="38%"> 11 + <stop offset="0%" stop-color="#3fb968" stop-opacity="0.15" /> 12 + <stop offset="100%" stop-color="#090f0c" stop-opacity="0" /> 13 + </radialGradient> 14 + </defs> 15 + 16 + <!-- Background --> 17 + <rect width="1200" height="630" fill="#090f0c" /> 18 + <rect width="1200" height="630" fill="url(#dots)" /> 19 + <rect width="1200" height="630" fill="url(#glowGrad)" /> 20 + 21 + <!-- Left accent bar --> 22 + <rect x="72" y="160" width="3" height="310" rx="2" fill="#3fb968" opacity="0.7" /> 23 + 24 + <!-- Eyebrow --> 25 + <text x="96" y="192" font-family="JetBrains Mono" font-size="18" fill="#6b9e7e" letter-spacing="3">MALACHITE / IMPORT</text> 26 + 27 + <!-- Headline --> 28 + <text x="96" y="280" font-family="Inter" font-size="76" font-weight="700" fill="#d8ede1" letter-spacing="-2">Import to Teal.</text> 29 + 30 + <!-- Subline --> 31 + <text x="96" y="334" font-family="Inter" font-size="26" fill="#6b9e7e">Choose a mode, sign in, upload your export.</text> 32 + <text x="96" y="366" font-family="Inter" font-size="26" fill="#6b9e7e">Everything runs in your browser.</text> 33 + 34 + <!-- Step indicator row --> 35 + <g transform="translate(96, 430)"> 36 + <circle cx="20" cy="20" r="20" fill="#3fb968" /> 37 + <text x="20" y="26" font-family="JetBrains Mono" font-size="16" font-weight="600" fill="#090f0c" text-anchor="middle">1</text> 38 + <line x1="40" y1="20" x2="100" y2="20" stroke="#1f3328" stroke-width="2" stroke-dasharray="4 4" /> 39 + 40 + <circle cx="120" cy="20" r="20" fill="#0e1a13" stroke="#1f3328" stroke-width="2" /> 41 + <text x="120" y="26" font-family="JetBrains Mono" font-size="16" fill="#6b9e7e" text-anchor="middle">2</text> 42 + <line x1="140" y1="20" x2="200" y2="20" stroke="#1f3328" stroke-width="2" stroke-dasharray="4 4" /> 43 + 44 + <circle cx="220" cy="20" r="20" fill="#0e1a13" stroke="#1f3328" stroke-width="2" /> 45 + <text x="220" y="26" font-family="JetBrains Mono" font-size="16" fill="#6b9e7e" text-anchor="middle">3</text> 46 + <line x1="240" y1="20" x2="300" y2="20" stroke="#1f3328" stroke-width="2" stroke-dasharray="4 4" /> 47 + 48 + <circle cx="320" cy="20" r="20" fill="#0e1a13" stroke="#1f3328" stroke-width="2" /> 49 + <text x="320" y="26" font-family="JetBrains Mono" font-size="16" fill="#6b9e7e" text-anchor="middle">4</text> 50 + <line x1="340" y1="20" x2="400" y2="20" stroke="#1f3328" stroke-width="2" stroke-dasharray="4 4" /> 51 + 52 + <circle cx="420" cy="20" r="20" fill="#0e1a13" stroke="#1f3328" stroke-width="2" /> 53 + <text x="420" y="26" font-family="JetBrains Mono" font-size="16" fill="#6b9e7e" text-anchor="middle">5</text> 54 + 55 + <text x="20" y="60" font-family="JetBrains Mono" font-size="12" fill="#3fb968" text-anchor="middle">MODE</text> 56 + <text x="120" y="60" font-family="JetBrains Mono" font-size="12" fill="#6b9e7e" text-anchor="middle">SIGN IN</text> 57 + <text x="220" y="60" font-family="JetBrains Mono" font-size="12" fill="#6b9e7e" text-anchor="middle">FILES</text> 58 + <text x="320" y="60" font-family="JetBrains Mono" font-size="12" fill="#6b9e7e" text-anchor="middle">OPTIONS</text> 59 + <text x="420" y="60" font-family="JetBrains Mono" font-size="12" fill="#6b9e7e" text-anchor="middle">RUN</text> 60 + </g> 61 + 62 + <!-- Right: mode cards --> 63 + <g transform="translate(780, 150)" filter="url(#glow)"> 64 + <rect x="0" y="0" width="340" height="330" rx="14" fill="#0e1a13" stroke="#1f3328" stroke-width="1.5" /> 65 + 66 + <rect x="20" y="20" width="300" height="68" rx="8" fill="#152019" stroke="#1f3328" stroke-width="1" /> 67 + <text x="44" y="50" font-family="Inter" font-size="17" font-weight="600" fill="#d8ede1">Last.fm</text> 68 + <text x="44" y="72" font-family="Inter" font-size="13" fill="#6b9e7e">Import scrobble history from CSV</text> 69 + 70 + <rect x="20" y="104" width="300" height="68" rx="8" fill="#152019" stroke="#1f3328" stroke-width="1" /> 71 + <text x="44" y="134" font-family="Inter" font-size="17" font-weight="600" fill="#d8ede1">Spotify</text> 72 + <text x="44" y="156" font-family="Inter" font-size="13" fill="#6b9e7e">Import play history from JSON</text> 73 + 74 + <rect x="20" y="188" width="300" height="68" rx="8" fill="#152019" stroke="#3fb968" stroke-width="1.5" /> 75 + <text x="44" y="218" font-family="Inter" font-size="17" font-weight="600" fill="#3fb968">Combined</text> 76 + <text x="44" y="240" font-family="Inter" font-size="13" fill="#6b9e7e">Merge both with deduplication</text> 77 + 78 + <text x="170" y="314" font-family="JetBrains Mono" font-size="13" fill="#2d8a4e" text-anchor="middle">+ sync · deduplicate</text> 79 + </g> 80 + 81 + <!-- Bottom accent --> 82 + <rect x="0" y="618" width="1200" height="12" fill="#3fb968" opacity="0.5" /> 83 + </svg>