the home site for me: also iteration 3 or 4 of my site

feat: move theme toggle to css

dunkirk.sh 6b289dc7 5459a6eb

verified
+20
content/blog/2024-12-16_airbuds.md
···
+
+++
+
title = "Airbuds"
+
date = 2024-12-16
+
slug = "airbuds"
+
description = "Trying to break their api."
+
+
[taxonomies]
+
tags = ["reverse engineering", "graphql"]
+
+
[extra]
+
has_toc = true
+
+++
+
+
Recently my cousin introduced me to the [Airbuds](https://airbuds.fm) app. Naturally I used it for a little bit. Slept a bit. And then booted up Proxypin to see if I could extract phone numbers from the app. With the base requests it appeared that I couldn't (:sadge:). I could get my phone number for my own profile however so I knew that it was likely stored in a user record somewhere (editor kieran: *umm yeah duh*). The more interesting part of this though was that it was a graphql api.
+
+
<!-- more -->
+
+
## Phase 2
+
+
Now knowing that it had a graphql api I wanted to see if there was a way to reverse engineer it. I have had suprisingly little experience with them but doing some quick ducking revealed that they can potentially have introspection enabled allowing us to get a full schema of what we can get. That sounds awesome but hopefully from a security standpoint unlikely to be enabled.
+30
content/blog/2025-01-01_spotify-to-apple-music.md
···
+
+++
+
title = "Exodus of Spotify Songs to the land of Apple Music"
+
date = 2025-01-01
+
slug = "spotify-to-apple-music"
+
description = "Homegrown solution rather than paying for it ^-^"
+
+
[taxonomies]
+
tags = ["apple", "music"]
+
+
[extra]
+
has_toc = true
+
+++
+
+
Today my family decided to get an Apple One subscription and use Apple Music instead of spotify. It makes sense from a cost standpoint (spotify is $20 a month vs $37.95 and `2TB` of storage plus all apple subscriptions) but I have about 3 years of history on spotify (1267 at time of writing) so manually transferring the songs isn't an option. I did some research but all I found was over priced apps and annoying python scripts.
+
+
<!-- more -->
+
+
{{ img(id="https://cloud-r47l8h2er-hack-club-bot.vercel.app/0img_3821.jpg" alt="screenshot of the apple music app saying welcome to apple music" caption="the proper horror this should/does instill 💀") }}
+
+
## Shortcut Time
+
+
I haven't played around with apple shortcuts near enough but I know that they can be quite powerful (case in point [eieio.games](https://eieio.games/blog/doom-in-the-ios-photos-app/)). I looked to see whether spotify had a shortcut to get songs out first but didn't find anything (come on spotify!) but then when I checked Apple Music it expectedly had quite a few options. One of the options is add to playlist which when I tested it initially with the share sheet as input could take a spotify url. That got me thinking; why can't I just import a file of urls on new lines? Turns out that's exactly what you can do. If you start with a file as the input and then bring it to a split text block then you can route that directly to the add songs block! Whats even better is that you don't even need some fancy looping system, you can simply dump thousands of songs into it and it takes care of it super easily.
+
+
{{ img(id="https://cloud-pbd6jl8ws-hack-club-bot.vercel.app/0img_3824.png" alt="screenshot of the shortcut" caption="if you want to try it yourself you could build the shortcut from scratch or you can use the link below") }}
+
+
Now the second part of the puzzle was exporting the liked playlist. I really didn't want to mess with the slack api and registering an oauth app but then I remembered that you can simple just hit control + a to select songs in the desktop app 🤦 and turns out if you copy it then it literally just chucks it all into your clipboard as spotify links on newlines. A quick `vi test.txt` and sending the file to myself over slack latter I could simply select the song file and use the share sheet to import it. It took a solid 35 seconds to import but gave a nice progress bar up top!
+
+
### Adendum
+
+
- [the apple shortcut] for your copy pasta pleasure
+17
sass/css/_theme-toggle.scss
···
+
#theme-toggle-label i.icon {
+
--icon-dark: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 21q-3.775 0-6.388-2.613T3 12q0-3.45 2.25-5.988T11 3.05q.625-.075.975.45t-.025 1.1q-.425.65-.638 1.375T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q.775 0 1.538-.225t1.362-.625q.525-.35 1.075-.037t.475.987q-.35 3.45-2.937 5.725T12 21Zm0-2q2.2 0 3.95-1.213t2.55-3.162q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.163T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.163 2.55T5 12q0 2.9 2.05 4.95T12 19Zm-.25-6.75Z'/%3E%3C/svg%3E");
+
+
--icon-light: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 15q1.25 0 2.125-.875T15 12q0-1.25-.875-2.125T12 9q-1.25 0-2.125.875T9 12q0 1.25.875 2.125T12 15Zm0 2q-2.075 0-3.538-1.463T7 12q0-2.075 1.463-3.538T12 7q2.075 0 3.538 1.463T17 12q0 2.075-1.463 3.538T12 17ZM2 13q-.425 0-.713-.288T1 12q0-.425.288-.713T2 11h2q.425 0 .713.288T5 12q0 .425-.288.713T4 13H2Zm18 0q-.425 0-.713-.288T19 12q0-.425.288-.713T20 11h2q.425 0 .713.288T23 12q0 .425-.288.713T22 13h-2Zm-8-8q-.425 0-.713-.288T11 4V2q0-.425.288-.713T12 1q.425 0 .713.288T13 2v2q0 .425-.288.713T12 5Zm0 18q-.425 0-.713-.288T11 22v-2q0-.425.288-.713T12 19q.425 0 .713.288T13 20v2q0 .425-.288.713T12 23ZM5.65 7.05L4.575 6q-.3-.275-.288-.7t.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7t-.275.7q-.275.3-.687.288T5.65 7.05ZM18 19.425l-1.05-1.075q-.275-.3-.275-.713t.275-.687q.275-.3.688-.287t.712.287L19.425 18q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3ZM16.95 7.05q-.3-.275-.288-.687t.288-.713L18 4.575q.275-.3.7-.288t.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275t-.7-.275ZM4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.712-.275t.688.275q.3.275.288.688t-.288.712L6 19.425q-.275.3-.7.288t-.725-.288ZM12 12Z'/%3E%3C/svg%3E");
+
+
--icon-toggle: var(--icon-light);
+
+
-webkit-mask-image: var(--icon-toggle);
+
mask-image: var(--icon-toggle);
+
background-color: var(--accent);
+
color: var(--accent);
+
+
width: 1.2rem;
+
height: 1.2rem;
+
transform: translateY(0.25rem);
+
overflow: visible;
+
}
+1
sass/css/main.scss
···
@use "copy-button";
@use "crt";
+
@use "theme-toggle";
-21
static/icons.svg
···
-
<svg xmlns="http://www.w3.org/2000/svg">
-
<symbol id="rss" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" />
-
<path fill="currentColor" d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93V10.1Z"/>
-
</symbol>
-
-
<symbol id="darkMode" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" />
-
<path fill="currentColor" d="M12 21q-3.775 0-6.388-2.613T3 12q0-3.45 2.25-5.988T11 3.05q.625-.075.975.45t-.025 1.1q-.425.65-.638 1.375T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q.775 0 1.538-.225t1.362-.625q.525-.35 1.075-.037t.475.987q-.35 3.45-2.937 5.725T12 21Zm0-2q2.2 0 3.95-1.213t2.55-3.162q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.163T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.163 2.55T5 12q0 2.9 2.05 4.95T12 19Zm-.25-6.75Z"/>
-
</symbol>
-
-
<symbol id="lightMode" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" />
-
<path fill="currentColor" d="M12 15q1.25 0 2.125-.875T15 12q0-1.25-.875-2.125T12 9q-1.25 0-2.125.875T9 12q0 1.25.875 2.125T12 15Zm0 2q-2.075 0-3.538-1.463T7 12q0-2.075 1.463-3.538T12 7q2.075 0 3.538 1.463T17 12q0 2.075-1.463 3.538T12 17ZM2 13q-.425 0-.713-.288T1 12q0-.425.288-.713T2 11h2q.425 0 .713.288T5 12q0 .425-.288.713T4 13H2Zm18 0q-.425 0-.713-.288T19 12q0-.425.288-.713T20 11h2q.425 0 .713.288T23 12q0 .425-.288.713T22 13h-2Zm-8-8q-.425 0-.713-.288T11 4V2q0-.425.288-.713T12 1q.425 0 .713.288T13 2v2q0 .425-.288.713T12 5Zm0 18q-.425 0-.713-.288T11 22v-2q0-.425.288-.713T12 19q.425 0 .713.288T13 20v2q0 .425-.288.713T12 23ZM5.65 7.05L4.575 6q-.3-.275-.288-.7t.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7t-.275.7q-.275.3-.687.288T5.65 7.05ZM18 19.425l-1.05-1.075q-.275-.3-.275-.713t.275-.687q.275-.3.688-.287t.712.287L19.425 18q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3ZM16.95 7.05q-.3-.275-.288-.687t.288-.713L18 4.575q.275-.3.7-.288t.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275t-.7-.275ZM4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.712-.275t.688.275q.3.275.288.688t-.288.712L6 19.425q-.275.3-.7.288t-.725-.288ZM12 12Z"/>
-
</symbol>
-
-
<symbol id="chevronLeft" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" />
-
<path fill="currentColor" d="M15.41 16.58L10.83 12l4.58-4.59L14 6l-6 6l6 6l1.41-1.42Z"/>
-
</symbol>
-
-
<symbol id="chevronRight" viewBox="0 0 24 24"><rect x="0" y="0" fill="none" stroke="none" />
-
<path fill="currentColor" d="M8.59 16.58L13.17 12L8.59 7.41L10 6l6 6l-6 6l-1.41-1.42Z"/>
-
</symbol>
-
</svg>
+22 -21
static/js/theme-toggle.js
···
const toggleButton = document.getElementById("theme-toggle");
-
const themeIcon = document.getElementById("theme-icon");
+
const themeIcon = document
+
.getElementById("theme-toggle-label")
+
.querySelector("i");
const themeSound = document.getElementById("theme-sound");
// Function to update the theme icon based on the current theme
const updateThemeIcon = (isDarkMode) => {
-
const themeMode = isDarkMode ? "darkMode" : "lightMode";
-
const iconPath = themeIcon
-
.querySelector("use")
-
.getAttribute("href")
-
.replace(/#.*$/, `#${themeMode}`);
-
themeIcon.querySelector("use").setAttribute("href", iconPath);
+
themeIcon.style.setProperty(
+
"--icon-toggle",
+
isDarkMode ? "var(--icon-dark)" : "var(--icon-light)",
+
);
};
// Function to update the theme based on the current mode
const updateTheme = (isDarkMode) => {
-
const theme = isDarkMode ? "dark" : "light";
-
document.documentElement.setAttribute("data-theme", theme);
-
updateThemeIcon(isDarkMode);
+
const theme = isDarkMode ? "dark" : "light";
+
document.documentElement.setAttribute("data-theme", theme);
+
updateThemeIcon(isDarkMode);
};
// Function to toggle the theme
const toggleTheme = () => {
-
const isDarkMode = toggleButton.checked;
-
updateTheme(isDarkMode);
-
themeSound.play();
-
localStorage.setItem("theme", isDarkMode ? "dark" : "light");
+
const isDarkMode = toggleButton.checked;
+
updateTheme(isDarkMode);
+
themeSound.currentTime = 0;
+
themeSound.play();
+
localStorage.setItem("theme", isDarkMode ? "dark" : "light");
};
// Event listener for theme toggle
···
// Function to initialize the theme based on the stored preference
const initializeTheme = () => {
-
const storedTheme = localStorage.getItem("theme");
-
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
-
const isDarkMode = storedTheme === "dark" || (!storedTheme && prefersDark);
-
toggleButton.checked = isDarkMode;
-
updateTheme(isDarkMode);
+
const storedTheme = localStorage.getItem("theme");
+
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
+
const isDarkMode = storedTheme === "dark" || (!storedTheme && prefersDark);
+
toggleButton.checked = isDarkMode;
+
updateTheme(isDarkMode);
};
// Initialize the theme
···
// Listen for changes in system preference
window
-
.matchMedia("(prefers-color-scheme: dark)")
-
.addEventListener("change", initializeTheme);
+
.matchMedia("(prefers-color-scheme: dark)")
+
.addEventListener("change", initializeTheme);
+1 -4
templates/header.html
···
<div>
<input type="checkbox" id="theme-toggle" style="display: none" />
<label for="theme-toggle" id="theme-toggle-label"
-
><svg id="theme-icon" class="icons">
-
<use
-
href="{{ get_url(path='/icons.svg#lightMode', trailing_slash=false) | safe }}"
-
></use></svg
+
><i class="icon"></i
></label>
<audio id="theme-sound">
<source