9 Commits

Author SHA1 Message Date
Dark Steveneq
5ac6248c66 better and better 2025-10-20 02:53:30 +02:00
Dark Steveneq
c168086601 pretty minor case of suffering 2025-10-19 22:35:30 +02:00
629408b02e Merge pull request 'heugh jazz' (#1) from fuckedupgit into master
Reviewed-on: ghostfox/qyouvideo#1
2025-10-19 03:01:51 +02:00
Dark Steveneq
902693bd40 heugh jazz 2025-10-19 02:58:12 +02:00
Dark Steveneq
e66cf1b137 Reapply "dwjakld"
This reverts commit e091c97305573c5c15e9073b999944736f370562.
2025-10-18 23:34:54 +02:00
Dark Steveneq
e091c97305 Revert "dwjakld"
This reverts commit 5c9b0ec49ac44159deb26f1aa0aea916f56d45d9.
2025-10-18 23:33:51 +02:00
Dark Steveneq
5c9b0ec49a dwjakld 2025-10-18 23:01:10 +02:00
Dark Steveneq
c91fba3ee2 plhabj workding 2025-10-18 23:00:53 +02:00
Dark Steveneq
6fc0736c2d fist comin 2025-10-18 21:40:02 +02:00
25 changed files with 744 additions and 782 deletions

View File

@@ -72,7 +72,7 @@ jobs:
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v3
with: with:
aqtversion: '==3.1.*' aqtversion: '==3.1.*'
version: '6.10.0' version: '6.8.0'
host: 'windows' host: 'windows'
target: 'desktop' target: 'desktop'
arch: 'win64_msvc2022_64' arch: 'win64_msvc2022_64'

1
.gitignore vendored
View File

@@ -32,6 +32,7 @@ qrc_*.cpp
Thumbs.db Thumbs.db
*.res *.res
*.rc *.rc
!resources/qyouvideo.rc
/.qmake.cache /.qmake.cache
/.qmake.stash /.qmake.stash

2
.vscode/launch.json vendored
View File

@@ -8,7 +8,7 @@
"name": "(gdb) Launch", "name": "(gdb) Launch",
"type": "cppdbg", "type": "cppdbg",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/build/appqyouradio", "program": "${workspaceFolder}/build/appqyouvideo",
"args": [], "args": [],
"stopAtEntry": false, "stopAtEntry": false,
"cwd": "${workspaceFolder}/build", "cwd": "${workspaceFolder}/build",

View File

@@ -1,49 +1,45 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) project(qyouvideo VERSION 0.1 LANGUAGES CXX)
project(qyouradio VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Core Quick Multimedia) find_package(Qt6 REQUIRED COMPONENTS Core Quick Multimedia)
find_package(DiscordSDK)
qt_standard_project_setup(REQUIRES 6.8) qt_standard_project_setup(REQUIRES 6.8)
configure_file(buildFlags.h.in buildFlags.h) qt_add_executable(appqyouvideo
qt_add_executable(appqyouradio
main.cpp main.cpp
buildFlags.h
player.cpp player.h player.cpp player.h
resources/qyouradio.rc resources/qyouvideo.rc
resources/resource.h resources/resource.h
) )
qt_add_resources(appqyouradio "resources" qt_add_resources(appqyouvideo "resources"
PREFIX "/" PREFIX "/"
FILES FILES
resources/logo.png resources/logo.png
) )
set_source_files_properties(Player.qml set_source_files_properties(ComponentCache.qml
PROPERTIES QT_QML_SINGLETON_TYPE TRUE) PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
qt_add_qml_module(appqyouradio qt_add_qml_module(appqyouvideo
URI qyouradio URI qyouvideo
VERSION 1.0 VERSION 1.0
QML_FILES QML_FILES
Main.qml Main.qml
# Player.qml VideoEntry.qml
ViewPlayer.qml ViewAbout.qml
ViewSettings.qml ViewVideoList.qml
VideoPlayer.qml
ComponentCache.qml
) )
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. # Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an # If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though. # explicit, fixed bundle identifier manually though.
set_target_properties(appqyouradio PROPERTIES set_target_properties(appqyouvideo PROPERTIES
# MACOSX_BUNDLE_GUI_IDENTIFIER com.example.appqyouradio # MACOSX_BUNDLE_GUI_IDENTIFIER com.example.appqyouradio
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
@@ -51,30 +47,24 @@ set_target_properties(appqyouradio PROPERTIES
WIN32_EXECUTABLE TRUE WIN32_EXECUTABLE TRUE
) )
target_include_directories(appqyouradio PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") target_link_libraries(appqyouvideo
target_link_libraries(appqyouradio
PUBLIC Qt6::Quick Qt6::Multimedia PUBLIC Qt6::Quick Qt6::Multimedia
) )
if (DiscordSDK_FOUND)
target_link_libraries(appqyouradio
PRIVATE DiscordSDK::DiscordSDK
)
endif()
include(GNUInstallDirs) include(GNUInstallDirs)
install(TARGETS appqyouradio install(TARGETS appqyouvideo
BUNDLE DESTINATION . BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
) )
qt_generate_deploy_app_script( qt_generate_deploy_app_script(
TARGET appqyouradio TARGET appqyouvideo
OUTPUT_SCRIPT deploy_script OUTPUT_SCRIPT deploy_script
NO_UNSUPPORTED_PLATFORM_ERROR NO_UNSUPPORTED_PLATFORM_ERROR
) )
install(SCRIPT ${deploy_script}) install(SCRIPT ${deploy_script})
add_subdirectory(QYRComponents) add_subdirectory(QYRComponents)

11
ComponentCache.qml Normal file
View File

@@ -0,0 +1,11 @@
pragma Singleton
import QtQuick 6.8
Item {
property var videoPlayer: Qt.createComponent("VideoPlayer.qml")
property var viewVideoList: Qt.createComponent("ViewVideoList.qml")
property var viewAbout: Qt.createComponent("ViewAbout.qml")
Component.onCompleted: Player.unloadVideo()
}

240
Main.qml
View File

@@ -9,193 +9,81 @@ ApplicationWindow {
id: root id: root
width: 1280 width: 1280
height: 800 height: 800
title: qsTr("QYouRadio") title: qsTr("QYouVideo")
Component.onCompleted: function() { flags: Qt.Dialog
console.log(Player.stations) modality: Qt.ApplicationModal
}
ColumnLayout { header: Rectangle {
anchors.fill: parent Layout.fillWidth: true
height: 43
Rectangle { color: Colors.surface0
Layout.fillWidth: true
height: 43
color: Colors.surface0
RowLayout {
anchors.fill: parent
anchors.margins: 3
Label {
Layout.leftMargin: 5
text: "QYouRadio"
heading: "h1"
// font.bold: true
}
Item {
Layout.leftMargin: 5
width: 320
height: 36
Rectangle {
anchors.fill: parent
id: nowplaying_root
// visible: Player.currentIndex != null
visible: anchors.bottomMargin < 42
color: Colors.primary
radius: 5
clip: true
NumberAnimation {
running: Player.playing && tabbar.currentIndex != Player.currentIndex && nowplaying_root.anchors.bottomMargin >= 42
target: nowplaying_root
property: "anchors.bottomMargin"
from: 42
to: 0
duration: 145
}
NumberAnimation {
running: !Player.playing || (tabbar.currentIndex == Player.currentIndex && nowplaying_root.anchors.bottomMargin < 42)
target: nowplaying_root
property: "anchors.bottomMargin"
from: 0
to: 42
duration: 145
}
Label {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 1
anchors.leftMargin: 4
anchors.rightMargin: 4
text: (Player.loading ? "Loading " : "Playing ") + qsTr(Player.currentStation.name)
heading: "h5"
}
Label {
id: nowplaying_title
anchors.bottom: parent.bottom
anchors.left: parent.left
// anchors.right: parent.right
anchors.bottomMargin: 1
anchors.leftMargin: 4
// anchors.rightMargin: 4
text: qsTr(Player.currentStation.songTitle)
heading: "h3"
SequentialAnimation {
running: nowplaying_title.width > 320
loops: Animation.Infinite
NumberAnimation { target: nowplaying_title; property: "anchors.leftMargin"; from: 4; to: 4; duration: 10000 }
NumberAnimation { target: nowplaying_title; property: "anchors.leftMargin"; from: 4; to: -nowplaying_title.width + 324; duration: nowplaying_title.width * 4 }
NumberAnimation { target: nowplaying_title; property: "anchors.leftMargin"; from: -nowplaying_title.width + 324; to: -nowplaying_title.width + 324; duration: 10000 }
NumberAnimation { target: nowplaying_title; property: "anchors.leftMargin"; from: -nowplaying_title.width + 324; to: 4; duration: nowplaying_title.width * 4 }
}
}
}
}
Item {
Layout.fillWidth: true
}
TabBar {
id: tabbar
spacing: 10
background: Item{}
TabButton {
text: qsTr("Autoradio")
}
TabButton {
text: qsTr("Live Mix")
}
TabButton {
text: qsTr("Deep Bass")
}
TabButton {
text: qsTr("Settings")
}
}
}
}
SwipeView {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: 20
interactive: false
currentIndex: tabbar.currentIndex
Loader {
// active: tabbar.currentIndex == 0
active: true
asynchronous: true
visible: status == Loader.Ready
sourceComponent: ViewPlayer {
index: 0
}
}
Loader {
// active: tabbar.currentIndex == 1
active: true
asynchronous: true
visible: status == Loader.Ready
sourceComponent: ViewPlayer {
index: 1
}
}
Loader {
// active: tabbar.currentIndex == 2
active: true
asynchronous: true
visible: status == Loader.Ready
sourceComponent: ViewPlayer {
index: 2
}
}
Component.onCompleted: contentItem.highlightMoveDuration = 160;
}
RowLayout { RowLayout {
Layout.fillWidth: false anchors.fill: parent
Layout.alignment: Qt.AlignHCenter anchors.margins: 3
Layout.topMargin: 30
Layout.bottomMargin: 30
width: 220
Button { Button {
Layout.rightMargin: 5 // visible: stack.depth > 1
text: Player.loading ? "Loading" : (Player.playing ? "Pause" : "Play") text: "Back"
onClicked: stack.popCurrentItem()
onClicked: function() {
if (Player.playing) {
Player.stopPlaying();
} else {
Player.startPlaying(tabbar.currentIndex);
}
}
} }
Slider { Label {
Layout.fillWidth: true id: logo
// visible: stack.currentItem.StackView.index == 0
Layout.leftMargin: 5 Layout.leftMargin: 5
from: 0.1 text: "QYouVideo"
to: 1.1 heading: "h1"
stepSize: 0.05 clip: true
value: Player.volume + 0.1
onMoved: Player.volume = value - 0.1
} }
Item {
Layout.fillWidth: true
}
Button {
text: qsTr("Videos")
outlined: true
implicitWidth: 80
onClicked: if (!(stack.currentItem instanceof ViewVideoList)) stack.push(ComponentCache.viewVideoList.createObject(stack))
}
Button {
text: qsTr("About")
outlined: true
implicitWidth: 80
onClicked: if (!(stack.currentItem instanceof ViewAbout)) stack.push(ComponentCache.viewAbout.createObject(stack))
}
Button {
text: qsTr("Upload")
}
}
}
StackView {
id: stack
// __wheelAreaScrollSpeed: 50
anchors.fill: parent
anchors.margins: 20
initialItem: ViewVideoList {}
function openVideo(id) {
// if (stack.find((item, index) => {
// // If found player instance
// if (item.id == id) {
// // stack.pop(index, StackView.Immediate);
// stack.push(item)
// return true;
// }
// return false;
// }) == null) {
// If didn't find player instance
stack.push(ComponentCache.videoPlayer.createObject(stack, {id: id}));
// }
} }
} }
} }

View File

@@ -1,122 +0,0 @@
import QtQuick 6.8
import QtMultimedia 6.8
pragma Singleton
Item {
readonly property string streamURLPrefix: "https://youradio.nonamesoft.xyz/youradio/api/"
readonly property string metadataURL: "https://youradio.nonamesoft.xyz/youradio/api/status-json.xsl"
readonly property var slugLookup: ({
"autoradio": 0,
"live": 1,
"bassboosted": 2
})
property var streams: ([
{
name: "Autoradio",
slug: "autoradio",
title: "",
listeners: 0
},
{
name: "Live Mix",
slug: "live",
title: "",
listeners: 0
},
{
name: "Deep Bass",
slug: "bassboosted",
title: "",
listeners: 0
}
])
property var failedConnAttempts: 0
property alias playing: player.playing
property alias volume: output.volume
property var loading: player.mediaStatus == Qt.LoadingMedia
property var currentIndex: null
property var currentStream: null
function startPlaying(index) {
if ((playing && index == currentIndex) || index < 0 || index >= streams.length) {
return;
}
if (playing) {
player.stop();
}
print("Starting playing stream no. " + index);
currentIndex = index;
currentStream = streams[index];
player.source = streamURLPrefix + currentStream.slug;
player.play();
}
function stopPlaying() {
if (!playing) {
return;
}
print("Stopping playback");
currentIndex = null;
currentStream = null;
player.source = "";
player.stop()
}
MediaPlayer {
id: player
source: ""
audioOutput: AudioOutput {
id: output
volume: 0.3
}
onErrorOccurred: function(error, errorString) {
const index = currentIndex
currentIndex = null
currentIndex = index;
}
}
Timer {
interval: 10000
repeat: true
running: true
triggeredOnStart: true
onTriggered: function() {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
parent.failedConnAttempts = 0;
// try {
const object = JSON.parse(xhr.responseText.toString()).icestats;
object.source.forEach(station => {
const index = parent.slugLookup[station.server_name];
if (index == null) {
console.warn("Unknown slug encountered in metadata: " + station.server_name);
return
}
parent.streams[index].title = station.title.replace(/\[[a-zA-Z0-9]{11}\]/, "");
parent.streams[index].listeners = station.listeners;
if (index == parent.currentIndex) {
parent.currentStream = parent.streams[index];
}
});
// } catch {
// console.error("Failed deserializing metadata response");
// }
}
}
xhr.open("GET", parent.metadataURL);
xhr.timeout = 10000;
xhr.ontimeout = function() {
console.log("Metadata request timed out after 10 seconds");
parent.failedConnAttempts++;
}
xhr.send();
}
}
}

View File

@@ -0,0 +1,33 @@
import QtQuick 6.8
import QtQuick.Controls 6.8
import QtQuick.Controls.Basic 6.8
BusyIndicator {
id: spinner
contentItem: Rectangle {
implicitWidth: 44
implicitHeight: 44
color: "transparent"
radius: 44
border.width: 4
border.color: Colors.primaryAlt
Rectangle {
width: 12
height: 12
anchors.top: parent.top
anchors.topMargin: 8
color: Colors.primary
radius: 12
}
RotationAnimator {
target: spinner
running: spinner.visible && spinner.running
from: 0
to: 360
loops: Animation.Infinite
duration: 1250
}
}
}

View File

@@ -25,6 +25,7 @@ qt_add_qml_module(QYRComponents
Label.qml Label.qml
TabButton.qml TabButton.qml
YouAds.qml YouAds.qml
QML_FILES BusyIndicator.qml
# SOURCES qyrcomponents.cpp qyrcomponents.h # SOURCES qyrcomponents.cpp qyrcomponents.h
) )

View File

@@ -5,6 +5,8 @@ import QtQuick.Controls.Basic 6.8
Slider { Slider {
snapMode: Slider.SnapAlways snapMode: Slider.SnapAlways
property real buffered
implicitWidth: 130 implicitWidth: 130
implicitHeight: 20 implicitHeight: 20
@@ -26,6 +28,17 @@ Slider {
background: Rectangle { background: Rectangle {
color: "#555" color: "#555"
radius: 5 radius: 5
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
visible: parent.parent.buffered != 0
width: parent.width * parent.parent.buffered
color: "#888"
radius: 5
}
} }
handle: Rectangle { handle: Rectangle {

80
VideoEntry.qml Normal file
View File

@@ -0,0 +1,80 @@
import QtQuick 6.8
import QtQuick.Controls 6.8
import QtQuick.Layouts 6.8
import QYRComponents 1.0
Rectangle {
anchors.margins: 10
width: 384
height: 224
color: area.containsMouse ? Colors.surface1 : Colors.surface0
radius: 10
required property string name
required property string id
required property var metadata
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
Image {
Layout.fillWidth: true
Layout.fillHeight: true
asynchronous: true
cache: true
source: "https://youvideo.nonamesoft.xyz/thumbnails/" + parent.parent.id
fillMode: Image.PreserveAspectFit
onStatusChanged: if (status == Image.Error) {
source = "https://youvideo.nonamesoft.xyz/thumbnails/audio.png";
children[0].visible = true
}
// This is a fucking mess
Label {
visible: false
anchors.fill: parent
heading: "h3"
font.bold: true
text: parent.parent.parent.name
wrapMode: Text.Wrap
clip: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
color: Colors.background
width: children[0].paintedWidth
height: children[0].paintedHeight
Label {
anchors.fill: parent
heading: "h4"
// text: new Date(parent.parent.parent.parent.metadata.duration * 1000).toISOString().slice(11, 19)
}
}
}
Label {
heading: "h3"
font.bold: true
text: parent.parent.name
Layout.fillWidth: true
clip: true
}
}
MouseArea {
anchors.fill: parent
id: area
hoverEnabled: true
onClicked: stack.openVideo(parent.id)
}
}

200
VideoPlayer.qml Normal file
View File

@@ -0,0 +1,200 @@
import QtQuick 6.8
import QtMultimedia 6.8
import QtQuick.Controls 6.8
import QtQuick.Layouts 6.8
import QYRComponents 1.0
Item {
// color: Colors.surface1
// radius: 10
StackView.onRemoved: destroy()
required property string id
property bool hasThumbnail: false
property bool loading: true
property bool failed: false
property string title
property string description
property string extension
property real position: 0
property real duration: 0
property real ratio
anchors.fill: parent
Component.onCompleted: Player.onPositionChanged = () => {
console.log("test print");
if (Player.id == id) {
position = Player.position
}
}
Component.onDestruction: if (Player.id == id) Player.unloadVideo()
onPositionChanged: if (Player.id == id) Player.position = position
ColumnLayout {
visible: !loading && !failed
anchors.fill: parent
Image {
Layout.fillWidth: true
// height: width * parent.parent.ratio
asynchronous: true
cache: true
source: parent.parent.hasThumbnail ? ("https://youvideo.nonamesoft.xyz/thumbnails/" + parent.parent.id) : "https://youvideo.nonamesoft.xyz/thumbnails/audio.png"
fillMode: Image.PreserveAspectFit
}
RowLayout {
Button {
text: Player.id == id ? (Player.loading ? "Loading" : (Player.playing ? "Pause" : "Play")) : "Play"
enabled: Player.id != id || !(Player.loading && Player.id == id)
onClicked: function() {
if (Player.id != id) {
Player.loadVideo(parent.parent.parent.id, parent.parent.parent.extension, parent.parent.parent.title, parent.parent.parent.position);
}
if (Player.playing) {
Player.pause();
} else {
Player.play();
}
}
}
Label {
heading: "h4"
text: new Date(parent.parent.parent.position).toISOString().slice(14, 19)
}
Slider {
Layout.fillWidth: true
from: 0
to: parent.parent.parent.duration
value: parent.parent.parent.position
buffered: Player.id == id ? Player.buffered : 0
onMoved: parent.parent.parent.position = value
}
Label {
heading: "h4"
text: new Date(parent.parent.parent.duration).toISOString().slice(14, 19)
}
Label {
text: Player.buffered
}
Slider {
from: 0
to: 1
value: Player.volume
onMoved: Player.volume = value
}
}
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
clip: true
text: parent.parent.title
heading: "h2"
font.bold: true
}
Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
clip: true
text: parent.parent.description
heading: "h4"
font.bold: true
}
Item {
Layout.fillHeight: true
}
}
BusyIndicator {
anchors.centerIn: parent
visible: loading
running: loading
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
anchors.centerIn: parent
visible: failed
Label {
text: "Couldn't get video details!"
heading: "h2"
font.bold: true
}
Label {
text: "This could mean that either your internet or YouVideo is down."
heading: "h4"
}
Label {
Layout.bottomMargin: 25
text: "Check your network connection and try again!"
heading: "h4"
}
Button {
text: "Retry"
onClicked: parent.parent.fetchData()
}
}
Timer {
interval: 0
running: true
onTriggered: parent.fetchData()
}
function fetchData() {
loading = true;
failed = false;
const xhr = new XMLHttpRequest;
xhr.open("GET", "https://youvideo.nonamesoft.xyz/youvideo/video/" + id);
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
loading = false;
if (xhr.status != 200) {
console.log("Invalid response");
failed = true;
return;
}
const data = JSON.parse(xhr.responseText);
console.log("Received video metadata");
title = data.name;
description = data.description;
extension = data.extension;
duration = data.metadata.duration * 1000;
ratio = data.metadata.size[0] / data.metadata.size[1];
if (ratio == NaN) {
ratio = 1.77777;
}
console.log(ratio);
if (Player.id == id) {
console.log("Already playing, this is not intended but possible in its current state so it's a feature, not a bug :3");
return;
}
Player.loadVideo(id, extension, title, position)
}
}
xhr.ontimeout = function() {
loading = false;
failed = true;
console.log("Request timed out");
}
xhr.send();
}
}

