diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index f496f7bab..7a66d90f6 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -376,6 +376,11 @@ Singleton { property int updateInterval: 3000 } + property JsonObject musicRecognition: JsonObject { + property int timeout: 16 + property int interval: 4 + } + property JsonObject search: JsonObject { property int nonAppResultDelay: 30 // This prevents lagging when typing property string engineBaseUrl: "https://www.google.com/search?q=" diff --git a/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js b/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js index 7ab21c3bb..f817a460b 100644 --- a/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js +++ b/dots/.config/quickshell/ii/modules/common/widgets/notification_utils.js @@ -17,13 +17,17 @@ function findSuitableMaterialSymbol(summary = "") { 'time': 'scheduleb', 'installed': 'download', 'configuration reloaded': 'reset_wrench', + 'unable': 'question_mark', + "couldn't": 'question_mark', 'config': 'reset_wrench', 'update': 'update', 'ai response': 'neurology', 'control': 'settings', 'upsca': 'compare', + 'music': 'queue_music', 'install': 'deployed_code_update', 'startswith:file': 'folder_copy', // Declarative startsWith check + }; const lowerSummary = summary.toLowerCase(); diff --git a/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml b/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml index fae5bf4de..11fc82966 100644 --- a/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/ServicesConfig.qml @@ -54,6 +54,35 @@ ContentPage { Config.options.resources.updateInterval = value; } } + + } + + ContentSection { + icon: "music_cast" + title: Translation.tr("Music Recognition") + + ConfigSpinBox { + icon: "timer_off" + text: Translation.tr("Total duration timeout (s)") + value: Config.options.musicRecognition.timeout + from: 10 + to: 100 + stepSize: 2 + onValueChanged: { + Config.options.musicRecognition.timeout = value; + } + } + ConfigSpinBox { + icon: "av_timer" + text: Translation.tr("Polling interval (s)") + value: Config.options.musicRecognition.interval + from: 2 + to: 10 + stepSize: 1 + onValueChanged: { + Config.options.musicRecognition.interval = value; + } + } } ContentSection { diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml index a7d743585..b5e49d3b0 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/AndroidQuickPanel.qml @@ -29,7 +29,7 @@ AbstractQuickPanel { readonly property real baseCellHeight: 56 // Toggles - readonly property list availableToggleTypes: ["network", "bluetooth", "idleInhibitor", "easyEffects", "nightLight", "darkMode", "cloudflareWarp", "gameMode", "screenSnip", "colorPicker", "onScreenKeyboard", "mic", "audio", "notifications", "powerProfile"] + readonly property list availableToggleTypes: ["network", "bluetooth", "idleInhibitor", "easyEffects", "nightLight", "darkMode", "cloudflareWarp", "gameMode", "screenSnip", "colorPicker", "onScreenKeyboard", "mic", "audio", "notifications", "powerProfile","musicRecognition"] readonly property int columns: Config.options.sidebar.quickToggles.android.columns readonly property list toggles: Config.ready ? Config.options.sidebar.quickToggles.android.toggles : [] readonly property list toggleRows: toggleRowsForList(toggles) diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidMusicRecognition.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidMusicRecognition.qml new file mode 100644 index 000000000..a1b9c6738 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidMusicRecognition.qml @@ -0,0 +1,102 @@ +import qs +import qs.modules.common +import qs.modules.common.widgets +import QtQuick +import Quickshell +import Quickshell.Io +import qs.services + + +AndroidQuickToggleButton { + id: root + + property int timeoutInterval: Config.options.musicRecognition.interval + property int timeoutDuration: Config.options.musicRecognition.timeout + + + property string monitorSource: "monitor" // "monitor" (system sound) , "input" (microphone) + + name: Translation.tr("Identify Music") + statusText: toggled ? Translation.tr("Listening...") : monitorSource === "monitor" ? Translation.tr("System sound") : Translation.tr("Microphone") + toggled: false + buttonIcon: toggled ? "music_cast" : (monitorSource === "monitor" ? "music_note" : "frame_person_mic") + + property var recognizedTrack: ({ title:"", subtitle:"", url:""}) + + function handleRecognition(jsonText) { + try { + var obj = JSON.parse(jsonText) + root.recognizedTrack = { + title: obj.track.title, + subtitle: obj.track.subtitle, + url: obj.track.url + } + musicReconizedProc.running = true + } catch(e) { + Quickshell.execDetached(["notify-send", Translation.tr("Couldn't recognize music"), Translation.tr("Perhaps what you're listening to is too niche"), "-a", "Shell"]) + } finally { + root.toggled = false + } + } + + + StyledToolTip { + text: Translation.tr("Recognize music | Right-click to toggle source") + } + + onClicked: { + root.toggled = !root.toggled + recognizeMusicProc.running = root.toggled + musicReconizedProc.running = false + } + + altAction: () => { + if (root.monitorSource === "monitor"){ + root.monitorSource = "input" + return + }else { + root.monitorSource = "monitor" + } + + } + + Process { + id: recognizeMusicProc + running: false + command: [`${Directories.scriptPath}/musicRecognition/recognize-music.sh`, "-i", root.timeoutInterval, "-t", root.timeoutDuration, "-s", root.monitorSource] + stdout: StdioCollector { + onStreamFinished: { + handleRecognition(this.text) + } + } + onExited: (exitCode, exitStatus) => { + if (exitCode === 1) { + Quickshell.execDetached(["notify-send", Translation.tr("Couldn't recognize music"), Translation.tr("Make sure you have songrec installed"), "-a", "Shell"]) + root.toggled = false + } + } + } + + Process { + id: musicReconizedProc + running: false + command: [ + "notify-send", + Translation.tr("Music Recognized"), + root.recognizedTrack.title + " - " + root.recognizedTrack.subtitle, + "-A", "Shazam", + "-A", "YouTube", + "-a", "Shell" + ] + stdout: StdioCollector { + onStreamFinished: { + if (this.text === "") return + if (this.text == 0){ + Qt.openUrlExternally(root.recognizedTrack.url); + } else { + Qt.openUrlExternally("https://www.youtube.com/results?search_query=" + root.recognizedTrack.title + " - " + root.recognizedTrack.subtitle); + } + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml index a8f79c843..aa5356e2e 100644 --- a/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml +++ b/dots/.config/quickshell/ii/modules/sidebarRight/quickToggles/androidStyle/AndroidToggleDelegateChooser.qml @@ -232,4 +232,17 @@ DelegateChooser { cellSize: modelData.size } } + DelegateChoice { roleValue: "musicRecognition"; AndroidMusicRecognition { + required property int index + required property var modelData + buttonIndex: root.startingIndex + index + buttonData: modelData + editMode: root.editMode + expandedSize: modelData.size > 1 + baseCellWidth: root.baseCellWidth + baseCellHeight: root.baseCellHeight + cellSpacing: root.spacing + cellSize: modelData.size + } } + } diff --git a/dots/.config/quickshell/ii/scripts/musicRecognition/recognize-music.sh b/dots/.config/quickshell/ii/scripts/musicRecognition/recognize-music.sh new file mode 100755 index 000000000..09b0361af --- /dev/null +++ b/dots/.config/quickshell/ii/scripts/musicRecognition/recognize-music.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +INTERVAL=2 +TOTAL_DURATION=30 +MIN_VALID_RESULT_LENGTH=300 +SOURCE_TYPE="monitor" # monitor | input +TMP_PATH="/tmp/quickshell/media/songrec" +TMP_RAW="$TMP_PATH/recording.raw" +TMP_MP3="$TMP_PATH/recording.mp3" + +while getopts "i:t:s:" opt; do + case $opt in + i) INTERVAL=$OPTARG ;; + t) TOTAL_DURATION=$OPTARG ;; + s) SOURCE_TYPE=$OPTARG ;; + *) exit 1 ;; + esac +done +if [ "$SOURCE_TYPE" = "monitor" ]; then + MONITOR_SOURCE=$(pactl list short sources 2>/dev/null | grep -m1 monitor | awk '{print $2}' || true) +elif [ "$SOURCE_TYPE" = "input" ]; then + MONITOR_SOURCE=$(pactl info | grep "Default Source:" | awk '{print $3}' || true) +else + echo "Invalid source type" + exit 1 +fi + +if [ -z "$MONITOR_SOURCE" ] || ! command -v songrec >/dev/null 2>&1; then + exit 1 +fi + +cleanup() { + rm -f "$TMP_RAW" "$TMP_MP3" + pkill -P $$ parec >/dev/null 2>&1 || true +} +trap cleanup EXIT + +mkdir -p "$TMP_PATH" +parec --device="$MONITOR_SOURCE" --format=s16le --rate=44100 --channels=2 > "$TMP_RAW" & +START_TIME=$(date +%s) + +while true; do + sleep "$INTERVAL" + CURRENT_TIME=$(date +%s) + ELAPSED=$((CURRENT_TIME - START_TIME)) + + if (( ELAPSED >= TOTAL_DURATION )); then + exit 0 + fi + + ffmpeg -f s16le -ar 44100 -ac 2 -i "$TMP_RAW" -acodec libmp3lame -y -hide_banner -loglevel error "$TMP_MP3" 2>/dev/null + RESULT=$(songrec audio-file-to-recognized-song "$TMP_MP3" 2>/dev/null || true) + + if echo "$RESULT" | grep -q '"matches": \[' && [ ${#RESULT} -gt $MIN_VALID_RESULT_LENGTH ]; then + echo "$RESULT" + exit 0 + fi +done