diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4d93dd --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# bp_audio +**bp** stands for **bilibili ported**, but also **bai piao**. + +![banner](https://superfashi.b0.upaiyun.com/wp-content/uploads/2018/02/banner-bp-audio.png) + +An out-of-the-box extractor for Bilibili's official FLAC music. + +It can download Bilibili-purchased music in *SQ* (aka lossless) version without an account whatsoever. While downloading, it can also acquire music's metadata, including album art. It can also download the whole *menu* (aka album) at one time. + +# Example +![Example-GIF](https://superfashi.b0.upaiyun.com/wp-content/uploads/2018/02/bp_example.gif) + +With metadata included: + +![Example-Meta](https://superfashi.b0.upaiyun.com/wp-content/uploads/2018/02/bp_example.png) + +Try running this command on your own computer. + +# Distribution +You can download the pre-compiled *x86_64 (amd64)* version [here](https://github.com/hanbang-wang/bp_audio/releases), which should be running without any problem on latest Windows devices. Requests for other versions will likely be ignored due to the reason in [License & Disclaimer](#license--disclaimer) section, so you will have to compile it by yourself: see [Compilation](#compilation) section below. + +# Compilation +First you'll need *[Qt framework](https://www.qt.io)*; *Qt Creator* is highly suggested. + +Next you'll need: + +- [gumbo-query](https://github.com/lazytiger/gumbo-query) for HTML DOM parsing. + + > Actually I can use regex, but that just makes me an idiot. + +- [TagLib](https://github.com/taglib/taglib) for audio file metadata. + +Then just `qmake bp_audio.pro` and `make`, as what you would do for every Qt project. + +# Usage +```bash +Usage: bp_audio +Get to know some free official FLAC music. + +Options: + -a, --au Audio AU number. + --menu Download whole menu. + -o, --out Output folder path. + -?, -h, --help Displays this help. + -v, --version Displays version information. +``` + +# License & Disclaimer +This little repo is unlicensed, meaning it's released into public domain and you can do anything you want. For more information, please refer to [http://unlicense.org](http://unlicense.org). + +This is only a proof-of-concept of how bad Bilibili's inner software and web developers areā€”the mixed use of params in `path` and `query`, the **useless** key validation of CDN, etc. Therefore, it shall only be used for research and educational purpose. + +Neither this project, nor its creator(s), is responsible for any misuses, illegal behaviors, and so forth. \ No newline at end of file diff --git a/biliAudio.pro b/bp_audio.pro similarity index 100% rename from biliAudio.pro rename to bp_audio.pro diff --git a/main.cpp b/main.cpp index 1c64f74..8512259 100644 --- a/main.cpp +++ b/main.cpp @@ -49,13 +49,17 @@ class PictureWrapper { } }; +const int bar_width = 50; +const QString app_name = "bp_audio"; +const QString app_version = "1.0"; + QCommandLineOption au_opt({"a", "au"}, "Audio AU number.", "au"); QCommandLineOption all_opt("menu", "Download whole menu."); QCommandLineOption output_opt({"o", "out"}, "Output folder path.", "path"); QCommandLineParser parser; QScopedPointer session; QDir output; -QHash> assets; +QHash > assets; QString find_audio(const QByteArray &data) { CDocument doc; @@ -64,6 +68,19 @@ QString find_audio(const QByteArray &data) { return QString::fromStdString(audio.nodeAt(0).attribute("src")); } +void download_progress(const qint64 &bytes_received, const qint64 &bytes_total) { + if (bytes_total == -1) return; + std::cerr << '['; + int pos = bar_width * bytes_received / bytes_total; + for (int i = 0; i < bar_width; ++i) { + if (i < pos) std::cerr << '='; + else if (i == pos) std::cerr << '>'; + else std::cerr << ' '; + } + std::cerr << "] " << int(100 * bytes_received / bytes_total) << " %\r"; + if (bytes_received >= bytes_total) std::cerr << '\n'; +} + void write_metadata(const QString &file, const QJsonObject &data, const int &track, const int &total) { TagLib::FLAC::File f(TagLib::FileName(file.toStdWString().data())); @@ -124,7 +141,7 @@ void download_song(const QJsonObject &data, const int &track = -1, const int &to QString file_name = QStringLiteral("%1 - %2.flac").arg(data["author"].toString(), data["title"].toString()); std::array buf{}; file_name.toWCharArray(buf.data()); - PathCleanupSpec(NULL, buf.data()); + PathCleanupSpec(output.absolutePath().toStdWString().data(), buf.data()); file_name = QString::fromWCharArray(buf.data()); const QString &au = QString::number(data["id"].toInt()); @@ -154,24 +171,27 @@ void download_song(const QJsonObject &data, const int &track = -1, const int &to QNetworkRequest song_flac(audio_url); QScopedPointer song_res(session->get(song_flac)); + QObject::connect(song_res.data(), &QNetworkReply::downloadProgress, download_progress); + QObject::connect(song_res.data(), &QNetworkReply::readyRead, &loop, &QEventLoop::quit); QObject::connect(song_res.data(), &QNetworkReply::finished, &loop, &QEventLoop::quit); - loop.exec(); - if (song_res->error() != QNetworkReply::NoError) { - qCritical("Error loading audio resource"); - qDebug(" %s", qUtf8Printable(song_res->errorString())); - std::exit(EXIT_FAILURE); - } - - qInfo("Writing to %s...", qUtf8Printable(file_name)); file_name = output.filePath(file_name); + qInfo("Writing to %s...", qUtf8Printable(file_name)); QFile f(file_name); if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - f.write(song_res->readAll()); + do { + loop.exec(); + if (song_res->error() != QNetworkReply::NoError) { + qCritical("Error loading audio resource"); + qDebug(" %s", qUtf8Printable(song_res->errorString())); + std::exit(EXIT_FAILURE); + } + f.write(song_res->readAll()); + } while (!song_res->isFinished()); f.close(); } write_metadata(file_name, data, track, total); - qInfo("Finished for %s...", qUtf8Printable(file_name)); + qInfo("Finished for %s", qUtf8Printable(file_name)); } void get_menu_info(const QString &menuid) { @@ -204,7 +224,7 @@ void get_menu_info(const QString &menuid) { QString folder_name = QStringLiteral("%1 - %2").arg(menus_response["mbnames"].toString(), menus_response["title"].toString()); std::array buf{}; folder_name.toWCharArray(buf.data()); - PathCleanupSpec(NULL, buf.data()); + PathCleanupSpec(output.absolutePath().toStdWString().data(), buf.data()); folder_name = QString::fromWCharArray(buf.data()); output.mkdir(folder_name); output.cd(folder_name); @@ -260,6 +280,8 @@ void get_song_info(const QString &au) int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); + a.setApplicationName(app_name); + a.setApplicationVersion(app_version); session.reset(new QNetworkAccessManager()); @@ -267,7 +289,8 @@ int main(int argc, char *argv[]) parser.addOption(all_opt); parser.addOption(output_opt); parser.addHelpOption(); - parser.setApplicationDescription("Free bili lossless audio."); + parser.addVersionOption(); + parser.setApplicationDescription("Get to know some free official FLAC music."); parser.process(a); const QString &au_number = parser.value(au_opt);