59
ViewAbout.qml Normal file
View File

@@ -0,0 +1,59 @@
import QtQuick 6.8
import QtQuick.Controls 6.8
import QtQuick.Layouts 6.8
import QYRComponents 1.0
ColumnLayout {
Label {
heading: "h2"
text: "About"
font.bold: true
}
Label {
heading: "h3"
text: "Youpiter"
font.bold: true
}
Label {
heading: "h4"
text: "Creator of YouVideo"
font.bold: true
}
Label {
heading: "h3"
text: "Ghostfox"
}
Label {
heading: "h4"
text: "Creator of QYouRadio"
}
Label {
heading: "h3"
text: "Ghostfox"
}
Label {
heading: "h4"
text: "Creator of QYouVideo"
}
Label {
heading: "h3"
text: "Qt Group"
}
Label {
heading: "h4"
text: "Creator of Qt, QML and QtQuick"
}
Item {
Layout.fillHeight: true
}
}

View File

@@ -1,51 +0,0 @@
import QtQuick 6.8
import QtQuick.Controls 6.8
import QtQuick.Controls.Basic 6.8
import QtQuick.Layouts 6.8
import QYRComponents 1.0
ColumnLayout {
Layout.fillWidth: true
property var index: null
Label {
Layout.fillWidth: true
Layout.bottomMargin: 20
text: qsTr(Player.stations[index].name)
font.bold: true
heading: "h2"
horizontalAlignment: Text.AlignHCenter
}
Item {
Layout.fillHeight: true
}
Label {
Layout.fillWidth: true
text: Player.stations[index].songTitle
font.pixelSize: 72
horizontalAlignment: Text.AlignHCenter
}
Label {
Layout.fillWidth: true
text: "Listening: " + Player.stations[index].listeners
heading: "h2"
horizontalAlignment: Text.AlignHCenter
}
Item {
Layout.fillHeight: true
}
Label {
Layout.fillWidth: true
Layout.topMargin: 20
visible: (Player.currentIndex > -1 && Player.currentIndex != parent.index)
text: qsTr("Another station is currently playing")
heading: "h2"
horizontalAlignment: Text.AlignHCenter
}
}

