Bart Simons

Ripping Spotify songs on macOS

 •  Filed under spotify, ripping, rip, songs, macos

Ever wanted to rip songs of Spotify on your Mac? Since there are no tools available that allow you to directly download songs from Spotify, I decided to create a so-called 'duct tape' solution that might help you out with ripping songs.

So, it works as follows: AppleScript bindings allow users to automate Spotify in the following way for example:

osascript -e "tell application \"Spotify\"" -e "play track \"spotify:track:21cp8L9Pei4AgysZVihjSv\"" -e "end tell"  

This will play the song Child In Time by Deep Purple for example. There are so many ways to control Spotify with AppleScript: lots of examples and/or AppleScripts are available on the internet.

To summarize: we are now able to control and automate Spotify with AppleScript. But, we also want to record songs. I came across a brilliant application called Piezo. Piezo is a simple to use app that allows Mac users to record the audio of a specific application, which makes it a perfect solution since we only want to record Spotify audio, right?

We also want to update the song's ID3 tags afterwards, since Spotify also makes song information available through AppleScript.

I decided to write my script in Python to help better integrate the 3 main tasks:

  • Creating a recording in Piezo
  • Start and stop playing the song in Spotify
  • Update ID3 tag information

I have included comments in my Python script so that you can easily understand what each part of the code does:

# recordsong.py - record a song on Spotify with the help of Piezo

# Example usage:
#
# python3 recordsong.py spotify:track:21cp8L9Pei4AgysZVihjSv

import subprocess, sys, os, time, shutil, eyed3  
from urllib.request import urlopen

# Setup variables
piezoStorageLocation = '/Users/bart/Music/Piezo/'  
ripStorageLocation   = '/Users/bart/Music/Ripped/'

# Clear all previous recordings if they exist
for f in os.listdir(piezoStorageLocation):  
    os.remove(os.path.join(piezoStorageLocation,f))

# Tell Spotify to pause, tell Piezo to record, tell Spotify to play a specified song
subprocess.Popen('osascript -e "tell application \\"Spotify\\" to pause"', shell=True, stdout=subprocess.PIPE).stdout.read()  
time.sleep(.300)  
subprocess.Popen('osascript -e "activate application \\"Piezo\\"" -e "tell application \\"System Events\\"" -e "keystroke \\"r\\" using {command down}" -e "end tell"', shell=True, stdout=subprocess.PIPE).stdout.read()  
subprocess.Popen('osascript -e "tell application \\"Spotify\\"" -e "play track \\"'+sys.argv[1]+'\\"" -e "end tell"', shell=True, stdout=subprocess.PIPE).stdout.read()

time.sleep(1)

# Get the artist name, track name, album name and album artwork URL from Spotify
artist  = subprocess.Popen('osascript -e "tell application \\"Spotify\\"" -e "current track\'s artist" -e "end tell"', shell=True, stdout=subprocess.PIPE).stdout.read().decode('utf-8').rstrip('\r\n')  
track   = subprocess.Popen('osascript -e "tell application \\"Spotify\\"" -e "current track\'s name" -e "end tell"', shell=True, stdout=subprocess.PIPE).stdout.read().decode('utf-8').rstrip('\r\n')  
album   = subprocess.Popen('osascript -e "tell application \\"Spotify\\"" -e "current track\'s album" -e "end tell"', shell=True, stdout=subprocess.PIPE).stdout.read().decode('utf-8').rstrip('\r\n')  
artwork = subprocess.Popen('osascript -e "tell application \\"Spotify\\"" -e "current track\'s artwork url" -e "end tell"', shell=True, stdout=subprocess.PIPE).stdout.read().decode('utf-8').rstrip('\r\n')

# Download album artwork
artworkData = urlopen(artwork).read()

# Check every 500 milliseconds if Spotify has stopped playing
while subprocess.Popen('osascript -e "tell application \\"Spotify\\"" -e "player state" -e "end tell"', shell=True, stdout=subprocess.PIPE).stdout.read() == b"playing\n":  
    time.sleep(.500)

# Spotify has stopped playing, stop the recording in Piezo
subprocess.Popen('osascript -e "activate application \\"Piezo\\"" -e "tell application \\"System Events\\"" -e "keystroke \\"r\\" using {command down}" -e "end tell"', shell=True, stdout=subprocess.PIPE).stdout.read()

time.sleep(.500)

# Create directory for the artist
if not os.path.exists(ripStorageLocation+artist):  
    os.makedirs(ripStorageLocation+artist)

# Create directory for the album
if not os.path.exists(ripStorageLocation+artist+"/"+album):  
    os.makedirs(ripStorageLocation+artist+"/"+album)

# Move MP3 file from Piezo folder to the folder containing rips.
for f in os.listdir(piezoStorageLocation):  
        if f.endswith(".mp3"):
            shutil.move(piezoStorageLocation+f, ripStorageLocation+artist+"/"+album+"/"+track+".mp3")

# Set and/or update ID3 information
musicFile = eyed3.load(ripStorageLocation+artist+"/"+album+"/"+track+".mp3")  
musicFile.tag.images.set(3, artworkData, "image/jpeg", sys.argv[1])  
musicFile.tag.artist = artist  
musicFile.tag.album  = album  
musicFile.tag.title  = track

musicFile.tag.save()  

Please note that this script should run in Python 3, not Python 2. Also, make sure that you have all dependencies installed:

pip3 install eyed3  
brew install libmagic  

Now, we can rip songs from Spotify without having to deal with the circumvention of DRM. Enjoy your music!

Send an SMS notification upon successful SSH authentication with Twilio

 •  Filed under ssh, sms, authentication, notification, twilio

Are your always curious about who logs into your server using SSH? Me too! Sometimes I am just a little paranoid and then I come up with these (crazy) ideas: sending an SMS when somebody successfully logs in over SSH. Why? Well: why not, right?

So, what do we need?

  • A server with an SSH daemon (duh...)
  • A Twilio account

So first of all, let me explain what Twilio is: it's an awesome company that makes placing and receiving calls, sending and receiving SMS and MMS, etcetera super simple. It operates over an API so that developers can create, integrate and automate telecommunication solutions into their applications.

Before continuing, make sure you have an account and make sure you write down your account SID and authentication token, both can be found on the Twilio console page.

Now, we need to prepare our server. Once any user successfully authenticates over SSH, we want to execute a script before any shell process gets spawned. This is how I did it:

First I created a script in the /opt directory called sshauthsms.sh with the following contents in it:

#!/bin/bash

curl -s -X POST 'https://api.twilio.com/2010-04-01/Accounts/YOURACCOUNTSIDGOESHERE/Messages.json' --data-urlencode 'To=+12345678901' --data-urlencode 'From=+15017250604' --data-urlencode 'Body=Somebody just succesfully SSHed into your server!' -u YOURACCOUNTSIDGOESHERE:YOURAUTHTOKENGOESHERE > /dev/null  

Make sure to replace the account SID and authentication token placeholders by the values stated in your Twilio console and replace the To and From phone numbers to your phone numbers. Also, don't forget to make the script executable:

chmod +x /opt/sshauthsms.sh  

There's only one more thing left to do: modifying the SSH daemon configuration file. For me (I use Ubuntu) it was located at /opt/ssh/sshd_config.

Just add the following line to the bottom of this file:

ForceCommand /opt/sshauthsms.sh; /bin/bash  

And restart your SSH server daemon with:

service sshd restart  

Try it out! For me it worked brilliantly:

It works!

Have a wonderful day! 🎉

Swift + Perfect + Authentication API Example

 •  Filed under swift, perfect, example, authentication

So you want to build your next backend API in Swift? No problem! Perfect has got you covered. Perfect is a collection of libraries to turn your next server-side Swift application into reality. It is blazingly fast and awesome!

So I decided to dive a little bit deeper into the possibilities of Swift and Perfect, and so I quickly decided that an authentication API would be a 'perfect' idea (see what I did there?) to get started. So, what do we need?

  • A computer running Linux or macOS
  • Swift 3 installed
  • A MySQL Server

That's basically all that's needed. Now we need a functional database, so I came up with a SQL table like this:

