Tools for the Atmosphere tools.slices.network
quickslice atproto html

feat: add album autocomplete search as fallback

+111 -6
+111 -6
teal-scrobble.html
··· 567 return data.recordings || []; 568 } 569 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(); 800 } 801 802 function renderArtistField() { ··· 853 } 854 } 855 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(); 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(); 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(); 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(); 1023 updateSubmitButton(); 1024 } 1025 ··· 1028 const m = Math.floor(secs / 60); 1029 const s = secs % 60; 1030 return `${m}:${s.toString().padStart(2, "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; 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; 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 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; 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 // =============================================================================