├── .gitignore
├── .prettierrc.yaml
├── icons
├── cdj-0.77rem.png
├── cdj-2.15rem.png
└── camelotdj-1.87rem.png
├── zip-me.sh
├── screenshot
├── hold-bin-1280x800.png
└── top100-1400x560.png
├── manifest.json
├── console-beatport.js
├── console-traxsource.js
├── src
├── options.html
├── options.css
├── content.css
└── content.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | **/.DS_Store
2 | *.zip
3 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | tabWidth: 4
2 | printWidth: 120
3 | singleQuote: true
--------------------------------------------------------------------------------
/icons/cdj-0.77rem.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qel/camelotdj/HEAD/icons/cdj-0.77rem.png
--------------------------------------------------------------------------------
/icons/cdj-2.15rem.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qel/camelotdj/HEAD/icons/cdj-2.15rem.png
--------------------------------------------------------------------------------
/icons/camelotdj-1.87rem.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qel/camelotdj/HEAD/icons/camelotdj-1.87rem.png
--------------------------------------------------------------------------------
/zip-me.sh:
--------------------------------------------------------------------------------
1 | rm camelotdj.zip
2 | zip camelotdj.zip -r manifest.json icons/* src/* -x "*.DS_Store" -x ".*"
--------------------------------------------------------------------------------
/screenshot/hold-bin-1280x800.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qel/camelotdj/HEAD/screenshot/hold-bin-1280x800.png
--------------------------------------------------------------------------------
/screenshot/top100-1400x560.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qel/camelotdj/HEAD/screenshot/top100-1400x560.png
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Camelot DJ",
3 | "version": "0.4.2",
4 | "homepage_url": "http://camelotdj.com",
5 | "description": "Filter Beatport track lists by key.",
6 | "short_name": "camelotdj",
7 | "manifest_version": 2,
8 | "permissions": [],
9 | "icons": {
10 | "16": "icons/cdj-0.77rem.png",
11 | "48": "icons/cdj-2.15rem.png",
12 | "128": "icons/camelotdj-1.87rem.png"
13 | },
14 | "options_page": "src/options.html",
15 | "content_scripts": [
16 | {
17 | "matches": ["https://www.beatport.com/*"],
18 | "run_at": "document_start",
19 | "js": ["src/content.js"],
20 | "css": ["src/content.css"]
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/console-beatport.js:
--------------------------------------------------------------------------------
1 | // Rewrite a Beatport Top 100 page -- add key/bpm column -- works in Chrome / Firefox
2 |
3 | // prettier-ignore
4 | beatportKeyToCamelotKey = {
5 | 'G♯ min': '1A', 'A♭ min': '1A', 'B maj': '1B',
6 | 'D♯ min': '2A', 'E♭ min': '2A', 'F♯ maj': '2B', 'G♭ maj': '2B',
7 | 'A♯ min': '3A', 'B♭ min': '3A', 'C♯ maj': '3B', 'D♭ maj': '3B',
8 | 'F min': '4A', 'G♯ maj': '4B', 'A♭ maj': '4B',
9 | 'C min': '5A', 'D♯ maj': '5B', 'E♭ maj': '5B',
10 | 'G min': '6A', 'A♯ maj': '6B', 'B♭ maj': '6B',
11 | 'D min': '7A', 'F maj': '7B',
12 | 'A min': '8A', 'C maj': '8B',
13 | 'E min': '9A', 'G maj': '9B',
14 | 'B min': '10A', 'D maj': '10B',
15 | 'F♯ min': '11A', 'G♭ min': '11A', 'A maj': '11B',
16 | 'C♯ min': '12A', 'D♭ min': '12A', 'E maj': '12B'
17 | };
18 | // prettier-ignore
19 | document.head.insertAdjacentHTML('beforeend', [
20 | ''
25 | ].join('\n'));
26 | if ((labelColumnHeader = document.querySelector('.bucket-track-header-col.buk-track-labels'))) {
27 | labelColumnHeader.innerHTML = '
BPM
KEY
';
28 | Array.from(document.querySelectorAll('li .buk-track-labels')).forEach((label, i) => {
29 | if ((info = window.Playables && window.Playables.tracks && window.Playables.tracks[i])) {
30 | label.innerHTML = `${info.bpm}
${beatportKeyToCamelotKey[info.key]}
`;
31 | }
32 | });
33 | }
34 |
--------------------------------------------------------------------------------
/console-traxsource.js:
--------------------------------------------------------------------------------
1 | // Traxsource.com Top 100 pages -- add key/bpm column
2 |
3 | // This script is pretty silly.
4 | // It fetches all 100 pages for each track to populate the BPM/key data.
5 |
6 | // prettier-ignore
7 | traxsourceKeyToCamelotKey = {
8 | 'G#min': '1A', 'Bmaj': '1B',
9 | 'D#min': '2A', 'F#maj': '2B',
10 | 'A#min': '3A', 'C#maj': '3B',
11 | 'Fmin': '4A', 'G#maj': '4B',
12 | 'Cmin': '5A', 'D#maj': '5B',
13 | 'Gmin': '6A', 'A#maj': '6B',
14 | 'Dmin': '7A', 'Fmaj': '7B',
15 | 'Amin': '8A', 'Cmaj': '8B',
16 | 'Emin': '9A', 'Gmaj': '9B',
17 | 'Bmin': '10A', 'Dmaj': '10B',
18 | 'F#min': '11A', 'Amaj': '11B',
19 | 'C#min': '12A', 'Emaj': '12B'
20 | };
21 | // prettier-ignore
22 | document.head.insertAdjacentHTML('beforeend', [
23 | ''
28 | ].join('\n'));
29 | if ((labelColumnHeader = document.querySelector('.trk-row.hdr .label'))) {
30 | labelColumnHeader.innerHTML = 'BPM
KEY
';
31 | Array.from(document.getElementsByClassName('trk-row play-trk')).forEach(trackRow => {
32 | const titleLink = trackRow.querySelector('.title a');
33 | const label = trackRow.getElementsByClassName('label')[0];
34 | if (titleLink && label) {
35 | fetch(titleLink.href)
36 | .then(r => r.text())
37 | .then(txt => {
38 | const det = txt.split('');
39 | if (det.length > 6) {
40 | const key = det[5].split('<')[0];
41 | const bpm = det[6].split('<')[0];
42 | label.innerHTML = ` ${bpm} ${traxsourceKeyToCamelotKey[key]} `;
43 | }
44 | });
45 | }
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/src/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | camelotdj
9 |
10 | Nothing to see here...
11 | The Options page does not currently have options.
12 |
13 |
14 | Top 100 Link Bar
15 |
18 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/options.css:
--------------------------------------------------------------------------------
1 |
2 | div.title {
3 | margin: 0;
4 | padding: 0;
5 | color: #94d500;
6 | font-size: 6rem;
7 | font-family: "Source Sans Pro", Arial, sans-serif;
8 | font-weight: 400;
9 | text-transform: none;
10 | letter-spacing: -0.035em;
11 | line-height: 0.975;
12 | text-shadow: 4px -2px 0.5px #141414;
13 | }
14 |
15 | div.title strong {
16 | color: #fff;
17 | font-size: 0.948em;
18 | font-weight: 400;
19 | text-transform: uppercase;
20 | }
21 |
22 | div.genres {
23 | width: fit-content;
24 | display: flex;
25 | flex-direction: column;
26 | align-items: center;
27 | }
28 |
29 | h1 {
30 | margin: 0;
31 | padding: 0;
32 | width: 100%;
33 | text-align: left;
34 | color: #fff;
35 | font-size: 1.8rem;
36 | font-family: Oswald, Helvetica, sans-serif;
37 | font-weight: 200;
38 | text-transform: uppercase;
39 | }
40 |
41 | div.genres > ul {
42 | display: flex;
43 | flex-direction: column;
44 | padding-inline-start: 0;
45 | margin-block-start: 0;
46 | margin-block-end: 0;
47 | }
48 |
49 | div.genres > div.header, div.genres > ul > li {
50 | padding: 0;
51 | display: flex;
52 | justify-content: space-around;
53 | }
54 |
55 | div.genres > div.header {
56 | width: 100%;
57 | border-top: 1px solid #494949;
58 | text-transform: uppercase;
59 | }
60 |
61 | div.genres > ul > li {
62 | margin: 0.1rem 0;
63 | padding: 0.25rem;
64 | background-color: #373737;
65 | }
66 |
67 | div.link-col {
68 | width: 7em;
69 | }
70 |
71 | div.name-col {
72 | width: 20em;
73 | }
74 |
75 | div.number-col {
76 | width: 3em;
77 | }
78 |
79 | div.genres > div.header > div, div.genres > ul > li > div {
80 | display: inline-block;
81 | padding: 0 0.5rem;
82 | text-align: left;
83 | }
84 |
85 |
86 | div {
87 | padding: 0.25rem 0;
88 | text-align: center;
89 | }
90 |
91 | p {
92 | color: #fff;
93 | font-size: 1.65rem;
94 | font-family: Ubuntu;
95 | font-weight: 400;
96 | text-transform: none;
97 | letter-spacing: .02em;
98 | line-height: 1.25;
99 | }
100 |
101 | p strong {
102 | /* this should be the same as bold which is inherent to the tag, but I'll put it here for reference */
103 | font-weight: 700;
104 | }
105 |
106 | a {
107 | font-size: 1.65rem;
108 | color: #39c1de;
109 | font-family: "Source Sans Pro", Arial, sans-serif;
110 | font-weight: 700;
111 | text-transform: none;
112 | letter-spacing: .02em;
113 | line-height: 1.25;
114 | text-decoration: none;
115 | }
116 |
117 | a:hover {
118 | text-decoration: underline;
119 | }
120 |
121 | body {
122 | width: 100%;
123 | margin: 2rem 0 0 0;
124 | padding: 0.33rem 0;
125 | display: flex;
126 | flex-direction: column;
127 | align-items: center;
128 | background-color: #262626;
129 | color: #fff;
130 | font-size: 1.1rem;
131 | font-family: "Source Sans Pro", Arial, sans-serif;
132 | font-weight: 400;
133 | }
134 |
--------------------------------------------------------------------------------
/src/content.css:
--------------------------------------------------------------------------------
1 | li.track p.buk-track-labels, li.top-ten-track span.top-ten-track-label {
2 | color: #fff;
3 | font-size: 1.35rem;
4 | letter-spacing: 1px;
5 | }
6 |
7 | p.buk-track-labels div, span.top-ten-track-label div {
8 | display: inline-block;
9 | width: 3.8rem;
10 | }
11 |
12 | p.buk-track-labels div:last-child, span.top-ten-track-label div:last-child {
13 | text-align: right;
14 | }
15 |
16 | .camelotdj-main {
17 | position: absolute;
18 | right: 0;
19 | margin-top: 0.35rem;
20 | margin-right: 1.5rem;
21 | }
22 |
23 | div.inner-container {
24 | display: flex;
25 | }
26 |
27 | div.inner-container:first-child {
28 | margin-top: 0.65rem;
29 | }
30 |
31 | a.key-label, div.link-label {
32 | padding-left: 0.6rem;
33 | margin-right: 0.4rem;
34 | background-color: #26262680;
35 | border-radius: 0.5rem 0.5rem 0 0;
36 | color: #fff;
37 | text-shadow: 4px -2px 0.5px #141414;
38 | letter-spacing: 1px;
39 | text-transform: uppercase;
40 | border-bottom: 1px solid #94d500;
41 | }
42 |
43 | a.key-label {
44 | height: fit-content;
45 | margin-top: 0.35rem;
46 | font-size: 1.25rem;
47 | font-weight: 300;
48 | }
49 |
50 | a.key-label strong {
51 | margin-left: 0.1rem;
52 | font-size: 1.55rem;
53 | font-weight: 600;
54 | }
55 |
56 | div.link-label {
57 | margin-top: 0.45rem;
58 | margin-left: 1.5rem;
59 | font-size: 1.05rem;
60 | font-weight: 400;
61 | }
62 |
63 | div.link-label strong {
64 | margin-left: 0.1rem;
65 | margin-right: 0.1rem;
66 | font-size: 1.4rem;
67 | font-weight: 600;
68 | }
69 |
70 | div.key-grid {
71 | height: 48px;
72 | width: 540px;
73 | background-color: #141414;
74 | border-radius: 0.5rem;
75 | box-shadow: 4px -2px 0.5px 1.5px rgba(0, 48, 0, 0.5);
76 | display: flex;
77 | flex-direction: column;
78 | justify-content: space-evenly;
79 | user-select: none;
80 | }
81 |
82 | div.key-grid:hover {
83 | cursor: pointer;
84 | }
85 |
86 | div.link-row {
87 | margin-top: 0.7rem;
88 | width: 556px;
89 | margin-right: -20px;
90 | }
91 |
92 | div.key-row, div.link-row {
93 | display: flex;
94 | justify-content: space-around;
95 | }
96 |
97 | div.key-col {
98 | font-size: 1.2em;
99 | font-weight: 700;
100 | letter-spacing: 1px;
101 | width: 3rem;
102 | padding-right: 0.2rem;
103 | text-align: right;
104 | }
105 |
106 | div.key-grid div.key-col:hover {
107 | border-radius: 0.3rem;
108 | background: #82bc00;
109 | color: #262626;
110 | }
111 |
112 | div.key-col.selected {
113 | border-radius: 0.3rem;
114 | background: #94d500;
115 | color: #262626;
116 | }
117 |
118 | div.key-col.included {
119 | border-radius: 0.3rem;
120 | background: #ff53a0;
121 | }
122 |
123 | div.link-row a {
124 | font-size: 1rem;
125 | font-weight: 600;
126 | }
127 |
128 | div.link-row a:hover, div.link-row a.selected {
129 | border-bottom: 1px solid #94d500;
130 | }
131 |
132 | div.link-row a:hover {
133 | text-shadow: 0 1px 3px #94d500AA
134 | }
135 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Camelot DJ
2 |
3 | **A Chrome estension for Beatport**
4 |
5 | Get it in the [Chrome Web Store](https://chrome.google.com/webstore/detail/camelot-dj/odinfabcmlogdpdkghggkkpdjlbeloeg).
6 |
7 |
8 |
9 | Camelot DJ adds BPM and key (in Camelot notation) to track lists.
10 | This applies to all Top 100 lists and your Hold Bin.
11 |
12 | 
13 |
14 |
15 |
16 | It also adds a selector to the page which lets you pick a key to mix with.
17 |
18 | The track lists are then filtered to show keys which mix well with the selected key.
19 |
20 | 
21 |
22 | Oh, and it adds some convenient links to the top 100 lists for the bigger genres.
23 |
24 |
25 |
26 | ### Console version
27 |
28 | Obviously a Chrome extension only works on Chrome, but ... if you want to see BPM/key on a Top 100 list, you can just cut/paste this into the console. And it works in Firefox, too!
29 |
30 | ```javascript
31 | // Rewrite a Beatport Top 100 page -- add key/bpm column
32 | beatportKeyToCamelotKey = {
33 | 'G♯ min': '1A', 'A♭ min': '1A', 'B maj': '1B',
34 | 'D♯ min': '2A', 'E♭ min': '2A', 'F♯ maj': '2B', 'G♭ maj': '2B',
35 | 'A♯ min': '3A', 'B♭ min': '3A', 'C♯ maj': '3B', 'D♭ maj': '3B',
36 | 'F min': '4A', 'G♯ maj': '4B', 'A♭ maj': '4B',
37 | 'C min': '5A', 'D♯ maj': '5B', 'E♭ maj': '5B',
38 | 'G min': '6A', 'A♯ maj': '6B', 'B♭ maj': '6B',
39 | 'D min': '7A', 'F maj': '7B',
40 | 'A min': '8A', 'C maj': '8B',
41 | 'E min': '9A', 'G maj': '9B',
42 | 'B min': '10A', 'D maj': '10B',
43 | 'F♯ min': '11A', 'G♭ min': '11A', 'A maj': '11B',
44 | 'C♯ min': '12A', 'D♭ min': '12A', 'E maj': '12B'
45 | };
46 | document.head.insertAdjacentHTML('beforeend', [
47 | ''
52 | ].join('\n'));
53 | if ((labelColumnHeader = document.querySelector('.bucket-track-header-col.buk-track-labels'))) {
54 | labelColumnHeader.innerHTML = 'BPM KEY ';
55 | Array.from(document.querySelectorAll('li .buk-track-labels')).forEach((label, i) => {
56 | if ((info = window.Playables && window.Playables.tracks && window.Playables.tracks[i])) {
57 | label.innerHTML = `${info.bpm} ${beatportKeyToCamelotKey[info.key]} `;
58 | }
59 | });
60 | }
61 | ```
62 |
63 |
64 |
65 | ### The code
66 |
67 | Doing this in the console is a lot easier than doing it in an extension because of sandboxing.
68 |
69 | We can't get to the window object, and Beatport uses a lot of AJAX, and the track data lives in a dynamically-loaded script tag. Injecting all the code to hijack XHR events gets really messy, so we just grab the script tag and evaluate it. Then we have a polling function which checks to see when the URL changes after an AJAX call, and then we reload the data.
70 |
71 |
72 |
73 | ### Contact
74 |
75 | If you think I picked terrible genres, feel free to ping me on Reddit and tell me how awful I am.
76 |
77 | If you think showing the matching major/minor keys is an affront to music theory, feel free to explain to me how I can be better at this mixing thing.
78 |
79 | [u/raquel-eve](https://www.reddit.com/user/raquel-eve)
80 |
--------------------------------------------------------------------------------
/src/content.js:
--------------------------------------------------------------------------------
1 | // prettier-ignore
2 | const beatportKeyToCamelotKey = {
3 | 'G♯ min': '1A', 'A♭ min': '1A', 'B maj': '1B',
4 | 'D♯ min': '2A', 'E♭ min': '2A', 'F♯ maj': '2B', 'G♭ maj': '2B',
5 | 'A♯ min': '3A', 'B♭ min': '3A', 'C♯ maj': '3B', 'D♭ maj': '3B',
6 | 'F min': '4A', 'G♯ maj': '4B', 'A♭ maj': '4B',
7 | 'C min': '5A', 'D♯ maj': '5B', 'E♭ maj': '5B',
8 | 'G min': '6A', 'A♯ maj': '6B', 'B♭ maj': '6B',
9 | 'D min': '7A', 'F maj': '7B',
10 | 'A min': '8A', 'C maj': '8B',
11 | 'E min': '9A', 'G maj': '9B',
12 | 'B min': '10A', 'D maj': '10B',
13 | 'F♯ min': '11A', 'G♭ min': '11A', 'A maj': '11B',
14 | 'C♯ min': '12A', 'D♭ min': '12A', 'E maj': '12B'
15 | };
16 |
17 | // prettier-ignore
18 | const linkGenres = {
19 | 'min•tech': 14,
20 | 'raw•tech': 92,
21 | 'techno': 6,
22 | 'mel•h+t': 90,
23 | 'prog•hs': 15,
24 | 'trance': 7,
25 | 'electro': 3,
26 | 'breaks': 9,
27 | 'ind•dnc': 37,
28 | 'tech•hs': 11,
29 | 'house': 5,
30 | 'orgnic•dt': 93,
31 | 'deep•hs': 12,
32 | };
33 |
34 | const HOLD_BIN_URL = 'https://www.beatport.com/hold-bin/tracks?per-page=150';
35 |
36 | const POLLING_INTERVAL = 100; // checking for URL changes and XHR responses that update lists
37 |
38 |
39 |
40 | // logging
41 | const myLog = (...args) => console.log.apply(console, ['CamelotDJ:'].concat(args));
42 |
43 | // convert key dataset to string -- note that the dataset stringifies everything
44 | const keyStr = datasetObj => datasetObj.keyNum + (datasetObj.minor.toString() === 'true' ? 'A' : 'B');
45 |
46 |
47 |
48 | // UI: build the key selector
49 | const keySelectButtons = [];
50 | const mainContainer = document.createElement('div');
51 | const keySelectContainer = document.createElement('div');
52 | const keySelectLabel = document.createElement('a');
53 | const keySelectGrid = document.createElement('div');
54 | mainContainer.className = 'camelotdj-main';
55 | keySelectContainer.className = 'inner-container';
56 | keySelectLabel.className = 'key-label';
57 | keySelectGrid.className = 'key-grid';
58 | keySelectLabel.innerHTML = 'CamelotDJ';
59 | keySelectLabel.href = HOLD_BIN_URL;
60 | keySelectContainer.appendChild(keySelectLabel);
61 | keySelectContainer.appendChild(keySelectGrid);
62 | for (const minor of [true, false]) {
63 | const rowDiv = document.createElement('div');
64 | rowDiv.className = 'key-row';
65 | for (let keyNum = 1; keyNum <= 12; keyNum += 1) {
66 | const colDiv = document.createElement('div');
67 | colDiv.className = 'key-col';
68 | colDiv.textContent = keyStr({ keyNum, minor });
69 | colDiv.dataset.keyNum = keyNum;
70 | colDiv.dataset.minor = minor;
71 | keySelectButtons.push(colDiv);
72 | rowDiv.appendChild(colDiv);
73 | }
74 | keySelectGrid.appendChild(rowDiv);
75 | }
76 | mainContainer.appendChild(keySelectContainer);
77 |
78 | // UI: build row of links
79 | const genreLinks = {};
80 | const linkContainer = document.createElement('div');
81 | const linkLabel = document.createElement('div');
82 | const linkRow = document.createElement('div');
83 | linkContainer.className = 'inner-container';
84 | linkLabel.className = 'link-label';
85 | linkRow.className = 'link-row';
86 | linkLabel.innerHTML = 'top100s';
87 | for (const [gName, gNum] of Object.entries(linkGenres)) {
88 | const a = document.createElement('a');
89 | a.href = `https://www.beatport.com/genre/${gName}/${gNum}/top-100`;
90 | a.textContent = gName;
91 | genreLinks[gNum] = a;
92 | linkRow.appendChild(a);
93 | }
94 | linkContainer.appendChild(linkLabel);
95 | linkContainer.appendChild(linkRow);
96 | mainContainer.appendChild(linkContainer);
97 |
98 |
99 |
100 | // match URL
101 |
102 | // split path example:
103 | //
104 | // ['', 'cart', 'cart', 'BaSe64sTuFf==']
105 | // ['', 'hold-bin']
106 | // ['', 'genre', 'techno', '6', 'top-100']
107 |
108 | const pathMathches = wildcardPath => {
109 | const splitPath = window.location.pathname.split('/');
110 | const splitWild = wildcardPath.split('/');
111 | if (splitPath.length < splitWild.length) return false;
112 | for (const seg in splitWild) {
113 | if (splitWild[seg] !== '*' && splitWild[seg] !== splitPath[seg]) return false;
114 | }
115 | return true;
116 | }
117 |
118 | const getGenreNumberSeg = () => {
119 | const splitPath = window.location.pathname.split('/');
120 | if (splitPath.length < 4 || splitPath[1] !== 'genre') return null;
121 | return splitPath[3];
122 | }
123 |
124 | const urlIsTop100 = () => pathMathches('/genre/*/*/top-100');
125 | const urlIsMainCart = () => pathMathches('/cart/cart');
126 | const urlIsCustomCart = () => pathMathches('/cart/*') && !urlIsMainCart();
127 | const urlIsHoldBin = () => pathMathches('/hold-bin');
128 | const urlIsSearchPage = () => pathMathches('/search/tracks');
129 |
130 |
131 |
132 | // global track data object
133 | let tracks = {};
134 |
135 | // parse JSON track data in page scripts
136 | const parseScriptTracks = (scriptElement, assignedVarName, ...nestedVarNames) => {
137 | const scriptText = scriptElement.text;
138 | const objStart = scriptText.indexOf('{', scriptText.indexOf(assignedVarName) + assignedVarName.length);
139 | const objEnd = scriptText.indexOf('};', objStart) + 1;
140 | let varData = JSON.parse(scriptText.slice(objStart, objEnd));
141 | nestedVarNames.forEach(v => {
142 | varData = varData[v];
143 | })
144 | myLog(varData.length, assignedVarName, 'tracks found.');
145 | varData.forEach(track => {
146 | if (!(track.id in tracks)) {
147 | if (track.bpm && track.key) {
148 | tracks[track.id] = {
149 | key: track.key,
150 | bpm: track.bpm,
151 | camelot: beatportKeyToCamelotKey[track.key]
152 | };
153 | }
154 | myLog(track.id, 'name:', track.name, 'mix:', track.mix, 'bpm:', track.bpm, 'key:', track.key);
155 | }
156 | });
157 | scriptElement.dataset.parsed = 'parsed'; // flag it so we don't keep reloading the same data
158 | }
159 |
160 |
161 |
162 | // global key selection vars
163 | let selectedKey = null;
164 | let matchingKeys = [];
165 |
166 | // Selects a key basted on {keyNum, minor} object and stores matching keys in matchingKeys
167 | const selectKey = datasetObj => {
168 | myLog('selectKey()', datasetObj);
169 |
170 | // reset all buttons to only the default class
171 | for (const btn of keySelectButtons) {
172 | btn.className = 'key-col';
173 | }
174 |
175 | if (datasetObj) {
176 | selectedKey = keyStr(datasetObj);
177 | const keyNum = parseInt(datasetObj.keyNum);
178 | const minor = datasetObj.minor.toString() === 'true';
179 |
180 | // put the selection in storage
181 | window.localStorage.setItem('camelotdj.keyNum', keyNum);
182 | window.localStorage.setItem('camelotdj.minor', minor);
183 |
184 | // prettier-ignore
185 | matchingKeys = [selectedKey].concat([
186 | { add: 0, flip: true }, // 8A -> 8B -- same key, flipped major/minor
187 | { add: 11, flip: false }, // 8A -> 7A -- key -1
188 | { add: 1, flip: false }, // 8A -> 9A -- key +1
189 | { add: 9, flip: true }, // 8A -> 5B -- key -3, flipped major/minor
190 | { add: 3, flip: true } // 8A -> 11B -- key +3, flipped major/minor
191 | ].map(x => keyStr({ keyNum: (keyNum + x.add - 1) % 12 + 1, minor: x.flip ? !minor : minor })));
192 |
193 | // apply the correct classes to the buttons
194 | for (const btn of keySelectButtons) {
195 | const btnKeyStr = keyStr(btn.dataset);
196 | if (btnKeyStr === selectedKey) {
197 | btn.classList.add('selected');
198 | } else if (matchingKeys.includes(btnKeyStr)) {
199 | btn.classList.add('included');
200 | }
201 | }
202 | } else {
203 | selectedKey = null;
204 |
205 | // clear stored key
206 | window.localStorage.removeItem('camelotdj.keyNum');
207 | window.localStorage.removeItem('camelotdj.minor');
208 | }
209 | };
210 |
211 |
212 |
213 | const update = () => {
214 | if (urlIsTop100()) myLog('URL is a Top 100 page.');
215 | if (urlIsMainCart()) myLog('URL is the Main Cart.');
216 | if (urlIsCustomCart()) myLog('URL is a custom cart.');
217 | if (urlIsHoldBin()) myLog('URL is the Hold Bin.');
218 | if (urlIsSearchPage()) myLog('URL is a search result track list.');
219 |
220 | // Set/clear selection class on the top-100 genre link bar
221 | for (gNum of Object.keys(genreLinks)) {
222 | if (urlIsTop100() && getGenreNumberSeg() === gNum) {
223 | genreLinks[gNum].className = 'selected';
224 | } else {
225 | genreLinks[gNum].className = '';
226 | }
227 | }
228 |
229 | // We only filter stuff when we're on a top-100 / hold-bin / custom cart
230 | const keySelectorActive = (urlIsTop100() || urlIsHoldBin() || urlIsCustomCart() || urlIsSearchPage());
231 | const filtering = keySelectorActive && selectedKey;
232 |
233 | if (keySelectorActive) {
234 | keySelectContainer.style.display = '';
235 | if (filtering) {
236 | myLog('filtering', matchingKeys);
237 | }
238 | } else {
239 | // hide the key selector thingy if we're not filtering
240 | keySelectContainer.style.display = 'none';
241 | }
242 |
243 | // track list headers
244 | Array.from(document.querySelectorAll('.bucket-track-header-col.buk-track-labels')).forEach(labelColumnHeader => {
245 | labelColumnHeader.innerHTML = 'BPM KEY ';
246 | });
247 |
248 | // Top 100s, carts and hold-bin
249 | document.querySelectorAll('ul.bucket-items').forEach(trackList => {
250 | // This is goofy because the hold-bin rows aren't as nested as the Top 100 rows.
251 | Array.from(trackList.getElementsByClassName('track')).forEach(trackRow => {
252 | const trackLabel = trackRow.getElementsByClassName('buk-track-labels')[0];
253 | if (trackLabel) {
254 | const trackId = trackRow.dataset.ecId || trackRow.getElementsByClassName('track-play')[0].dataset.id;
255 |
256 | if (tracks[trackId]) {
257 | const { bpm, camelot } = tracks[trackId];
258 |
259 | if (filtering && matchingKeys.indexOf(camelot) == -1) {
260 | // hide rows that are filtered out
261 | trackRow.style.display = 'none';
262 | } else {
263 | trackRow.style.display = '';
264 | trackLabel.innerHTML = `${bpm} ${camelot} `;
265 | }
266 | } else {
267 | trackLabel.style.whiteSpace = 'noWrap';
268 | trackLabel.style.color = '#8c8c8c';
269 | trackLabel.innerText = 'no track data';
270 | }
271 | }
272 | });
273 |
274 | // tag that we've labelled the list so that we can see when it gets reloaded
275 | trackList.dataset.labeled = 'labeled';
276 | });
277 |
278 | // Top 10s
279 | Array.from(document.getElementsByClassName('top-ten-track-label')).forEach(topTenLabel => {
280 | const topTenRow = topTenLabel.parentNode.parentNode;
281 | const trackId = topTenRow.dataset.ecId;
282 | const { bpm, camelot } = tracks[trackId];
283 | topTenLabel.innerHTML = `${bpm} ${camelot} `;
284 | });
285 | };
286 |
287 |
288 |
289 | const checkForUpdate = () => {
290 | Array.from(document.getElementsByClassName('bucket-items')).forEach(trackListElement => {
291 | if (!trackListElement.dataset.labeled && trackListElement.firstElementChild.classList.contains('track')) {
292 | myLog('New track list found.');
293 | const trackScript = document.getElementById('data-objects');
294 | if (trackScript) {
295 | if (!trackScript.dataset.parsed) {
296 | parseScriptTracks(trackScript, 'window.Playables', 'tracks')
297 | }
298 | const nES = trackScript.nextElementSibling;
299 | const cartScript = nES !== null && nES.id === '' && nES.text.indexOf('window.localCart') !== -1 && nES;
300 | if (cartScript && !cartScript.dataset.parsed) {
301 | parseScriptTracks(cartScript, 'window.localCart', 'items');
302 | }
303 | update(trackListElement);
304 | } else {
305 | myLog('Waiting for track data script.');
306 | }
307 | }
308 | });
309 | };
310 |
311 | const loadHandler = e => {
312 | // add UI go page
313 | document.getElementsByClassName('header-bg-wrap')[0].appendChild(mainContainer);
314 |
315 | // load key selection from local storage
316 | const keyNum = window.localStorage.getItem('camelotdj.keyNum');
317 | const minor = window.localStorage.getItem('camelotdj.minor');
318 | if (keyNum && minor) {
319 | selectKey({ keyNum, minor });
320 | }
321 |
322 | // always polling for unlabeled track lists
323 | updateInterval = setInterval(checkForUpdate, POLLING_INTERVAL);
324 | };
325 | document.addEventListener('DOMContentLoaded', loadHandler);
326 |
327 | const clickHandler = e => {
328 | const { classList, dataset } = e.target;
329 | if (classList.contains('key-col')) {
330 | if (classList.contains('selected')) {
331 | myLog('clickHandler() un-selected', dataset);
332 | selectKey(null);
333 | } else {
334 | myLog('clickHandler() selected', dataset);
335 | selectKey(dataset);
336 | }
337 | update();
338 | }
339 | };
340 | document.addEventListener('click', clickHandler);
341 |
--------------------------------------------------------------------------------
|