tangled
alpha
login
or
join now
slices.network
/
tools
Tools for the Atmosphere
tools.slices.network
quickslice
atproto
html
7
fork
atom
overview
issues
1
pulls
pipelines
feat: add album autocomplete search as fallback
chadtmiller.com
1 month ago
abb47d0c
fdc43914
+111
-6
1 changed file
expand all
collapse all
unified
split
teal-scrobble.html
+111
-6
teal-scrobble.html
···
567
return data.recordings || [];
568
}
569
0
0
0
0
0
0
0
0
0
0
0
0
0
570
function debounce(fn, ms) {
571
return (...args) => {
572
clearTimeout(searchTimeout);
···
762
763
<div class="form-row">
764
<div class="form-group">
765
-
<label>Album</label>
766
-
<input type="text" id="album-display" class="read-only" readonly placeholder="Auto-filled" />
767
</div>
768
<div class="form-group" style="flex: 0 0 80px;">
769
<label>Duration</label>
···
797
798
renderArtistField();
799
renderTrackField();
0
800
}
801
802
function renderArtistField() {
···
853
}
854
}
855
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
856
// =============================================================================
857
// ARTIST SEARCH HANDLERS
858
// =============================================================================
···
907
};
908
909
state.selectedRecording = null;
910
-
document.getElementById("album-display").value = "";
911
document.getElementById("duration-display").value = "";
912
913
renderArtistField();
914
renderTrackField();
0
915
updateSubmitButton();
916
}
917
918
function clearArtist() {
919
state.selectedArtist = null;
920
state.selectedRecording = null;
921
-
document.getElementById("album-display").value = "";
922
document.getElementById("duration-display").value = "";
923
924
renderArtistField();
925
renderTrackField();
0
926
updateSubmitButton();
927
}
928
···
1005
})) || [{ artistName: state.selectedArtist.name, artistMbId: state.selectedArtist.mbid }],
1006
};
1007
1008
-
document.getElementById("album-display").value = state.selectedRecording.releaseName || "";
1009
document.getElementById("duration-display").value = durationSecs
1010
? formatDuration(durationSecs)
1011
: "";
1012
1013
renderTrackField();
0
1014
updateSubmitButton();
1015
}
1016
1017
function clearRecording() {
1018
state.selectedRecording = null;
1019
-
document.getElementById("album-display").value = "";
1020
document.getElementById("duration-display").value = "";
1021
1022
renderTrackField();
0
1023
updateSubmitButton();
1024
}
1025
···
1028
const m = Math.floor(secs / 60);
1029
const s = secs % 60;
1030
return `${m}:${s.toString().padStart(2, "0")}`;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1031
}
1032
1033
// =============================================================================
···
567
return data.recordings || [];
568
}
569
570
+
async function searchReleases(query, artistMbid) {
571
+
if (query.length < 2) return [];
572
+
573
+
const fullQuery = `${query} AND arid:${artistMbid}`;
574
+
const url = `${MB_API}/release?query=${encodeURIComponent(fullQuery)}&fmt=json&limit=10`;
575
+
const res = await fetch(url, { headers: MB_HEADERS });
576
+
577
+
if (!res.ok) throw new Error("MusicBrainz search failed");
578
+
579
+
const data = await res.json();
580
+
return data.releases || [];
581
+
}
582
+
583
function debounce(fn, ms) {
584
return (...args) => {
585
clearTimeout(searchTimeout);
···
775
776
<div class="form-row">
777
<div class="form-group">
778
+
<label>Album <span style="font-weight: normal; color: var(--text-secondary);">(optional)</span></label>
779
+
<div id="album-field"></div>
780
</div>
781
<div class="form-group" style="flex: 0 0 80px;">
782
<label>Duration</label>
···
810
811
renderArtistField();
812
renderTrackField();
813
+
renderAlbumField();
814
}
815
816
function renderArtistField() {
···
867
}
868
}
869
870
+
function renderAlbumField() {
871
+
const container = document.getElementById("album-field");
872
+
const disabled = !state.selectedArtist;
873
+
const albumName = state.selectedRecording?.releaseName || "";
874
+
875
+
container.innerHTML = `
876
+
<div class="autocomplete-wrapper">
877
+
<input
878
+
type="text"
879
+
id="album-input"
880
+
placeholder="${disabled ? "Select an artist first" : "Search or leave blank..."}"
881
+
value="${esc(albumName)}"
882
+
${disabled ? "disabled" : ""}
883
+
oninput="handleAlbumInput(this.value)"
884
+
onfocus="handleAlbumInput(this.value)"
885
+
/>
886
+
<div id="album-dropdown" class="autocomplete-dropdown hidden"></div>
887
+
</div>
888
+
`;
889
+
}
890
+
891
// =============================================================================
892
// ARTIST SEARCH HANDLERS
893
// =============================================================================
···
942
};
943
944
state.selectedRecording = null;
0
945
document.getElementById("duration-display").value = "";
946
947
renderArtistField();
948
renderTrackField();
949
+
renderAlbumField();
950
updateSubmitButton();
951
}
952
953
function clearArtist() {
954
state.selectedArtist = null;
955
state.selectedRecording = null;
0
956
document.getElementById("duration-display").value = "";
957
958
renderArtistField();
959
renderTrackField();
960
+
renderAlbumField();
961
updateSubmitButton();
962
}
963
···
1040
})) || [{ artistName: state.selectedArtist.name, artistMbId: state.selectedArtist.mbid }],
1041
};
1042
0
1043
document.getElementById("duration-display").value = durationSecs
1044
? formatDuration(durationSecs)
1045
: "";
1046
1047
renderTrackField();
1048
+
renderAlbumField();
1049
updateSubmitButton();
1050
}
1051
1052
function clearRecording() {
1053
state.selectedRecording = null;
0
1054
document.getElementById("duration-display").value = "";
1055
1056
renderTrackField();
1057
+
renderAlbumField();
1058
updateSubmitButton();
1059
}
1060
···
1063
const m = Math.floor(secs / 60);
1064
const s = secs % 60;
1065
return `${m}:${s.toString().padStart(2, "0")}`;
1066
+
}
1067
+
1068
+
// =============================================================================
1069
+
// ALBUM SEARCH HANDLERS
1070
+
// =============================================================================
1071
+
1072
+
const handleAlbumInput = debounce(async (query) => {
1073
+
const dropdown = document.getElementById("album-dropdown");
1074
+
1075
+
// Update the release name in state as user types
1076
+
if (state.selectedRecording) {
1077
+
state.selectedRecording.releaseName = query || null;
1078
+
state.selectedRecording.releaseMbid = null;
1079
+
}
1080
+
1081
+
if (!state.selectedArtist || query.length < 2) {
1082
+
dropdown.classList.add("hidden");
1083
+
return;
1084
+
}
1085
+
1086
+
dropdown.innerHTML = `<div class="autocomplete-status">Searching...</div>`;
1087
+
dropdown.classList.remove("hidden");
1088
+
1089
+
try {
1090
+
const releases = await searchReleases(query, state.selectedArtist.mbid);
1091
+
1092
+
if (releases.length === 0) {
1093
+
dropdown.innerHTML = `<div class="autocomplete-status">No albums found</div>`;
1094
+
return;
1095
+
}
1096
+
1097
+
dropdown.innerHTML = releases
1098
+
.map((r, i) => {
1099
+
const date = r.date ? r.date.substring(0, 4) : "";
1100
+
const artUrl = r.id ? `https://coverartarchive.org/release/${r.id}/front-250` : "";
1101
+
1102
+
return `
1103
+
<div class="autocomplete-item" onclick="selectRelease(${i})" data-index="${i}">
1104
+
<div class="autocomplete-item-art">
1105
+
${artUrl ? `<img src="${artUrl}" alt="" onerror="this.style.display='none'">` : ""}
1106
+
</div>
1107
+
<div class="autocomplete-item-info">
1108
+
<div class="autocomplete-item-title">${esc(r.title)}</div>
1109
+
${date ? `<div class="autocomplete-item-subtitle">${date}</div>` : ""}
1110
+
</div>
1111
+
</div>
1112
+
`;
1113
+
})
1114
+
.join("");
1115
+
1116
+
dropdown.dataset.releases = JSON.stringify(releases);
1117
+
} catch (error) {
1118
+
dropdown.innerHTML = `<div class="autocomplete-status" style="color: var(--error-text)">Search failed</div>`;
1119
+
}
1120
+
}, 300);
1121
+
1122
+
function selectRelease(index) {
1123
+
const dropdown = document.getElementById("album-dropdown");
1124
+
const releases = JSON.parse(dropdown.dataset.releases || "[]");
1125
+
const release = releases[index];
1126
+
1127
+
if (!release) return;
1128
+
1129
+
if (state.selectedRecording) {
1130
+
state.selectedRecording.releaseName = release.title;
1131
+
state.selectedRecording.releaseMbid = release.id;
1132
+
}
1133
+
1134
+
dropdown.classList.add("hidden");
1135
+
document.getElementById("album-input").value = release.title;
1136
}
1137
1138
// =============================================================================