Un inicio

This commit is contained in:
Ruben Garcalia
2026-08-07 04:35:28 +02:00
parent 2e80cb5c01
commit 4d1232821a
13 changed files with 519 additions and 0 deletions

2
.clangd Normal file
View File

@@ -0,0 +1,2 @@
CompileFlags:
Remove: [-mno-direct-extern-access]

4
.gitignore vendored
View File

@@ -11,6 +11,10 @@
*.dll *.dll
*.dylib *.dylib
build/
.cache/
# Qt-es # Qt-es
object_script.*.Release object_script.*.Release
object_script.*.Debug object_script.*.Debug

44
CMakeLists.txt Normal file
View File

@@ -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
)

View File

@@ -1,2 +1,4 @@
# youtube-playlist # 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

127
lib/logger.hpp Normal file
View File

@@ -0,0 +1,127 @@
#pragma once
#include <QtGlobal>
#include <algorithm>
#include <cstdlib>
#include <format>
#include <iostream>
#include <ostream>
#include <qcoreapplication.h>
#include <string>
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<int>(LogLevel::DEBUG_LEVEL_INFO) <
static_cast<int>(getCurrentLevel())) {
return;
}
std::cout << "\033[32m[Info]\033[0m " << message << std::endl;
}
inline void warn(std::string_view message) {
if (static_cast<int>(LogLevel::DEBUG_LEVEL_WARN) <
static_cast<int>(getCurrentLevel())) {
return;
}
std::clog << "\033[33m[Warn]\033[0m " << message << std::endl;
}
inline void error(std::string_view message) {
if (static_cast<int>(LogLevel::DEBUG_LEVEL_ERROR) <
static_cast<int>(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<int>(msgLevel) < static_cast<int>(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();
}
}

View File

@@ -0,0 +1,60 @@
#pragma once
#include <QObject>
#include <QString>
#include <QUrl>
#include <format>
#include <qcoreapplication.h>
#include <vector>
#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<QString> m_queue = std::vector<QString>();
QUrl m_currentVideoSource = QString("");
};

35
src/main.cpp Normal file
View File

@@ -0,0 +1,35 @@
#include "logger.hpp"
#include "playlist-controller.hpp"
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QUrl>
#include <filesystem>
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();
}

87
ui/FadingFeedback.qml Normal file
View File

@@ -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";
}
}
}

1
ui/icons/pause.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M176 96C149.5 96 128 117.5 128 144L128 496C128 522.5 149.5 544 176 544L240 544C266.5 544 288 522.5 288 496L288 144C288 117.5 266.5 96 240 96L176 96zM400 96C373.5 96 352 117.5 352 144L352 496C352 522.5 373.5 544 400 544L464 544C490.5 544 512 522.5 512 496L512 144C512 117.5 490.5 96 464 96L400 96z"/></svg>

After

Width:  |  Height:  |  Size: 527 B

1
ui/icons/play.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M187.2 100.9C174.8 94.1 159.8 94.4 147.6 101.6C135.4 108.8 128 121.9 128 136L128 504C128 518.1 135.5 531.2 147.6 538.4C159.7 545.6 174.8 545.9 187.2 539.1L523.2 355.1C536 348.1 544 334.6 544 320C544 305.4 536 291.9 523.2 284.9L187.2 100.9z"/></svg>

After

Width:  |  Height:  |  Size: 470 B

156
ui/main.qml Normal file
View File

@@ -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
}
}
}
}

BIN
ui/media/Puta.mp4 Normal file

Binary file not shown.

BIN
ui/media/video.mp4 Normal file

Binary file not shown.