View File

@@ -1,180 +0,0 @@
import QtQuick 6.8
import QtQuick.Controls 6.8
import QtQuick.Controls.Basic 6.8
import QtQuick.Layouts 6.8
import QYRComponents 1.0
ApplicationWindow {
width: 840
height: 560
visible: true
title: qsTr("Settings")
RowLayout {
anchors.fill: parent
Container {
id: settingsCategory
Layout.fillWidth: false
Layout.fillHeight: true
Layout.rightMargin: 15
implicitWidth: 240
clip: true
contentItem: ListView {
spacing: 7.5
model: settingsCategory.contentModel
snapMode: ListView.SnapOneItem
orientation: ListView.Vertical
}
Button {
text: qsTr("Appearance")
width: 240
outlined: true
onClicked: settingsCategory.currentIndex = 0
}
Button {
text: qsTr("Language")
width: 240
outlined: true
onClicked: settingsCategory.currentIndex = 1
}
Button {
text: qsTr("Playback Settings")
width: 240
outlined: true
onClicked: settingsCategory.currentIndex = 2
}
Button {
text: qsTr("About")
width: 240
outlined: true
onClicked: settingsCategory.currentIndex = 3
}
}
SwipeView {
Layout.fillWidth: true
Layout.fillHeight: true
orientation: Qt.Vertical
interactive: true
currentIndex: tabbar.currentIndex
Loader {
// active: tabbar.currentIndex == 0
asynchronous: true
visible: status == Loader.Ready
sourceComponent: ColumnLayout {
Label {
text: "Appearance"
heading: "h1"
font.bold: true
}
}
}
Loader {
// active: tabbar.currentIndex == 1
asynchronous: true
visible: status == Loader.Ready
sourceComponent: ColumnLayout {
Label {
text: "Language"
heading: "h1"
font.bold: true
}
}
}
Loader {
// active: tabbar.currentIndex == 2
asynchronous: true
visible: status == Loader.Ready
sourceComponent: ColumnLayout {
Label {
text: "Playback Settings"
heading: "h1"
font.bold: true
}
}
}
Loader {
// active: tabbar.currentIndex == 3
asynchronous: true
visible: status == Loader.Ready
sourceComponent: ColumnLayout {
Label {
text: "About"
heading: "h1"
font.bold: true
}
Label {
text: "YouRadio"
heading: "h2"
font.bold: true
}
Label {
text: "by Youpiter"
heading: "h3"
font.bold: true
}
Label {
text: "Music source"
heading: "base"
font.bold: true
}
Label {
text: "QYouRadio"
heading: "h3"
font.bold: true
}
Label {
text: "by Ghostfox"
heading: "h3"
font.bold: true
}
Label {
text: "Client development"
heading: "base"
font.bold: true
}
Label {
text: "Attribution"
heading: "h1"
font.bold: true
}
Label {
text: "Qt"
heading: "h2"
font.bold: true
}
Label {
text: "by Qt Group Inc."
heading: "h3"
font.bold: true
}
Label {
text: "Open-Source library on which QYouRadio is built uppon, licensed under LGPL-3.0"
heading: "base"
font.bold: true
}
}
}
}
}
}

