require('dotenv').config(); const { Readable } = require("stream"); const { finished } = require("stream/promises"); const { searchMusics } = require('fix-esm').require("node-youtube-music"); const { GetListByKeyword } = require("youtube-search-api"); const skewered = require("skewered"); const { createClient } = require("fix-esm").require("webdav"); const path = require("path"); const { readFileSync, writeFileSync } = require("fs"); const io = require('socket.io-client'); const { program } = require('commander'); function pathGenerator({ url, name, channelName }) { return path.join(process.env.NEXTCLOUD_FOLDER, `${url}-${skewered(`${name} - ${channelName}`)}.mp3`) } function checkLastSong({ name, channelName }) { const curSong = readFileSync(path.join(__dirname, "last_song.txt"), 'utf8'); return (curSong === `${name} - ${channelName}`); } function writeLastSong({ name, channelName }) { writeFileSync(path.join(__dirname, "last_song.txt"), `${name} - ${channelName}`, { recursive: true, encoding: 'utf-8' }); return null; } async function getVideo({ msName, msArtist }) { let trackData; if (msName != null && msArtist != null) { trackData = { name: msName, artist: { name: msArtist } }; } else { trackData = await fetch( `https://${process.env.LASTFM_INSTANCE}/2.0/?method=user.getrecenttracks&user=${process.env.LASTFM_USERNAME}&api_key=${process.env.LASTFM_API_KEY}&format=json&limit=1&extended=1` ).then(x => x.json()).then(data => data.recenttracks.track[0]); } if (checkLastSong({ name: trackData.name, channelName: trackData.artist.name })) { console.log("Last song check failed"); return null; } else { console.log("Last song check succeeded, writing file"); writeLastSong({ name: trackData.name, channelName: trackData.artist.name }); } let selectedVideo = { url: "", name: "", channelName: "" }; const musicList = await searchMusics(`${trackData.artist.name} - ${trackData.name}`); const youtubeMusicVideo = musicList.find((song) => { return skewered(song.title).includes(skewered(trackData.name)); }); console.log(youtubeMusicVideo); if (youtubeMusicVideo == null) { const videoList = await GetListByKeyword(`${trackData.artist.name} - ${trackData.name}`, false, 1, [ {type: "video"} ]); const musicVideo = videoList.items[0]; selectedVideo = { url: musicVideo.id, name: musicVideo.title, channelName: musicVideo.channelTitle }; } else selectedVideo = { url: youtubeMusicVideo.youtubeId, name: youtubeMusicVideo.title, channelName: youtubeMusicVideo.artists[0].name } selectedVideo.channelName = selectedVideo.channelName.replace(/\s-\sTopic/ig, ""); return selectedVideo; } async function checkNextcloud({ url, name, channelName }, nextcloudClient) { const fileName = pathGenerator({ url, name, channelName }); return await nextcloudClient.exists(fileName); } async function downloadFromCobalt({ url, name, channelName }) { const processor = await fetch(`https://${process.env.COBALT_INSTANCE}/api/json`, { method: "POST", body: JSON.stringify({ url: `https://youtu.be/${url}`, vCodec: "h264", vQuality: "144", aFormat: "mp3", filenamePattern: "basic", isAudioOnly: true, disableMetadata: false }), headers: [ ["Accept", "application/json"], ["Content-Type", "application/json"], ] }).then(x => x.json()); let processorURL; switch (processor.status) { case 'error': case 'rate-limit': throw new Error(`Error! (${processor.text})`); case 'redirect': processorURL = processor.audio || processor.url; break; case 'picker': throw new Error("Can't handle picker!"); case 'stream': const streamProcessor = await fetch(`${processor.url}&p=1`).then(x => x.json()); if (streamProcessor.status === "continue") processorURL = processor.audio || processor.url; else processorURL = processor.audio || processor.url; break; default: processorURL = processor.audio || processor.url; break; } const { body } = await fetch(processorURL); return { fileStream: Readable.fromWeb(body), url, name, channelName }; } async function uploadToNextcloud({ fileStream, url, name, channelName }, nextcloudClient) { await nextcloudClient.createDirectory(process.env.NEXTCLOUD_FOLDER, { recursive: true, }); const fileName = pathGenerator({ url, name, channelName }); await finished(fileStream.pipe(nextcloudClient.createWriteStream(fileName))); return fileName; } async function main() { program .option('-s, --song ', 'Look up a song', null) .option('-a, --artist ', 'Look up an artist', null); program.parse(process.argv); const options = program.opts(); const socket = io.connect(`http://localhost:${process.env.NOTIFICATION_SERVER_PORT}`, {reconnect: true}); socket.on('connect', function (s) { console.log('Successfully connected to notification server'); }); const video = await getVideo({ msName: options.song, msArtist: options.artist }); console.log(video); if (video == null) return dismantle(socket); const nextcloudClient = createClient( `https://${process.env.NEXTCLOUD_INSTANCE}/remote.php/dav/files/${process.env.NEXTCLOUD_USERNAME}/`, { username: process.env.NEXTCLOUD_USERNAME, password: process.env.NEXTCLOUD_PASSWORD, } ); if (await checkNextcloud(video, nextcloudClient)) return dismantle(socket); socket.emit('nodeMessage', { message: `New song found - ${video.name} from ${video.channelName}`, password: process.env.NOTIFICATION_SERVER_PASSWORD }) const cobalt = await downloadFromCobalt(video); console.log(cobalt); socket.emit('nodeMessage', { message: `Nextcloud upload running - ${cobalt.name} from ${cobalt.channelName}`, password: process.env.NOTIFICATION_SERVER_PASSWORD }); const nextcloud = await uploadToNextcloud(cobalt, nextcloudClient); console.log(nextcloud); socket.emit('nodeMessage', { message: `Nextcloud upload finished - ${cobalt.name} from ${cobalt.channelName}`, password: process.env.NOTIFICATION_SERVER_PASSWORD }) return dismantle(socket); } main(); function dismantle(socket) { socket.disconnect(); return null; }