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