97
ViewVideoList.qml Normal file
View File

@@ -0,0 +1,97 @@
import QtQuick 6.8
import QtQuick.Controls 6.8
import QtQuick.Layouts 6.8
import QYRComponents 1.0
Item {
anchors.fill: parent
property bool loading: true
property bool failed: false
GridView {
width: Math.floor(parent.width / cellWidth) * cellWidth
visible: !loading || !failed
anchors.fill: parent
anchors.horizontalCenter: parent.horizontalCenter
model: ListModel { id: model }
clip: true
cellWidth: 384
cellHeight: 224
delegate: VideoEntry {}
Component.onCompleted: console.log(parent.width / cellWidth)
}
BusyIndicator {
anchors.centerIn: parent
visible: loading
running: loading
}
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
anchors.centerIn: parent
visible: failed
Label {
text: "Couldn't get videos!"
heading: "h2"
font.bold: true
}
Label {
text: "This could mean that either your internet or YouVideo is down."
heading: "h4"
}
Label {
Layout.bottomMargin: 25
text: "Check your network connection and try again!"
heading: "h4"
}
Button {
text: "Retry"
onClicked: parent.parent.fetchData()
}
}
Timer {
interval: 0
running: true
onTriggered: parent.fetchData()
}
function fetchData() {
model.clear();
loading = true;
failed = false;
const xhr = new XMLHttpRequest;
xhr.open("GET", "https://youvideo.nonamesoft.xyz/youvideo/api/videos");
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
loading = false;
if (xhr.status != 200) {
console.log("Invalid response");
failed = true;
return;
}
const data = JSON.parse(xhr.responseText);
console.log("Received data, found " + data.length + " videos");
data.forEach(video => {
model.append(video);
});
}
}
xhr.ontimeout = function() {
loading = false;
failed = true;
console.log("Request timed out");
}
xhr.send();
}
}

