Skip to content
This repository was archived by the owner on Jan 7, 2025. It is now read-only.

Commit 579dbe1

Browse files
committed
Initial pass at making it work
1 parent f21ae20 commit 579dbe1

20 files changed

+536
-2
lines changed

.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
output/*.txt
2+
output/*.jpg
3+
output/*.json
4+
15
# Binaries for programs and plugins
26
*.exe
37
*.exe~

README.md

+78-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,78 @@
1-
# supbox
2-
Pull details about the currently playing track from Rekordbox v6 in formats compatible with Audio Hijack, OBS or just in JSON.
1+
This tool is for users of Pioneer's [Rekordbox v6](https://rekordbox.com/en/) to gain access to their currently playing track. This data can be used for any number of purposes, but a large one is online streaming of audio and video. Built-in support for OBS and Audio Hijack are included.
2+
3+
# Currently only supported on macOS
4+
5+
Since I use macOS, and I have an install of Rekordbox on macOS that's what I have it working with. **However**, there's no reason that somebody who's using Rekordbox on Windows can't help out and make the tweaks needed to point to the file locations on a Windows install. If there's any demand maybe I'll install a VM and install Rekordbox and I'll do it myself.
6+
7+
# Overview
8+
9+
1. Open the _config/config.yaml_ file.
10+
2. Make sure the _application_ path points to your copy of Rekordbox.app (version 6) on your computer.
11+
3. Decide where you want the output files to go such as an "output" directory. Create this directory if needed.
12+
4. Decide if you are using [OBS](https://obsproject.com/), [Audio Hijack](https://rogueamoeba.com/audiohijack/), or JSON output.
13+
5. You can remove any of the config file output destinations you're not using.
14+
15+
16+
# Using with [OBS](https://obsproject.com/)
17+
18+
If you're using OBS the file path of _nowplaying-obs-*_ would generate three files:
19+
* nowplaying-obs-artist.txt (just the artist name)
20+
* nowplaying-obs-track.txt (just the track name)
21+
* nowplaying-obs-track.jpg (just the track image)
22+
23+
[Within OBS you can then point to these files](https://www.reddit.com/r/Twitch/comments/4k6jpv/obs_studio_text_source_from_file/) and add the Metadata to your broadcast.
24+
25+
Here's an example setup:
26+
27+
<center>
28+
<a href="doc/obs1.png">
29+
<img src="doc/obs1.png" width=40% style="padding: 20px">
30+
</a>
31+
<a href="doc/obs2.png">
32+
<img src="doc/obs2.png" width=40% style="padding: 20px">
33+
</a>
34+
</center>
35+
36+
# Using with [Rogue Ameba Audio Hijack](https://rogueamoeba.com/audiohijack/)
37+
38+
You can point Audio Hijack at a specially formatted text file and the software will read it and treat it as the current Metadata. This file location is configured in the config file as _audioHijack_ under _output_.
39+
40+
For example you can set it to _output/nowplaying-audiohijack.txt_ and the contents of the file would look something like
41+
42+
```
43+
Title: Retaliate
44+
Artist: VNV Nation
45+
Artwork: /Users/me/Library/Pioneer/rekordbox/share/PIONEER/Artwork/bd3/82718-334f-482d-ad0a-82a1f8ba2507/artwork.jpg
46+
```
47+
48+
Here's an example setup:
49+
50+
<center>
51+
<a href="doc/audioHijack1.png">
52+
<img src="doc/audioHijack1.png" width=40% style="padding: 20px">
53+
</a>
54+
<a href="doc/audioHijack2.png">
55+
<img src="doc/audioHijack2.png" width=40% style="padding: 20px">
56+
</a>
57+
</center>
58+
59+
[Please read the details that Rogue Ameba has provided](https://rogueamoeba.com/support/knowledgebase/?showArticle=AudioHijack-Metadata) under the __Metadata from a “Now Playing.txt” file__ section if you have questions.
60+
61+
# JSON Output
62+
63+
An option to generate a JSON file with the Metadata is available for other uses. Upload it to a web server, have your text to speech engine read the current track, whatever ideas you can come up with.
64+
65+
```
66+
{"ID":"6d655f64-f2e5-4ec9-bd51-3a2c86984c8e","artist":"VNV Nation","track":"Retaliate","imagePath":"/Users/gabek/Library/Pioneer/rekordbox/share/PIONEER/Artwork/bd3/82718-334f-482d-ad0a-82a1f8ba2507/artwork.jpg"}
67+
```
68+
69+
# Disclaimers
70+
71+
* This tool relies on Rekordbox marking a track as **"played"** before we know it's the most recently played track. I've found this currently happens one minute into playback. **This is not optimal**, as I'd really love to know this immediately. However, this is what Rekordbox is doing and I'll keep looking to see if there's any other way to work with this.
72+
73+
* We're doing things with Recordbox that are unsupported and even likely completely unwanted by Pioneer. Things could change by them at any moment and even shut this down completely.
74+
75+
# Thank yous
76+
77+
* rekordcloud went into detail about the internals of Rekordbox v6 https://rekord.cloud/blog/technical-inspection-of-rekordbox-6-and-its-new-internals.
78+
* LePopal's PRACT-OBS had some code that pointed me in the right direction with getting the database decryption to work and gave me the idea to add OBS support. https://github.com/LePopal/PRACT-OBS/

audioHijackOutput.go

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
)
7+
8+
func writeAudioHijackFileOutput(track Track, filename string) {
9+
f, err := os.Create(filename)
10+
if err != nil {
11+
panic(err)
12+
}
13+
14+
defer f.Close()
15+
16+
if track.Name != "" {
17+
fmt.Fprintln(f, "Title:", track.Name)
18+
}
19+
20+
if track.Artist != "" {
21+
fmt.Fprintln(f, "Artist:", track.Artist)
22+
}
23+
24+
if track.ImagePath != "" {
25+
fmt.Fprintln(f, "Artwork:", "file://"+track.ImagePath)
26+
}
27+
28+
}

config.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package main
2+
3+
import (
4+
"io/ioutil"
5+
"log"
6+
7+
"gopkg.in/yaml.v2"
8+
)
9+
10+
// Config struct
11+
type Config struct {
12+
Rekordbox Rekordbox `yaml:"rekordbox"`
13+
Output Output `yaml:"output"`
14+
PollingInterval string `yaml:"pollingInterval"`
15+
}
16+
17+
type Rekordbox struct {
18+
OptionsFile string `yaml:"optionsFile"`
19+
ApplicationPath string `yaml:"application"`
20+
}
21+
22+
type Output struct {
23+
AudioHijackStyleFile string `yaml:"audioHijack"`
24+
JSONStyleFile string `yaml:"json"`
25+
OBSStyleFileTemplate string `yaml:"obs"`
26+
}
27+
28+
func getConfig() Config {
29+
filePath := "config/config.yaml"
30+
31+
if !FileExists(filePath) {
32+
log.Fatal("ERROR: valid config/config.yaml is required")
33+
}
34+
35+
yamlFile, err := ioutil.ReadFile(filePath)
36+
37+
var config Config
38+
err = yaml.Unmarshal(yamlFile, &config)
39+
if err != nil {
40+
panic(err)
41+
}
42+
return config
43+
}

config/config.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
pollingInterval: 5s
2+
3+
rekordbox:
4+
application: /Applications/rekordbox 6/rekordbox.app
5+
6+
output:
7+
audioHijack: output/nowplaying-audiohijack.txt
8+
json: output/nowplaying.json
9+
obs: output/nowplaying-obs-*

crypto.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import (
4+
"github.com/andreburgaud/crypt2go/ecb"
5+
_ "github.com/xeodou/go-sqlcipher"
6+
"golang.org/x/crypto/blowfish"
7+
)
8+
9+
func decrypt(ct, key []byte) []byte {
10+
block, err := blowfish.NewCipher(key)
11+
if err != nil {
12+
panic(err.Error())
13+
}
14+
mode := ecb.NewECBDecrypter(block)
15+
pt := make([]byte, len(ct))
16+
mode.CryptBlocks(pt, ct)
17+
if err != nil {
18+
panic(err.Error())
19+
}
20+
return pt
21+
}

database.go

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package main
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
)
7+
8+
func getDatabaseDSN(filePath string, encryptionKey string) string {
9+
dsn := fmt.Sprintf("file:"+filePath+"?_key='%s'", encryptionKey)
10+
return dsn
11+
}
12+
13+
func getRecentTrack(db *sql.DB, config Config) Track {
14+
row := db.QueryRow("SELECT h.ID, Title, Name, ImagePath FROM djmdSongHistory AS h JOIN djmdContent AS c on h.ContentID = c.ID LEFT JOIN djmdArtist as a on c.ArtistID = a.ID GROUP BY h.created_at ORDER BY h.created_at DESC LIMIT 1")
15+
16+
var ID string
17+
var Title string
18+
var Name string
19+
var ImagePath string
20+
21+
row.Scan(&ID, &Title, &Name, &ImagePath)
22+
23+
if ImagePath != "" {
24+
dataPath := getDataPath()
25+
ImagePath = getImagePath(dataPath, ImagePath)
26+
}
27+
28+
return Track{ID: ID, Artist: Name, Name: Title, ImagePath: ImagePath}
29+
}

doc/audioHijack1.png

430 KB
Loading

doc/audioHijack2.png

2.35 MB
Loading

doc/obs1.png

213 KB
Loading

doc/obs2.png

805 KB
Loading

go.mod

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/gabek/supbox
2+
3+
go 1.14
4+
5+
require (
6+
github.com/andreburgaud/crypt2go v0.11.0
7+
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
8+
github.com/mutecomm/go-sqlcipher v0.0.0-20190227152316-55dbde17881f // indirect
9+
github.com/xeodou/go-sqlcipher v0.0.0-20200505024025-122bc51f252d // indirect
10+
gopkg.in/yaml.v2 v2.3.0 // indirect
11+
)

go.sum

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
2+
github.com/andreburgaud/crypt2go v0.11.0 h1:2V2ccmaEDZuWTEU8tVMJkzueMknwpGPPQW5yqtjKeso=
3+
github.com/andreburgaud/crypt2go v0.11.0/go.mod h1:CNP8Da8FgPs0UEwpNh63NTb0zCPfQVlbY80BmdT42uw=
4+
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
5+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c=
8+
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
9+
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
10+
github.com/mutecomm/go-sqlcipher v0.0.0-20190227152316-55dbde17881f h1:hd3r+uv9DNLScbOrnlj82rBldHQf3XWmCeXAWbw8euQ=
11+
github.com/mutecomm/go-sqlcipher v0.0.0-20190227152316-55dbde17881f/go.mod h1:MyUWrZlB1aI5bs7j9/pJ8ckLLZ4QcCYcNiSbsAW32D4=
12+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
14+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
15+
github.com/xeodou/go-sqlcipher v0.0.0-20200505024025-122bc51f252d h1:VI0yV4ELL01PdNuEwVVqWUYujcwWsq2P5N6ZNAlRI0I=
16+
github.com/xeodou/go-sqlcipher v0.0.0-20200505024025-122bc51f252d/go.mod h1:aZ06jyRpOCqbZdcLUsn8agGfXzlKkHbQp/CjwRKwxSQ=
17+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
18+
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 h1:TjszyFsQsyZNHwdVdZ5m7bjmreu0znc2kRYsEml9/Ww=
19+
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
20+
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
21+
golang.org/x/net v0.0.0-20190225153610-fe579d43d832/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
22+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
23+
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
24+
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
25+
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
26+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
27+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
28+
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
29+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
30+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
31+
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
32+
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

jsonOutput.go

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
)
8+
9+
func getJSONForTrack(track Track) string {
10+
b, err := json.Marshal(track)
11+
if err != nil {
12+
panic(err)
13+
}
14+
15+
return string(b)
16+
}
17+
18+
func writeNowPlayingJSONOutput(track Track, filename string) {
19+
f, err := os.Create(filename)
20+
if err != nil {
21+
panic(err)
22+
}
23+
24+
defer f.Close()
25+
26+
json := getJSONForTrack(track)
27+
fmt.Fprintln(f, json)
28+
}

main.go

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package main
2+
3+
import (
4+
"encoding/base64"
5+
"log"
6+
7+
"database/sql"
8+
"time"
9+
)
10+
11+
var config Config = getConfig()
12+
13+
var database *sql.DB
14+
15+
var currentTrackID string
16+
17+
func main() {
18+
// Files and paths
19+
rekordboxConfig := getRekordboxConfig(config.Rekordbox.OptionsFile)
20+
asarFilePath := getAsarFilePath(config.Rekordbox.ApplicationPath)
21+
dataPath := getDataPath()
22+
databaseFilePath := getDatabaseFilePath(dataPath)
23+
24+
// Database decryption
25+
encodedPasswordData := getEncryptedPasswordDataFromConfig(rekordboxConfig)
26+
decodedPasswordData, err := base64.StdEncoding.DecodeString(encodedPasswordData)
27+
passwordString := getEncryptedPassword(asarFilePath)
28+
password := []byte(passwordString)
29+
decryptedBytes := decrypt(decodedPasswordData, password)
30+
encryptionKey := string(decryptedBytes)
31+
32+
// Open the Database
33+
dsn := getDatabaseDSN(databaseFilePath, encryptionKey)
34+
db, err := sql.Open("sqlite3", dsn)
35+
36+
if err != nil {
37+
panic(err)
38+
}
39+
40+
database = db
41+
defer database.Close()
42+
43+
// Start polling
44+
pollingInterval, err := time.ParseDuration(config.PollingInterval)
45+
46+
if err != nil {
47+
panic(err)
48+
}
49+
50+
startTimer(pollingInterval)
51+
}
52+
53+
func startTimer(pollingInterval time.Duration) {
54+
run()
55+
56+
tick := time.Tick(pollingInterval)
57+
for range tick {
58+
run()
59+
}
60+
}
61+
62+
func run() {
63+
track := getRecentTrack(database, config)
64+
if track.ID == currentTrackID {
65+
return
66+
}
67+
68+
currentTrackID = track.ID
69+
70+
log.Printf("%+v\n", track)
71+
72+
if config.Output.AudioHijackStyleFile != "" {
73+
writeAudioHijackFileOutput(track, config.Output.AudioHijackStyleFile)
74+
}
75+
if config.Output.JSONStyleFile != "" {
76+
writeNowPlayingJSONOutput(track, config.Output.JSONStyleFile)
77+
}
78+
79+
if config.Output.OBSStyleFileTemplate != "" {
80+
writeOBSFilesOutput(track, config.Output.OBSStyleFileTemplate)
81+
}
82+
}

0 commit comments

Comments
 (0)