CREATE TABLE `users` (  
  `id` int(32) NOT NULL,
  `username` varchar(128) NOT NULL,
  `password` varchar(128) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

ALTER TABLE `users`  
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `username` (`username`),
  ADD UNIQUE KEY `id` (`id`);

INSERT INTO `users` (`id`, `username`, `password`) VALUES  
(1, 'user1', '7c12772809c1c0c3deda6103b10fdfa0'),
(2, 'user2', '7c12772809c1c0c3deda6103b10fdfa0');

ALTER TABLE `users`  
  MODIFY `id` int(32) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;

Perfect. Now - for the next part - initialize a new Swift package in an empty folder:

swift package init --type executable  

This will generate an empty package, so now it's time to edit our Package.swift file. You'll need to add 2 repositories to your project, this is how I have done that:

import PackageDescription

let package = Package(  
    name: "backend-api",
    dependencies: [
        .Package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", majorVersion: 2),
        .Package(url: "https://github.com/PerfectlySoft/Perfect-MySQL.git", majorVersion: 2)
    ]
)

Cool. So now it is about time to work on the real deal - programming! I've split the project into two separate swift files

  • main.swift : this is for all things related to the web server processing
  • DBOperations.swift : this is a provider to execute MySQL (prepared) statements in the form of a function

My main.swift:

import PerfectLib  
import PerfectHTTP  
import PerfectHTTPServer  
import Foundation

let server = HTTPServer()  
var routes = Routes()

server.serverPort = 8000

routes.add(method: .get, uri: "/", handler: {  
    request, response in

    response.status = HTTPResponseStatus.ok
    response.setHeader(.contentType, value: "application/json")

    response.setBody(string: "{\"status\":\"Backend server is up and running\"}")
    response.completed()
})

routes.add(method: .get, uri: "/oauth", handler: {  
    request, response in

    response.setHeader(.contentType, value: "application/json")

    let username = request.param(name: "username", defaultValue: "")
    let password = request.param(name: "password", defaultValue: "")

    if ((username ?? "").isEmpty || (password ?? "").isEmpty) {
        response.status = HTTPResponseStatus.ok
        response.setBody(string: "{\"status\":\"Invalid request\"}")
    } else {
        if (username ?? "").characters.count >= 8 || (username ?? "").characters.count <= 24{
            if (password ?? "").characters.count == 32 {
                if (username ?? "").rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil && (password ?? "").rangeOfCharacter(from: CharacterSet.alphanumerics.inverted) == nil {
                    response.status = HTTPResponseStatus.ok
                    let queryResult: [[[String: String]]] = executeQuery(query: "SELECT * FROM users WHERE username = ?", parameters: [username ?? ""])
                    if (queryResult.count == 1) {
                        if  ((queryResult[0][2]["password"] ?? "") as String == password ?? "") {
                            response.setBody(string: "{\"status\":\"Authentication request succeeded!\"}")
                        } else {
                            response.setBody(string: "{\"status\":\"Invalid request\"}")
                        }
                    } else {
                        response.setBody(string: "{\"status\":\"Invalid request\"}")
                    }
                } else {
                    response.status = HTTPResponseStatus.ok
                    response.setBody(string: "{\"status\":\"Invalid request\"}")
                }
            } else {
                response.status = HTTPResponseStatus.ok
                response.setBody(string: "{\"status\":\"Invalid request\"}")
            }
        } else {
            response.status = HTTPResponseStatus.ok
            response.setBody(string: "{\"status\":\"Invalid request\"}")
        }
    }

    response.completed()
})

server.addRoutes(routes)

do {  
    try server.start()
} catch PerfectError.networkError(let err, let msg) {
    print("Network error thrown: \(err) \(msg)")
}

My DBOperations.swift:

import MySQL

let sqlHost     = "127.0.0.1"  
let sqlUser     = "app_authentication"  
let sqlPassword = "testing01"  
let sqlDB       = "app_authentication"

func executeQuery(query: String, parameters: [String]? = nil) -> [[[String: String]]] {  
    var fStore = [[[String: String]]]()

    let mysql = MySQL()
    let connection = mysql.connect(host: sqlHost, user: sqlUser, password: sqlPassword, db: sqlDB)

    guard connection else {
        print(mysql.errorMessage())
        return [[[String: String]]]()
    }

    defer {
        mysql.close()
    }

    let fQuery = MySQLStmt(mysql)

    _ = fQuery.prepare(statement: query)

    if parameters != nil {
        for parameter in parameters! {
            fQuery.bindParam(parameter)
        }
    }

    _ = fQuery.execute()

    let fResults = fQuery.results()

    _ = fResults.forEachRow {
        e in

        var fSubStore = [[String: String]]()

        for i in 0..<e.count {
            let fFieldName: String? = fQuery.fieldInfo(index: i)!.name
            fSubStore.append(["\(fFieldName!)": "\(e[i]!)"])
        }

        fStore.append(fSubStore)
    }

    return fStore
}

You need to save both of these files inside the Sources folder of your package.

To build and run your app, run this in the root of your package directory:

swift build  
.build/debug/backend-api

Now it is time to test if everything works:
It works!

So: this is how you can build authentication APIs with Swift. Please bare in mind that all code examples given are NOT production ready. I know that it works, but I can definitely already see some points of improvements - too bad that I don't have time to fix that. This demo project is soon to be expected on my GitHub. Thanks for reading and have a nice day ;)

Pretty URLs in Apache - Getting Started Tutorial

 •  Filed under getting started, tutorial, Pretty URLs, Apache, mod_rewrite

Ever wanted to create beautiful URLs for your self-hosted websites? This page has got you covered with all just about the information you need to get started. All functionality which is demonstrated on this page is made possible by mod_rewrite, also known as the 'rewrite' module. This module gives system administrators the possibility to rewrite URL paths, as I shall demonstrate:

Let's say Mr. X has a website called mywebsite.corp with a single page called test.php. He currently visits his page like this:

mywebsite.corp before any modification

Mr.X is not happy with how the php file extension is exposed to the user, and wants to make his page URL look better by removing the php extension. The first thing he has to do is to enable the rewrite module which is fairly simple:

a2enmod rewrite  

Now, all that is left to do is to write the rewrite rule inside a Directory tag in apache.conf like this:

<Directory /var/www/html>  
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
        RewriteEngine On
        RewriteRule ^test$ /test.php
</Directory>  

Restart your web server with service apache2 restart and let's see if things work the way we want:

mywebsite.corp after modification

It works! Now, let's dive deeper into what the RewriteRule line does

^ is the character to indicate the root URL of your website
$ is the character to indicate the end of the URL to match

This has been a simple demonstration of what you can do and accomplish with mod_rewrite. However, you can do so much more with this module. Let's take a look at regular expressions and how to use with rewrite rules:

mywebsite.corp before second modification

With this directory block:

<Directory /var/www/html>  
        Options Indexes FollowSymLinks
        AllowOverride None
        Require all granted
        RewriteEngine On
        RewriteRule ^users/(\d+)$ /users.php?id=$1
</Directory>  

We are able to create an URL rewrite that functions like this:

mywebsite.corp after second modification

As you can see, you can use capture groups to capture URL parameters which you can use like in the previous example. Pretty handy!

This is just a handful of possibilities, I hope that you find 'em useful. Thanks for reading!

Mirrored object storage with Minio

 •  Filed under mirrored, distributed, object storage, minio

Object storage is on the rise - and that's not without a reason. When you have to deal with large pieces of data, it is very necessary to keep your data organised. Minio offers a perfectly fitted solution that is open source and well documented: it's an Amazon S3 compatible object storage server written in Go. Want to learn more about the project? This is their homepage.

Setting up a single node is a piece of cake, as I shall demonstrate.

# Create a directory for storing 'objects'
mkdir ~/ObjectStorage

# Download and prepare the Minio mode
wget https://dl.minio.io/server/minio/release/linux-amd64/minio  
chmod +x minio  
export MINIO_ACCESS_KEY=yourusernamegoeshere  
export MINIO_SECRET_KEY=yourpasswordgoeshere

# Run the Minio node
./minio server ~/ObjectStorage

It's that easy! You can now access your Minio node at port 9000, try it out in your browser.

Running a clustered of two nodes is also very easy. Let's say that we have two servers in a subnet:

minio01: 10.129.17.224  
minio02: 10.129.17.227  

We also have two disks on each server, so that means we also have the following folders mounted to their own disk:

/opt/storage01: disk 1
/opt/storage02: disk 2

Run the following command on both servers to initialise the cluster:

MINIO_ACCESS_KEY=yourusernamegoeshere MINIO_SECRET_KEY=yourpasswordgoeshere ./minio server http://10.129.17.227/opt/storage01 http://10.129.17.227/opt/storage02 http://10.129.17.224/opt/storage01 http://10.129.17.224/opt/storage02  

It automatically detects if a server is up or down, and automatically syncs data between nodes.