View File

@@ -14,7 +14,7 @@
, wrapQtAppsHook , wrapQtAppsHook
}: }:
stdenv.mkDerivation { stdenv.mkDerivation {
pname = "qyouradio"; pname = "qyouvideo";
version = "1.0"; version = "1.0";
src = ./.; src = ./.;

View File

@@ -1 +0,0 @@
#cmakedefine01 DiscordSDK_FOUND

View File

@@ -1,103 +0,0 @@
# Locate Discord Social SDK library and headers.
#
# Copyright (c) 2025 hat_kid
# https://github.com/thehatkid/DiscordSocialSDKExample
#
# Usage of this module as follows:
# find_package(DiscordSDK)
#
# Variables defined by this module:
# DISCORDSDK_FOUND Whether was found library and headers.
# DISCORDSDK_INCLUDE_DIR SDK include path.
# DISCORDSDK_LIBRARY SDK shared library path.
# DISCORDSDK_IMPLIB Win32: SDK object library path to link.
# Set SDK root directory path (You can change it to different path)
set(DISCORDSDK_ROOT_DIR "${CMAKE_SOURCE_DIR}/ext/discord_social_sdk" CACHE PATH "Discord Social SDK root path")
# Set SDK library build variant
set(DISCORDSDK_VARIANT "release" CACHE STRING "Discord Social SDK library variant")
set_property(CACHE DISCORDSDK_VARIANT PROPERTY STRINGS "release" "debug")
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(DISCORDSDK_VARIANT "debug")
endif()
# Find SDK include directory path
find_path(
DISCORDSDK_INCLUDE_DIR
NAMES cdiscord.h discordpp.h
PATHS ${DISCORDSDK_ROOT_DIR}/include
DOC "Discord Social SDK include directory"
NO_DEFAULT_PATH
)
# Find SDK library path
if(WIN32)
find_file(
DISCORDSDK_LIBRARY
NAMES discord_partner_sdk.dll
PATHS ${DISCORDSDK_ROOT_DIR}/bin/${DISCORDSDK_VARIANT}
DOC "Discord Social SDK Windows Dynamic Link Library (.dll)"
NO_DEFAULT_PATH
)
find_file(
DISCORDSDK_IMPLIB
NAMES discord_partner_sdk.lib
PATHS ${DISCORDSDK_ROOT_DIR}/lib/${DISCORDSDK_VARIANT}
DOC "Discord Social SDK Windows Object Library (.lib)"
NO_DEFAULT_PATH
)
else()
find_library(
DISCORDSDK_LIBRARY
NAMES libdiscord_partner_sdk discord_partner_sdk
PATHS ${DISCORDSDK_ROOT_DIR}/lib/${DISCORDSDK_VARIANT}
DOC "Discord Social SDK shared library"
NO_DEFAULT_PATH
)
endif()
mark_as_advanced(
DISCORDSDK_ROOT_DIR
DISCORDSDK_VARIANT
DISCORDSDK_INCLUDE_DIR
DISCORDSDK_LIBRARY
)
include(FindPackageHandleStandardArgs)
if(WIN32)
find_package_handle_standard_args(
DiscordSDK
REQUIRED_VARS DISCORDSDK_IMPLIB DISCORDSDK_LIBRARY DISCORDSDK_INCLUDE_DIR
)
mark_as_advanced(DISCORDSDK_IMPLIB)
else()
find_package_handle_standard_args(
DiscordSDK
REQUIRED_VARS DISCORDSDK_LIBRARY DISCORDSDK_INCLUDE_DIR
)
endif()
if(NOT DiscordSDK_FOUND)
message("Could NOT find Discord Social SDK redistributable! Please check for SDK files in ${DISCORDSDK_ROOT_DIR}")
else()
# Add imported shared library as DiscordSDK::DiscordSDK
add_library(DiscordSDK::DiscordSDK SHARED IMPORTED)
set_target_properties(
DiscordSDK::DiscordSDK PROPERTIES
IMPORTED_LOCATION ${DISCORDSDK_LIBRARY}
INTERFACE_COMPILE_DEFINITIONS DISCORDPP_IMPLEMENTATION
INTERFACE_INCLUDE_DIRECTORIES ${DISCORDSDK_INCLUDE_DIR}
)
if(WIN32)
set_target_properties(
DiscordSDK::DiscordSDK PROPERTIES
IMPORTED_IMPLIB ${DISCORDSDK_IMPLIB}
)
endif()
endif()

View File

6
flake.lock generated
View File

@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1760315168, "lastModified": 1760807995,
"narHash": "sha256-qWDhFoiz6VSd+S+rVzC2m5u8xuAzxRsuDI8OojbPEZ4=", "narHash": "sha256-Xpg9h3/uNVMJXZq2LzBDaDM1u057ox0wnZGvxSogNY0=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "160e3dfce302bb2fcd03761b84248c7534f3b948", "rev": "a2975bcaf36af01a1279b84ab575fbc12d472b6a",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -1,9 +1,6 @@
#include <QIcon> #include <QIcon>
#include <QGuiApplication> #include <QGuiApplication>
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QDebug>
#include "buildFlags.h"
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
@@ -13,15 +10,9 @@ int main(int argc, char *argv[])
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
&app, []() { QCoreApplication::exit(-1); }, &app, []() { QCoreApplication::exit(-1); },
Qt::QueuedConnection); Qt::QueuedConnection);
engine.loadFromModule("qyouradio", "Main"); engine.loadFromModule("qyouvideo", "Main");
app.setWindowIcon(QIcon(":/resources/logo.png")); app.setWindowIcon(QIcon(":/resources/logo.png"));
#if DiscordSDK_FOUND
qInfo("Has Discord Social SDK: true");
#else
qInfo("Has Discord Social SDK: false");
#endif
return app.exec(); return app.exec();
} }

View File

@@ -1,33 +1,65 @@
#include "player.h" #include "player.h"
// Public // HOLY GRAIL
// https://doc.qt.io/qt-6/qaudiobufferoutput.html
Player::Player(QObject *parent) : QObject(parent), Player::Player(QObject *parent) : QObject(parent),
player(this), output(this), timer(this), m_currentIndex(0), m_stations({ player(this), output(this)
Station{"Autoradio", "autoradio", "", 0},
Station{"Live Mix", "live", "", 0},
Station{"Bass Boosted", "bassboosted", "", 0}
})
{ {
QObject::connect(&this->player, &QMediaPlayer::mediaStatusChanged, this, [this] () {
const QMediaPlayer::MediaStatus status = this->player.mediaStatus();
if (status == QMediaPlayer::NoMedia)
{
qDebug("Player::Player(): NoMedia");
this->unloadVideo();
}
else if (status == QMediaPlayer::BufferingMedia || status == QMediaPlayer::LoadingMedia)
{
qDebug("Player::Player() loadingChanged");
this->m_loading = true;
emit this->loadingChanged();
}
else if (status == QMediaPlayer::EndOfMedia)
{
qDebug("Player::Player() playingChanged");
emit this->playingChanged();
}
else
{
if (this->m_position == 0)
{
this->player.play();
}
this->m_loading = false;
emit this->loadingChanged();
}
});
QObject::connect(&this->player, &QMediaPlayer::positionChanged, this, [this] () {
this->m_position = this->player.position();
emit this->positionChanged();
});
// QObject::connect(&this->player, &QMediaPlayer::bufferProgressChanged, this, [this] () {
// this->m_buffered = this->player.bufferedTimeRange().latestTime() / this->m_duration;
// emit this->bufferedChanged();
// });
QObject::connect(&this->player, &QMediaPlayer::errorOccurred, this, [this] () {
this->m_failed = true;
qDebug("Player::Player() failedChanged");
emit this->failedChanged();
});
QObject::connect(&this->player, &QMediaPlayer::playbackStateChanged, this, [this] () { QObject::connect(&this->player, &QMediaPlayer::playbackStateChanged, this, [this] () {
qDebug("Player::Player()->Lambda: Playing got changed!"); qDebug("Player::Player() playingChanged");
emit this->playingChanged(); emit this->playingChanged();
}); });
QObject::connect(&this->output, &QAudioOutput::volumeChanged, this, [this] () { QObject::connect(&this->output, &QAudioOutput::volumeChanged, this, [this] () {
qDebug("Player::Player()->Lambda: Volume got changed!");
emit this->volumeChanged(); emit this->volumeChanged();
}); });
this->timer.start(metadataFetchTimeout);
QObject::connect(&this->timer, &QTimer::timeout, this, [this] () {
qDebug("Player::Player()->Lambda: Timer triggered!");
if (this->m_currentIndex > -1)
{
emit this->currentStationChanged();
}
emit this->stationsChanged();
});
this->output.setVolume(0.2); this->output.setVolume(0.2);
this->player.setAudioOutput(&this->output); this->player.setAudioOutput(&this->output);
@@ -39,75 +71,111 @@ Player::~Player()
qDebug("Player::~Player(): Destructed"); qDebug("Player::~Player(): Destructed");
} }
// ********
// Read
// ********
bool Player::playing() const bool Player::playing() const
{ {
return this->player.playbackState() == QMediaPlayer::PlayingState; return this->player.playbackState() == QMediaPlayer::PlayingState;
} }
float Player::volume() const float Player::volume() const
{ {
return this->output.volume(); return this->output.volume();
} }
// *********
// Write
// *********
void Player::setVolume(float newVolume) void Player::setVolume(float newVolume)
{ {
this->output.setVolume(newVolume); this->output.setVolume(newVolume);
} }
const QList<Station> Player::stations() const void Player::setPosition(float newPosition)
{ {
return this->m_stations; this->player.setPosition(newPosition);
} }
const int Player::currentIndex() const
{
return this->m_currentIndex;
}
const Station* Player::currentStation() const
// ***********
// Methods
// ***********
// For QYouRadio:
// QEventLoop loop(this);
// QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
// loop.exec();
// const QJsonDocument data = QJsonDocument::fromJson(reply->readAll());
void Player::loadVideo(QString id, QString extension, QString title, float position)
{ {
if (this->m_currentIndex < 0 || this->m_currentIndex >= this->m_stations.length()) if (this->m_id != id)
{ {
qWarning("Player::currentStation(): Tried accessing out of bounds."); this->unloadVideo();
return nullptr;
} }
return &this->m_stations[this->m_currentIndex];
this->m_id = id;
emit this->idChanged();
this->player.setSource(QUrl("https://youvideo.nonamesoft.xyz/youvideo/api/videofile/with_extension/" + id + extension));
this->m_title = title;
emit this->titleChanged();
this->m_position = position;
emit this->positionChanged();
this->m_buffered = 0;
emit this->bufferedChanged();
this->m_loading = true;
emit this->loadingChanged();
this->m_failed = false;
emit this->failedChanged();
qDebug() << "Player::loadVideo(): Loaded video https://youvideo.nonamesoft.xyz/youvideo/api/videofile/with_extension/" + id + extension;
} }
// Public slots void Player::unloadVideo() {
void Player::startPlaying(u_int8_t index) qDebug("Player::unloadVideo(): Video unloaded");
{
if (index < 0 || index >= this->m_stations.length())
{
qWarning("Player::startPlaying(): Tried accessing out of bounds.");
return;
}
if (this->playing()) {
this->stopPlaying();
}
this->m_currentIndex = index;
emit this->currentIndexChanged();
emit this->currentStationChanged();
this->player.setSource(QUrl("https://youradio.nonamesoft.xyz/youradio/api/" + this->m_stations[index].m_slug)); this->m_id = "";
this->player.play(); emit this->idChanged();
qDebug("Player::startPlaying(): Starting playing");
}
void Player::stopPlaying()
{
if (!this->playing())
{
qWarning("Player::stopPlaying(): Prevented redundant stopPlaying() call.");
return;
}
this->player.stop();
this->player.setSource(QUrl("")); this->player.setSource(QUrl(""));
this->player.stop();
this->m_currentIndex = -1; this->m_title = "";
emit this->currentIndexChanged(); emit this->titleChanged();
emit this->currentStationChanged();
qDebug("Player::stopPlaying(): Stopping playback..."); this->m_loading = false;
emit this->loadingChanged();
this->m_failed = false;
emit this->failedChanged();
} }
void Player::play()
{
if (this->m_id.length() == 0 || this->m_loading)
{
return;
}
this->player.play();
}
void Player::pause()
{
if (this->m_id.length() == 0 || this->m_loading)
{
return;
}
this->player.pause();
}

