diff --git a/posts/_metadata.json b/posts/_metadata.json index a3a9d3c..be43d3a 100644 --- a/posts/_metadata.json +++ b/posts/_metadata.json @@ -1,122 +1,138 @@ -{ - "meta": { - "title": "Meta", - "slug": "meta", - "filename": "meta", - "date": "01/08/2023", - "author": "jetstream0/Prussia", - "tags": ["meta", "code", "project", "web", "markdown", "typescript_javascript", "css"] - }, - "llm": { - "title": "LLM", - "slug": "llm", - "filename": "llm", - "date": "16/09/2023", - "author": "jetstream0/Prussia", - "tags": ["opinion"] - }, - "hex-to-bytes-and-back": { - "title": "Hex to Bytes and Back", - "slug": "hex-to-bytes-and-back", - "filename": "hex_to_bytes_and_back", - "date": "15/09/2023", - "author": "jetstream0/Prussia", - "tags": ["typescript_javascript", "code", "math"] - }, - "rss-feed": { - "title": "RSS!", - "slug": "rss-feed", - "filename": "rss_feed", - "date": "19/08/2023", - "author": "jetstream0/Prussia", - "tags": ["meta", "typescript_javascript", "project", "web"] - }, - "fermats-little-theorem": { - "title": "Fermats Little Theorem", - "slug": "fermats-little-theorem", - "filename": "fermats_little_theorem", - "date": "12/08/2023", - "author": "jetstream0/Prussia", - "tags": ["code", "typescript_javascript", "math"] - }, - "wikipedia-rabbitholes": { - "title": "Wikipedia Rabbitholes", - "slug": "wikipedia-rabbitholes", - "filename": "wikipedia_rabbitholes", - "date": "09/08/2023", - "author": "jetstream0/Prussia", - "tags": ["reading", "history", "wikipedia"] - }, - "eve": { - "title": "Eve", - "slug": "eve", - "filename": "eve", - "date": "06/08/2023", - "author": "jetstream0/Prussia", - "tags": ["cryptography"] - }, - "month-start-unix": { - "title": "Finding the Unix Timestamp of the Start of the Month with Javascript", - "slug": "month-start-unix", - "filename": "month_start_unix", - "date": "01/04/2023", - "author": "jetstream0/Prussia", - "tags": ["code", "web", "typescript_javascript"] - }, - "190k-faucet": { - "title": "190000 Payouts!", - "slug": "190-faucet", - "filename": "190k_faucet", - "date": "12/02/2023", - "author": "jetstream0/Prussia", - "tags": ["project", "web", "milestone", "cryptocurrency"] - }, - "adding-commas": { - "title": "Adding Commas to Numbers", - "slug": "adding-commas", - "filename": "adding_commas", - "date": "15/11/2022", - "author": "jetstream0/Prussia", - "tags": ["code", "typescript_javascript"] - }, - "solving-problems-with-a-timeout": { - "title": "Solving Problems With a Timeout", - "slug": "solving-problems-with-a-timeout", - "filename": "solving_problems_with_a_timeout", - "date": "19/08/2022", - "author": "jetstream0/Prussia", - "tags": ["bot", "typescript_javascript"] - }, - "gobanme-v1-2": { - "title": "GoBanMe v1.2", - "slug": "gobanme-v1-2", - "filename": "gobanme_v1-2", - "date": "30/05/2022", - "author": "jetstream0/Prussia", - "tags": ["release", "cryptocurrency"] - }, - "fake-typing-effect": { - "title": "Making a Fake Typing Effect", - "slug": "fake-typing-effect", - "filename": "fake_typing_effect", - "date": "27/01/2022", - "author": "jetstream0/Prussia", - "tags": ["code", "web", "typescript_javascript"] - }, - "ryuji-docs": { - "title": "Ryuji Documentation", - "slug": "ryuji-docs", - "filename": "ryuji_docs", - "date": "02/08/2023", - "author": "jetstream0/Prussia", - "tags": ["code", "project", "web", "docs", "typescript_javascript"] - }, - "saki-docs": { - "title": "Saki Documentation", - "slug": "saki-docs", - "filename": "saki_docs", - "date": "02/08/2023", - "author": "jetstream0/Prussia", - "tags": ["code", "project", "web", "build", "docs", "typescript_javascript"] - } -} +{ + "meta": { + "title": "Meta", + "slug": "meta", + "filename": "meta", + "date": "01/08/2023", + "author": "jetstream0/Prussia", + "tags": ["meta", "code", "project", "web", "markdown", "typescript_javascript", "css"] + }, + "downloading-my-spotify-playlist-for-free": { + "title": "Downloading my Spotify Playlist for Free", + "slug": "downloading-my-spotify-playlist-for-free", + "filename": "downloading_my_spotify_playlist_for_free", + "date": "27/10/2023", + "author": "jetstream0/Prussia", + "tags": ["code", "typescript_javascript", "bash"] + }, + "ryuji-rust": { + "title": "Ryuji Rust", + "slug": "ryuji-rust", + "filename": "ryuji_rust", + "date": "27/10/2023", + "author": "jetstream0/Prussia", + "tags": ["rust", "project"] + }, + "llm": { + "title": "LLM", + "slug": "llm", + "filename": "llm", + "date": "16/09/2023", + "author": "jetstream0/Prussia", + "tags": ["opinion"] + }, + "hex-to-bytes-and-back": { + "title": "Hex to Bytes and Back", + "slug": "hex-to-bytes-and-back", + "filename": "hex_to_bytes_and_back", + "date": "15/09/2023", + "author": "jetstream0/Prussia", + "tags": ["typescript_javascript", "code", "math"] + }, + "rss-feed": { + "title": "RSS!", + "slug": "rss-feed", + "filename": "rss_feed", + "date": "19/08/2023", + "author": "jetstream0/Prussia", + "tags": ["meta", "typescript_javascript", "project", "web"] + }, + "fermats-little-theorem": { + "title": "Fermats Little Theorem", + "slug": "fermats-little-theorem", + "filename": "fermats_little_theorem", + "date": "12/08/2023", + "author": "jetstream0/Prussia", + "tags": ["code", "typescript_javascript", "math"] + }, + "wikipedia-rabbitholes": { + "title": "Wikipedia Rabbitholes", + "slug": "wikipedia-rabbitholes", + "filename": "wikipedia_rabbitholes", + "date": "09/08/2023", + "author": "jetstream0/Prussia", + "tags": ["reading", "history", "wikipedia"] + }, + "eve": { + "title": "Eve", + "slug": "eve", + "filename": "eve", + "date": "06/08/2023", + "author": "jetstream0/Prussia", + "tags": ["cryptography"] + }, + "month-start-unix": { + "title": "Finding the Unix Timestamp of the Start of the Month with Javascript", + "slug": "month-start-unix", + "filename": "month_start_unix", + "date": "01/04/2023", + "author": "jetstream0/Prussia", + "tags": ["code", "web", "typescript_javascript"] + }, + "190k-faucet": { + "title": "190000 Payouts!", + "slug": "190-faucet", + "filename": "190k_faucet", + "date": "12/02/2023", + "author": "jetstream0/Prussia", + "tags": ["project", "web", "milestone", "cryptocurrency"] + }, + "adding-commas": { + "title": "Adding Commas to Numbers", + "slug": "adding-commas", + "filename": "adding_commas", + "date": "15/11/2022", + "author": "jetstream0/Prussia", + "tags": ["code", "typescript_javascript"] + }, + "solving-problems-with-a-timeout": { + "title": "Solving Problems With a Timeout", + "slug": "solving-problems-with-a-timeout", + "filename": "solving_problems_with_a_timeout", + "date": "19/08/2022", + "author": "jetstream0/Prussia", + "tags": ["bot", "typescript_javascript"] + }, + "gobanme-v1-2": { + "title": "GoBanMe v1.2", + "slug": "gobanme-v1-2", + "filename": "gobanme_v1-2", + "date": "30/05/2022", + "author": "jetstream0/Prussia", + "tags": ["release", "cryptocurrency"] + }, + "fake-typing-effect": { + "title": "Making a Fake Typing Effect", + "slug": "fake-typing-effect", + "filename": "fake_typing_effect", + "date": "27/01/2022", + "author": "jetstream0/Prussia", + "tags": ["code", "web", "typescript_javascript"] + }, + "ryuji-docs": { + "title": "Ryuji Documentation", + "slug": "ryuji-docs", + "filename": "ryuji_docs", + "date": "02/08/2023", + "author": "jetstream0/Prussia", + "tags": ["code", "project", "web", "docs", "typescript_javascript"] + }, + "saki-docs": { + "title": "Saki Documentation", + "slug": "saki-docs", + "filename": "saki_docs", + "date": "02/08/2023", + "author": "jetstream0/Prussia", + "tags": ["code", "project", "web", "build", "docs", "typescript_javascript"] + } +} diff --git a/posts/downloading_my_spotify_playlist_for_free.md b/posts/downloading_my_spotify_playlist_for_free.md new file mode 100644 index 0000000..5b05b4d --- /dev/null +++ b/posts/downloading_my_spotify_playlist_for_free.md @@ -0,0 +1,74 @@ +Like most people, I like listening to music. + +Also like most people, I don't like listening to ads or paying subscriptions^\[0\]^. Spotify's free tier is pretty good, and I can block their ads by using the web player and uBlock Origin. + +I'm mostly content with that, but there are still some annoyances - I can't listen offline, the UI is a little frustrating, and even though the ads are blocked, the player still sometimes freezes when an ad is supposed to be playing (so the next song isn't played, have to reload the page). Combined with not morally being a fan of relying on a for-profit third-party service for my music, I decided today to write a few scripts to download all the songs on my favourite Spotify playlist (around 60 songs). + +First step is getting all the song and artist names on my playlist from Spotify. I did find an [Spotify API endpoint](https://developer.spotify.com/documentation/web-api/reference/get-playlists-tracks), but it seemed like a pain with OAuth required. Instead, I just opened the playlist in my browser. After scrolling down to load all the playlist tracks, I ran something similar to the following to get all the track names: + +```javascript +[...document.querySelectorAll(".iCQtmPqY0QvkumAOuCjr")].map((d) => d.innerText) +``` + +After a bit of processing with Javascript, vim find-and-replace commands, and a few manual corrections, I ended up with a `songs.txt` file in this format: + +``` +1|Alice in Freezer|Orangestar +2|無人駅|n-buna +...and so on +``` + +Now that we have all the song and artist names, we need to fetch the Youtube urls of the songs, so we can download them with [yt-dlp](https://github.com/yt-dlp/yt-dlp). + +It turns out the Youtube search API also needs an API key, so I used the [usetube](https://github.com/valerebron/usetube) npm package, which scrapes the actual web page instead of using the API. Here's the code: + +```javascript +const fs = require("fs"); +const yt = require("usetube"); + +async function main() { + let songs = fs.readFileSync("../songs.txt", "utf-8").split("\n"); + for (let i = 0; i < songs.length; i++) { + let song_name = songs[i].split("|")[1]; + let artist_name = songs[i].split("|")[2]; + console.log(song_name+" "+artist_name); + const videos = (await yt.searchVideo(song_name+" "+artist_name)).videos; + songs[i] = songs[i]+"|https://youtube.com/watch?v="+videos[0].id; + console.log(songs[i].split("|")[3]); + } + fs.writeFileSync("../songs.txt", songs.join("\n"), "utf-8"); +} + +main(); +``` + +Now `songs.txt` looks a little like this: + +``` +1|Alice in Freezer|Orangestar|https://youtube.com/watch?v=jQmYZWjLwzw +2|無人駅|n-buna|https://youtube.com/watch?v=G8PFPUCNOg4 +...and so on +``` + +The final step is just writing a bash script to download all the songs. I don't really know much bash, but after a few StackOverflow searches, I got a working script: + +```bash +#!/bin/bash + +while IFS= read -r line; do + IFS='|' read -ra ADDR <<< "$line" + for i in "${!ADDR[@]}"; do + printf "\n${ADDR[$i]}" + if [ $i -eq 3 ]; then + yt_dlp -x --audio-format mp3 "${ADDR[$i]}" + fi + done +done < songs.txt +``` + +Yay! + +This was a pretty "boring" project that didn't involve much thinking or code. But being able to automate small, tedious stuff like this is a pretty underrated perk of having even a little programming knowledge. + +=== +- \[0\]: It's almost like artists are people too, and need to make money to support themselves! In all seriousness though, Spotify pays artists something like $0.003 USD per stream. It makes much more sense to support them through buying albums or going to concerts, instead of wasting time listening to ads. diff --git a/posts/meta.md b/posts/meta.md index cc39e1f..38d0f36 100644 --- a/posts/meta.md +++ b/posts/meta.md @@ -1,184 +1,187 @@ -There used to be a different blog here. But it wasn't very good, [code](https://github.com/jetstream0/Markup-to-HTML) *and* writing-wise. So I decided to rewrite everything. - -The old blog was an express server that looked for `.md` files in a directory, converted them to HTML, and served it. That worked mostly fine. However, the converter didn't support many Markdown features, and was pretty buggy. Using an express server also meant that the site wasn't static, and had limited options for hosting. - -Replit's free tier was pretty unreliable, and Render only allowed one free web service per account, which I was already using to run my [faucet](https://faucet.prussia.dev). I could've created a new Render account, but they recently started requiring credit card verification, which I didn't really want to do. - -Besides, making the blog static instead of relying on an express server wouldn't be that hard. A month before, I had already wrote a better (?) or at least more fully featured Markdown to HTML parser, [Makoto](https://github.com/jetstream0/Makoto-Markdown-to-HTML), so that was already one of the problems with the old blog solved. - -Anyways, once I decided to start completely rewriting the blog, I established some goals that I wanted the new blog to accomplish. - -## Technical Goals -- Static, so it can be deployed by eg, Github Pages or Cloudflare Pages -- Built from scratch with no third-party dependencies (builtin modules like `path` and `fs` are ok of course, `makoto` is not third party, also doesn't count) -- Use Typescript -- No Javascript running client side - the web pages should be pure HTML and CSS -- Style-wise, should be minimalistic, nothing fancy -- Load quickly and be small in size - -## Non-Technical Goals -- Make two things I can call "Ryuji" and "Saki" to go along with "Makoto" (those are the three main characters of one of the best manga series ever) -- Move over some of the old blog posts (only the stuff I like), after rewriting them -- Start writing stuff on the blog again, at least semi-regularly - -## Code -Hedgeblog (oh, that's what I'm calling it by the way) is made of three components: Makoto (Markdown to HTML parser), Ryuji (templating language), and Saki (build system). - -You can find the code on [Github](https://github.com/jetstream0/hedgeblog). - -### Makoto -Makoto is the Markdown-to-HTML parser, made with no dependencies. It was made around two months before Ryuji and Saki, and is meant to be more of a standalone thing. This is the sole npm dependency of the project. I `npm install`ed it instead of just copying the file over mostly because I published Makoto to npm and wanted to make sure it worked. Also, it has a different license, documentation and stuff. - -All the standard Markdown are supported (headers, bolds, italics, images, links, blockquotes, unordered lists, ordered lists, code, code blocks...), as well as superscripts and tables (although tables are probably buggy). Makoto also does some pretty neat stuff like passing on the language of the code block (if given) as a class in the resulting div: `code-`, or automatically add ids to headers, so they can have anchor links. - -It also has a very cool warnings feature, which isn't used in this project, but can be seen in action if you use the [Makoto Web Editor](https://makoto.prussia.dev). - -### Ryuji -Ryuji is a simple templating system that supports `if` statements, `for` loops, components, and inserting variables. It isn't quite as fully featured as Jinja/Nunjucks, but on the upside, Ryuji is less than 280 lines of code, and worked very well for my usecase. I think it's pretty cool. - -Here's a quick overview of the syntax: - -```html -[[ component:navbar ]] -

You can insert variables. My favourite food is: [[ favourite_food ]]

-

And make sure the variable is truthy then do something.

-[[ if:show_secrets ]] - -[[ endif ]] -
-

Variables are by default sanitized so HTML/CSS/JS can't actually be executed, but you can disable this.

- [[ html:html_from_database ]] -
-[[ for:members:member ]] -

[[ member ]] is a proud member of our group!

-[[ endfor ]] -``` - -After finishing writing Ryuji, and writing some tests to make sure it all worked, I realized that I needed a few features that were missing. - -There was no way to see the current index in a for loop, or the max index (length-1) of whatever variable we were looping over. This was needed for the tag links in the post. - -In addition, if statements only checked if the variable was truthy (not `false` or `0` or `""`), but I needed if statements making sure two variables were equal, as well as if statements making sure two variables were *not* equal. You can see this being used in the post's tags along with the new for loop features, as well as the "Next Post" link at the bottom of the post. - -So, I added those features. Let's take a look of these new features being used to show and link post tags. - -Formatting the code a little nicer, this is what it looks like: - -``` -[[ for:post.tags:tag:index:max ]] - [[ tag ]][[ if:index:!max ]], [[ endif ]] -[[ endfor ]] -``` - -Ok, in the first line (`for:post.tags:tag:index:max`), we are looping over the variable `post.tags`, and assigning each item in `post.tags` as the variable `tag`. That's nothing new, what's new is the `:index:max` portion. `index` is the index variable, starting at 0 and incrementing every loop, while `max` is the maximum index (the length of the variable to loop over - 1). - -If you look at the Ryuji code, now you can see that it is looping over the tags of the post, and creating a link for each tag. If the tag is *not* the last tag (`index` is not equal to `max`), we will also add a comma (and a space). - -Here's the equivalent python code, if it helps: - -```python -html = "" -max = len(post.tags)-1 -index = 0 -for tag in post.tags: - html += ""+tag+"" - if index != max: - html += "," - index += 1 -``` - -While Ryuji is meant for HTML, there is no reason it can't be used for other formats. - -Take a look at the [docs](/posts/ryuji-docs) for Ryuji. - -### Saki -Saki is the build system that uses Ryuji templates to generate all the HTML files, and then outputs everything (including static files, of course) to the `build` directory. Even more simple than Ryuji, it is just around 70 lines of code. - -Here are the very short [docs](/posts/saki-docs) for Saki. - -## Putting It All Together -When building, the program (`index.ts`) reads `/posts/_metadata.json` and passes all the post metadata information to the `templates/index.html` template, which is the home page! Then, it renders all the posts with the `templates/post.html` template and outputs to `/posts/*`. Next, it looks again at the post metadata and gets all the tags used. Once it has all the tags, pages for the tags are generated at `/tags/*`. That page lists all the posts with that tag. Scroll up and try it! It also outputs the `static` directory, as well, static files. - -> ### Tip: Serve Without The `.html` -> Say we want to serve a HTML file at `/posts/example` instead of `/posts/example.html`. Just create a directory called `example` and put `example.html` inside it, renaming it `index.html`. -> Basically, `/posts/example.html` becomes `/posts/example/index.html`, and now the HTML file is served at `/posts/example`. You probably already knew that, but if you didn't now you know ^\[citation needed]^. - -## Buttons Without Javascript -If you think back to around 950 words ago, you may recall that this site has zero frontend Javascript (or WebAssembly, as cool as it is). If you didn't recall that, I just reminded you. I will be sending an invoice later. - -So how does the "Show MD", "Fancy Title", and Dark/Light theme toggle work? The key thing here is that all three of these are checkboxes. Yes, even the Dark/Light theme toggle is a checkbox. That toggle hides its checkbox and takes advantage of the fact you can click on a `