diff --git a/.gitignore b/.gitignore --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ *.swp *_manifest.* *.embed.manifest +bin/ bin/autotests .clang_complete bin/falkon.app diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -10,6 +10,7 @@ add_subdirectory(StatusBarIcons) add_subdirectory(TabManager) add_subdirectory(VerticalTabs) +add_subdirectory(YoutubeDownload) if (GNOME_KEYRING_FOUND) add_subdirectory(GnomeKeyringPasswords) diff --git a/src/plugins/YoutubeDownload/CMakeLists.txt b/src/plugins/YoutubeDownload/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/CMakeLists.txt @@ -0,0 +1,20 @@ +set( YoutubeDownload_SRCS + yticon.cpp + ytinterface.cpp + ytsettings.cpp + ytprocess.cpp +) + +set( YoutubeDownload_UIS + ytsettings.ui +) +qt5_wrap_ui(UIS ${YoutubeDownload_UIS}) + +set( YoutubeDownload_RSCS + yt.qrc +) +qt5_add_resources(RSCS ${YoutubeDownload_RSCS}) + +add_library(YoutubeDownload MODULE ${YoutubeDownload_SRCS} ${UIS} ${RSCS}) +install(TARGETS YoutubeDownload DESTINATION ${FALKON_INSTALL_PLUGINDIR}) +target_link_libraries(YoutubeDownload FalkonPrivate) diff --git a/src/plugins/YoutubeDownload/data/icon-white.svg b/src/plugins/YoutubeDownload/data/icon-white.svg new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/data/icon-white.svg @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/plugins/YoutubeDownload/data/icon.svg b/src/plugins/YoutubeDownload/data/icon.svg new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/data/icon.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/src/plugins/YoutubeDownload/metadata.desktop b/src/plugins/YoutubeDownload/metadata.desktop new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/metadata.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Name=Youtube Download +Comment=Adds ability to download youtube videos + +Icon=:ytdownload/data/icon.svg +Type=Service + +X-Falkon-Author=Luca Gasperini +X-Falkon-Email=info@xsoftware.eu +X-Falkon-Version=0.1.0 +X-Falkon-Settings=true diff --git a/src/plugins/YoutubeDownload/yt.qrc b/src/plugins/YoutubeDownload/yt.qrc new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/yt.qrc @@ -0,0 +1,7 @@ + + + metadata.desktop + data/icon.svg + data/icon-white.svg + + diff --git a/src/plugins/YoutubeDownload/yticon.h b/src/plugins/YoutubeDownload/yticon.h new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/yticon.h @@ -0,0 +1,21 @@ +#ifndef YTICON_H +#define YTICON_H + +#include + +class YtIcon : public AbstractButtonInterface +{ + Q_OBJECT + +public: + explicit YtIcon(QObject *parent = nullptr); + + QString id() const override; + QString name() const override; + +private: + void updateState(); + void clicked(ClickController *controller); +}; + +#endif //YTICON_H diff --git a/src/plugins/YoutubeDownload/yticon.cpp b/src/plugins/YoutubeDownload/yticon.cpp new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/yticon.cpp @@ -0,0 +1,41 @@ +#include "yticon.h" + +YtIcon::YtIcon(QObject *parent) + : AbstractButtonInterface(parent) +{ + setIcon(QIcon::fromTheme(QSL("im-youtube"), QIcon(QSL(":ytdownload/data/icon.svg")))); + setTitle(tr("Download video")); + setToolTip(tr("Download video")); + + connect(this, &AbstractButtonInterface::clicked, this, &YtIcon::clicked); + + updateState(); +} + +QString YtIcon::id() const +{ + return QSL("youtube-download"); +} + +QString YtIcon::name() const +{ + return tr("Download Youtube"); +} + +void YtIcon::updateState() +{ +/* setVisible(m_manager->downloadsCount() > 0); + const int count = m_manager->activeDownloadsCount(); + if (count > 0) { + setBadgeText(QString::number(count)); + } else { + setBadgeText(QString()); + }*/ +} + +void YtIcon::clicked(ClickController *controller) +{ + Q_UNUSED(controller) + + //mApp->downloadManager()->show(); +} diff --git a/src/plugins/YoutubeDownload/ytinterface.h b/src/plugins/YoutubeDownload/ytinterface.h new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/ytinterface.h @@ -0,0 +1,61 @@ +#ifndef YTINTERFACE_H +#define YTINTERFACE_H + +// Include plugininterface.h for your version of Falkon +#include "plugininterface.h" +#include "yticon.h" +#include "browserwindow.h" + +#include +#include +#include + +class YtInterface : public QObject, public PluginInterface +{ + Q_OBJECT + Q_INTERFACES(PluginInterface) + Q_PLUGIN_METADATA(IID "Falkon.Browser.plugin.YtInterface") + +public: + explicit YtInterface(); + + DesktopFile metaData() const override; + void init(InitState state, const QString &settingsPath) override; + void unload() override; + bool testPlugin() override; + void showSettings(QWidget *parent) override; + void saveSettings(); + void loadSettings(); + + QString getOutputFile(const QString &url); + + bool s_debug; + bool s_askalways; + bool s_metadata; + bool s_subtitle; + bool s_thumbnail; + bool s_extractaudio; + bool s_useproxy; + bool s_askalwaysfile; + QString s_formataudio; + QString s_formatvideo; + QString s_defaultdir; + + int s_audioquality; + + QString s_executable; + +private slots: + void actionSlot(); + void downloadFinished(const QString &file); + void windowCreated(BrowserWindow* window); + +private: + + int dialogSettings(QWidget* parent = nullptr); + YtIcon* m_download; + WebView* m_view; + QString m_settingsPath; +}; + +#endif // YTINTERFACE_H diff --git a/src/plugins/YoutubeDownload/ytinterface.cpp b/src/plugins/YoutubeDownload/ytinterface.cpp new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/ytinterface.cpp @@ -0,0 +1,189 @@ + +#include "ytinterface.h" +#include "ytsettings.h" +#include "ytprocess.h" +#include "browserwindow.h" +#include "webview.h" +#include "pluginproxy.h" +#include "mainapplication.h" +#include "sidebar.h" +#include "webhittestresult.h" +#include "../config.h" +#include "desktopfile.h" +#include "ytsettings.h" +#include "tabwidget.h" +#include "tabbar.h" +#include "desktopnotificationsfactory.h" +#include "networkmanager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +YtInterface::YtInterface() + : QObject() +{ + // Don't do anything expensive in constructor! + // It will be called even if user doesn't have the plugin allowed +} + +DesktopFile YtInterface::metaData() const +{ + return DesktopFile(QSL(":ytdownload/metadata.desktop")); +} + +void YtInterface::init(InitState state, const QString &settingsPath) +{ + Q_UNUSED(state) + + m_download = new YtIcon(); + m_download->setIcon(QIcon(QL1S(":ytdownload/data/icon.svg"))); + connect(m_download, SIGNAL(clicked(ClickController*)), this, SLOT(actionSlot())); + connect(mApp->plugins(), SIGNAL(mainWindowCreated(BrowserWindow*)), this, SLOT(windowCreated(BrowserWindow*))); + + m_settingsPath = settingsPath + QL1S("/ytdownload/ytdownload.ini"); + loadSettings(); +} + +void YtInterface::unload() +{ + mApp->getWindow()->navigationBar()->removeToolButton(m_download); +} + +bool YtInterface::testPlugin() +{ + // This function is called right after init() + // There should be some testing if plugin is loaded correctly + // If this function returns false, plugin is automatically unloaded + + return (Qz::VERSION == QLatin1String(FALKON_VERSION)); +} + +int YtInterface::dialogSettings(QWidget* parent) +{ + YtSettings* settings = new YtSettings(this, parent); + return settings->exec(); +} + +void YtInterface::showSettings(QWidget* parent) +{ + dialogSettings(parent); +} + +void YtInterface::windowCreated(BrowserWindow *window) +{ + window->navigationBar()->addToolButton(m_download); +} + +void YtInterface::actionSlot() +{ + YtProcess* exe = new YtProcess(nullptr, s_debug); + QStringList args; + QString outputfile; + QUrl url = mApp->getWindow()->tabWidget()->webTab(mApp->getWindow()->tabWidget()->currentIndex())->url(); + + connect(exe, SIGNAL(downloadFinished(QString)), this, SLOT(downloadFinished(QString))); + + if(s_askalways) + if(dialogSettings() == QDialog::Rejected) + return; + if(s_askalwaysfile) + outputfile = QFileDialog::getSaveFileName(nullptr, "Save Video", s_defaultdir + "/" + YtProcess::getVideoTitle(s_executable, url.toString())); + else + outputfile = s_defaultdir + "/" + YtProcess::getVideoTitle(s_executable, url.toString()); + + if(outputfile.isEmpty()) + return; + + if(s_useproxy) + args << "--proxy" << "HTTP://" + mApp->networkManager()->proxy().hostName() + ":" + QString::number(mApp->networkManager()->proxy().port()); + if(s_debug) + args.append("--verbose"); + if(s_metadata) + args.append(QL1S("--add-metadata")); + if(s_subtitle) + args.append(QL1S("--write-sub")); + if(s_thumbnail) + args.append(QL1S("--embed-thumbnail")); + if(s_extractaudio) + args.append(QL1S("--extract-audio")); + + args << "--audio-format" << s_formataudio; + args << "--audio-quality" << QString::number(s_audioquality); + if(!s_extractaudio) + args << "--recode-video" << s_formatvideo; + + exe->setExecutable(s_executable); + exe->setArguments(args); + exe->setUrl(url); + exe->setOutputFile(outputfile); + exe->start(); +} + +void YtInterface::saveSettings() +{ + QSettings settings(m_settingsPath, QSettings::IniFormat); + settings.beginGroup("General"); + settings.setValue("Debug", s_debug); + settings.setValue("AskFilename", s_askalwaysfile); + settings.setValue("AskAlways", s_askalways); + settings.setValue("ExecutablePath", s_executable); + settings.endGroup(); + settings.beginGroup("Output"); + settings.setValue("DefaultDir", s_defaultdir); + settings.setValue("ExtractAudio", s_extractaudio); + settings.endGroup(); + settings.beginGroup("Format"); + settings.setValue("Video", s_formatvideo); + settings.setValue("Audio", s_formataudio); + settings.setValue("AudioQuality", s_audioquality); + settings.endGroup(); + settings.beginGroup("Other"); + settings.setValue("Metadata", s_metadata); + settings.setValue("Subtitle", s_subtitle); + settings.setValue("Thumbnail", s_thumbnail); + settings.endGroup(); + +} + +void YtInterface::loadSettings() +{ + QSettings settings(m_settingsPath, QSettings::IniFormat); + settings.beginGroup("General"); + s_debug = settings.value("Debug", false).toBool(); + s_askalwaysfile = settings.value("AskFilename", true).toBool(); + s_askalways = settings.value("AskAlways", false).toBool(); + s_executable = settings.value("ExecutablePath", "/bin/youtube-dl").toString(); + settings.endGroup(); + settings.beginGroup("Output"); + s_defaultdir = settings.value("DefaultDir", QDir::homePath()).toString(); + s_extractaudio = settings.value("ExtractAudio", false).toBool(); + settings.endGroup(); + settings.beginGroup("Format"); + s_formatvideo = settings.value("Video", "mp4").toString(); + s_formataudio = settings.value("Audio", "m4a").toString(); + s_audioquality = settings.value("AudioQuality", 5).toInt(); + settings.endGroup(); + settings.beginGroup("Other"); + s_metadata = settings.value("Metadata", false).toBool(); + s_subtitle = settings.value("Subtitle", false).toBool(); + s_thumbnail = settings.value("Thumbnail", false).toBool(); + settings.endGroup(); +} + +void YtInterface::downloadFinished(const QString &file) +{ + DesktopNotificationsFactory* notify = new DesktopNotificationsFactory(); + QString f = file + "." + (s_extractaudio ? s_formataudio : s_formatvideo); + if(QFile::exists(f)) + notify->showNotification(QPixmap(QL1S(":ytdownload/data/icon-white.svg")),"Youtube video downloaded", "The youtube video has been downloaded!"); + else + notify->showNotification(QPixmap(QL1S(":ytdownload/data/icon-white.svg")),"Download failed", "The youtube video cannot be downloaded!"); +} diff --git a/src/plugins/YoutubeDownload/ytprocess.h b/src/plugins/YoutubeDownload/ytprocess.h new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/ytprocess.h @@ -0,0 +1,41 @@ +#ifndef YTPROCESS_H +#define YTPROCESS_H + +#include +#include +#include + +class YtProcess : public QThread +{ + Q_OBJECT +public: + YtProcess(QObject* parent = nullptr, bool debug = false); + ~YtProcess(); + + void run(); + static bool test(const QString& e); + + static QString getVideoTitle(const QString& _exe, const QString& _url); + + void setExecutable(const QString& _exe); + void setArguments(const QStringList& _args); + void setUrl(const QUrl &_url); + void setOutputFile(const QString &out); + + QString executable(); + QStringList arguments(); + QUrl url(); + QString outputfile(); + +signals: + void readOutput(const QString& out); + void downloadFinished(const QString& file); +private: + QProcess* proc; + QString exe; + QStringList args; + QUrl u; + QString of; +}; + +#endif //YTPROCESS_H diff --git a/src/plugins/YoutubeDownload/ytprocess.cpp b/src/plugins/YoutubeDownload/ytprocess.cpp new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/ytprocess.cpp @@ -0,0 +1,82 @@ +#include "ytprocess.h" +#include + +YtProcess::YtProcess(QObject* parent, bool debug) : QThread(parent) +{ + proc = new QProcess; + if(debug) + proc->setProcessChannelMode(QProcess::ForwardedChannels); +} + +YtProcess::~YtProcess() +{ +} + +void YtProcess::run() +{ + if(exe.isEmpty() || args.isEmpty() || u.isEmpty() || of.isEmpty()) + { + qWarning() << "[YoutubeDownload][FAIL] program or arguments are not found"; + return; + } + + QStringList temp = args; + temp << "-o" << of + ".%(ext)s" << u.toString(); + + proc->start(exe, temp, QIODevice::ReadOnly); + if(!proc->waitForStarted()) + return; + + proc->waitForReadyRead(); + while(proc->bytesAvailable()) + { + emit readOutput(proc->readLine()); + } + proc->waitForFinished(); + emit downloadFinished(of); + +} + +QString YtProcess::getVideoTitle(const QString& _exe, const QString &_url) +{ + QProcess* p = new QProcess(); + QStringList a; + QString offset; + a << "--get-filename" << "-o" << "%(title)s" << _url; + + p->start(_exe, a, QIODevice::ReadOnly); + if(!p->waitForStarted()) + return ""; + + + p->waitForReadyRead(); + offset = p->readAll(); + return offset; +} + +bool YtProcess::test(const QString &e) +{ + QProcess* p = new QProcess(); + QStringList a; + a << "--get-filename" << "--restrict-filenames" << "-o" << "%(title)s" << "BaW_jenozKc"; + + p->start(e, a, QIODevice::ReadOnly); + if(!p->waitForStarted()) + return false; + + QString b; + while(p->waitForReadyRead()) + b.append(p->readAll()); + + return b == "youtube-dl_test_video_a\n"; +} + +void YtProcess::setExecutable(const QString &_exe) { exe = _exe; } +void YtProcess::setArguments(const QStringList& _args) { args = _args; } +void YtProcess::setUrl(const QUrl &_url) { u = _url; } +void YtProcess::setOutputFile(const QString &out) { of = out; } + +QString YtProcess::executable() { return exe; } +QStringList YtProcess::arguments() { return args; } +QUrl YtProcess::url() { return u; } +QString YtProcess::outputfile() { return of; } diff --git a/src/plugins/YoutubeDownload/ytsettings.h b/src/plugins/YoutubeDownload/ytsettings.h new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/ytsettings.h @@ -0,0 +1,36 @@ +#ifndef YTSETTINGS_H +#define YTSETTINGS_H + +#include +#include "ytinterface.h" +#include "ytprocess.h" + +namespace Ui { +class YtSettings; +} + +class YtSettings : public QDialog +{ + Q_OBJECT + +public: + explicit YtSettings(YtInterface *plugin, QWidget *parent = nullptr); + ~YtSettings(); + +private Q_SLOTS: + + void on_buttonBox_accepted(); + + void on_buttonBox_rejected(); + + void selectFile(); + void selectDir(); + void changeExec(const QString& file); + +private: + + Ui::YtSettings *ui; + YtInterface *m_plugin; +}; + +#endif diff --git a/src/plugins/YoutubeDownload/ytsettings.cpp b/src/plugins/YoutubeDownload/ytsettings.cpp new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/ytsettings.cpp @@ -0,0 +1,87 @@ +#include "ui_ytsettings.h" +#include "ytsettings.h" +#include +#include +#include +#include +#include + +YtSettings::YtSettings(YtInterface *plugin, QWidget *parent) : QDialog(parent), ui(new Ui::YtSettings), m_plugin(plugin) +{ + setAttribute(Qt::WA_DeleteOnClose); + ui->setupUi(this); + + m_plugin->loadSettings(); + + ui->comboAudioExtention->setCurrentText(m_plugin->s_formataudio); + ui->comboVideoExtention->setCurrentText(m_plugin->s_formatvideo); + ui->lineExec->setText(m_plugin->s_executable); + ui->lineDefaultDir->setText(m_plugin->s_defaultdir); + ui->checkMetadata->setChecked(m_plugin->s_metadata); + ui->checkSubtitle->setChecked(m_plugin->s_subtitle); + ui->checkThumbnail->setChecked(m_plugin->s_thumbnail); + ui->checkAskAlways->setChecked(m_plugin->s_askalways); + ui->checkMetadata->setChecked(m_plugin->s_metadata); + ui->checkDebug->setChecked(m_plugin->s_debug); + ui->checkExtract->setChecked(m_plugin->s_extractaudio); + ui->checkAskAlwaysFile->setChecked(m_plugin->s_askalwaysfile); + ui->checkProxy->setChecked(m_plugin->s_useproxy); + ui->sliderQuality->setValue(m_plugin->s_audioquality); + + connect(ui->buttonBox, SIGNAL(accepted()), this, SLOT(on_buttonBox_accepted())); + connect(ui->buttonBox, SIGNAL(rejected()), this, SLOT(on_buttonBox_rejected())); + connect(ui->toolExecutable, SIGNAL(clicked(bool)), this, SLOT(selectFile())); + connect(ui->toolDir, SIGNAL(clicked(bool)), this, SLOT(selectDir())); + connect(ui->lineExec, SIGNAL(textChanged(QString)), this, SLOT(changeExec(QString))); +} + +YtSettings::~YtSettings() +{ + delete ui; +} + +void YtSettings::on_buttonBox_accepted() +{ + if(!ui->lineExec->text().isEmpty()) + m_plugin->s_executable = ui->lineExec->text(); + + m_plugin->s_metadata = ui->checkMetadata->isChecked(); + m_plugin->s_subtitle = ui->checkSubtitle->isChecked(); + m_plugin->s_thumbnail = ui->checkThumbnail->isChecked(); + m_plugin->s_askalways = ui->checkAskAlways->isChecked(); + m_plugin->s_debug = ui->checkDebug->isChecked(); + m_plugin->s_extractaudio = ui->checkExtract->isChecked(); + m_plugin->s_askalwaysfile = ui->checkAskAlwaysFile->isChecked(); + m_plugin->s_useproxy = ui->checkProxy->isChecked(); + + m_plugin->s_formatvideo = ui->comboVideoExtention->currentText(); + m_plugin->s_formataudio = ui->comboAudioExtention->currentText(); + m_plugin->s_defaultdir = ui->lineDefaultDir->text(); + m_plugin->s_audioquality = ui->sliderQuality->value(); + + m_plugin->saveSettings(); + accept(); +} + +void YtSettings::on_buttonBox_rejected() +{ + reject(); +} + +void YtSettings::selectFile() +{ + ui->lineExec->setText(QFileDialog::getOpenFileName(this, "Select youtube-dl executable file.", m_plugin->s_executable)); +} + +void YtSettings::selectDir() +{ + ui->lineDefaultDir->setText(QFileDialog::getExistingDirectory(this, "Select download directory.", m_plugin->s_defaultdir)); +} + +void YtSettings::changeExec(const QString& file) +{ + if(!YtProcess::test(file)) + ui->lineExec->setStyleSheet("QLineEdit { color : red; }"); + else + ui->lineExec->setStyleSheet(""); +} diff --git a/src/plugins/YoutubeDownload/ytsettings.ui b/src/plugins/YoutubeDownload/ytsettings.ui new file mode 100644 --- /dev/null +++ b/src/plugins/YoutubeDownload/ytsettings.ui @@ -0,0 +1,260 @@ + + + YtSettings + + + + 0 + 0 + 460 + 344 + + + + Youtube Download Settings + + + + + + + + + m4a + + + + + aac + + + + + flac + + + + + mp3 + + + + + opus + + + + + vorbis + + + + + wav + + + + + + + + 10 + + + 5 + + + Qt::Horizontal + + + + + + + + mp4 + + + + + avi + + + + + flv + + + + + ogg + + + + + webm + + + + + mkv + + + + + + + + Audio Quality + + + Qt::AlignCenter + + + + + + + Audio Format + + + Qt::AlignCenter + + + + + + + Video Format + + + Qt::AlignCenter + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + /bin/youtube-dl + + + + + + + ... + + + + + + + Youtube-dl executable path + + + Qt::AlignCenter + + + + + + + + + + + Add Metadata + + + + + + + Enable Debug + + + + + + + Add Thumbnail + + + + + + + Ask always settings + + + + + + + Add Subtitle (ENG) + + + + + + + Extract Audio + + + + + + + Ask always output filename + + + + + + + Use Proxy + + + + + + + + + + + + + + ... + + + + + + + Default download location + + + Qt::AlignCenter + + + + + + + + + +