diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..3621d02 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Remove: [-mno-direct-extern-access] diff --git a/.gitignore b/.gitignore index 5c70465..84e8374 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ *.dll *.dylib +build/ + +.cache/ + # Qt-es object_script.*.Release object_script.*.Debug diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..55da5df --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,44 @@ +cmake_minimum_required(VERSION 3.16) +project(youtube-playlist LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +find_package(Qt6 REQUIRED COMPONENTS Gui Qml Quick Multimedia Svg QuickEffects) + +set(CMAKE_AUTOMOC ON) + +qt_standard_project_setup() + +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") +file(GLOB_RECURSE HEADERS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/lib/*.hpp") + +include_directories("${CMAKE_CURRENT_SOURCE_DIR}/lib") + +qt_add_executable(appyoutube-playlist + ${SOURCES} + ${HEADERS} +) + +qt_add_qml_module(appyoutube-playlist + URI App + VERSION 1.0 + QML_FILES + ui/main.qml + ui/FadingFeedback.qml + RESOURCES + ui/icons/pause.svg + ui/icons/play.svg +) + +target_link_libraries(appyoutube-playlist + PRIVATE + Qt6::Gui + Qt6::Qml + Qt6::Quick + Qt6::Multimedia + Qt6::Svg + Qt6::QuickEffects +) diff --git a/README.md b/README.md index ad44556..8c5226e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # youtube-playlist + +Pues una app sencilla con Qt para reproducir canciones desde youtube sin los cortes de mierda que tiene, para disfrutar de los albums como toca diff --git a/lib/logger.hpp b/lib/logger.hpp new file mode 100644 index 0000000..eeb0aa9 --- /dev/null +++ b/lib/logger.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +enum class LogLevel { + DEBUG_LEVEL_TRACE = 0, + DEBUG_LEVEL_INFO = 1, + DEBUG_LEVEL_WARN = 2, + DEBUG_LEVEL_ERROR = 3, + DEBUG_LEVEL_OFF = 4 +}; + +inline LogLevel getLogLevelFromEnv() { + const char *env = std::getenv("DEBUG_LEVEL"); + if (!env) { + return LogLevel::DEBUG_LEVEL_ERROR; + } + + std::string level(env); + std::transform(level.begin(), level.end(), level.begin(), ::tolower); + + if (level == "trace" || level == "0") + return LogLevel::DEBUG_LEVEL_TRACE; + if (level == "info" || level == "1") + return LogLevel::DEBUG_LEVEL_INFO; + if (level == "warn" || level == "2") + return LogLevel::DEBUG_LEVEL_WARN; + if (level == "error" || level == "3") + return LogLevel::DEBUG_LEVEL_ERROR; + if (level == "off" || level == "4") + return LogLevel::DEBUG_LEVEL_OFF; + + return LogLevel::DEBUG_LEVEL_ERROR; +} + +namespace Logger { + +inline LogLevel getCurrentLevel() { + static const LogLevel currentLevel = getLogLevelFromEnv(); + return currentLevel; +} + +inline void info(std::string_view message) { + if (static_cast(LogLevel::DEBUG_LEVEL_INFO) < + static_cast(getCurrentLevel())) { + return; + } + std::cout << "\033[32m[Info]\033[0m " << message << std::endl; +} + +inline void warn(std::string_view message) { + if (static_cast(LogLevel::DEBUG_LEVEL_WARN) < + static_cast(getCurrentLevel())) { + return; + } + std::clog << "\033[33m[Warn]\033[0m " << message << std::endl; +} + +inline void error(std::string_view message) { + if (static_cast(LogLevel::DEBUG_LEVEL_ERROR) < + static_cast(getCurrentLevel())) { + return; + } + std::cerr << "\033[31m[Error] " << message << "\033[0m" << std::endl; +} + +} // namespace Logger + +inline void MessageHandler(QtMsgType type, const QMessageLogContext &context, + const QString &msg) { + static const LogLevel currentLevel = getLogLevelFromEnv(); + + LogLevel msgLevel; + std::string prefix; + std::string color; + std::string reset = "\033[0m"; + + switch (type) { + case QtDebugMsg: + msgLevel = LogLevel::DEBUG_LEVEL_TRACE; + prefix = "[Trace]"; + color = "\033[36m"; + break; + case QtInfoMsg: + msgLevel = LogLevel::DEBUG_LEVEL_INFO; + prefix = "[Info]"; + color = "\033[32m"; + break; + case QtWarningMsg: + msgLevel = LogLevel::DEBUG_LEVEL_WARN; + prefix = "[Warn]"; + color = "\033[33m"; + break; + case QtCriticalMsg: + msgLevel = LogLevel::DEBUG_LEVEL_ERROR; + prefix = "[Error]"; + color = "\033[31m"; + break; + case QtFatalMsg: + msgLevel = LogLevel::DEBUG_LEVEL_ERROR; + prefix = "[Fatal]"; + color = "\033[35m"; + break; + } + + if (static_cast(msgLevel) < static_cast(currentLevel)) { + return; + } + + std::string context_str = " "; + if (context.file && context.line) { + context_str = std::format(" ({}:{}) ", context.file, context.line); + } + + std::cerr << color << prefix << reset << context_str << msg.toStdString() << std::endl; + + if (type == QtFatalMsg) { + std::abort(); + } +} diff --git a/lib/playlist-controller.hpp b/lib/playlist-controller.hpp new file mode 100644 index 0000000..fcd6705 --- /dev/null +++ b/lib/playlist-controller.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "logger.hpp" + +class PlaylistController : public QObject { + Q_OBJECT + Q_PROPERTY(QUrl currentVideoSource READ currentVideoSource NOTIFY + currentVideoSourceChanged) + +public: + explicit PlaylistController(QObject *parent = nullptr) : QObject(parent) {} + + QUrl currentVideoSource() const { return m_currentVideoSource; } + + Q_INVOKABLE void loadNextVideo() { + if (m_queue.size() == 0) { + Logger::info("Nothing left to play"); + m_playing = false; + return; + } + + QString video = m_queue.front(); + Logger::info(std::format("Playing: {} in video {}", video.toStdString(), 1)); + m_queue.erase(m_queue.begin()); + + QUrl newUrl = QUrl::fromLocalFile(video); + if (m_currentVideoSource != newUrl) { + m_currentVideoSource = newUrl; + emit currentVideoSourceChanged(); + } + } + + // Tengo que rehacer esto en algun momento para que tome la url y la procese en otro thread + // Y ya que me pongo con ello usar el doble buffer y permitir cargar listas enteras + Q_INVOKABLE void addToQueue(const QString path) { + int n_elements = m_queue.size(); + + m_queue.push_back(path); + Logger::info(std::format("Adding: {}", path.toStdString())); + + if (n_elements == 0 && !m_playing) { + m_playing = true; + loadNextVideo(); + } + } + +signals: + void currentVideoSourceChanged(); + +private: + bool m_playing = false; + std::vector m_queue = std::vector(); + QUrl m_currentVideoSource = QString(""); +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..8c09392 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,35 @@ +#include "logger.hpp" +#include "playlist-controller.hpp" +#include +#include +#include +#include +#include + +int main(int argc, char *argv[]) { + qInstallMessageHandler(MessageHandler); + std::filesystem::path cwd = std::filesystem::current_path(); + Logger::info(cwd.string()); + + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + + PlaylistController playlist; + engine.rootContext()->setContextProperty("playlist", &playlist); + + const QUrl url(QStringLiteral("qrc:/App/ui/main.qml")); + + QObject::connect( + &engine, &QQmlApplicationEngine::objectCreationFailed, &app, + []() { QCoreApplication::exit(-1); }, Qt::QueuedConnection); + + engine.load(url); + + Logger::info("Engine loaded"); + + playlist.addToQueue("../ui/media/video.mp4"); + playlist.addToQueue("../ui/media/Puta.mp4"); + + return app.exec(); +} diff --git a/ui/FadingFeedback.qml b/ui/FadingFeedback.qml new file mode 100644 index 0000000..ef08f43 --- /dev/null +++ b/ui/FadingFeedback.qml @@ -0,0 +1,87 @@ +pragma ComponentBehavior: Bound +import QtQuick + +Item { + id: root + anchors.centerIn: parent + + width: 100 + height: width + + opacity: 0 + + function trigger() { + hideTimer.stop(); + root.state = "hidden"; + root.state = "visible"; + hideTimer.start(); + } + + function get_opacity() { + return root.opacity + } + + function hide() { + root.opacity = 0; + root.state = "hidden"; + } + + // IconImage { + // id: innerIcon + // anchors.fill: parent + // source: videoPlayer.playbackState === MediaPlayer.PlayingState ? "qrc:/App/ui/icons/play.svg" : "qrc:/App/ui/icons/pause.svg" + // color: "white" + // fillMode: Image.PreserveAspectFit + // + // layer.enabled: true + // layer.effect: MultiEffect { + // shadowEnabled: true + // shadowColor: "black" + // shadowBlur: 0.5 + // } + // } + + states: [ + State { + name: "visible" + PropertyChanges { + root.opacity: 1 + } + }, + State { + name: "hidden" + PropertyChanges { + root.opacity: 0 + } + } + ] + + transitions: [ + Transition { + from: "hidden" + to: "visible" + NumberAnimation { + properties: "opacity" + duration: 150 + easing.type: Easing.OutCubic + } + }, + Transition { + from: "visible" + to: "hidden" + NumberAnimation { + properties: "opacity" + duration: 400 + easing.type: Easing.InOutCubic + } + } + ] + Timer { + id: hideTimer + interval: 800 + repeat: false + onTriggered: { + root.state = "hidden"; + } + } +} diff --git a/ui/icons/pause.svg b/ui/icons/pause.svg new file mode 100644 index 0000000..a410811 --- /dev/null +++ b/ui/icons/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/icons/play.svg b/ui/icons/play.svg new file mode 100644 index 0000000..31acd57 --- /dev/null +++ b/ui/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/main.qml b/ui/main.qml new file mode 100644 index 0000000..7d53b78 --- /dev/null +++ b/ui/main.qml @@ -0,0 +1,156 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls.impl +import QtMultimedia +import QtQuick.Effects + +Window { + id: window + width: 800 + height: 600 + visible: true + title: "YouAlbumTube" + color: "#1e1e2e" + + function togglePause() { + videoPlayer.playbackState == MediaPlayer.PlayingState ? videoPlayer.pause() : videoPlayer.play(); + + seekFeedback.hide(); + playFeedback.trigger(); + } + + property int times: 0 + property int dir: 0 + + function seek(time) { + let opacity = seekFeedback.get_opacity(); + + if (opacity <= 0) { + window.times = 0; + } + + window.times++; + + if (time > 0) { + if (dir < 0) { + window.times = 1; + } + dir = 1; + + videoPlayer.position = Math.min(videoPlayer.duration, videoPlayer.position + time); + innerText.text = "+" + (5 * window.times); + } else if (time <= 0) { + if (dir > 0) { + window.times = 1; + } + dir = -1; + + videoPlayer.position = Math.max(0, videoPlayer.position + time); + innerText.text = "-" + (5 * window.times); + } + + playFeedback.hide(); + seekFeedback.trigger(); + } + + Shortcut { + sequence: "Space" + onActivated: window.togglePause() + } + + Shortcut { + sequence: "Left" + onActivated: window.seek(-5000) + } + + Shortcut { + sequence: "Right" + onActivated: window.seek(5000) + } + + MouseArea { + anchors.fill: parent + onClicked: window.togglePause() + } + + Text { + anchors.centerIn: parent + text: "No content\nAdd it to playlist!" + font.pixelSize: 32 + font.bold: true + color: "#cdd6f4" + horizontalAlignment: Text.AlignHCenter + } + + // Estudiar el doble buffer, parece ser no tan coñazo como podria esperar + // mañana lo intento implementar + Video { + id: videoPlayer + anchors.fill: parent + anchors.centerIn: parent + source: playlist.currentVideoSource + visible: true + loops: 0 + playbackRate: 1 + + onSourceChanged: { + console.log("Now playing:", source); + if (source.toString() !== "") { + videoPlayer.play(); + } + } + + onStopped: { + playlist.loadNextVideo(); + } + + onVisibleChanged: { + console.log("Cambio la visibilidad"); + } + } + + FadingFeedback { + id: playFeedback + + IconImage { + id: innerIcon + anchors.centerIn: parent + anchors.fill: parent + source: videoPlayer.playbackState === MediaPlayer.PlayingState ? "qrc:/App/ui/icons/play.svg" : "qrc:/App/ui/icons/pause.svg" + color: "white" + fillMode: Image.PreserveAspectFit + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowColor: "black" + shadowBlur: 0.5 + } + } + } + + FadingFeedback { + id: seekFeedback + + Text { + id: innerText + anchors.fill: parent + anchors.centerIn: parent + text: "" + color: "white" + // fontSizeMode: Text.Fit + horizontalAlignment: Text.AlignHCenter + + font.pixelSize: 72 + font.bold: true + + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + shadowColor: "black" + shadowBlur: 0.5 + } + } + } +} diff --git a/ui/media/Puta.mp4 b/ui/media/Puta.mp4 new file mode 100644 index 0000000..8fca99d Binary files /dev/null and b/ui/media/Puta.mp4 differ diff --git a/ui/media/video.mp4 b/ui/media/video.mp4 new file mode 100644 index 0000000..643dfb9 Binary files /dev/null and b/ui/media/video.mp4 differ