forked from Shinonome/dots-hyprland
Music recognition toggle (like Shazam) with SongRec (#2301)
This commit is contained in:
@@ -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="
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -29,7 +29,7 @@ AbstractQuickPanel {
|
||||
readonly property real baseCellHeight: 56
|
||||
|
||||
// Toggles
|
||||
readonly property list<string> availableToggleTypes: ["network", "bluetooth", "idleInhibitor", "easyEffects", "nightLight", "darkMode", "cloudflareWarp", "gameMode", "screenSnip", "colorPicker", "onScreenKeyboard", "mic", "audio", "notifications", "powerProfile"]
|
||||
readonly property list<string> 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<var> toggles: Config.ready ? Config.options.sidebar.quickToggles.android.toggles : []
|
||||
readonly property list<var> toggleRows: toggleRowsForList(toggles)
|
||||
|
||||
+102
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -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
|
||||
} }
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user