mirror of
https://github.com/cgzirim/seek-tune.git
synced 2025-12-17 08:54:19 +00:00
Pushing to GitHub after many changes.
This commit is contained in:
parent
439b5442f5
commit
845f43b5bf
25 changed files with 19611 additions and 541 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -3,6 +3,9 @@
|
||||||
#
|
#
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
|
*.ogg
|
||||||
|
*.m4a
|
||||||
|
*.zip
|
||||||
*.exe~
|
*.exe~
|
||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
|
|
|
||||||
18623
client/package-lock.json
generated
18623
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -6,10 +6,18 @@
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@testing-library/jest-dom": "^5.17.0",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"extendable-media-recorder": "^9.1.15",
|
||||||
|
"extendable-media-recorder-wav-encoder": "^7.0.109",
|
||||||
|
"framer-motion": "^11.0.25",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-animated-numbers": "^0.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-icons": "^5.0.1",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
|
"react-slick": "^0.30.2",
|
||||||
|
"react-toastify": "^8.1.0",
|
||||||
"simple-peer": "^9.11.1",
|
"simple-peer": "^9.11.1",
|
||||||
|
"slick-carousel": "^1.8.1",
|
||||||
"socket.io-client": "^2.5.0",
|
"socket.io-client": "^2.5.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
|
@ -25,18 +33,11 @@
|
||||||
"react-app/jest"
|
"react-app/jest"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": [
|
||||||
"production": [
|
">0.2%",
|
||||||
">0.2%",
|
"not dead",
|
||||||
"not dead",
|
"not op_mini all"
|
||||||
"not op_mini all"
|
],
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"react-error-overlay": "^6.0.9"
|
"react-error-overlay": "^6.0.9"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Peer from "simple-peer";
|
import Peer from "simple-peer";
|
||||||
import io, { managers } from "socket.io-client";
|
import io from "socket.io-client";
|
||||||
import Form from "./Form";
|
import Form from "./components/Form";
|
||||||
|
import Listen from "./components/Listen";
|
||||||
|
import CarouselSliders from "./components/CarouselSliders";
|
||||||
|
import AnimatedNumber from "react-animated-numbers";
|
||||||
|
import { FaMasksTheater, FaMicrophoneLines } from "react-icons/fa6";
|
||||||
|
import { LiaLaptopSolid } from "react-icons/lia";
|
||||||
|
import { ToastContainer, toast, Slide } from "react-toastify";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
|
|
||||||
// const socket = io.connect('http://localhost:5000/');
|
// const socket = io.connect('http://localhost:5000/');
|
||||||
var socket = io("http://localhost:5000/");
|
var socket = io("http://localhost:5000/");
|
||||||
|
|
@ -10,98 +17,165 @@ function App() {
|
||||||
const [offer, setOffer] = useState();
|
const [offer, setOffer] = useState();
|
||||||
const [stream, setStream] = useState();
|
const [stream, setStream] = useState();
|
||||||
const [matches, setMatches] = useState([]);
|
const [matches, setMatches] = useState([]);
|
||||||
const [serverEngaged, setServerEngaged] = useState(false);
|
const [totalSongs, setTotalSongs] = useState(10);
|
||||||
|
const [isListening, setisListening] = useState(false);
|
||||||
|
const [audioInput, setAudioInput] = useState("device");
|
||||||
const [peerConnection, setPeerConnection] = useState();
|
const [peerConnection, setPeerConnection] = useState();
|
||||||
|
const [serverEngaged, setServerEngaged] = useState(false);
|
||||||
|
|
||||||
|
function record() {
|
||||||
|
const mediaDevice =
|
||||||
|
audioInput == "device"
|
||||||
|
? navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices)
|
||||||
|
: navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
||||||
|
|
||||||
|
mediaDevice({ audio: true })
|
||||||
|
.then(function (stream) {
|
||||||
|
const audioTracks = stream.getAudioTracks();
|
||||||
|
const audioStream = new MediaStream(audioTracks);
|
||||||
|
|
||||||
|
for (const track of stream.getVideoTracks()) {
|
||||||
|
track.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaRecorder = new MediaRecorder(audioStream);
|
||||||
|
const chunks = [];
|
||||||
|
|
||||||
|
mediaRecorder.ondataavailable = function (e) {
|
||||||
|
chunks.push(e.data);
|
||||||
|
console.log("DataType: ", e.data);
|
||||||
|
// if (e.data.type.startsWith("audio")) {
|
||||||
|
// chunks.push(e.data);
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
mediaRecorder.addEventListener("stop", () => {
|
||||||
|
const blob = new Blob(chunks, { type: "audio/mpeg" }); // Assuming MP3 format
|
||||||
|
|
||||||
|
// Convert Blob to byte array
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsArrayBuffer(blob);
|
||||||
|
|
||||||
|
reader.onloadend = () => {
|
||||||
|
if (reader.result) {
|
||||||
|
var binary = "";
|
||||||
|
var bytes = new Uint8Array(reader.result);
|
||||||
|
var len = bytes.byteLength;
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert byte array to base64
|
||||||
|
const base64data = btoa(binary);
|
||||||
|
|
||||||
|
console.log("Base64:", base64data);
|
||||||
|
// Send the base64 data to the backend (assuming socket.emit exists)
|
||||||
|
socket.emit("blob", base64data);
|
||||||
|
} else {
|
||||||
|
console.error("Error reading Blob as array buffer");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Start recording
|
||||||
|
mediaRecorder.start();
|
||||||
|
|
||||||
|
// Stop recording after 5 seconds
|
||||||
|
setTimeout(function () {
|
||||||
|
mediaRecorder.stop();
|
||||||
|
}, 15000);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.error("Error accessing media devices.", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function cleanUp() {
|
function cleanUp() {
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
|
console.log("Cleaning tracks");
|
||||||
stream.getTracks().forEach((track) => track.stop());
|
stream.getTracks().forEach((track) => track.stop());
|
||||||
}
|
}
|
||||||
setOffer(null);
|
|
||||||
setStream(null);
|
setStream(null);
|
||||||
setPeerConnection(null);
|
setisListening(false);
|
||||||
setServerEngaged(false);
|
console.log("Cleanup complete.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to initiate the client peer
|
function createPeerConnection() {
|
||||||
function initiateClientPeer(stream = null) {
|
|
||||||
const peer = new Peer({
|
const peer = new Peer({
|
||||||
initiator: true,
|
initiator: true,
|
||||||
trickle: false,
|
trickle: false,
|
||||||
stream: stream,
|
stream: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
let offerHasBeenSet = false;
|
// Handle peer events:
|
||||||
|
peer.on("signal", (offerData) => {
|
||||||
peer.on("signal", (data) => {
|
console.log("Setting Offer!");
|
||||||
if (!offerHasBeenSet) {
|
setOffer(JSON.stringify(offerData));
|
||||||
console.log("Setting Offer!");
|
setPeerConnection(peer);
|
||||||
setOffer(JSON.stringify(data));
|
|
||||||
offerHasBeenSet = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
peer.on("close", () => {
|
peer.on("close", () => {
|
||||||
|
cleanUp();
|
||||||
|
setServerEngaged(false);
|
||||||
console.log("CONNECTION CLOSED");
|
console.log("CONNECTION CLOSED");
|
||||||
});
|
});
|
||||||
|
|
||||||
peer.on("error", (err) => {
|
peer.on("error", (err) => {
|
||||||
console.error("An error occurred:", err);
|
console.error("An error occurred:", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
setPeerConnection(peer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("Offer updated:", offer);
|
if (offer) {
|
||||||
let renegotiated = false;
|
console.log("Offer updated:", offer);
|
||||||
|
|
||||||
if (offer && stream && !renegotiated) {
|
|
||||||
let offerEncoded = btoa(offer);
|
let offerEncoded = btoa(offer);
|
||||||
socket.emit("engage", offerEncoded);
|
socket.emit("engage", offerEncoded);
|
||||||
|
|
||||||
socket.on("serverEngaged", (answer) => {
|
socket.on("serverEngaged", (answer) => {
|
||||||
console.log("ServerSDP: ", answer);
|
console.log("ServerSDP: ", answer);
|
||||||
let decodedAnswer = atob(answer);
|
let decodedAnswer = atob(answer);
|
||||||
peerConnection.signal(decodedAnswer);
|
if (!serverEngaged && !stream && !peerConnection.destroyed) {
|
||||||
console.log("Engaging Server");
|
peerConnection.signal(decodedAnswer);
|
||||||
|
}
|
||||||
|
console.log("Engaged Server");
|
||||||
setServerEngaged(true);
|
setServerEngaged(true);
|
||||||
renegotiated = true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [offer]);
|
}, [offer]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initiateClientPeer();
|
socket.on("connect", () => {
|
||||||
|
createPeerConnection();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("matches", (matches) => {
|
||||||
|
matches = JSON.parse(matches);
|
||||||
|
if (matches) {
|
||||||
|
setMatches(matches);
|
||||||
|
console.log("Matches: ", matches);
|
||||||
|
} else {
|
||||||
|
console.log("No Matches");
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanUp();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("downloadStatus", (msg) => {
|
||||||
|
console.log("downloadStatus: ", msg);
|
||||||
|
msg = JSON.parse(msg);
|
||||||
|
const msgTypes = ["info", "success", "error"];
|
||||||
|
if (msg.type !== undefined && msgTypes.includes(msg.type)) {
|
||||||
|
toast[msg.type](() => <div>{msg.message}</div>);
|
||||||
|
} else {
|
||||||
|
toast(msg.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("totalSongs", (songsCount) => {
|
||||||
|
console.log("Total songs in DB: ", songsCount);
|
||||||
|
setTotalSongs(songsCount);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// socket.on("connect", () => {
|
|
||||||
// initiateClientPeer();
|
|
||||||
// });
|
|
||||||
|
|
||||||
socket.on("matches", (matches) => {
|
|
||||||
matches = JSON.parse(matches);
|
|
||||||
if (matches) {
|
|
||||||
setMatches(matches);
|
|
||||||
console.log("Matches: ", matches);
|
|
||||||
} else {
|
|
||||||
console.log("No Matches");
|
|
||||||
}
|
|
||||||
cleanUp();
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("downloadStatus", (msg) => {
|
|
||||||
console.log("downloadStatus: ", msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("albumStat", (msg) => {
|
|
||||||
console.log("Album stat: ", msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("playlistStat", (msg) => {
|
|
||||||
console.log("Playlist stat: ", msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const emitTotalSongs = () => {
|
const emitTotalSongs = () => {
|
||||||
socket.emit("totalSongs", "");
|
socket.emit("totalSongs", "");
|
||||||
|
|
@ -112,84 +186,106 @@ function App() {
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
socket.on("totalSongs", (totalSongs) => {
|
function stopListening() {
|
||||||
console.log("Total songs in DB: ", totalSongs);
|
console.log("Pause Clicked");
|
||||||
});
|
cleanUp();
|
||||||
|
peerConnection.destroy();
|
||||||
|
|
||||||
const streamAudio = () => {
|
setTimeout(() => {
|
||||||
navigator.mediaDevices
|
createPeerConnection();
|
||||||
.getDisplayMedia({ audio: true })
|
}, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startListening() {
|
||||||
|
const mediaDevice =
|
||||||
|
audioInput === "device"
|
||||||
|
? navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices)
|
||||||
|
: navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
|
||||||
|
|
||||||
|
mediaDevice({ audio: true })
|
||||||
.then((stream) => {
|
.then((stream) => {
|
||||||
|
console.log("isListening: ", isListening);
|
||||||
peerConnection.addStream(stream);
|
peerConnection.addStream(stream);
|
||||||
|
setisListening(true);
|
||||||
|
|
||||||
// Renegotiate
|
|
||||||
let initOfferEncoded = btoa(offer);
|
|
||||||
socket.emit("initOffer", initOfferEncoded);
|
|
||||||
|
|
||||||
socket.on("initAnswer", (answer) => {
|
|
||||||
let decodedAnswer = atob(answer);
|
|
||||||
peerConnection.signal(decodedAnswer);
|
|
||||||
console.log("Renogotiated");
|
|
||||||
});
|
|
||||||
|
|
||||||
// End of Renegotiation
|
|
||||||
|
|
||||||
peerConnection.on("signal", (data) => {
|
|
||||||
setOffer(JSON.stringify(data));
|
|
||||||
console.log("Offer should be reset");
|
|
||||||
});
|
|
||||||
setStream(stream);
|
setStream(stream);
|
||||||
|
stream.getVideoTracks()[0].onended = stopListening;
|
||||||
|
stream.getAudioTracks()[0].onended = stopListening;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error accessing user media:", error);
|
console.error("Error accessing user media:", error);
|
||||||
// Handle error
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!offer || !peerConnection) {
|
const handleLaptopIconClick = () => {
|
||||||
// If offer is not set, create a new one
|
console.log("Laptop icon clicked");
|
||||||
console.log("NO OFFER. CREATING OFFER");
|
setAudioInput("device");
|
||||||
initiateClientPeer(stream);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const disengageServer = () => {
|
const handleMicrophoneIconClick = () => {
|
||||||
peerConnection.destroy();
|
console.log("Microphone icon clicked");
|
||||||
|
setAudioInput("mic");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<h1>New App</h1>
|
<h1>New App</h1>
|
||||||
<div>
|
<h4 style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||||
{serverEngaged ? (
|
<AnimatedNumber
|
||||||
<button disabled={true}>Listening</button>
|
includeComma
|
||||||
) : (
|
animateToNumber={totalSongs}
|
||||||
<button onClick={() => streamAudio()}>Listen</button>
|
config={{ tension: 89, friction: 40 }}
|
||||||
)}
|
animationType={"calm"}
|
||||||
{serverEngaged && (
|
/>
|
||||||
<button onClick={() => disengageServer()}>Stop Listening</button>
|
Songs
|
||||||
)}
|
</h4>
|
||||||
|
<div className="listen">
|
||||||
|
<Listen
|
||||||
|
stopListening={stopListening}
|
||||||
|
disable={!serverEngaged}
|
||||||
|
startListening={startListening}
|
||||||
|
isListening={isListening}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Form socket={socket} />
|
<div className="audio-input">
|
||||||
<div>
|
<div
|
||||||
{matches.map((match, index) => {
|
onClick={handleLaptopIconClick}
|
||||||
const [h, m, s] = match.timestamp.split(":");
|
className={
|
||||||
const timestamp =
|
audioInput !== "device"
|
||||||
parseInt(h, 10) * 120 + parseInt(m, 10) * 60 + parseInt(s, 10);
|
? "audio-input-device"
|
||||||
|
: "audio-input-device active-audio-input"
|
||||||
return (
|
}
|
||||||
<iframe
|
>
|
||||||
key={index}
|
<LiaLaptopSolid style={{ height: 20, width: 20 }} />
|
||||||
width="460"
|
</div>
|
||||||
height="284"
|
<div
|
||||||
src={`https://www.youtube.com/embed/${match.youtubeid}?start=${timestamp}`}
|
onClick={handleMicrophoneIconClick}
|
||||||
title={match.songname}
|
className={
|
||||||
frameBorder="0"
|
audioInput !== "mic"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
? "audio-input-mic"
|
||||||
allowFullScreen
|
: "audio-input-mic active-audio-input"
|
||||||
></iframe>
|
}
|
||||||
);
|
>
|
||||||
})}
|
<FaMicrophoneLines style={{ height: 20, width: 20 }} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="youtube">
|
||||||
|
<CarouselSliders matches={matches} />
|
||||||
|
</div>
|
||||||
|
<Form socket={socket} toast={toast} />
|
||||||
|
<ToastContainer
|
||||||
|
position="top-center"
|
||||||
|
autoClose={5000}
|
||||||
|
hideProgressBar={false}
|
||||||
|
newestOnTop={false}
|
||||||
|
closeOnClick
|
||||||
|
rtl={false}
|
||||||
|
pauseOnFocusLoss
|
||||||
|
draggable
|
||||||
|
pauseOnHover
|
||||||
|
theme="light"
|
||||||
|
transition={Slide}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
import React, { Component } from "react";
|
|
||||||
|
|
||||||
class Form extends Component {
|
|
||||||
initialState = { spotifyUrl: "" };
|
|
||||||
|
|
||||||
state = this.initialState;
|
|
||||||
|
|
||||||
handleChange = (event) => {
|
|
||||||
const { name, value } = event.target;
|
|
||||||
|
|
||||||
this.setState({ [name]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
submitForm = () => {
|
|
||||||
const { spotifyUrl } = this.state;
|
|
||||||
const { socket } = this.props;
|
|
||||||
if (this.spotifyURLisValid(spotifyUrl) === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.emit("newDownload", spotifyUrl);
|
|
||||||
console.log("newDownload: ", spotifyUrl);
|
|
||||||
this.setState(this.initialState);
|
|
||||||
};
|
|
||||||
|
|
||||||
spotifyURLisValid = (url) => {
|
|
||||||
if (url.length === 0) {
|
|
||||||
console.log("Spotify URL required");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitURL = url.split("/");
|
|
||||||
if (splitURL.length < 2) {
|
|
||||||
console.log("Invalid Spotify URL format");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let spotifyID = splitURL[splitURL.length - 1];
|
|
||||||
if (spotifyID.includes("?")) {
|
|
||||||
spotifyID = spotifyID.split("?")[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the Spotify ID is alphanumeric
|
|
||||||
if (!/^[a-zA-Z0-9]+$/.test(spotifyID)) {
|
|
||||||
console.log("Invalid Spotify ID format");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the Spotify ID is of expected length
|
|
||||||
if (spotifyID.length !== 22) {
|
|
||||||
console.log("Invalid Spotify ID length");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional validation logic can be added here
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { spotifyUrl } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form>
|
|
||||||
<label htmlFor="spotifyUrl">spotifyUrl</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="spotifyUrl"
|
|
||||||
id="spotifyUrl"
|
|
||||||
value={spotifyUrl}
|
|
||||||
placeholder="https://open.spotify.com/.../..."
|
|
||||||
onChange={this.handleChange}
|
|
||||||
/>
|
|
||||||
<input type="button" value="Submit" onClick={this.submitForm} />
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Form;
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
|
|
||||||
function TableHeader () {
|
|
||||||
return (
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Job</th>
|
|
||||||
<th>Remove</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TableBody (props) {
|
|
||||||
const rows = props.characterData.map((row, index) => {
|
|
||||||
return (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.name}</td>
|
|
||||||
<td>{row.job}</td>
|
|
||||||
<td>
|
|
||||||
<button onClick={() => props.removeCharacter(index)}>Delete</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return <tbody>{rows}</tbody>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function Table (props) {
|
|
||||||
const { characterData, removeCharacter } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table>
|
|
||||||
<TableHeader />
|
|
||||||
<TableBody characterData={characterData} removeCharacter={removeCharacter} />
|
|
||||||
</table>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Table
|
|
||||||
62
client/src/components/CarouselSliders.js
Normal file
62
client/src/components/CarouselSliders.js
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import React from "react";
|
||||||
|
import styles from "./styles/CarouselSliders.module.css";
|
||||||
|
|
||||||
|
const CarouselSliders = (props) => {
|
||||||
|
const [activeIdx, setActiveIdx] = React.useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.CarouselSliders}>
|
||||||
|
{!props.matches.length ? null : (
|
||||||
|
<div className={styles.Slider}>
|
||||||
|
{props.matches.map((match, index) => {
|
||||||
|
const [h, m, s] = match.timestamp.split(":");
|
||||||
|
const timestamp =
|
||||||
|
parseInt(h, 10) * 360 + parseInt(m, 10) * 60 + parseInt(s, 10);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
id={`slide-${index}`}
|
||||||
|
className={styles.SlideItem}
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
className="iframe-youtube"
|
||||||
|
src={`https://www.youtube.com/embed/${match.youtubeid}?start=${timestamp}`}
|
||||||
|
title={match.songname}
|
||||||
|
frameBorder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
allowFullScreen
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.Circles}>
|
||||||
|
{props.matches.map((_, index) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
className={
|
||||||
|
index !== activeIdx
|
||||||
|
? styles.Link
|
||||||
|
: `${styles.Link} ${styles.ActiveLink}`
|
||||||
|
}
|
||||||
|
href={`#slide-${index}`}
|
||||||
|
onClick={() => {
|
||||||
|
setActiveIdx(index);
|
||||||
|
}}
|
||||||
|
></a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CarouselSliders;
|
||||||
58
client/src/components/Form.js
Normal file
58
client/src/components/Form.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import styles from "./styles/Form.module.css";
|
||||||
|
|
||||||
|
const Form = ({ socket }) => {
|
||||||
|
const initialState = { spotifyUrl: "" };
|
||||||
|
const [formState, setFormState] = useState(initialState);
|
||||||
|
|
||||||
|
const handleChange = (event) => {
|
||||||
|
const { name, value } = event.target;
|
||||||
|
setFormState({ ...formState, [name]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = () => {
|
||||||
|
const { spotifyUrl } = formState;
|
||||||
|
if (spotifyURLisValid(spotifyUrl) === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit("newDownload", spotifyUrl);
|
||||||
|
console.log("newDownload: ", spotifyUrl);
|
||||||
|
setFormState(initialState);
|
||||||
|
};
|
||||||
|
|
||||||
|
const spotifyURLisValid = (url) => {
|
||||||
|
if (url.length === 0) {
|
||||||
|
console.log("Spotify URL required");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitURL = url.split("/");
|
||||||
|
if (splitURL.length < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { spotifyUrl } = formState;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className={styles.Form}>
|
||||||
|
<div style={{ flexGrow: 1 }}>
|
||||||
|
<div>Download songs to the server</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="spotifyUrl"
|
||||||
|
id="spotifyUrl"
|
||||||
|
value={spotifyUrl}
|
||||||
|
placeholder="https://open.spotify.com/.../..."
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input type="button" value="Submit" onClick={submitForm} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Form;
|
||||||
67
client/src/components/Listen.js
Normal file
67
client/src/components/Listen.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import styles from "./styles/Listen.module.css";
|
||||||
|
|
||||||
|
const ListenButton = ({ isListening, onClick, disable }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled={disable}
|
||||||
|
onClick={onClick}
|
||||||
|
className={
|
||||||
|
disable
|
||||||
|
? styles.ListenButton
|
||||||
|
: `${styles.ListenButton} ${styles.Enabled}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{disable ? "Loading..." : isListening ? "Listening..." : "Listen"}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Listen = ({ disable, startListening, stopListening, isListening }) => {
|
||||||
|
const [listen, setListen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isListening === false && listen === true) {
|
||||||
|
setListen(false);
|
||||||
|
}
|
||||||
|
}, [isListening]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (listen) {
|
||||||
|
startListening();
|
||||||
|
} else {
|
||||||
|
if (isListening) {
|
||||||
|
stopListening();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [listen]);
|
||||||
|
|
||||||
|
const toggleListen = () => {
|
||||||
|
setListen(!listen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
isListening
|
||||||
|
? `${styles.CirlceItems} ${styles.Play}`
|
||||||
|
: `${styles.CirlceItems} ${styles.Pause}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.CircleItem}></div>
|
||||||
|
<div className={styles.CircleItem}></div>
|
||||||
|
<div className={styles.CircleItem}></div>
|
||||||
|
<ListenButton
|
||||||
|
isListening={isListening}
|
||||||
|
onClick={toggleListen}
|
||||||
|
disable={disable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Listen;
|
||||||
93
client/src/components/styles/CarouselSliders.module.css
Normal file
93
client/src/components/styles/CarouselSliders.module.css
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
.CarouselSliders {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Slider {
|
||||||
|
/* height: 90vmin; */
|
||||||
|
width: 100%;
|
||||||
|
/* background-color: #c1c1c1; */
|
||||||
|
display: flex;
|
||||||
|
overscroll-behavior-inline: contain;
|
||||||
|
scroll-snap-type: inline mandatory;
|
||||||
|
overflow-x: scroll;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Slider::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Slider {
|
||||||
|
scrollbar-width: 12px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
.Slider::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 20px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Slider::-webkit-scrollbar-track {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.SlideItem {
|
||||||
|
/* height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 90%; */
|
||||||
|
margin-left: 20px;
|
||||||
|
flex-grow: 1;
|
||||||
|
scroll-snap-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SlideItem:nth-child(1) { background-color: rebeccapurple;}
|
||||||
|
.SlideItem:nth-child(2) { background-color: dodgerblue;}
|
||||||
|
.SlideItem:nth-child(3) { background-color: greenyellow;}
|
||||||
|
|
||||||
|
.iframe-youtube {
|
||||||
|
width: 420px;
|
||||||
|
height: 210px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
.iframe-youtube {
|
||||||
|
width: 420px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Slider {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Circles {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.Circles .Link {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
margin: 0 3px;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #c1c1c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Link.ActiveLink {
|
||||||
|
background-color: #6a0dad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.iframe-youtube {
|
||||||
|
width: 820px;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
.iframe-youtube {
|
||||||
|
width: 420px;
|
||||||
|
height: 210px;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
client/src/components/styles/Form.module.css
Normal file
46
client/src/components/styles/Form.module.css
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
Form {
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 550px) {
|
||||||
|
Form {
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.FormStatus {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
background-color: #3498db;
|
||||||
|
border-radius: 2px;
|
||||||
|
animation: slideUp 0.8s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.FormStatus.NoMessage {
|
||||||
|
/* animation: slideDown 0.8s forwards; */
|
||||||
|
transform: translateX(-50%) translateY(100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .slideDown {
|
||||||
|
transform: translateX(-50%) translateY(100%);
|
||||||
|
} */
|
||||||
80
client/src/components/styles/Listen.module.css
Normal file
80
client/src/components/styles/Listen.module.css
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
.CirlceItems {
|
||||||
|
width: 35vmin;
|
||||||
|
height: 35vmin;
|
||||||
|
position: relative;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CircleItem {
|
||||||
|
background-color: dodgerblue;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Play > .CircleItem {
|
||||||
|
animation: expandFadeOut 3s infinite ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Pause > .CircleItem {
|
||||||
|
animation-play-state: paused;
|
||||||
|
/* transition: opacity 3s ease-out;
|
||||||
|
opacity: 0;
|
||||||
|
animation: expandFadeOut 3s ease-in-out;
|
||||||
|
animation-iteration-count: 1;
|
||||||
|
animation-fill-mode: forwards; */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.CircleItem:nth-child(1) { animation-delay: 1s; }
|
||||||
|
.CircleItem:nth-child(2) { animation-delay: 2s; }
|
||||||
|
.CircleItem:nth-child(3) { animation-delay: 3s; }
|
||||||
|
|
||||||
|
.ListenButton,
|
||||||
|
.PauseButton {
|
||||||
|
position: absolute;
|
||||||
|
left: 36%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ListenButton {
|
||||||
|
height: 30%;
|
||||||
|
width: 30%;
|
||||||
|
background-color: dodgerblue;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: aliceblue;
|
||||||
|
font-size: 10px;
|
||||||
|
transition-duration: 300ms;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ListenButton:hover {
|
||||||
|
background-color: dodgerblue;;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ListenButton.Enabled:hover {
|
||||||
|
scale: 1.1;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #0366ee;;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes expandFadeOut {
|
||||||
|
0% {
|
||||||
|
opacity: 0.55;
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,10 @@
|
||||||
/* 2 */
|
/* 2 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.iframe-youtube {
|
||||||
|
width: 460px;height: 284px;
|
||||||
|
}}
|
||||||
/* Sections
|
/* Sections
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
/**
|
/**
|
||||||
|
|
@ -43,6 +47,40 @@ main {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.App {
|
||||||
|
width: 65%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.songs {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-input {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-input-device {
|
||||||
|
padding: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-input-mic {
|
||||||
|
padding: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-audio-input {
|
||||||
|
padding: 5px;
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #c1c1c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Correct the font size and margin on `h1` elements within `section` and
|
* Correct the font size and margin on `h1` elements within `section` and
|
||||||
* `article` contexts in Chrome, Firefox, and Safari.
|
* `article` contexts in Chrome, Firefox, and Safari.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from "react-dom/client";
|
||||||
import './index.css';
|
import "./index.css";
|
||||||
import App from './App';
|
import App from "./App";
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from "./reportWebVitals";
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|
|
||||||
2
main.go
2
main.go
|
|
@ -93,7 +93,7 @@ func MatchSampleAudio(track *webrtc.TrackRemote) (string, error) {
|
||||||
// sampleAudio = nil
|
// sampleAudio = nil
|
||||||
if len(match) > 0 {
|
if len(match) > 0 {
|
||||||
fmt.Println("FOUND A MATCH! - ", match)
|
fmt.Println("FOUND A MATCH! - ", match)
|
||||||
return match, nil
|
// return match, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case <-stop:
|
case <-stop:
|
||||||
|
|
|
||||||
56
server.go
56
server.go
|
|
@ -1,10 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"song-recognition/shazam"
|
||||||
"song-recognition/signal"
|
"song-recognition/signal"
|
||||||
"song-recognition/spotify"
|
"song-recognition/spotify"
|
||||||
"song-recognition/utils"
|
"song-recognition/utils"
|
||||||
|
|
@ -154,13 +157,17 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = spotify.DlSingleTrack(spotifyURL, tmpSongDir)
|
totalDownloads, err := spotify.DlSingleTrack(spotifyURL, tmpSongDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
socket.Emit("downloadStatus", fmt.Sprintf("Failed to download '%s' by '%s'", trackInfo.Title, trackInfo.Artist))
|
socket.Emit("downloadStatus", fmt.Sprintf("Failed to download '%s' by '%s'", trackInfo.Title, trackInfo.Artist))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.Emit("downloadStatus", fmt.Sprintf("'%s' by '%s' was downloaded", trackInfo.Title, trackInfo.Artist))
|
if totalDownloads != 1 {
|
||||||
|
socket.Emit("downloadStatus", fmt.Sprintf("'%s' by '%s' failed to download", trackInfo.Title, trackInfo.Artist))
|
||||||
|
} else {
|
||||||
|
socket.Emit("downloadStatus", fmt.Sprintf("'%s' by '%s' was downloaded", trackInfo.Title, trackInfo.Artist))
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("=> Only Spotify Album/Playlist/Track URL's are supported.")
|
fmt.Println("=> Only Spotify Album/Playlist/Track URL's are supported.")
|
||||||
|
|
@ -168,6 +175,45 @@ func main() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.OnEvent("/", "blob", func(socket socketio.Conn, base64data string) {
|
||||||
|
// Decode base64 data
|
||||||
|
decodedData, err := base64.StdEncoding.DecodeString(base64data)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error: Failed to decode base64 data:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the decoded data to a file
|
||||||
|
err = ioutil.WriteFile("recorded_audio.ogg", decodedData, 0644)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error: Failed to write file to disk:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Audio saved successfully.")
|
||||||
|
|
||||||
|
matches, err := shazam.Match(decodedData)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error: Failed to match:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(matches)
|
||||||
|
|
||||||
|
if len(matches) > 5 {
|
||||||
|
jsonData, err = json.Marshal(matches[:5])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Log error: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.Emit("matches", string(jsonData))
|
||||||
|
|
||||||
|
fmt.Println("BLOB: ", matches)
|
||||||
|
})
|
||||||
|
|
||||||
server.OnEvent("/", "engage", func(s socketio.Conn, encodedOffer string) {
|
server.OnEvent("/", "engage", func(s socketio.Conn, encodedOffer string) {
|
||||||
log.Println("engage: ", encodedOffer)
|
log.Println("engage: ", encodedOffer)
|
||||||
|
|
||||||
|
|
@ -180,7 +226,7 @@ func main() {
|
||||||
|
|
||||||
// Set a handler for when a new remote track starts, this handler saves buffers to disk as
|
// Set a handler for when a new remote track starts, this handler saves buffers to disk as
|
||||||
// an Ogg file.
|
// an Ogg file.
|
||||||
oggFile, err := oggwriter.New("output.ogg", 44100, 1)
|
oggFile, err := oggwriter.New("output.ogg", 48000, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
@ -188,9 +234,9 @@ func main() {
|
||||||
peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
|
||||||
codec := track.Codec()
|
codec := track.Codec()
|
||||||
if strings.EqualFold(codec.MimeType, webrtc.MimeTypeOpus) {
|
if strings.EqualFold(codec.MimeType, webrtc.MimeTypeOpus) {
|
||||||
fmt.Println("Got Opus track, saving to disk as output.opus (44.1 kHz, 1 channel)")
|
// fmt.Println("Got Opus track, saving to disk as output.opus (44.1 kHz, 1 channel)")
|
||||||
// signal.SaveToDisk(oggFile, track)
|
// signal.SaveToDisk(oggFile, track)
|
||||||
// TODO turn match to json here
|
|
||||||
matches, err := signal.MatchSampleAudio(track)
|
matches, err := signal.MatchSampleAudio(track)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
|
||||||
161
shazam/shazam.go
161
shazam/shazam.go
|
|
@ -38,7 +38,7 @@ func Match(sampleAudio []byte) ([]primitive.M, error) {
|
||||||
|
|
||||||
db, err := utils.NewDbClient()
|
db, err := utils.NewDbClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error connecting to DB: %d", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ func Match(sampleAudio []byte) ([]primitive.M, error) {
|
||||||
for _, chunkfgp := range chunkFingerprints {
|
for _, chunkfgp := range chunkFingerprints {
|
||||||
listOfChunkTags, err := db.GetChunkTags(chunkfgp)
|
listOfChunkTags, err := db.GetChunkTags(chunkfgp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error getting chunk data with fingerprint %d: %v", chunkfgp, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, chunkTag := range listOfChunkTags {
|
for _, chunkTag := range listOfChunkTags {
|
||||||
|
|
@ -69,6 +69,21 @@ func Match(sampleAudio []byte) ([]primitive.M, error) {
|
||||||
matches := make(map[string][]int)
|
matches := make(map[string][]int)
|
||||||
|
|
||||||
for songKey, timestamps := range songsTimestamps {
|
for songKey, timestamps := range songsTimestamps {
|
||||||
|
|
||||||
|
timestampsInSeconds, err := timestampsInSeconds(timestamps)
|
||||||
|
if err != nil && err.Error() == "insufficient timestamps" {
|
||||||
|
continue
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
maxPeak, differenceSum, err := getMaxPeak(timestampsInSeconds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Printf("%s MaxPeak: %v, DifferenceSum: %d\n", songKey, maxPeak, differenceSum)
|
||||||
|
fmt.Println("=====================================================\n")
|
||||||
|
|
||||||
differences, err := timeDifference(timestamps)
|
differences, err := timeDifference(timestamps)
|
||||||
if err != nil && err.Error() == "insufficient timestamps" {
|
if err != nil && err.Error() == "insufficient timestamps" {
|
||||||
continue
|
continue
|
||||||
|
|
@ -76,7 +91,7 @@ func Match(sampleAudio []byte) ([]primitive.M, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s DIFFERENCES: %d\n", songKey, differences)
|
// fmt.Printf("%s DIFFERENCES: %d\n", songKey, differences)
|
||||||
if len(differences) >= 2 {
|
if len(differences) >= 2 {
|
||||||
matches[songKey] = differences
|
matches[songKey] = differences
|
||||||
if len(differences) > maxMatchCount {
|
if len(differences) > maxMatchCount {
|
||||||
|
|
@ -88,10 +103,10 @@ func Match(sampleAudio []byte) ([]primitive.M, error) {
|
||||||
|
|
||||||
sortedChunkTags := sortMatchesByTimeDifference(matches, chunkTags)
|
sortedChunkTags := sortMatchesByTimeDifference(matches, chunkTags)
|
||||||
|
|
||||||
fmt.Println("SORTED CHUNK TAGS: ", sortedChunkTags)
|
// fmt.Println("SORTED CHUNK TAGS: ", sortedChunkTags)
|
||||||
fmt.Println("MATCHES: ", matches)
|
// fmt.Println("MATCHES: ", matches)
|
||||||
fmt.Println("MATCH: ", maxMatch)
|
fmt.Println("MATCH: ", maxMatch)
|
||||||
fmt.Println()
|
// fmt.Println()
|
||||||
return sortedChunkTags, nil
|
return sortedChunkTags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,6 +138,120 @@ func sortMatchesByTimeDifference(matches map[string][]int, chunkTags map[string]
|
||||||
return sortedChunkTags
|
return sortedChunkTags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func timestampsInSeconds(timestamps []string) ([]int, error) {
|
||||||
|
layout := "15:04:05"
|
||||||
|
|
||||||
|
timestampsInSeconds := make([]int, len(timestamps))
|
||||||
|
for i, ts := range timestamps {
|
||||||
|
parsedTime, err := time.Parse(layout, ts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing timestamp %q: %w", ts, err)
|
||||||
|
}
|
||||||
|
hours := parsedTime.Hour()
|
||||||
|
minutes := parsedTime.Minute()
|
||||||
|
seconds := parsedTime.Second()
|
||||||
|
timestampsInSeconds[i] = (hours * 3600) + (minutes * 60) + seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
return timestampsInSeconds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getMaxPeak identifies clusters of timestamps (peaks) within a sequence where the differences between adjacent timestamps
|
||||||
|
// are below a certain threshold. It returns the largest peak, the sum of differences within that peak, and an error if any.
|
||||||
|
func getMaxPeak(timestamps []int) ([]int, int, error) {
|
||||||
|
if len(timestamps) < 2 {
|
||||||
|
return nil, 0, fmt.Errorf("insufficient timestamps")
|
||||||
|
}
|
||||||
|
|
||||||
|
var peaks [][]int
|
||||||
|
maxDifference := 15
|
||||||
|
|
||||||
|
var cluster []int
|
||||||
|
|
||||||
|
// Iterate over timestamps to identify peaks
|
||||||
|
for i := 0; i < len(timestamps)-1; i++ {
|
||||||
|
minuend, subtrahend := timestamps[i], timestamps[i+1]
|
||||||
|
|
||||||
|
// Ensure timestamps are in ascending order
|
||||||
|
if minuend > subtrahend {
|
||||||
|
peaks = append(peaks, cluster)
|
||||||
|
cluster = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
difference := int(math.Abs(float64(minuend - subtrahend)))
|
||||||
|
|
||||||
|
// Check if the difference is within the maximum allowed difference
|
||||||
|
if len(cluster) == 0 && difference <= maxDifference {
|
||||||
|
cluster = append(cluster, minuend, subtrahend)
|
||||||
|
} else if difference <= maxDifference {
|
||||||
|
cluster = append(cluster, subtrahend)
|
||||||
|
} else if difference > maxDifference {
|
||||||
|
peaks = append(peaks, cluster)
|
||||||
|
cluster = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify the largest peak(s)
|
||||||
|
largestPeak := [][]int{peaks[0]}
|
||||||
|
for _, peak := range peaks[1:] {
|
||||||
|
if len(peak) == len(largestPeak[0]) {
|
||||||
|
largestPeak = append(largestPeak, peak)
|
||||||
|
} else if len(peak) > len(largestPeak[0]) {
|
||||||
|
largestPeak = nil
|
||||||
|
largestPeak = append(largestPeak, peak)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the case where there are multiple largest peaks,
|
||||||
|
// identify and return the largest peak with the smallest sum of differences
|
||||||
|
if len(largestPeak) > 1 {
|
||||||
|
fmt.Println("Largest Peak > 1: ", largestPeak)
|
||||||
|
|
||||||
|
// Deduplicate largest peaks in order to get accurate sum of difference
|
||||||
|
var largestPeakDeDuplicated [][]int
|
||||||
|
for _, peak := range largestPeak {
|
||||||
|
largestPeakDeDuplicated = append(largestPeakDeDuplicated, deduplicate(peak))
|
||||||
|
}
|
||||||
|
fmt.Println("Largest Peak deduplicated: ", largestPeakDeDuplicated)
|
||||||
|
|
||||||
|
minDifferenceSum := math.Inf(1)
|
||||||
|
var peakWithMinDifferenceSum []int
|
||||||
|
for idx, peak := range largestPeakDeDuplicated {
|
||||||
|
if len(peak) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
differenceSum := 0.0
|
||||||
|
for i := len(peak) - 1; i >= 1; i-- {
|
||||||
|
differenceSum += math.Abs(float64(peak[i] - peak[i-1]))
|
||||||
|
}
|
||||||
|
if differenceSum < minDifferenceSum {
|
||||||
|
minDifferenceSum = differenceSum
|
||||||
|
fmt.Printf("%v vs %v\n", largestPeak[idx], peak)
|
||||||
|
peakWithMinDifferenceSum = largestPeak[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the case where no peak with the min difference sum was identified,
|
||||||
|
// probably because they were all duplicates, return the first from the largestspeaks
|
||||||
|
if len(peakWithMinDifferenceSum) == 0 {
|
||||||
|
peakWithMinDifferenceSum = largestPeak[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return peakWithMinDifferenceSum, int(minDifferenceSum), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, return the largest peak
|
||||||
|
maxPeak := largestPeak[0]
|
||||||
|
differenceSum := 0
|
||||||
|
for i := len(maxPeak) - 1; i >= 1; i-- {
|
||||||
|
differenceSum += maxPeak[i] - maxPeak[i-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxPeak, differenceSum, nil
|
||||||
|
}
|
||||||
|
|
||||||
func timeDifference(timestamps []string) ([]int, error) {
|
func timeDifference(timestamps []string) ([]int, error) {
|
||||||
if len(timestamps) < 2 {
|
if len(timestamps) < 2 {
|
||||||
return nil, fmt.Errorf("insufficient timestamps")
|
return nil, fmt.Errorf("insufficient timestamps")
|
||||||
|
|
@ -144,17 +273,26 @@ func timeDifference(timestamps []string) ([]int, error) {
|
||||||
|
|
||||||
// sort.Ints(timestampsInSeconds)
|
// sort.Ints(timestampsInSeconds)
|
||||||
|
|
||||||
differences := []int{}
|
differencesSet := map[int]struct{}{}
|
||||||
|
var differences []int
|
||||||
|
|
||||||
for i := len(timestampsInSeconds) - 1; i >= 1; i-- {
|
for i := len(timestampsInSeconds) - 1; i >= 1; i-- {
|
||||||
difference := timestampsInSeconds[i] - timestampsInSeconds[i-1]
|
difference := timestampsInSeconds[i] - timestampsInSeconds[i-1]
|
||||||
// maxSeconds = 15
|
// maxSeconds = 15
|
||||||
if difference > 0 && difference <= 15 {
|
if difference > 0 && difference <= 15 {
|
||||||
|
differencesSet[difference] = struct{}{}
|
||||||
differences = append(differences, difference)
|
differences = append(differences, difference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return differences, nil
|
differencesList := []int{}
|
||||||
|
if len(differencesSet) > 0 {
|
||||||
|
for k := range differencesSet {
|
||||||
|
differencesList = append(differencesList, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return timestampsInSeconds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunkify divides the input audio signal into chunks and calculates the Short-Time Fourier Transform (STFT) for each chunk.
|
// Chunkify divides the input audio signal into chunks and calculates the Short-Time Fourier Transform (STFT) for each chunk.
|
||||||
|
|
@ -204,7 +342,10 @@ func FingerprintChunks(chunks [][]complex128, chunkTag *ChunkTag) ([]int64, map[
|
||||||
if chunkTag != nil {
|
if chunkTag != nil {
|
||||||
// bytesPerSecond = (samplingRate * bitDepth * channels) / 8
|
// bytesPerSecond = (samplingRate * bitDepth * channels) / 8
|
||||||
chunksPerSecond = (chunkSize - hopSize) / samplingRate
|
chunksPerSecond = (chunkSize - hopSize) / samplingRate
|
||||||
chunksPerSecond = 9
|
chunksPerSecond = len(chunks)
|
||||||
|
|
||||||
|
fmt.Println("CHUNKS PER SECOND: ", chunksPerSecond)
|
||||||
|
chunksPerSecond = 3
|
||||||
fmt.Println("CHUNKS PER SECOND: ", chunksPerSecond)
|
fmt.Println("CHUNKS PER SECOND: ", chunksPerSecond)
|
||||||
// if chunkSize == 4096 {
|
// if chunkSize == 4096 {
|
||||||
// chunksPerSecond = 10
|
// chunksPerSecond = 10
|
||||||
|
|
@ -219,7 +360,7 @@ func FingerprintChunks(chunks [][]complex128, chunkTag *ChunkTag) ([]int64, map[
|
||||||
if chunkCount == chunksPerSecond {
|
if chunkCount == chunksPerSecond {
|
||||||
chunkCount = 0
|
chunkCount = 0
|
||||||
chunkTime = chunkTime.Add(1 * time.Second)
|
chunkTime = chunkTime.Add(1 * time.Second)
|
||||||
// fmt.Println(chunkTime.Format("15:04:05"))
|
fmt.Println(chunkTime.Format("15:04:05"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
18
shazam/utils.go
Normal file
18
shazam/utils.go
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package shazam
|
||||||
|
|
||||||
|
// deduplicate returns a list of unique integers from the given array.
|
||||||
|
// The order of the given array is not preserved in the result.
|
||||||
|
func deduplicate(array []int) []int {
|
||||||
|
uniqueMap := make(map[int]struct{})
|
||||||
|
|
||||||
|
for _, num := range array {
|
||||||
|
uniqueMap[num] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var uniqueList []int
|
||||||
|
for num := range uniqueMap {
|
||||||
|
uniqueList = append(uniqueList, num)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueList
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pion/interceptor"
|
"github.com/pion/interceptor"
|
||||||
"github.com/pion/interceptor/pkg/intervalpli"
|
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
|
||||||
|
|
@ -67,7 +66,7 @@ func SaveToBytes(track *webrtc.TrackRemote) ([]byte, error) {
|
||||||
|
|
||||||
func MatchSampleAudio(track *webrtc.TrackRemote) ([]primitive.M, error) {
|
func MatchSampleAudio(track *webrtc.TrackRemote) ([]primitive.M, error) {
|
||||||
// Use time.After to stop after 15 seconds
|
// Use time.After to stop after 15 seconds
|
||||||
stop := time.After(50 * time.Second)
|
stop := time.After(20 * time.Second)
|
||||||
|
|
||||||
// Use a ticker to process sampleAudio every 2 seconds
|
// Use a ticker to process sampleAudio every 2 seconds
|
||||||
ticker := time.NewTicker(2 * time.Second)
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
|
|
@ -90,6 +89,7 @@ func MatchSampleAudio(track *webrtc.TrackRemote) ([]primitive.M, error) {
|
||||||
|
|
||||||
// Reset sampleAudio for fresh input
|
// Reset sampleAudio for fresh input
|
||||||
// sampleAudio = nil
|
// sampleAudio = nil
|
||||||
|
|
||||||
// if len(matches) > 0 {
|
// if len(matches) > 0 {
|
||||||
// fmt.Println("FOUND A MATCH! - ", matches)
|
// fmt.Println("FOUND A MATCH! - ", matches)
|
||||||
// jsonData, err := json.Marshal(matches)
|
// jsonData, err := json.Marshal(matches)
|
||||||
|
|
@ -132,7 +132,7 @@ func SetupWebRTC(encodedOffer string) *webrtc.PeerConnection {
|
||||||
// Setup the codecs you want to use.
|
// Setup the codecs you want to use.
|
||||||
// We'll use Opus, but you can also define your own
|
// We'll use Opus, but you can also define your own
|
||||||
if err := m.RegisterCodec(webrtc.RTPCodecParameters{
|
if err := m.RegisterCodec(webrtc.RTPCodecParameters{
|
||||||
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 44100, Channels: 1, SDPFmtpLine: "", RTCPFeedback: nil},
|
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 1, SDPFmtpLine: "", RTCPFeedback: nil},
|
||||||
PayloadType: 111,
|
PayloadType: 111,
|
||||||
}, webrtc.RTPCodecTypeAudio); err != nil {
|
}, webrtc.RTPCodecTypeAudio); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -144,16 +144,16 @@ func SetupWebRTC(encodedOffer string) *webrtc.PeerConnection {
|
||||||
|
|
||||||
// Register a intervalpli factory
|
// Register a intervalpli factory
|
||||||
// This interceptor sends a PLI every 3 seconds. A PLI causes a keyframe to be generated by the sender.
|
// This interceptor sends a PLI every 3 seconds. A PLI causes a keyframe to be generated by the sender.
|
||||||
intervalPliFactory, err := intervalpli.NewReceiverInterceptor()
|
// intervalPliFactory, err := intervalpli.NewReceiverInterceptor()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
panic(err)
|
// panic(err)
|
||||||
}
|
// }
|
||||||
i.Add(intervalPliFactory)
|
// i.Add(intervalPliFactory)
|
||||||
|
|
||||||
// Use the default set of Interceptors
|
// // Use the default set of Interceptors
|
||||||
if err = webrtc.RegisterDefaultInterceptors(m, i); err != nil {
|
// if err = webrtc.RegisterDefaultInterceptors(m, i); err != nil {
|
||||||
panic(err)
|
// panic(err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Create the API object with the MediaEngine
|
// Create the API object with the MediaEngine
|
||||||
api := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i))
|
api := webrtc.NewAPI(webrtc.WithMediaEngine(m), webrtc.WithInterceptorRegistry(i))
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,27 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"song-recognition/shazam"
|
"song-recognition/shazam"
|
||||||
"song-recognition/utils"
|
"song-recognition/utils"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
// "song-recognition/youtube"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/kkdai/youtube/v2"
|
"github.com/kkdai/youtube/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var yellow = color.New(color.FgYellow)
|
var yellow = color.New(color.FgYellow)
|
||||||
|
|
||||||
func DlSingleTrack(url, savePath string) error {
|
func DlSingleTrack(url, savePath string) (int, error) {
|
||||||
trackInfo, err := TrackInfo(url)
|
trackInfo, err := TrackInfo(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Getting track info...")
|
fmt.Println("Getting track info...")
|
||||||
|
|
@ -34,12 +32,12 @@ func DlSingleTrack(url, savePath string) error {
|
||||||
track := []Track{*trackInfo}
|
track := []Track{*trackInfo}
|
||||||
|
|
||||||
fmt.Println("Now, downloading track...")
|
fmt.Println("Now, downloading track...")
|
||||||
_, err = dlTrack(track, savePath)
|
totalTracksDownloaded, err := dlTrack(track, savePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return totalTracksDownloaded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DlPlaylist(url, savePath string) (int, error) {
|
func DlPlaylist(url, savePath string) (int, error) {
|
||||||
|
|
@ -52,7 +50,6 @@ func DlPlaylist(url, savePath string) (int, error) {
|
||||||
fmt.Println("Now, downloading playlist...")
|
fmt.Println("Now, downloading playlist...")
|
||||||
totalTracksDownloaded, err := dlTrack(tracks, savePath)
|
totalTracksDownloaded, err := dlTrack(tracks, savePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +80,12 @@ func dlTrack(tracks []Track, path string) (int, error) {
|
||||||
numCPUs := runtime.NumCPU()
|
numCPUs := runtime.NumCPU()
|
||||||
semaphore := make(chan struct{}, numCPUs)
|
semaphore := make(chan struct{}, numCPUs)
|
||||||
|
|
||||||
|
db, err := utils.NewDbClient()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
for _, t := range tracks {
|
for _, t := range tracks {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(track Track) {
|
go func(track Track) {
|
||||||
|
|
@ -100,30 +103,65 @@ func dlTrack(tracks []Track, path string) (int, error) {
|
||||||
Title: track.Title,
|
Title: track.Title,
|
||||||
}
|
}
|
||||||
|
|
||||||
// id1, err := VideoID(*trackCopy)
|
// check if song exists
|
||||||
|
songExists, _ := db.SongExists(trackCopy.Title, trackCopy.Artist, "")
|
||||||
|
if songExists {
|
||||||
|
logMessage := fmt.Sprintf("'%s' by '%s' already downloaded\n", trackCopy.Title, trackCopy.Artist)
|
||||||
|
slog.Info(logMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ytID, err := GetYoutubeId(*trackCopy)
|
ytID, err := GetYoutubeId(*trackCopy)
|
||||||
if ytID == "" || err != nil {
|
if ytID == "" || err != nil {
|
||||||
yellow.Printf("Error (1): '%s' by '%s' could not be downloaded\n", trackCopy.Title, trackCopy.Artist)
|
logMessage := fmt.Sprintf("Error (0): '%s' by '%s' could not be downloaded: %s\n", trackCopy.Title, trackCopy.Artist, err)
|
||||||
|
slog.Error(logMessage)
|
||||||
|
yellow.Printf(logMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if YouTube ID exists
|
||||||
|
ytIdExists, _ := db.SongExists("", "", ytID)
|
||||||
|
if ytIdExists { // try to get the YouTube ID again
|
||||||
|
logMessage := fmt.Sprintf("YouTube ID exists. Trying again: %s\n", ytID)
|
||||||
|
slog.Warn(logMessage)
|
||||||
|
|
||||||
|
ytID, err := GetYoutubeId(*trackCopy)
|
||||||
|
if ytID == "" || err != nil {
|
||||||
|
logMessage := fmt.Sprintf("Error (1): '%s' by '%s' could not be downloaded: %s\n", trackCopy.Title, trackCopy.Artist, err)
|
||||||
|
slog.Info(logMessage)
|
||||||
|
yellow.Printf(logMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ytIdExists, _ := db.SongExists("", "", ytID)
|
||||||
|
if ytIdExists {
|
||||||
|
logMessage := fmt.Sprintf("'%s' by '%s' could not be downloaded: YouTube ID (%s) exists\n", trackCopy.Title, trackCopy.Artist, ytID)
|
||||||
|
slog.Error(logMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trackCopy.Title, trackCopy.Artist = correctFilename(trackCopy.Title, trackCopy.Artist)
|
trackCopy.Title, trackCopy.Artist = correctFilename(trackCopy.Title, trackCopy.Artist)
|
||||||
err = getAudio(ytID, path, trackCopy.Title, trackCopy.Artist)
|
fileName := fmt.Sprintf("%s - %s.m4a", trackCopy.Title, trackCopy.Artist)
|
||||||
|
filePath := filepath.Join(path, fileName)
|
||||||
|
|
||||||
|
err = getAudio(ytID, path, filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
yellow.Printf("Error (2): '%s' by '%s' could not be downloaded: %s\n", trackCopy.Title, trackCopy.Artist, err)
|
logMessage := fmt.Sprintf("Error (2): '%s' by '%s' could not be downloaded: %s\n", trackCopy.Title, trackCopy.Artist, err)
|
||||||
|
yellow.Printf(logMessage)
|
||||||
|
slog.Error(logMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Process and save audio
|
|
||||||
filename := fmt.Sprintf("%s - %s.m4a", trackCopy.Title, trackCopy.Artist)
|
err = processAndSaveSong(filePath, trackCopy.Title, trackCopy.Artist, ytID)
|
||||||
route := filepath.Join(path, filename)
|
|
||||||
err = processAndSaveSong(route, trackCopy.Title, trackCopy.Artist, ytID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
yellow.Println("Error processing audio: ", err)
|
yellow.Println("Error processing audio: ", err)
|
||||||
|
logMessage := fmt.Sprintf("Failed to process song ('%s' by '%s'): %s\n", trackCopy.Title, trackCopy.Artist, err)
|
||||||
|
slog.Error(logMessage)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
trackCopy.Title, trackCopy.Artist = correctFilename(trackCopy.Title, trackCopy.Artist)
|
// Consider removing this and deleting the song file after processing
|
||||||
filePath := fmt.Sprintf("%s%s - %s.m4a", path, trackCopy.Title, trackCopy.Artist)
|
|
||||||
|
|
||||||
if err := addTags(filePath, *trackCopy); err != nil {
|
if err := addTags(filePath, *trackCopy); err != nil {
|
||||||
yellow.Println("Error adding tags: ", filePath)
|
yellow.Println("Error adding tags: ", filePath)
|
||||||
return
|
return
|
||||||
|
|
@ -155,7 +193,7 @@ func dlTrack(tracks []Track, path string) (int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* github.com/kkdai/youtube */
|
/* github.com/kkdai/youtube */
|
||||||
func getAudio(id, path, title, artist string) error {
|
func getAudio(id, path, filePath string) error {
|
||||||
dir, err := os.Stat(path)
|
dir, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -165,22 +203,6 @@ func getAudio(id, path, title, artist string) error {
|
||||||
return errors.New("the path is not valid (not a dir)")
|
return errors.New("the path is not valid (not a dir)")
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := utils.NewDbClient()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error connecting to DB: %d", err)
|
|
||||||
}
|
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
// Check if the song has been processed and saved before
|
|
||||||
songKey := fmt.Sprintf("%s - %s", title, artist)
|
|
||||||
songExists, err := db.SongExists(songKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if songExists {
|
|
||||||
return fmt.Errorf("song exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
client := youtube.Client{}
|
client := youtube.Client{}
|
||||||
video, err := client.GetVideo(id)
|
video, err := client.GetVideo(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -191,15 +213,12 @@ func getAudio(id, path, title, artist string) error {
|
||||||
/* change the FindByItag parameter to 139 if you want smaller files (but with a bitrate of 48k) */
|
/* change the FindByItag parameter to 139 if you want smaller files (but with a bitrate of 48k) */
|
||||||
formats := video.Formats.Itag(140)
|
formats := video.Formats.Itag(140)
|
||||||
|
|
||||||
filename := fmt.Sprintf("%s - %s.m4a", title, artist)
|
|
||||||
route := filepath.Join(path, filename)
|
|
||||||
|
|
||||||
/* in some cases, when attempting to download the audio
|
/* in some cases, when attempting to download the audio
|
||||||
using the library github.com/kkdai/youtube,
|
using the library github.com/kkdai/youtube,
|
||||||
the download fails (and shows the file size as 0 bytes)
|
the download fails (and shows the file size as 0 bytes)
|
||||||
until the second or third attempt. */
|
until the second or third attempt. */
|
||||||
var fileSize int64
|
var fileSize int64
|
||||||
file, err := os.Create(route)
|
file, err := os.Create(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -214,46 +233,13 @@ func getAudio(id, path, title, artist string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fileSize, _ = GetFileSize(route)
|
fileSize, _ = GetFileSize(filePath)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveAudioToFile(audioReader io.Reader, path, title, artist string) error {
|
|
||||||
dir, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !dir.IsDir() {
|
|
||||||
return errors.New("the path is not valid (not a dir)")
|
|
||||||
}
|
|
||||||
|
|
||||||
filename := fmt.Sprintf("%s - %s.m4a", title, artist)
|
|
||||||
route := filepath.Join(path, filename)
|
|
||||||
|
|
||||||
/* in some cases, when attempting to download the audio
|
|
||||||
using the library github.com/kkdai/youtube,
|
|
||||||
the download fails (and shows the file size as 0 bytes)
|
|
||||||
until the second or third attempt. */
|
|
||||||
file, err := os.Create(route)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
// Copy the audio stream to the file
|
|
||||||
_, err = io.Copy(file, audioReader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addTags(file string, track Track) error {
|
func addTags(file string, track Track) error {
|
||||||
tempFile := file
|
tempFile := file
|
||||||
index := strings.Index(file, ".m4a")
|
index := strings.Index(file, ".m4a")
|
||||||
|
|
@ -292,60 +278,17 @@ func addTags(file string, track Track) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fixes some invalid file names (windows is the capricious one) */
|
func processAndSaveSong(songFilePath, songTitle, songArtist, ytID string) error {
|
||||||
func correctFilename(title, artist string) (string, string) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
invalidChars := []byte{'<', '>', '<', ':', '"', '\\', '/', '|', '?', '*'}
|
|
||||||
for _, invalidChar := range invalidChars {
|
|
||||||
title = strings.ReplaceAll(title, string(invalidChar), "")
|
|
||||||
artist = strings.ReplaceAll(artist, string(invalidChar), "")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
title = strings.ReplaceAll(title, "/", "\\")
|
|
||||||
artist = strings.ReplaceAll(artist, "/", "\\")
|
|
||||||
}
|
|
||||||
|
|
||||||
return title, artist
|
|
||||||
}
|
|
||||||
|
|
||||||
func processAndSaveSong(m4aFile, songTitle, songArtist, ytID string) error {
|
|
||||||
db, err := utils.NewDbClient()
|
db, err := utils.NewDbClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error connecting to DB: %d", err)
|
return err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// Check if the song has been processed and saved before
|
audioBytes, err := convertStereoToMono(songFilePath)
|
||||||
songKey := fmt.Sprintf("%s - %s", songTitle, songArtist)
|
|
||||||
songExists, err := db.SongExists(songKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error checking if song exists: %v", err)
|
return fmt.Errorf("error converting song to mono: %v", err)
|
||||||
}
|
}
|
||||||
if songExists {
|
|
||||||
fmt.Println("Song exists: ", songKey)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert M4A file to mono
|
|
||||||
m4aFileMono := strings.TrimSuffix(m4aFile, filepath.Ext(m4aFile)) + "_mono.m4a"
|
|
||||||
// defer os.Remove(m4aFileMono)
|
|
||||||
audioBytes, err := ConvertM4aToMono(m4aFile, m4aFileMono)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error converting M4A file to mono: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run ffprobe to get metadata of the input file
|
|
||||||
cmd := exec.Command("ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries", "stream=bit_depth,sample_rate", "-of", "default=noprint_wrappers=1:nokey=1", m4aFileMono)
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error running ffprobe: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the output to extract bit depth and sampling rate
|
|
||||||
lines := strings.Split(string(output), "\n")
|
|
||||||
// bitDepth, _ := strconv.Atoi(strings.TrimSpace(lines[1]))
|
|
||||||
sampleRate, _ := strconv.Atoi(strings.TrimSpace(lines[0]))
|
|
||||||
fmt.Printf("SAMPLE RATE for %s: %v", songTitle, sampleRate)
|
|
||||||
|
|
||||||
chunkTag := shazam.ChunkTag{
|
chunkTag := shazam.ChunkTag{
|
||||||
SongTitle: songTitle,
|
SongTitle: songTitle,
|
||||||
|
|
@ -353,24 +296,23 @@ func processAndSaveSong(m4aFile, songTitle, songArtist, ytID string) error {
|
||||||
YouTubeID: ytID,
|
YouTubeID: ytID,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate fingerprints
|
// Fingerprint song
|
||||||
chunks := shazam.Chunkify(audioBytes)
|
chunks := shazam.Chunkify(audioBytes)
|
||||||
_, fingerprints := shazam.FingerprintChunks(chunks, &chunkTag)
|
_, fingerprints := shazam.FingerprintChunks(chunks, &chunkTag)
|
||||||
|
|
||||||
// Save fingerprints to MongoDB
|
// Save fingerprints in DB
|
||||||
for fgp, ctag := range fingerprints {
|
for fgp, ctag := range fingerprints {
|
||||||
err := db.InsertChunkTag(fgp, ctag)
|
err := db.InsertChunkTag(fgp, ctag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error inserting document: %v", err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the song as processed
|
err = db.RegisterSong(songTitle, songArtist, ytID)
|
||||||
err = db.RegisterSong(songKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Fingerprints saved to MongoDB successfully")
|
fmt.Println("Fingerprints saved in MongoDB successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,6 @@ func TrackInfo(url string) (*Track, error) {
|
||||||
endpoint := trackInitialPath + endpointQuery + "&extensions=" + EncodeParam(trackEndPath)
|
endpoint := trackInitialPath + endpointQuery + "&extensions=" + EncodeParam(trackEndPath)
|
||||||
|
|
||||||
statusCode, jsonResponse, err := request(endpoint)
|
statusCode, jsonResponse, err := request(endpoint)
|
||||||
// fmt.Print("TRACK INFO: ", jsonResponse)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error on getting track info: %w", err)
|
return nil, fmt.Errorf("error on getting track info: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -108,12 +107,6 @@ func TrackInfo(url string) (*Track, error) {
|
||||||
return nil, fmt.Errorf("received non-200 status code: %d", statusCode)
|
return nil, fmt.Errorf("received non-200 status code: %d", statusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
// track := &Track{
|
|
||||||
// Title: gjson.Get(jsonResponse, "data.trackUnion.name").String(),
|
|
||||||
// Artist: gjson.Get(jsonResponse, "data.trackUnion.firstArtist.items.0.profile.name").String(),
|
|
||||||
// Album: gjson.Get(jsonResponse, "data.trackUnion.albumOfTrack.name").String(),
|
|
||||||
// }
|
|
||||||
|
|
||||||
var allArtists []string
|
var allArtists []string
|
||||||
|
|
||||||
if firstArtist := gjson.Get(jsonResponse, "data.trackUnion.firstArtist.items.0.profile.name").String(); firstArtist != "" {
|
if firstArtist := gjson.Get(jsonResponse, "data.trackUnion.firstArtist.items.0.profile.name").String(); firstArtist != "" {
|
||||||
|
|
@ -130,11 +123,14 @@ func TrackInfo(url string) (*Track, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
durationInSeconds := int(gjson.Get(jsonResponse, "data.trackUnion.duration.totalMilliseconds").Int())
|
||||||
|
durationInSeconds = durationInSeconds / 1000
|
||||||
|
|
||||||
track := &Track{
|
track := &Track{
|
||||||
Title: gjson.Get(jsonResponse, "data.trackUnion.name").String(),
|
Title: gjson.Get(jsonResponse, "data.trackUnion.name").String(),
|
||||||
Artist: gjson.Get(jsonResponse, "data.trackUnion.firstArtist.items.0.profile.name").String(),
|
Artist: gjson.Get(jsonResponse, "data.trackUnion.firstArtist.items.0.profile.name").String(),
|
||||||
Artists: allArtists,
|
Artists: allArtists,
|
||||||
Duration: int(gjson.Get(jsonResponse, "data.trackUnion.duration.totalMilliseconds").Int()),
|
Duration: durationInSeconds,
|
||||||
Album: gjson.Get(jsonResponse, "data.trackUnion.albumOfTrack.name").String(),
|
Album: gjson.Get(jsonResponse, "data.trackUnion.albumOfTrack.name").String(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,17 +252,20 @@ func proccessItems(jsonResponse, resourceType string) []Track {
|
||||||
songTitle := map[bool]string{true: "itemV2.data.name", false: "track.name"}[resourceType == "playlist"]
|
songTitle := map[bool]string{true: "itemV2.data.name", false: "track.name"}[resourceType == "playlist"]
|
||||||
artistName := map[bool]string{true: "itemV2.data.artists.items.0.profile.name", false: "track.artists.items.0.profile.name"}[resourceType == "playlist"]
|
artistName := map[bool]string{true: "itemV2.data.artists.items.0.profile.name", false: "track.artists.items.0.profile.name"}[resourceType == "playlist"]
|
||||||
albumName := map[bool]string{true: "itemV2.data.albumOfTrack.name", false: "data.albumUnion.name"}[resourceType == "playlist"]
|
albumName := map[bool]string{true: "itemV2.data.albumOfTrack.name", false: "data.albumUnion.name"}[resourceType == "playlist"]
|
||||||
|
duration := map[bool]string{true: "itemV2.data.trackDuration.totalMilliseconds", false: "track.duration.totalMilliseconds"}[resourceType == "playlist"]
|
||||||
|
|
||||||
var tracks []Track
|
var tracks []Track
|
||||||
items := gjson.Get(jsonResponse, itemList).Array()
|
items := gjson.Get(jsonResponse, itemList).Array()
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
track := &Track{
|
durationInSeconds := int(item.Get(duration).Int()) / 1000
|
||||||
Title: item.Get(songTitle).String(),
|
|
||||||
Artist: item.Get(artistName).String(),
|
|
||||||
Album: map[bool]string{true: item.Get(albumName).String(), false: gjson.Get(jsonResponse, albumName).String()}[resourceType == "playlist"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
track := &Track{
|
||||||
|
Title: item.Get(songTitle).String(),
|
||||||
|
Artist: item.Get(artistName).String(),
|
||||||
|
Duration: durationInSeconds,
|
||||||
|
Album: map[bool]string{true: item.Get(albumName).String(), false: gjson.Get(jsonResponse, albumName).String()}[resourceType == "playlist"],
|
||||||
|
}
|
||||||
tracks = append(tracks, *track.buildTrack())
|
tracks = append(tracks, *track.buildTrack())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -44,38 +46,51 @@ func DeleteFile(filePath string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert M4A file from stereo to mono
|
/* fixes some invalid file names (windows is the capricious one) */
|
||||||
func ConvertM4aToMono(inputFile, outputFile string) ([]byte, error) {
|
func correctFilename(title, artist string) (string, string) {
|
||||||
cmd := exec.Command("ffprobe", "-v", "error", "-show_entries", "stream=channels", "-of", "default=noprint_wrappers=1:nokey=1", inputFile)
|
if runtime.GOOS == "windows" {
|
||||||
|
invalidChars := []byte{'<', '>', '<', ':', '"', '\\', '/', '|', '?', '*'}
|
||||||
|
for _, invalidChar := range invalidChars {
|
||||||
|
title = strings.ReplaceAll(title, string(invalidChar), "")
|
||||||
|
artist = strings.ReplaceAll(artist, string(invalidChar), "")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
title = strings.ReplaceAll(title, "/", "\\")
|
||||||
|
artist = strings.ReplaceAll(artist, "/", "\\")
|
||||||
|
}
|
||||||
|
|
||||||
|
return title, artist
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertStereoToMono(stereoFilePath string) ([]byte, error) {
|
||||||
|
fileExt := filepath.Ext(stereoFilePath)
|
||||||
|
monoFilePath := strings.TrimSuffix(stereoFilePath, fileExt) + "_mono" + fileExt
|
||||||
|
defer os.Remove(monoFilePath)
|
||||||
|
|
||||||
|
// Check the number of channels in the stereo audio
|
||||||
|
cmd := exec.Command("ffprobe", "-v", "error", "-show_entries", "stream=channels", "-of", "default=noprint_wrappers=1:nokey=1", stereoFilePath)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error running ffprobe: %v, %v", err, string(output))
|
return nil, fmt.Errorf("error getting number of channels: %v, %v", err, string(output))
|
||||||
}
|
}
|
||||||
|
|
||||||
audioBytes, err := ioutil.ReadFile(inputFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error reading input file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
channels := strings.TrimSpace(string(output))
|
channels := strings.TrimSpace(string(output))
|
||||||
|
|
||||||
|
audioBytes, err := ioutil.ReadFile(stereoFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading stereo file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if channels != "1" {
|
if channels != "1" {
|
||||||
// Convert to mono
|
// Convert stereo to mono and downsample by 44100/2
|
||||||
cmd = exec.Command("ffmpeg", "-i", inputFile, "-af", "pan=mono|c0=c0", outputFile)
|
cmd = exec.Command("ffmpeg", "-i", stereoFilePath, "-af", "pan=mono|c0=c0", monoFilePath)
|
||||||
|
// cmd = exec.Command("ffmpeg", "-i", stereoFilePath, "-af", "pan=mono|c0=c0", "-ar", "22050", monoFilePath)
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return nil, fmt.Errorf("error running ffmpeg: %v", err)
|
return nil, fmt.Errorf("error converting stereo to mono: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resample to 8192 Hz
|
audioBytes, err = ioutil.ReadFile(monoFilePath)
|
||||||
// resampledFile := strings.TrimSuffix(outputFile, filepath.Ext(outputFile)) + "_resampled.m4a"
|
|
||||||
// cmd = exec.Command("ffmpeg", "-i", outputFile, "-ar", "8192", resampledFile)
|
|
||||||
// output, err = cmd.CombinedOutput()
|
|
||||||
// if err := cmd.Run(); err != nil {
|
|
||||||
// return nil, fmt.Errorf("error resampling: %v, %v", err, string(output))
|
|
||||||
// }
|
|
||||||
|
|
||||||
audioBytes, err = ioutil.ReadFile(outputFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error reading input file: %v", err)
|
return nil, fmt.Errorf("error reading mono file: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ func convertStringDurationToSeconds(durationStr string) int {
|
||||||
|
|
||||||
// GetYoutubeId takes the query as string and returns the search results video ID's
|
// GetYoutubeId takes the query as string and returns the search results video ID's
|
||||||
func GetYoutubeId(track Track) (string, error) {
|
func GetYoutubeId(track Track) (string, error) {
|
||||||
songDurationInSeconds := track.Duration * 60
|
songDurationInSeconds := track.Duration
|
||||||
searchQuery := fmt.Sprintf("'%s' %s %s", track.Title, track.Artist, track.Album)
|
searchQuery := fmt.Sprintf("'%s' %s %s", track.Title, track.Artist, track.Album)
|
||||||
|
|
||||||
searchResults, err := ytSearch(searchQuery, 10)
|
searchResults, err := ytSearch(searchQuery, 10)
|
||||||
|
|
@ -99,8 +99,7 @@ func GetYoutubeId(track Track) (string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Else return the first result if nothing is found
|
return "", fmt.Errorf("could not settle on a song from search result for: %s", searchQuery)
|
||||||
return searchResults[0].ID, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getContent(data []byte, index int) []byte {
|
func getContent(data []byte, index int) []byte {
|
||||||
|
|
@ -114,6 +113,8 @@ func ytSearch(searchTerm string, limit int) (results []*SearchResult, err error)
|
||||||
"https://www.youtube.com/results?search_query=%s", url.QueryEscape(searchTerm),
|
"https://www.youtube.com/results?search_query=%s", url.QueryEscape(searchTerm),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// fmt.Println("Search URL: ", ytSearchUrl)
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", ytSearchUrl, nil)
|
req, err := http.NewRequest("GET", ytSearchUrl, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("cannot get youtube page")
|
return nil, errors.New("cannot get youtube page")
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ func NewDbClient() (*DbClient, error) {
|
||||||
clientOptions := options.Client().ApplyURI(dbUri)
|
clientOptions := options.Client().ApplyURI(dbUri)
|
||||||
client, err := mongo.Connect(context.Background(), clientOptions)
|
client, err := mongo.Connect(context.Background(), clientOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("error connecting to MongoDB: %d", err)
|
||||||
}
|
}
|
||||||
return &DbClient{client: client}, nil
|
return &DbClient{client: client}, nil
|
||||||
}
|
}
|
||||||
|
|
@ -45,25 +45,50 @@ func (db *DbClient) TotalSongs() (int, error) {
|
||||||
return int(total), nil
|
return int(total), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DbClient) SongExists(key string) (bool, error) {
|
func (db *DbClient) SongExists(songTitle, songArtist, ytID string) (bool, error) {
|
||||||
existingSongsCollection := db.client.Database("song-recognition").Collection("existing-songs")
|
existingSongsCollection := db.client.Database("song-recognition").Collection("existing-songs")
|
||||||
filter := bson.M{"_id": key}
|
|
||||||
|
key := fmt.Sprintf("%s - %s", songTitle, songArtist)
|
||||||
|
var filter bson.M
|
||||||
|
|
||||||
|
if len(ytID) == 0 {
|
||||||
|
filter = bson.M{"_id": key}
|
||||||
|
} else {
|
||||||
|
filter = bson.M{"ytID": ytID}
|
||||||
|
}
|
||||||
|
|
||||||
var result bson.M
|
var result bson.M
|
||||||
if err := existingSongsCollection.FindOne(context.Background(), filter).Decode(&result); err == nil {
|
if err := existingSongsCollection.FindOne(context.Background(), filter).Decode(&result); err == nil {
|
||||||
return true, nil
|
return true, nil
|
||||||
} else if err != mongo.ErrNoDocuments {
|
} else if err != mongo.ErrNoDocuments {
|
||||||
return false, fmt.Errorf("error querying registered songs: %v", err)
|
return false, fmt.Errorf("failed to retrieve registered songs: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DbClient) RegisterSong(key string) error {
|
func (db *DbClient) RegisterSong(songTitle, songArtist, ytID string) error {
|
||||||
existingSongsCollection := db.client.Database("song-recognition").Collection("existing-songs")
|
existingSongsCollection := db.client.Database("song-recognition").Collection("existing-songs")
|
||||||
_, err := existingSongsCollection.InsertOne(context.Background(), bson.M{"_id": key})
|
|
||||||
|
// Create a compound unique index on ytID and key, if it doesn't already exist
|
||||||
|
indexModel := mongo.IndexModel{
|
||||||
|
Keys: bson.D{{"ytID", 1}, {"key", 1}},
|
||||||
|
Options: options.Index().SetUnique(true),
|
||||||
|
}
|
||||||
|
_, err := existingSongsCollection.Indexes().CreateOne(context.Background(), indexModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error registering song: %v", err)
|
return fmt.Errorf("failed to create unique index: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to insert the song with ytID and key
|
||||||
|
key := fmt.Sprintf("%s - %s", songTitle, songArtist)
|
||||||
|
_, err = existingSongsCollection.InsertOne(context.Background(), bson.M{"_id": key, "ytID": ytID})
|
||||||
|
if err != nil {
|
||||||
|
if mongo.IsDuplicateKeyError(err) {
|
||||||
|
return fmt.Errorf("song with ytID or key already exists: %v", err)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to register song: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -82,7 +107,7 @@ func (db *DbClient) InsertChunkTag(chunkfgp int64, chunkTag interface{}) error {
|
||||||
update := bson.M{"$push": bson.M{"chunkTags": chunkTag}}
|
update := bson.M{"$push": bson.M{"chunkTags": chunkTag}}
|
||||||
_, err := chunksCollection.UpdateOne(context.Background(), filter, update)
|
_, err := chunksCollection.UpdateOne(context.Background(), filter, update)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error updating chunk data: %v", err)
|
return fmt.Errorf("failed to update chunkTags: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
} else if err != mongo.ErrNoDocuments {
|
} else if err != mongo.ErrNoDocuments {
|
||||||
|
|
@ -92,7 +117,7 @@ func (db *DbClient) InsertChunkTag(chunkfgp int64, chunkTag interface{}) error {
|
||||||
// If the document doesn't exist, insert a new document
|
// If the document doesn't exist, insert a new document
|
||||||
_, err = chunksCollection.InsertOne(context.Background(), bson.M{"fingerprint": chunkfgp, "chunkTags": []interface{}{chunkTag}})
|
_, err = chunksCollection.InsertOne(context.Background(), bson.M{"fingerprint": chunkfgp, "chunkTags": []interface{}{chunkTag}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error inserting chunk data: %v", err)
|
return fmt.Errorf("failed to insert chunk tag: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -109,7 +134,7 @@ func (db *DbClient) GetChunkTags(chunkfgp int64) ([]primitive.M, error) {
|
||||||
if err == mongo.ErrNoDocuments {
|
if err == mongo.ErrNoDocuments {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("error retrieving chunk data: %w", err)
|
return nil, fmt.Errorf("failed to retrieve chunk tag: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var listOfChunkTags []primitive.M
|
var listOfChunkTags []primitive.M
|
||||||
|
|
@ -137,7 +162,7 @@ func (db *DbClient) GetChunkTagForSong(songTitle, songArtist string) (bson.M, er
|
||||||
if err == mongo.ErrNoDocuments {
|
if err == mongo.ErrNoDocuments {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("error finding chunk: %v", err)
|
return nil, fmt.Errorf("failed to find chunk: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunkTag map[string]interface{}
|
var chunkTag map[string]interface{}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue