#include "./youtubedownload.h" #include "../application/utils.h" #include "resources/config.h" #include #include using namespace CppUtilities; using namespace Application; namespace Network { QJsonObject YoutubeDownload::m_itagInfo = QJsonObject(); /*! * \class YoutubeDownload * \brief Download implementation for YouTube videos. */ /*! * \brief Constructs a new YoutubeDownload for the specified \a url. */ YoutubeDownload::YoutubeDownload(const QUrl &url, QObject *parent) : HttpDownloadWithInfoRequst(url, parent) { } /*! * \brief Constructs a new YoutubeDownload for the specified video \a id. */ YoutubeDownload::YoutubeDownload(const QString &id, QObject *parent) : HttpDownloadWithInfoRequst(QUrl(QStringLiteral("https://www.youtube.com/watch?v=%1").arg(id)), parent) { } Download *YoutubeDownload::infoRequestDownload(bool &success, QString &reasonForFail) { const QUrl &url = initialUrl(); QString videoId; if (url.hasQuery()) { videoId = QUrlQuery(url.query(QUrl::FullyDecoded)).queryItemValue("v", QUrl::FullyDecoded); } else if (url.host(QUrl::FullyDecoded).contains(QLatin1String("youtu.be"), Qt::CaseInsensitive)) { videoId = url.path(QUrl::FullyDecoded); videoId.remove(0, 1); } if (videoId.isEmpty()) { success = false; reasonForFail = tr("The video ID couldn't be identified."); return nullptr; } else { setId(videoId); success = true; return new HttpDownload(QUrl(QStringLiteral("https://www.youtube.com/get_video_info?video_id=%1&asv=3&el=detailpage&hl=en_US").arg(videoId))); } } void YoutubeDownload::evalVideoInformation(Download *, QBuffer *videoInfoBuffer) { if (m_itagInfo.isEmpty()) { m_itagInfo = loadJsonObjectFromResource(QStringLiteral(":/jsonobjects/itaginfo")); } QString videoInfo(videoInfoBuffer->readAll()); QStringList completeFields = videoInfo.split(QChar('&'), Qt::SkipEmptyParts, Qt::CaseSensitive); for (const QString &completeField : completeFields) { QStringList fieldParts = completeField.split(QChar('='), Qt::SkipEmptyParts, Qt::CaseSensitive); if (fieldParts.count() < 2) { continue; } m_fields.insert(QUrl::fromPercentEncoding(fieldParts.at(0).toUtf8()), QUrl::fromPercentEncoding(fieldParts.at(1).toUtf8())); } QString status = m_fields.value(QStringLiteral("status")); if (status == QLatin1String("ok")) { QString title = m_fields.value(QStringLiteral("title")); if (!title.isEmpty()) { setTitle(title.replace(QChar('+'), QChar(' '))); } QString uploader = m_fields.value(QStringLiteral("author")); if (!uploader.isEmpty()) { setUploader(uploader.replace(QChar('+'), QChar(' '))); } bool ok; double duration = m_fields.value(QStringLiteral("length_seconds")).toDouble(&ok); if (ok) { setDuration(TimeSpan::fromSeconds(duration)); } QString rating = m_fields.value(QStringLiteral("avg_rating")); if (!rating.isEmpty()) { setRating(rating); } QStringList fmtFieldIds = QStringList() << QStringLiteral("url_encoded_fmt_stream_map") << QStringLiteral("adaptive_fmts"); for (const QString &fmtFieldId : fmtFieldIds) { QString fmtField = m_fields.value(fmtFieldId, QString()); if (!fmtField.isEmpty()) { QStringList sections = fmtField.split(QChar(','), Qt::SkipEmptyParts, Qt::CaseSensitive); for (const QString §ion : sections) { QStringList fmtParts = section.split(QChar('&'), Qt::SkipEmptyParts, Qt::CaseSensitive); QString itag, urlPart1, urlPart2, name; QJsonObject itagObj; for (const QString fmtPart : fmtParts) { QStringList fmtSubParts = fmtPart.split(QChar('='), Qt::SkipEmptyParts, Qt::CaseSensitive); if (fmtSubParts.count() >= 2) { QString fieldIdentifier = fmtSubParts.at(0).toLower(); if (fieldIdentifier == QLatin1String("url")) { urlPart1 = QUrl::fromPercentEncoding(fmtSubParts.at(1).toUtf8()); } else if (fieldIdentifier == QLatin1String("sig")) { urlPart2 = QUrl::fromPercentEncoding(fmtSubParts.at(1).toUtf8()); } else if (fieldIdentifier == QLatin1String("itag")) { itag = fmtSubParts.at(1); } } } if (!itag.isEmpty() && !urlPart1.isEmpty()) { if (m_itagInfo.contains(itag)) { itagObj = m_itagInfo.value(itag).toObject(); name.append(itagObj.value(QStringLiteral("container")).toString()); const QString videoCodec = itagObj.value(QStringLiteral("videoCodec")).toString(); const QString audioCodec = itagObj.value(QStringLiteral("audioCodec")).toString(); if (!videoCodec.isEmpty()) { name.append(QChar('/')); name.append(videoCodec); } if (!audioCodec.isEmpty()) { name.append(QChar('/')); name.append(audioCodec); } if (!videoCodec.isEmpty()) { name.append(QStringLiteral(", ")); name.append(itagObj.value(QStringLiteral("videoResolution")).toString()); } if (videoCodec.isEmpty()) { name.append(tr(", no video")); const QString audioBitrate = itagObj.value(QStringLiteral("audioBitrate")).toString(); if (!audioBitrate.isEmpty()) { name.append(tr(", %1 kbit/s").arg(audioBitrate)); } } if (audioCodec.isEmpty()) { name.append(tr(", no audio")); } name.append(QStringLiteral(" (")); name.append(itag); name.append(QStringLiteral(")")); } else { name = itag; } QByteArray url; url.append(urlPart1.toUtf8()); if (!urlPart2.isEmpty()) { url.append("&signature="); url.append(urlPart2.toUtf8()); } addDownloadUrl(name, QUrl::fromPercentEncoding(url)); m_itags.append(itag); } } } } if (availableOptionCount()) { reportInitiated(true); } else { reportInitiated(false, tr("Couldn't pharse the video info. The status of the video info is ok, but it seems like YouTube changed something in their API.")); } } else { QString reason = m_fields.value("reason"); if (reason.isEmpty()) { reportInitiated(false, tr("Failed to retrieve the video info. The reason couldn't be identified. It seems like YouTube changed something in their API.")); } else { reportInitiated(false, tr("Failed to retrieve the video info. The reason returned by Youtube is: \"%1\".").arg(reason.replace(QChar('+'), QChar(' ')))); } } } QString YoutubeDownload::videoInfo(QString field, const QString &defaultValue) { return m_fields.value(field, defaultValue); } QString YoutubeDownload::suitableFilename() const { auto filename = Download::suitableFilename(); // get chosen option, the original option (not the redirection!) is required auto originalOption = chosenOption(); while (originalOption != options().at(originalOption).redirectionOf()) { originalOption = options().at(originalOption).redirectionOf(); } QString extension; if (originalOption < static_cast(m_itags.size())) { const auto itag = m_itags.at(originalOption); if (m_itagInfo.contains(itag)) { const auto itagObj = m_itagInfo.value(itag).toObject(); extension = itagObj.value(QStringLiteral("ext")).toString(); if (extension.isEmpty()) { extension = itagObj.value(QStringLiteral("container")).toString().toLower(); } } } if (extension.isEmpty()) { extension = QStringLiteral("flv"); // assume flv } extension.insert(0, QStringLiteral(".")); if (!filename.endsWith(extension)) { filename.append(extension); } return filename; } QString YoutubeDownload::typeName() const { return tr("YouTube"); } } // namespace Network