View File

@@ -1,73 +1,60 @@
#pragma once #pragma once
#include <QtMultimedia> #include <QtMultimedia>
#include <QtConcurrent/QtConcurrent>
#include <QDebug> #include <QDebug>
#include <QObject> #include <QObject>
#include <QList>
#include <QTimer>
#include <QtQmlIntegration/qqmlintegration.h> #include <QtQmlIntegration/qqmlintegration.h>
constexpr int metadataFetchTimeout = 10000;
constexpr std::string_view metadataEndpoint = "https://youradio.nonamesoft.xyz/youradio/api/status-json.xsl";
struct Station
{
Q_GADGET
Q_PROPERTY(QString name MEMBER m_name);
Q_PROPERTY(QString slug MEMBER m_slug);
Q_PROPERTY(QString songTitle MEMBER m_songTitle);
Q_PROPERTY(int listeners MEMBER m_listeners);
public:
QString m_name;
QString m_slug;
QString m_songTitle;
int m_listeners;
};
class Player : public QObject class Player : public QObject
{ {
Q_OBJECT Q_OBJECT
QML_SINGLETON QML_SINGLETON
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(bool playing READ playing NOTIFY playingChanged FINAL) Q_PROPERTY(QString title MEMBER m_title NOTIFY titleChanged FINAL)
Q_PROPERTY(float volume READ volume WRITE setVolume NOTIFY volumeChanged FINAL) Q_PROPERTY(QString id MEMBER m_id NOTIFY idChanged FINAL)
Q_PROPERTY(const QList<Station> stations READ stations NOTIFY stationsChanged FINAL) Q_PROPERTY(float position MEMBER m_position WRITE setPosition NOTIFY positionChanged FINAL)
Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentIndexChanged FINAL) Q_PROPERTY(float buffered MEMBER m_buffered NOTIFY bufferedChanged FINAL)
Q_PROPERTY(const Station* currentStation READ currentStation NOTIFY currentStationChanged FINAL) Q_PROPERTY(bool loading MEMBER m_loading NOTIFY loadingChanged FINAL)
Q_PROPERTY(bool failed MEMBER m_failed NOTIFY failedChanged FINAL)
Q_PROPERTY(bool playing READ playing NOTIFY playingChanged FINAL)
Q_PROPERTY(float volume READ volume WRITE setVolume NOTIFY volumeChanged FINAL)
public: public:
Player(QObject *parent = nullptr); Player(QObject *parent = nullptr);
~Player(); ~Player();
bool playing() const; bool playing() const;
float volume() const;
float volume() const; void setPosition(float newPosition);
void setVolume(float newVolume); void setVolume(float newVolume);
const QList<Station> stations() const;
const int currentIndex() const;
const Station* currentStation() const;
public slots: public slots:
void startPlaying(u_int8_t index); void loadVideo(QString id, QString extension, QString title, float position);
void stopPlaying(); void unloadVideo();
void play();
void pause();
signals: signals:
void titleChanged();
void idChanged();
void positionChanged();
void bufferedChanged();
void loadingChanged();
void failedChanged();
void playingChanged(); void playingChanged();
void volumeChanged(); void volumeChanged();
void stationsChanged();
void currentIndexChanged();
void currentStationChanged();
private: private:
QMediaPlayer player; QMediaPlayer player;
QAudioOutput output; QAudioOutput output;
QTimer timer; \
QString m_title = "";
int m_currentIndex; QString m_id = "";
QList<Station> m_stations; float m_position = 0;
float m_buffered = 0;
bool m_loading = false;
bool m_failed = false;
}; };

View File

@@ -57,12 +57,12 @@ BEGIN
BLOCK "040904b0" BLOCK "040904b0"
BEGIN BEGIN
VALUE "CompanyName", "LAMINAX CO." VALUE "CompanyName", "LAMINAX CO."
VALUE "FileDescription", "QYouRadio" VALUE "FileDescription", "QYouVideo"
VALUE "FileVersion", "1.0.0.1" VALUE "FileVersion", "1.0.0.1"
VALUE "InternalName", "qyouradio" VALUE "InternalName", "qyouvideo"
VALUE "LegalCopyright", "Copyright (C) Ghostfox 2025" VALUE "LegalCopyright", "Copyright (C) Ghostfox 2025"
//VALUE "OriginalFilename", "appqyouradio.exe" //VALUE "OriginalFilename", "appqyouvideo.exe"
VALUE "ProductName", "QYouRadio" VALUE "ProductName", "QYouVideo"
VALUE "ProductVersion", "1.0.0.1" VALUE "ProductVersion", "1.0.0.1"
END END
END END