diff --git a/.cargo/config.toml b/.cargo/config.toml index 8f02876..81e15f9 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ [net] -offline = true +# offline = true diff --git a/Cargo.lock b/Cargo.lock index bdef7aa..cf6689b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,6 +246,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + [[package]] name = "jobserver" version = "0.1.25" @@ -275,6 +281,7 @@ dependencies = [ "libsqlite3-sys", "serde", "serde_derive", + "youtube_dl", ] [[package]] @@ -297,6 +304,15 @@ dependencies = [ "cc", ] +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.5.0" @@ -403,6 +419,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + [[package]] name = "scratch" version = "1.0.2" @@ -414,6 +436,9 @@ name = "serde" version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] [[package]] name = "serde_derive" @@ -426,6 +451,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "syn" version = "1.0.109" @@ -506,6 +542,15 @@ dependencies = [ "nom", ] +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -608,3 +653,15 @@ name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "youtube_dl" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3525e9e8fe49f1c976a6d1ed2878b2637a0c1d202ef2a32529ad4b64f4b73f" +dependencies = [ + "log", + "serde", + "serde_json", + "wait-timeout", +] diff --git a/Cargo.nix b/Cargo.nix index c1d02dc..f06153a 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -369,6 +369,13 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.6" = overridableMkRustCrate (profileName: rec { + name = "itoa"; + version = "1.0.6"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"; }; + }); + "registry+https://github.com/rust-lang/crates.io-index".jobserver."0.1.25" = overridableMkRustCrate (profileName: rec { name = "jobserver"; version = "0.1.25"; @@ -405,6 +412,7 @@ in libsqlite3_sys = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libsqlite3-sys."0.24.2" { inherit profileName; }; serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.152" { inherit profileName; }; serde_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_derive."1.0.152" { profileName = "__noProfile"; }; + youtube_dl = rustPackages."registry+https://github.com/rust-lang/crates.io-index".youtube_dl."0.8.0" { inherit profileName; }; }; buildDependencies = { cxx_qt_build = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".cxx-qt-build."0.5.1" { profileName = "__noProfile"; }; @@ -445,6 +453,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".log."0.4.17" = overridableMkRustCrate (profileName: rec { + name = "log"; + version = "0.4.17"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"; }; + dependencies = { + cfg_if = rustPackages."registry+https://github.com/rust-lang/crates.io-index".cfg-if."1.0.0" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".memchr."2.5.0" = overridableMkRustCrate (profileName: rec { name = "memchr"; version = "2.5.0"; @@ -598,6 +616,13 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.13" = overridableMkRustCrate (profileName: rec { + name = "ryu"; + version = "1.0.13"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"; }; + }); + "registry+https://github.com/rust-lang/crates.io-index".scratch."1.0.2" = overridableMkRustCrate (profileName: rec { name = "scratch"; version = "1.0.2"; @@ -612,8 +637,13 @@ in src = fetchCratesIo { inherit name version; sha256 = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"; }; features = builtins.concatLists [ [ "default" ] + [ "derive" ] + [ "serde_derive" ] [ "std" ] ]; + dependencies = { + serde_derive = buildRustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_derive."1.0.152" { profileName = "__noProfile"; }; + }; }); "registry+https://github.com/rust-lang/crates.io-index".serde_derive."1.0.152" = overridableMkRustCrate (profileName: rec { @@ -631,6 +661,22 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.95" = overridableMkRustCrate (profileName: rec { + name = "serde_json"; + version = "1.0.95"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744"; }; + features = builtins.concatLists [ + [ "default" ] + [ "std" ] + ]; + dependencies = { + itoa = rustPackages."registry+https://github.com/rust-lang/crates.io-index".itoa."1.0.6" { inherit profileName; }; + ryu = rustPackages."registry+https://github.com/rust-lang/crates.io-index".ryu."1.0.13" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.152" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".syn."1.0.109" = overridableMkRustCrate (profileName: rec { name = "syn"; version = "1.0.109"; @@ -736,6 +782,16 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".wait-timeout."0.2.0" = overridableMkRustCrate (profileName: rec { + name = "wait-timeout"; + version = "0.2.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"; }; + dependencies = { + ${ if hostPlatform.isUnix then "libc" else null } = rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.138" { inherit profileName; }; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".wasi."0.11.0+wasi-snapshot-preview1" = overridableMkRustCrate (profileName: rec { name = "wasi"; version = "0.11.0+wasi-snapshot-preview1"; @@ -879,4 +935,20 @@ in src = fetchCratesIo { inherit name version; sha256 = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"; }; }); + "registry+https://github.com/rust-lang/crates.io-index".youtube_dl."0.8.0" = overridableMkRustCrate (profileName: rec { + name = "youtube_dl"; + version = "0.8.0"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "fd3525e9e8fe49f1c976a6d1ed2878b2637a0c1d202ef2a32529ad4b64f4b73f"; }; + features = builtins.concatLists [ + [ "default" ] + ]; + dependencies = { + log = rustPackages."registry+https://github.com/rust-lang/crates.io-index".log."0.4.17" { inherit profileName; }; + serde = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.152" { inherit profileName; }; + serde_json = rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.95" { inherit profileName; }; + wait_timeout = rustPackages."registry+https://github.com/rust-lang/crates.io-index".wait-timeout."0.2.0" { inherit profileName; }; + }; + }); + } diff --git a/Cargo.toml b/Cargo.toml index da3dcf2..bb17ffc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ cxx-qt-lib = "0.5.1" dirs = "5.0.0" diesel = { version = "2.0.3", features = ["sqlite"] } libsqlite3-sys = { version = ">=0.17.2, <0.26.0", features = ["bundled"] } +youtube_dl = "0.8.0" # ffmpeg-next = "6.0.0" # cxx-qt-build generates C++ code from the `#[cxx_qt::bridge]` module diff --git a/build.rs b/build.rs index 4d2ad88..7b087e2 100644 --- a/build.rs +++ b/build.rs @@ -10,5 +10,6 @@ fn main() { .file("src/rust/image_model.rs") .file("src/rust/video_model.rs") .file("src/rust/presentation_model.rs") + .file("src/rust/ytdl.rs") .build(); } diff --git a/src/main.cpp b/src/main.cpp index a57ddf3..a772fce 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -53,6 +53,7 @@ #include "cxx-qt-gen/slide_obj.cxxqt.h" #include "cxx-qt-gen/slide_model.cxxqt.h" #include "cxx-qt-gen/settings.cxxqt.h" +#include "cxx-qt-gen/ytdl.cxxqt.h" // #include "cxx-qt-gen/image_model.cxxqt.h" static QWindow *windowFromEngine(QQmlApplicationEngine *engine) @@ -203,6 +204,7 @@ int main(int argc, char *argv[]) qmlRegisterType("org.presenter", 1, 0, "ImageSqlModel"); qmlRegisterType("org.presenter", 1, 0, "PresentationSqlModel"); qmlRegisterType("org.presenter", 1, 0, "FileHelper"); + qmlRegisterType("org.presenter", 1, 0, "Ytdl"); qmlRegisterType("org.presenter", 1, 0, "ServiceThing"); qmlRegisterType("org.presenter", 1, 0, "SlideHelper"); qmlRegisterSingletonInstance("org.presenter", 1, 0, diff --git a/src/qml/presenter/Library.qml b/src/qml/presenter/Library.qml index e0e8c7a..38bf09d 100644 --- a/src/qml/presenter/Library.qml +++ b/src/qml/presenter/Library.qml @@ -68,6 +68,7 @@ Item { count: innerModel.count() newItemFunction: (function() { videoProxyModel.setFilterRegularExpression(""); + newVideo.open(); }) deleteItemFunction: (function(rows) { videoProxyModel.deleteVideos(rows) @@ -75,6 +76,19 @@ Item { } + Presenter.NewVideo { + id: newVideo + } + + Timer { + id: videoDLTimer + interval: 3000 + running: !newVideo.sheetOpen + onTriggered: { + newVideo.clear(); + } + } + Presenter.LibraryItem { id: imageLibrary Layout.alignment: Qt.AlignTop diff --git a/src/qml/presenter/NewVideo.qml b/src/qml/presenter/NewVideo.qml new file mode 100644 index 0000000..25c14a7 --- /dev/null +++ b/src/qml/presenter/NewVideo.qml @@ -0,0 +1,133 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 as Controls +import QtQuick.Dialogs 1.3 +import QtQuick.Layouts 1.15 +import QtGraphicalEffects 1.15 +import org.kde.kirigami 2.13 as Kirigami +import "./" as Presenter +import org.presenter 1.0 +import Qt.labs.settings 1.0 + +Kirigami.OverlaySheet { + id: root + + property bool ytdlLoaded: false + + header: Kirigami.Heading { + text: "Add a new video" + } + + ColumnLayout { + Controls.ToolBar { + id: toolbar + Layout.fillWidth: true + RowLayout { + anchors.fill: parent + Controls.Label { + id: videoInputLabel + text: "Enter a video" + } + + Controls.TextField { + id: videoInput + Layout.fillWidth: true + hoverEnabled: true + placeholderText: "Enter a video url..." + text: "" + onEditingFinished: videoInput.text.startsWith("http") ? loadVideo() : showPassiveNotification("Coach called, this isn't it."); + } + } + } + + Item { + id: centerItem + Layout.preferredHeight: Kirigami.Units.gridUnit * 25 + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + visible: true + + Controls.BusyIndicator { + id: loadingIndicator + anchors.centerIn: parent + running: ytdl.loading + } + + Ytdl { + id: ytdl + loaded: false + loading: false + } + + /* Rectangle { */ + /* color: "blue" */ + /* anchors.fill: parent */ + /* } */ + ColumnLayout { + id: loadedItem + anchors.fill: parent + visible: ytdl.loaded + Image { + id: thumbnailImage + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + Layout.fillWidth: true + Layout.preferredHeight: width * 9 / 16 + source: ytdl.thumbnail + fillMode: Image.PreserveAspectFit + clip: true + + Item { + id: mask + anchors.fill: thumbnailImage + visible: false + + Rectangle { + color: "white" + radius: 20 + anchors.centerIn: parent + width: thumbnailImage.paintedWidth + height: thumbnailImage.paintedHeight + } + } + OpacityMask { + anchors.fill: thumbnailImage + source: thumbnailImage + maskSource: mask + } + + } + Item { + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.fillWidth: true + Layout.fillHeight: true + Controls.Label { + id: videoTitle + text: ytdl.title + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + Controls.Button { + anchors.right: parent.right + text: "Ok" + onClicked: { clear(); root.close();} + } + } + } + + } + } + + function loadVideo() { + if (ytdl.getVideo(videoInput.text)) + loadingIndicator.visible = true; + } + + function clear() { + ytdl.title = ""; + ytdl.thumbnail = ""; + ytdl.loaded = false; + ytdl.loading = false; + } +} diff --git a/src/qml/presenter/VideoEditor.qml b/src/qml/presenter/VideoEditor.qml index 3eebb0d..0dc90a1 100644 --- a/src/qml/presenter/VideoEditor.qml +++ b/src/qml/presenter/VideoEditor.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts 1.15 import org.kde.kirigami 2.13 as Kirigami import "./" as Presenter import mpv 1.0 +import org.presenter 1.0 Item { id: root diff --git a/src/resources.qrc b/src/resources.qrc index 200bc17..ac9a49b 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -23,6 +23,7 @@ qml/presenter/PreviewSlide.qml qml/presenter/Settings.qml qml/presenter/RangedSlider.qml + qml/presenter/NewVideo.qml assets/parallel.jpg assets/black.jpg diff --git a/src/rust/lib.rs b/src/rust/lib.rs index f8c6a1e..6bf6e44 100644 --- a/src/rust/lib.rs +++ b/src/rust/lib.rs @@ -8,4 +8,5 @@ mod settings; mod slide_model; mod slide_obj; pub mod video_model; +pub mod ytdl; // mod video_thumbnail; diff --git a/src/rust/ytdl.rs b/src/rust/ytdl.rs new file mode 100644 index 0000000..b7c0147 --- /dev/null +++ b/src/rust/ytdl.rs @@ -0,0 +1,69 @@ +#[cxx_qt::bridge] +mod ytdl { + use dirs; + use std::{fs, thread}; + use youtube_dl::YoutubeDl; + + unsafe extern "C++" { + include!("cxx-qt-lib/qurl.h"); + type QUrl = cxx_qt_lib::QUrl; + include!("cxx-qt-lib/qstring.h"); + type QString = cxx_qt_lib::QString; + } + + #[derive(Clone, Default)] + #[cxx_qt::qobject] + pub struct Ytdl { + #[qproperty] + title: QString, + #[qproperty] + thumbnail: QUrl, + #[qproperty] + loaded: bool, + #[qproperty] + loading: bool, + } + + impl qobject::Ytdl { + #[qinvokable] + pub fn get_video(mut self: Pin<&mut Self>, url: QUrl) -> bool { + if !url.is_valid() { + false + } else { + let data_dir = dirs::data_local_dir().unwrap(); + if let Some(mut data_dir) = dirs::data_local_dir() { + data_dir.push("librepresenter"); + data_dir.push("ytdl"); + if !data_dir.exists() { + fs::create_dir(&data_dir); + } + println!("{:?}", data_dir); + } + self.as_mut().set_loading(true); + let thread = self.qt_thread(); + thread::spawn(move || { + let url = url.to_string(); + let output_dirs = "/home/chris/Videos/"; + let ytdl = YoutubeDl::new(url) + .socket_timeout("15") + .output_directory(output_dirs) + .download(true) + .run() + .unwrap(); + let output = ytdl.into_single_video().unwrap(); + println!("{:?}", output.title); + println!("{:?}", output.thumbnail); + let title = QString::from(&output.title); + let thumbnail = QUrl::from(&output.thumbnail.unwrap()); + thread.queue(move |mut qobject_ytdl| { + qobject_ytdl.as_mut().set_loaded(true); + qobject_ytdl.as_mut().set_loading(false); + qobject_ytdl.as_mut().set_title(title); + qobject_ytdl.as_mut().set_thumbnail(thumbnail); + }) + }); + true + } + } + } +}