Skip to content

Commit 6ea4101

Browse files
committed
Add manual analysis uploaded with bookmarklet
1 parent f627bc8 commit 6ea4101

File tree

13 files changed

+338
-14
lines changed

13 files changed

+338
-14
lines changed

_web/_includes/go-js.html

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,13 @@
326326
gotTheAnalysis(data);
327327
},
328328
error: function(xhr, textStatus, error) {
329-
if (xhr.responseJSON && typeof xhr.responseJSON.error === "string") {
330-
error = xhr.responseJSON.error;
329+
if (xhr.responseJSON) {
330+
if (typeof xhr.responseJSON.error === "string") {
331+
error = xhr.responseJSON.error;
332+
}
333+
if (xhr.responseJSON.show_manual_analysis_info) {
334+
$("#manual-analysis").show();
335+
}
331336
}
332337
info("Sorry, can't find info for that track: " + error)
333338
}
@@ -2254,8 +2259,13 @@
22542259
gotTheAnalysis(data);
22552260
},
22562261
error: function(xhr, textStatus, error) {
2257-
if (xhr.responseJSON && typeof xhr.responseJSON.error === "string") {
2258-
error = xhr.responseJSON.error;
2262+
if (xhr.responseJSON) {
2263+
if (typeof xhr.responseJSON.error === "string") {
2264+
error = xhr.responseJSON.error;
2265+
}
2266+
if (xhr.responseJSON.show_manual_analysis_info) {
2267+
$("#manual-analysis").show();
2268+
}
22592269
}
22602270
info("Sorry, can't find info for that track: " + error)
22612271
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
function downloadAudioAnalysis(trackId, playerSDK) {
2+
const bearerToken = playerSDK._client?._transport?._lastToken;
3+
bearerToken && fetch(`https://api.spotify.com/v1/audio-analysis/${trackId}`, {
4+
headers: { 'Authorization': `Bearer ${bearerToken}`, 'Content-Type': 'application/json' }
5+
}).then(r => r.ok && r.json().then(data => {
6+
const url = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
7+
const link = document.createElement('a');
8+
link.href = url;
9+
link.download = `${trackId}.json`;
10+
link.click();
11+
URL.revokeObjectURL(url);
12+
}));
13+
}
14+
15+
function checkForSongChanges(playerSDK) {
16+
let currentTrackId = null;
17+
const updateTrack = (playerState) => {
18+
const currentTrack = playerState?.track_window?.current_track;
19+
if (currentTrack && currentTrack.content_type === 'music' && currentTrack.id !== currentTrackId) {
20+
currentTrackId = currentTrack.id;
21+
downloadAudioAnalysis(currentTrackId, playerSDK);
22+
}
23+
};
24+
updateTrack(playerSDK._controller?._state);
25+
playerSDK._listeners['state_changed'].push({ listener: (e) => updateTrack(e.data?.state), options: {} });
26+
}
27+
28+
const waitForPlayerSDKInterval = setInterval(() => {
29+
const nowPlayingBar = document.querySelector('[data-testid=\'now-playing-bar\']');
30+
if (nowPlayingBar) {
31+
let fiberNode = null;
32+
for (const key in nowPlayingBar) {
33+
if (key.startsWith('__reactFiber$')) {
34+
fiberNode = nowPlayingBar[key];
35+
break;
36+
}
37+
}
38+
while (fiberNode && !fiberNode.memoizedProps?.value?._map) fiberNode = fiberNode?.return;
39+
if (fiberNode) for (const [k, v] of fiberNode.memoizedProps.value._map) {
40+
if (k.toString() === 'Symbol(PlayerSDK)') {
41+
const playerSDK = v?.instance?.harmony;
42+
if (playerSDK) {
43+
clearInterval(waitForPlayerSDKInterval);
44+
checkForSongChanges(playerSDK);
45+
break;
46+
}
47+
}
48+
}
49+
}
50+
});

_web/_includes/manual_analysis.html

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<div id="manual-analysis" style="display: none;">
2+
<h1>How to Use the Spotify Analysis Downloader</h1>
3+
4+
<p>
5+
The Spotify Analysis Downloader lets you download analysis data for the track currently playing on Spotify and any track changes until you close or reload the tab. It only works on desktop browsers. If you want to listen to the song on mobile, upload the analysis from a desktop browser first. Follow these steps to use it:
6+
</p>
7+
8+
<ol>
9+
<li>
10+
<strong>Show your bookmarks bar:</strong>
11+
<ul>
12+
<li><strong>Chrome:</strong> Press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>B</kbd> (Windows) or <kbd>Cmd</kbd> + <kbd>Shift</kbd> + <kbd>B</kbd> (Mac) to toggle the bookmark bar.</li>
13+
<li><strong>Firefox:</strong> Click the menu button and select <strong>Library</strong>, then <strong>Bookmarks</strong>, and <strong>Bookmarking Tools</strong>, and select <strong>Show Bookmarks Toolbar</strong>.</li>
14+
<li><strong>Edge:</strong> Press <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>B</kbd> or go to <strong>Settings</strong> &gt; <strong>Appearance</strong> &gt; <strong>Show favorites bar</strong> and set to <strong>Always</strong>.</li>
15+
<li><strong>Safari:</strong> Go to <strong>View</strong> in the top menu and select <strong>Show Favorites Bar</strong>.</li>
16+
</ul>
17+
</li>
18+
<li>
19+
<strong>Bookmark the following link:</strong>
20+
Drag this button to your bookmark bar to save it as a bookmark:
21+
<a href="javascript:(() => { {% include manual-analysis-script.js %} })();" class="btn btn-small btn-default">Spotify Analysis Downloader</a>
22+
</li>
23+
<li>
24+
<strong>Open Spotify:</strong>
25+
Use this link to open Spotify in your browser and log in (Premium account not required):
26+
<a href="https://open.spotify.com" target="_blank" class="btn btn-small btn-default">Open Spotify</a>.
27+
</li>
28+
<li>
29+
<strong>Play a Spotify track:</strong>
30+
Start any track on Spotify to begin downloading analysis data for it. Pausing or muting the track will not impact the downloader.
31+
</li>
32+
<li>
33+
<strong>Use the bookmarklet:</strong>
34+
While the song is playing, click the "Spotify Analysis Downloader" bookmarklet from your bookmark bar. This action will download the analysis file for the current track.
35+
</li>
36+
<li>
37+
<strong>Continue listening:</strong>
38+
The downloader will automatically save analysis data for each new track you play until you close or reload the tab.
39+
</li>
40+
<li>
41+
<strong>Locate your downloads:</strong>
42+
The downloaded files will be in your downloads folder and named after the track IDs (e.g., `03UrZgTINDqvnUMbbIMhql.json`).
43+
</li>
44+
<li>
45+
<strong>Upload the analysis file:</strong>
46+
Match the filename with the track ID of the song you tried to play (see the URL) and use the form below to upload the analysis file for processing.
47+
</li>
48+
</ol>
49+
50+
<h2>Upload the Analysis File</h2>
51+
<p>Select the JSON file you downloaded that corresponds to the song you tried to play on The Eternal Jukebox:</p>
52+
53+
<div class="l-cb" id="l-analysis-upload">
54+
<form id="analysis-upload-form">
55+
<input id="analysis-upload" type="file" name="song_upload" accept=".json">
56+
</form>
57+
<div class='progress_outer' id="analysis-progress" style="background-color: green; max-width: 300px;">
58+
<div id='_progress' class='progress'></div>
59+
</div>
60+
</div>
61+
62+
<script>
63+
$("#analysis-upload").change(function () {
64+
var params = {};
65+
var q = document.URL.split('?')[1];
66+
if (q !== undefined) {
67+
q = q.split('&');
68+
for (var i = 0; i < q.length; i++) {
69+
var pv = q[i].split('=');
70+
var p = pv[0];
71+
var v = pv[1];
72+
params[p] = v;
73+
}
74+
}
75+
if (!'id' in params) {
76+
$("#error").text("You must be on a song page to upload analysis data.");
77+
$("#error").show();
78+
return;
79+
}
80+
var id = params['id'];
81+
82+
if ($("#analysis-upload")[0].files[0].name.split('.')[0] !== id) {
83+
$("#error").text("This is not the correct analysis file for this song. The correct file should be called " + id + ".json.");
84+
$("#error").show();
85+
return;
86+
}
87+
88+
$("#error").hide();
89+
90+
$.ajax({
91+
url: "/api/analysis/upload/" + id,
92+
type: "POST",
93+
data: new FormData($('#analysis-upload-form')[0]),
94+
processData: false,
95+
contentType: false,
96+
headers: {
97+
"X-XSRF-TOKEN": document.cookie.substring(document.cookie.indexOf("XSRF-TOKEN")).split(";")[0].split("=").slice(1).join("=")
98+
},
99+
xhr: function () {
100+
var xhr = new window.XMLHttpRequest();
101+
xhr.upload.addEventListener("progress", function (evt) {
102+
if (evt.lengthComputable) {
103+
var percentComplete = evt.loaded / evt.total;
104+
percentComplete = percentComplete * 100;
105+
$('#analysis-progress').text(percentComplete + '%');
106+
$('#analysis-progress').css('width', percentComplete + '%');
107+
}
108+
}, false);
109+
return xhr;
110+
},
111+
success: function () {
112+
window.location.reload(true);
113+
},
114+
error: function (xhr, textStatus, error) {
115+
if (xhr.responseJSON && typeof xhr.responseJSON.error === "string") {
116+
error = xhr.responseJSON.error;
117+
}
118+
$("#error").text("Sorry, could not upload analysis data: " + error);
119+
$("#error").show();
120+
}
121+
});
122+
});
123+
</script>
124+
</div>

_web/_layouts/go.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
{% include tweet.html %}
1919
</div>
2020

21+
{% include manual_analysis.html %}
2122
<div id='error'></div>
2223
<div id="hero">
2324
<div id="stats">

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ repositories {
1919
ext {
2020
kotlin_version = '1.9.25'
2121
vertx_version = '3.9.16'
22-
jackson_version = '2.17.2'
22+
jackson_version = '2.18.2'
2323
new_pipe_extractor_version = 'master-SNAPSHOT'
2424
}
2525

config_template.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"AUDIO_FOLDER": "data/audio",
1212
"EXTERNAL_AUDIO_FOLDER": "data/external_audio",
1313
"UPLOADED_AUDIO_FOLDER": "data/uploaded_audio",
14+
"UPLOADED_ANALYSIS_FOLDER": "data/uploaded_analysis",
1415
"PROFILE_FOLDER": "data/profile",
1516
"LOG_FOLDER": "data/log"
1617
},

config_template.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ storageOptions:
1111
AUDIO_FOLDER: data/audio
1212
EXTERNAL_AUDIO_FOLDER: data/external_audio
1313
UPLOADED_AUDIO_FOLDER: data/uploaded_audio
14+
UPLOADED_ANALYSIS_FOLDER: data/uploaded_analysis
1415
PROFILE_FOLDER: data/profile
1516
LOG_FOLDER: data/log
1617

envvar_config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ storageOptions:
1818
AUDIO_FOLDER: data/audio
1919
EXTERNAL_AUDIO_FOLDER: data/external_audio
2020
UPLOADED_AUDIO_FOLDER: data/uploaded_audio
21+
UPLOADED_ANALYSIS_FOLDER: data/uploaded_analysis
2122
PROFILE_FOLDER: data/profile
2223
LOG_FOLDER: data/log
2324

src/main/kotlin/org/abimon/eternalJukebox/data/storage/GoogleStorage.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ object GoogleStorage : IStorage {
5151
private val storageSupportsCors: MutableList<EnumStorageType>
5252

5353
private val publicStorageTypes = arrayOf(
54+
EnumStorageType.UPLOADED_ANALYSIS,
5455
EnumStorageType.UPLOADED_AUDIO,
5556
EnumStorageType.ANALYSIS,
5657
EnumStorageType.AUDIO,

src/main/kotlin/org/abimon/eternalJukebox/data/storage/IStorage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ interface IStorage {
1313

1414
val disabledStorageTypes: List<EnumStorageType>
1515
get() = storageOptions.let { storageOptionMap ->
16-
EnumStorageType.values().filter { enumStorageType -> storageOptionMap["${enumStorageType.name.uppercase(Locale.getDefault())}_IS_DISABLED"]?.toString()?.toBoolean() ?: false }
16+
EnumStorageType.entries.filter { enumStorageType -> storageOptionMap["${enumStorageType.name.uppercase(Locale.getDefault())}_IS_DISABLED"]?.toString()?.toBoolean() ?: false }
1717
}
1818

1919
/**

0 commit comments

Comments
 (0)