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 567 return data.recordings || []; 568 568 } 569 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 + 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 - <label>Album</label> 766 - <input type="text" id="album-display" class="read-only" readonly placeholder="Auto-filled" /> 778 + <label>Album <span style="font-weight: normal; color: var(--text-secondary);">(optional)</span></label> 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 + renderAlbumField(); 800 814 } 801 815 802 816 function renderArtistField() { ··· 853 867 } 854 868 } 855 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 + 856 891 // ============================================================================= 857 892 // ARTIST SEARCH HANDLERS 858 893 // ============================================================================= ··· 907 942 }; 908 943 909 944 state.selectedRecording = null; 910 - document.getElementById("album-display").value = ""; 911 945 document.getElementById("duration-display").value = ""; 912 946 913 947 renderArtistField(); 914 948 renderTrackField(); 949 + renderAlbumField(); 915 950 updateSubmitButton(); 916 951 } 917 952 918 953 function clearArtist() { 919 954 state.selectedArtist = null; 920 955 state.selectedRecording = null; 921 - document.getElementById("album-display").value = ""; 922 956 document.getElementById("duration-display").value = ""; 923 957 924 958 renderArtistField(); 925 959 renderTrackField(); 960 + renderAlbumField(); 926 961 updateSubmitButton(); 927 962 } 928 963 ··· 1005 1040 })) || [{ artistName: state.selectedArtist.name, artistMbId: state.selectedArtist.mbid }], 1006 1041 }; 1007 1042 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 + renderAlbumField(); 1014 1049 updateSubmitButton(); 1015 1050 } 1016 1051 1017 1052 function clearRecording() { 1018 1053 state.selectedRecording = null; 1019 - document.getElementById("album-display").value = ""; 1020 1054 document.getElementById("duration-display").value = ""; 1021 1055 1022 1056 renderTrackField(); 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 + } 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; 1031 1136 } 1032 1137 1033 1138 // =============================================================================