Compare commits
10
Commits
176335dc07
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe6dc020ca | ||
|
|
8001311b52 | ||
|
|
4fc7457052 | ||
|
|
96276d7639 | ||
|
|
070e583bd2 | ||
|
|
280c84ac6e | ||
|
|
98d54c58a0 | ||
|
|
ee378b19a3 | ||
|
|
a33cab8326 | ||
|
|
01b51efa63 |
@@ -1,2 +1,3 @@
|
||||
build
|
||||
.cache
|
||||
builds
|
||||
|
||||
+12
-7
@@ -7,6 +7,9 @@ project(YTMusicDL26)
|
||||
|
||||
add_executable(YTMusicDL26
|
||||
src/main.cpp
|
||||
src/playlist.cpp
|
||||
src/video.cpp
|
||||
src/utils.cpp
|
||||
)
|
||||
|
||||
target_include_directories(YTMusicDL26 PUBLIC
|
||||
@@ -16,16 +19,18 @@ target_include_directories(YTMusicDL26 PUBLIC
|
||||
|
||||
include(cmake/CPM.cmake)
|
||||
|
||||
# CPMAddPackage(
|
||||
# NAME id3v2lib
|
||||
# GIT_REPOSITORY https://github.com/larsbs/id3v2lib.git
|
||||
# GIT_TAG master
|
||||
# )
|
||||
set(CURL_USE_LIBPSL OFF CACHE BOOL "" FORCE)
|
||||
|
||||
# target_link_libraries(YTMusicDL26 PUBLIC
|
||||
# )
|
||||
CPMAddPackage("gh:nlohmann/json#v3.12.0")
|
||||
CPMAddPackage("gh:curl/curl#curl-8_21_0")
|
||||
|
||||
target_link_libraries(YTMusicDL26 PUBLIC
|
||||
nlohmann_json::nlohmann_json
|
||||
CURL::libcurl
|
||||
)
|
||||
|
||||
file(COPY ${CMAKE_SOURCE_DIR}/bin DESTINATION ${CMAKE_BINARY_DIR})
|
||||
file(COPY ${CMAKE_SOURCE_DIR}/config DESTINATION ${CMAKE_BINARY_DIR})
|
||||
|
||||
if (WIN32)
|
||||
target_link_options(YTMusicDL26 PRIVATE
|
||||
|
||||
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
mkdir builds
|
||||
cd builds
|
||||
|
||||
# Build for linux
|
||||
mkdir linux
|
||||
cd linux
|
||||
cmake -GNinja ../..
|
||||
ninja
|
||||
cd ..
|
||||
|
||||
# Linux release
|
||||
rm -r linux-release
|
||||
mkdir linux-release
|
||||
cd linux-release
|
||||
cp ../linux/YTMusicDL26 .
|
||||
mkdir bin
|
||||
cp -r ../linux/bin/linux bin/
|
||||
cp -r ../linux/config .
|
||||
cd ..
|
||||
zip -r linux-release.zip linux-release
|
||||
|
||||
# Build for windows
|
||||
mkdir windows
|
||||
cd windows
|
||||
cmake -DCMAKE_TOOLCHAIN_FILE=~/TC-mingw.cmake -GNinja ../..
|
||||
ninja
|
||||
cd ..
|
||||
|
||||
# Windows release
|
||||
rm -r windows-release
|
||||
mkdir windows-release
|
||||
cd windows-release
|
||||
cp ../windows/YTMusicDL26.exe .
|
||||
mkdir bin
|
||||
cp -r ../windows/bin/windows bin/
|
||||
cp -r ../windows/config .
|
||||
cd ..
|
||||
zip -r windows-release.zip windows-release
|
||||
|
||||
cd ..
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#define TMP_DOWNLOAD_FOLDER "tmp/download"
|
||||
#define MAX_THREADS 4
|
||||
|
||||
#ifdef _WIN32
|
||||
#define BIN_FOLDER "bin/windows"
|
||||
#define BIN_YT_DLP "bin/windows/yt-dlp.exe"
|
||||
#define BIN_FFMPEG "bin/windows/ffmpeg.exe"
|
||||
#define BIN_DENO "bin/windows/deno.exe"
|
||||
#else
|
||||
#define BIN_FOLDER "bin/linux"
|
||||
#define BIN_YT_DLP "bin/linux/yt-dlp"
|
||||
#define BIN_FFMPEG "bin/linux/ffmpeg"
|
||||
#define BIN_DENO "bin/linux/deno"
|
||||
#endif
|
||||
+21
-174
@@ -1,190 +1,37 @@
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
#include <regex>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define TMP_DOWNLOAD_FOLDER "tmp/download"
|
||||
#define MAX_THREADS 4
|
||||
#include "config.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
// This runs in shell, if put through a website interface it would allow for injection
|
||||
auto ytdlp(const std::string& args) -> bool {
|
||||
system(std::format("bin/linux/yt-dlp {}", args).c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
auto metadata(const std::string& file, const std::string& artist, const std::string& album, size_t index) {
|
||||
std::string name = std::filesystem::path(file).filename().string();
|
||||
|
||||
system(std::format("bin/linux/ffmpeg "
|
||||
"-hide_banner -loglevel error "
|
||||
"-i \"{}\" -c copy "
|
||||
"-metadata title=\"{}\" "
|
||||
"-metadata artist=\"{}\" "
|
||||
"-metadata album=\"{}\" "
|
||||
"-metadata track=\"{}\" "
|
||||
"\"{}/{}_{}.mp3\"",
|
||||
file, name, artist, album, index, TMP_DOWNLOAD_FOLDER, index, name)
|
||||
.c_str());
|
||||
}
|
||||
|
||||
auto cleanTmpDownloadFolder() -> bool {
|
||||
for (const auto& entry : std::filesystem::directory_iterator(TMP_DOWNLOAD_FOLDER)) {
|
||||
std::filesystem::remove_all(entry.path());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto urlsFromPlaylist(const std::string& url) -> std::vector<std::string> {
|
||||
auto main(int argc, char* argv[]) -> int {
|
||||
std::vector<std::string> urls;
|
||||
|
||||
std::filesystem::remove("urls.txt");
|
||||
ytdlp(
|
||||
std::format("--js-runtimes \"deno:bin/linux/deno\" "
|
||||
"--quiet "
|
||||
"--flat-playlist "
|
||||
"--print-to-file \"%(webpage_url)s\" "
|
||||
"urls.txt \"{}\"",
|
||||
url));
|
||||
printf(
|
||||
"Welcome to YTMusicDL26! You can use playlists or video from youtube or youtube music (youtube music is "
|
||||
"better).\n");
|
||||
printf("Type \"end\" when your finished.\n");
|
||||
|
||||
std::ifstream f("urls.txt");
|
||||
if (!f.is_open()) {
|
||||
printf("ERROR: Couldn't open urls.txt\n");
|
||||
return urls;
|
||||
}
|
||||
int index = 0;
|
||||
while (true) {
|
||||
std::string input;
|
||||
printf("%d: ", index);
|
||||
std::cin >> input;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
urls.push_back(line);
|
||||
}
|
||||
|
||||
if (urls.empty()) printf("ERROR: Couldn't find any urls for playlist %s\n", url.c_str());
|
||||
return urls;
|
||||
}
|
||||
|
||||
auto dataFromYtdlpPlaylist(const std::string& playlistUrl, const std::string& data) -> std::string {
|
||||
ytdlp(
|
||||
std::format("--quiet "
|
||||
"--no-warnings "
|
||||
"--flat-playlist "
|
||||
"--playlist-items 1 "
|
||||
"--print-to-file \"%({})s\" data.txt \"{}\"",
|
||||
data, playlistUrl));
|
||||
|
||||
std::ifstream f("data.txt");
|
||||
if (!f.is_open()) {
|
||||
printf("ERROR: Couldn't open data.txt\n");
|
||||
}
|
||||
|
||||
std::string s;
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
s = line;
|
||||
}
|
||||
|
||||
return s;
|
||||
};
|
||||
|
||||
auto playlistTitle(const std::string& playlistUrl) {
|
||||
std::string s = dataFromYtdlpPlaylist(playlistUrl, "playlist_title");
|
||||
s = std::regex_replace(s, std::regex("Album - "), "");
|
||||
return s;
|
||||
}
|
||||
|
||||
auto channelName(const std::string& playlistUrl) {
|
||||
std::string s = dataFromYtdlpPlaylist(playlistUrl, "channel");
|
||||
s = std::regex_replace(s, std::regex(" - Topic"), "");
|
||||
return s;
|
||||
}
|
||||
|
||||
auto downloadVideo(const std::string& url) -> bool {
|
||||
if (url == "") {
|
||||
printf("ERROR: Cannot download video with empty url\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
ytdlp(
|
||||
std::format("--js-runtimes \"deno:bin/linux/deno\" "
|
||||
"--quiet "
|
||||
"-f \"bestaudio/best\" "
|
||||
"-x --audio-format mp3 "
|
||||
"-o \"{}/%(title)s.%(ext)s\" "
|
||||
"{}",
|
||||
TMP_DOWNLOAD_FOLDER, url));
|
||||
|
||||
printf("Finished downloading video %s.\n", url.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
auto downloadPlaylist(std::string playlistUrl) -> bool {
|
||||
std::vector<std::string> videoUrls = urlsFromPlaylist(playlistUrl);
|
||||
|
||||
if (videoUrls.empty()) {
|
||||
printf("ERROR: Tried to download empty playlist, %s\n", playlistUrl.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!cleanTmpDownloadFolder()) {
|
||||
printf("ERROR: Playlist download couldn't clean tmp download folder\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
playlistUrl = std::regex_replace(playlistUrl, std::regex("music.youtube"), "www.youtube");
|
||||
|
||||
int count = 0;
|
||||
|
||||
for (size_t i = 0; i < videoUrls.size(); i += MAX_THREADS) {
|
||||
std::vector<std::future<void>> futures;
|
||||
|
||||
size_t end = std::min(i + MAX_THREADS, videoUrls.size());
|
||||
|
||||
for (size_t j = i; j < end; ++j) {
|
||||
futures.push_back(std::async(std::launch::async, [&videoUrls, j]() { downloadVideo(videoUrls[j]); }));
|
||||
if (input == "end") {
|
||||
break;
|
||||
}
|
||||
|
||||
for (auto& future : futures) {
|
||||
future.get();
|
||||
count++;
|
||||
printf("Finished [%d/%d] in playlist %s\n", count, (int)videoUrls.size(), playlistUrl.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
printf("Finished downloading playlist.\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto writeAlbumMetadataToTmpFolder(const std::string& playlistUrl) -> bool {
|
||||
std::string artist = channelName(playlistUrl);
|
||||
std::string album = playlistTitle(playlistUrl);
|
||||
printf("Starting to write metadata for %s by %s.\n", album.c_str(), artist.c_str());
|
||||
|
||||
size_t index = 1;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(TMP_DOWNLOAD_FOLDER)) {
|
||||
metadata(entry.path(), artist, album, index);
|
||||
printf("Finished writting metadata for %s.\n", entry.path().c_str());
|
||||
std::filesystem::remove(entry.path());
|
||||
urls.push_back(input);
|
||||
index++;
|
||||
}
|
||||
|
||||
printf("Finished writing metadata for %s by %s.\n", album.c_str(), artist.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto main(int argc, char* argv[]) -> int {
|
||||
auto musicDir = "/home/ethanscharlie/Downloads/tmp";
|
||||
std::string url = "https://music.youtube.com/playlist?list=OLAK5uy_kwG9Ln_fS_R-Oa8F-JIgs8PPBoRgDe8ok";
|
||||
printf("Starting rips...\n");
|
||||
|
||||
std::string musicDir = musicFolder();
|
||||
std::filesystem::create_directories(TMP_DOWNLOAD_FOLDER);
|
||||
|
||||
url = std::regex_replace(url, std::regex("music.youtube"), "www.youtube");
|
||||
downloadPlaylist(url);
|
||||
writeAlbumMetadataToTmpFolder(url);
|
||||
for (const std::string& url : urls) {
|
||||
autoDetermineRip(musicDir, url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#include "playlist.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "config.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
Playlist::Playlist(const std::string& playlistUrl) {
|
||||
this->playlistUrl = std::regex_replace(playlistUrl, std::regex("music.youtube"), "www.youtube");
|
||||
}
|
||||
|
||||
auto Playlist::rip(const std::string& musicFolder) -> bool {
|
||||
printf("Writting to data.json\n");
|
||||
json data = writeAndReturnDataJson(playlistUrl);
|
||||
|
||||
if (!download()) {
|
||||
printf("ERROR: Failed to download playlsit\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!writeMetadata(data)) {
|
||||
printf("ERROR: Failed to write metatdata for playlsit\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string artist;
|
||||
std::string album;
|
||||
aristAndAlbumName(data, artist, album);
|
||||
|
||||
if (!moveAndSortTmpFilesToFolder(musicFolder, artist, album)) {
|
||||
printf("ERROR: Failed to move files to music folder\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Playlist::download() -> bool {
|
||||
std::vector<std::string> videoUrls = urlsFromPlaylist(playlistUrl);
|
||||
|
||||
if (videoUrls.empty()) {
|
||||
printf("ERROR: Tried to download empty playlist, %s\n", playlistUrl.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!cleanTmpDownloadFolder()) {
|
||||
printf("ERROR: Playlist download couldn't clean tmp download folder\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
playlistUrl = std::regex_replace(playlistUrl, std::regex("music.youtube"), "www.youtube");
|
||||
|
||||
int count = 0;
|
||||
|
||||
for (size_t i = 0; i < videoUrls.size(); i += MAX_THREADS) {
|
||||
std::vector<std::future<void>> futures;
|
||||
|
||||
size_t end = std::min(i + MAX_THREADS, videoUrls.size());
|
||||
|
||||
for (size_t j = i; j < end; ++j) {
|
||||
futures.push_back(std::async(std::launch::async, [&videoUrls, j]() { downloadVideo(videoUrls[j]); }));
|
||||
}
|
||||
|
||||
for (auto& future : futures) {
|
||||
future.get();
|
||||
count++;
|
||||
printf("Finished [%d/%d] in playlist %s\n", count, (int)videoUrls.size(), playlistUrl.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
printf("Finished downloading playlist.\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Playlist::writeMetadata(const json& data) -> bool {
|
||||
std::string artist;
|
||||
std::string album;
|
||||
aristAndAlbumName(data, artist, album);
|
||||
|
||||
printf("Starting to write metadata for %s by %s.\n", album.c_str(), artist.c_str());
|
||||
|
||||
printf("Finding album cover.\n");
|
||||
std::string coverUrl = data["thumbnails"][1]["url"];
|
||||
downloadImageToTmpFolder(coverUrl);
|
||||
|
||||
printf("Writting data to files.\n");
|
||||
|
||||
size_t index = 1;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(TMP_DOWNLOAD_FOLDER)) {
|
||||
if (std::filesystem::path(entry.path().string()).filename().string() == "cover.jpg") continue;
|
||||
|
||||
metadata(entry.path().string(), artist, album, index);
|
||||
printf("Finished writting metadata for %s.\n", entry.path().string().c_str());
|
||||
std::filesystem::remove(entry.path().string());
|
||||
index++;
|
||||
}
|
||||
|
||||
printf("Finished writting metadata for %s by %s.\n", album.c_str(), artist.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Playlist::aristAndAlbumName(const nlohmann::json& data, std::string& artist, std::string& album) -> bool {
|
||||
artist = data["entries"][0]["uploader"];
|
||||
artist = std::regex_replace(artist, std::regex(" - Topic"), "");
|
||||
|
||||
album = data["title"];
|
||||
album = std::regex_replace(album, std::regex("Album - "), "");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Playlist::urlsFromPlaylist(const std::string& url) -> std::vector<std::string> {
|
||||
std::vector<std::string> urls;
|
||||
|
||||
std::string urlsFileLocation = "tmp/urls.txt";
|
||||
|
||||
std::filesystem::remove(urlsFileLocation);
|
||||
ytdlp(
|
||||
std::format("--js-runtimes \"deno:{}\" "
|
||||
"--quiet "
|
||||
"--flat-playlist "
|
||||
"--print-to-file \"%(webpage_url)s\" "
|
||||
"{} \"{}\"",
|
||||
BIN_DENO, urlsFileLocation, url));
|
||||
|
||||
std::ifstream f(urlsFileLocation);
|
||||
if (!f.is_open()) {
|
||||
printf("ERROR: Couldn't open urls.txt\n");
|
||||
return urls;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
urls.push_back(line);
|
||||
}
|
||||
|
||||
if (urls.empty()) printf("ERROR: Couldn't find any urls for playlist %s\n", url.c_str());
|
||||
return urls;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class Playlist {
|
||||
std::string playlistUrl;
|
||||
|
||||
public:
|
||||
Playlist(const std::string& playlistUrl);
|
||||
auto rip(const std::string& musicFolder) -> bool;
|
||||
|
||||
private:
|
||||
auto download() -> bool;
|
||||
auto writeMetadata(const nlohmann::json& data) -> bool;
|
||||
auto aristAndAlbumName(const nlohmann::json& data, std::string& artist, std::string& album) -> bool;
|
||||
auto urlsFromPlaylist(const std::string& url) -> std::vector<std::string>;
|
||||
};
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
#include <curl/curl.h>
|
||||
#include <curl/easy.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
|
||||
#include "config.hpp"
|
||||
#include "playlist.hpp"
|
||||
#include "video.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// This runs in shell, if put through a website interface it would allow for injection
|
||||
auto ytdlp(const std::string& args) -> bool {
|
||||
system(std::format("{} {}", BIN_YT_DLP, args).c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
auto metadata(const std::string& file, const std::string& artist, const std::string& album, size_t index) -> void {
|
||||
std::string name = std::filesystem::path(file).filename().string();
|
||||
name = std::regex_replace(name, std::regex(".mp3"), "");
|
||||
|
||||
system(std::format("{} "
|
||||
"-hide_banner -loglevel error "
|
||||
"-i \"{}\" "
|
||||
"-i \"{}/cover.jpg\" "
|
||||
"-map 0:0 "
|
||||
"-map 1:0 "
|
||||
"-c copy "
|
||||
"-id3v2_version 3 -metadata:s:v title=\"Album cover\" -metadata:s:v comment=\"Cover (front)\" "
|
||||
"-metadata title=\"{}\" "
|
||||
"-metadata artist=\"{}\" "
|
||||
"-metadata album=\"{}\" "
|
||||
"-metadata track=\"{}\" "
|
||||
"\"{}/{}_{}.mp3\"",
|
||||
BIN_FFMPEG, file, TMP_DOWNLOAD_FOLDER, name, artist, album, index, TMP_DOWNLOAD_FOLDER, index,
|
||||
name)
|
||||
.c_str());
|
||||
}
|
||||
|
||||
auto cleanTmpDownloadFolder() -> bool {
|
||||
for (const auto& entry : std::filesystem::directory_iterator(TMP_DOWNLOAD_FOLDER)) {
|
||||
std::filesystem::remove_all(entry.path().string());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto downloadImageToTmpFolder(std::string url) -> bool {
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
printf("ERROR: Failed to initialize curl.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
url = std::regex_replace(url, std::regex("https"), "http");
|
||||
auto outputPath = std::filesystem::path(TMP_DOWNLOAD_FOLDER) / "cover.jpg";
|
||||
FILE* file = std::fopen(outputPath.string().c_str(), "wb");
|
||||
if (!file) {
|
||||
curl_easy_cleanup(curl);
|
||||
printf("ERROR: Failed to create cover file.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, nullptr);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, file);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
std::fclose(file);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
printf("ERROR: %s\n", curl_easy_strerror(res));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto downloadVideo(const std::string& url) -> bool {
|
||||
if (url == "") {
|
||||
printf("ERROR: Cannot download video with empty url\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
ytdlp(
|
||||
std::format("--js-runtimes \"deno:{}\" "
|
||||
"--quiet "
|
||||
"-f \"bestaudio/best\" "
|
||||
"-x --audio-format mp3 "
|
||||
"-o \"{}/%(title)s.%(ext)s\" "
|
||||
"{}",
|
||||
BIN_DENO, TMP_DOWNLOAD_FOLDER, url));
|
||||
|
||||
printf("Finished downloading video %s.\n", url.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
auto writeAndReturnDataJson(const std::string& url) -> json {
|
||||
std::filesystem::remove("tmp/data.json");
|
||||
ytdlp(
|
||||
std::format("--js-runtimes \"deno:{}\" "
|
||||
"--dump-single-json "
|
||||
"\"{}\" "
|
||||
"> tmp/data.json",
|
||||
BIN_DENO, url));
|
||||
|
||||
std::ifstream f("tmp/data.json");
|
||||
if (!f) {
|
||||
printf("ERROR: Couldn't open data.json\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
return json::parse(f);
|
||||
}
|
||||
|
||||
auto moveAndSortTmpFilesToFolder(const std::string& folder, const std::string& artist, const std::string& album)
|
||||
-> bool {
|
||||
std::filesystem::path folderPath(folder);
|
||||
std::filesystem::path path = folderPath / artist / album;
|
||||
std::filesystem::create_directories(path);
|
||||
|
||||
for (const auto& entry : std::filesystem::directory_iterator(path)) {
|
||||
std::filesystem::remove_all(entry.path().string());
|
||||
}
|
||||
|
||||
for (const auto& entry : std::filesystem::directory_iterator(TMP_DOWNLOAD_FOLDER)) {
|
||||
std::filesystem::rename(entry, path / entry.path().filename());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto autoDetermineRip(const std::string& musicFolder, const std::string& url) -> bool {
|
||||
if (std::regex_search(url, std::regex("youtube.com/playlist"))) {
|
||||
return Playlist(url).rip(musicFolder);
|
||||
}
|
||||
|
||||
if (std::regex_search(url, std::regex("youtube.com/watch"))) {
|
||||
return Video(url).rip(musicFolder);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
auto musicFolder() -> std::string {
|
||||
#ifdef _WIN32
|
||||
const char* userProfile = std::getenv("USERPROFILE");
|
||||
if (userProfile) return std::format("{}/Music", userProfile);
|
||||
#else
|
||||
const char* home = std::getenv("HOME");
|
||||
if (home) return std::filesystem::path(home) / "Music";
|
||||
#endif
|
||||
|
||||
return "";
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <string>
|
||||
|
||||
auto ytdlp(const std::string& args) -> bool;
|
||||
|
||||
auto metadata(const std::string& file, const std::string& artist, const std::string& album, size_t index) -> void;
|
||||
|
||||
auto cleanTmpDownloadFolder() -> bool;
|
||||
|
||||
auto downloadImageToTmpFolder(std::string url) -> bool;
|
||||
|
||||
auto downloadVideo(const std::string& url) -> bool;
|
||||
|
||||
auto writeAndReturnDataJson(const std::string& url) -> nlohmann::json;
|
||||
|
||||
auto moveAndSortTmpFilesToFolder(const std::string& folder, const std::string& artist, const std::string& album)
|
||||
-> bool;
|
||||
|
||||
auto autoDetermineRip(const std::string& musicFolder, const std::string& url) -> bool;
|
||||
|
||||
auto musicFolder() -> std::string;
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
#include "video.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <future>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "config.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
Video::Video(const std::string& videoUrl) {
|
||||
this->videoUrl = std::regex_replace(videoUrl, std::regex("music.youtube"), "www.youtube");
|
||||
}
|
||||
|
||||
auto Video::rip(const std::string& musicFolder) -> bool {
|
||||
printf("Writting to data.json\n");
|
||||
json data = writeAndReturnDataJson(videoUrl);
|
||||
|
||||
if (!download()) {
|
||||
printf("ERROR: Failed to download playlsit\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!writeMetadata(data)) {
|
||||
printf("ERROR: Failed to write metatdata for playlsit\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string artist;
|
||||
std::string album;
|
||||
aristAndAlbumName(data, artist, album);
|
||||
|
||||
if (!moveAndSortTmpFilesToFolder(musicFolder, artist, album)) {
|
||||
printf("ERROR: Failed to move files to music folder\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Video::download() -> bool {
|
||||
if (videoUrl == "") {
|
||||
printf("ERROR: Tried to download empty video, %s\n", videoUrl.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!cleanTmpDownloadFolder()) {
|
||||
printf("ERROR: Video download couldn't clean tmp download folder\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!downloadVideo(videoUrl)) {
|
||||
printf("ERROR: Failed to download video\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("Finished downloading video %s\n", videoUrl.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Video::writeMetadata(const nlohmann::json& data) -> bool {
|
||||
std::string artist;
|
||||
std::string album;
|
||||
aristAndAlbumName(data, artist, album);
|
||||
|
||||
printf("Starting to write metadata for %s by %s.\n", album.c_str(), artist.c_str());
|
||||
|
||||
printf("Finding album cover.\n");
|
||||
std::string coverUrl = data["thumbnails"][1]["url"];
|
||||
downloadImageToTmpFolder(coverUrl);
|
||||
|
||||
printf("Writting data to files.\n");
|
||||
|
||||
size_t index = 1;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(TMP_DOWNLOAD_FOLDER)) {
|
||||
if (std::filesystem::path(entry.path().string()).filename().string() == "cover.jpg") continue;
|
||||
|
||||
metadata(entry.path().string(), artist, album, index);
|
||||
printf("Finished writting metadata for %s.\n", entry.path().string().c_str());
|
||||
std::filesystem::remove(entry.path().string());
|
||||
index++;
|
||||
}
|
||||
|
||||
printf("Finished writting metadata for %s by %s.\n", album.c_str(), artist.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Video::aristAndAlbumName(const nlohmann::json& data, std::string& artist, std::string& album) -> bool {
|
||||
artist = data["uploader"];
|
||||
artist = std::regex_replace(artist, std::regex(" - Topic"), "");
|
||||
|
||||
album = data["title"];
|
||||
album = std::regex_replace(album, std::regex("Album - "), "");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <string>
|
||||
|
||||
class Video {
|
||||
std::string videoUrl;
|
||||
|
||||
public:
|
||||
Video(const std::string& videoUrl);
|
||||
auto rip(const std::string& musicFolder) -> bool;
|
||||
|
||||
private:
|
||||
auto download() -> bool;
|
||||
auto writeMetadata(const nlohmann::json& data) -> bool;
|
||||
auto aristAndAlbumName(const nlohmann::json& data, std::string& artist, std::string& album) -> bool;
|
||||
};
|
||||
Reference in New Issue
Block a user