263 lines
No EOL
7.5 KiB
JavaScript
263 lines
No EOL
7.5 KiB
JavaScript
require('dotenv').config();
|
|
const { Readable, PassThrough } = 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 NodeID3 = require('node-id3');
|
|
|
|
function pathGenerator({ url, name, channelName }) {
|
|
return `${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() {
|
|
const 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]);
|
|
|
|
const tags = {
|
|
title: trackData.name,
|
|
artist: trackData.artist.name,
|
|
album: trackData.album["#text"] || trackData.name,
|
|
APIC: "./cover.png",
|
|
userDefinedUrl: [{
|
|
description: "Last.FM page",
|
|
url: trackData.url
|
|
}]
|
|
};
|
|
|
|
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));
|
|
});
|
|
|
|
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 {
|
|
tags,
|
|
...selectedVideo,
|
|
albumCover: trackData.image?.at(-1)["#text"]
|
|
};
|
|
}
|
|
|
|
async function checkNextcloud({ url, name, channelName, tags }, nextcloudClient) {
|
|
const fileName = pathGenerator({ url, name, channelName });
|
|
|
|
return await nextcloudClient.exists(path.join(process.env.NEXTCLOUD_FOLDER, tags.album, fileName));
|
|
}
|
|
|
|
async function checkNextcloudAlbum({ tags }, nextcloudClient) {
|
|
return await nextcloudClient.exists(path.join(process.env.NEXTCLOUD_FOLDER, tags.album, "cover.png"));
|
|
}
|
|
|
|
async function downloadFromCobalt({ url }) {
|
|
const processor = await fetch(`https://${process.env.COBALT_INSTANCE}/api/json`, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
url: `https://youtu.be/${url}`,
|
|
vCodec: "h264",
|
|
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 = Buffer.from(await fetch(processorURL).then(x => x.arrayBuffer()));
|
|
|
|
return body;
|
|
}
|
|
|
|
async function downloadAlbumCover({ albumCover }) {
|
|
const body = Buffer.from(await fetch(albumCover).then(x => x.arrayBuffer()));
|
|
|
|
return body;
|
|
}
|
|
|
|
function applyTags({ fileStream, tags }) {
|
|
const body = NodeID3.update(tags, fileStream);
|
|
|
|
return body;
|
|
}
|
|
|
|
async function uploadToNextcloud({ fileStream, url, name, channelName, tags, albumCover }, nextcloudClient) {
|
|
await nextcloudClient.createDirectory(path.join(process.env.NEXTCLOUD_FOLDER, tags.album), {
|
|
recursive: true,
|
|
});
|
|
|
|
const fileBufferStream = new PassThrough().end(fileStream);
|
|
|
|
const fileName = pathGenerator({ url, name, channelName });
|
|
|
|
await finished(fileBufferStream.pipe(nextcloudClient.createWriteStream(path.join(process.env.NEXTCLOUD_FOLDER, tags.album, fileName))));
|
|
|
|
if (albumCover) {
|
|
const albumBufferStream = new PassThrough().end(albumCover);
|
|
|
|
await finished(albumBufferStream.pipe(nextcloudClient.createWriteStream(path.join(process.env.NEXTCLOUD_FOLDER, tags.album, "cover.png"))));
|
|
}
|
|
|
|
|
|
return fileName;
|
|
}
|
|
|
|
async function main() {
|
|
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();
|
|
|
|
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
|
|
})
|
|
|
|
let albumCover;
|
|
if (!(await checkNextcloudAlbum(video, nextcloudClient)) && video.albumCover != null) {
|
|
albumCover = await downloadAlbumCover(video);
|
|
}
|
|
|
|
socket.emit('nodeMessage', {
|
|
message: `Cobalt download starting - ${video.name} from ${video.channelName}`,
|
|
password: process.env.NOTIFICATION_SERVER_PASSWORD
|
|
})
|
|
|
|
const cobalt = await downloadFromCobalt(video);
|
|
|
|
socket.emit('nodeMessage', {
|
|
message: `Cobalt download finished - ${video.name} from ${video.channelName}`,
|
|
password: process.env.NOTIFICATION_SERVER_PASSWORD
|
|
});
|
|
|
|
const taggedMusic = applyTags({
|
|
fileStream: cobalt,
|
|
tags: video.tags
|
|
});
|
|
|
|
socket.emit('nodeMessage', {
|
|
message: `Nextcloud upload running - ${video.name} from ${video.channelName}`,
|
|
password: process.env.NOTIFICATION_SERVER_PASSWORD
|
|
});
|
|
|
|
const nextcloud = await uploadToNextcloud(
|
|
{
|
|
...video,
|
|
fileStream: taggedMusic,
|
|
albumCover: albumCover
|
|
},
|
|
nextcloudClient
|
|
);
|
|
|
|
socket.emit('nodeMessage', {
|
|
message: `Nextcloud upload finished - ${video.name} from ${video.channelName}`,
|
|
password: process.env.NOTIFICATION_SERVER_PASSWORD
|
|
})
|
|
|
|
return dismantle(socket);
|
|
}
|
|
|
|
main();
|
|
|
|
function dismantle(socket) {
|
|
socket.disconnect();
|
|
return null;
|
|
} |