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 e091c97305.
2025-10-18 23:34:54 +02:00
Dark Steveneq
e091c97305 Revert "dwjakld"
This reverts commit 5c9b0ec49a.
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
with:
aqtversion: '==3.1.*'
version: '6.10.0'
version: '6.8.0'
host: 'windows'
target: 'desktop'
arch: 'win64_msvc2022_64'

1
.gitignore vendored
View File

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

2
.vscode/launch.json vendored
View File

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

View File

@@ -1,49 +1,45 @@
cmake_minimum_required(VERSION 3.16)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
project(qyouradio VERSION 0.1 LANGUAGES CXX)
project(qyouvideo VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS Core Quick Multimedia)
find_package(DiscordSDK)
qt_standard_project_setup(REQUIRES 6.8)
configure_file(buildFlags.h.in buildFlags.h)
qt_add_executable(appqyouradio
qt_add_executable(appqyouvideo
main.cpp
buildFlags.h
player.cpp player.h
resources/qyouradio.rc
resources/qyouvideo.rc
resources/resource.h
)
qt_add_resources(appqyouradio "resources"
qt_add_resources(appqyouvideo "resources"
PREFIX "/"
FILES
resources/logo.png
)
set_source_files_properties(Player.qml
set_source_files_properties(ComponentCache.qml
PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
qt_add_qml_module(appqyouradio
URI qyouradio
qt_add_qml_module(appqyouvideo
URI qyouvideo
VERSION 1.0
QML_FILES
Main.qml
# Player.qml
ViewPlayer.qml
ViewSettings.qml
VideoEntry.qml
ViewAbout.qml
ViewVideoList.qml
VideoPlayer.qml
ComponentCache.qml
)
# 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
# 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_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
@@ -51,30 +47,24 @@ set_target_properties(appqyouradio PROPERTIES
WIN32_EXECUTABLE TRUE
)
target_include_directories(appqyouradio PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
target_link_libraries(appqyouradio
target_link_libraries(appqyouvideo
PUBLIC Qt6::Quick Qt6::Multimedia
)
if (DiscordSDK_FOUND)
target_link_libraries(appqyouradio
PRIVATE DiscordSDK::DiscordSDK
)
endif()
include(GNUInstallDirs)
install(TARGETS appqyouradio
install(TARGETS appqyouvideo
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
qt_generate_deploy_app_script(
TARGET appqyouradio
TARGET appqyouvideo
OUTPUT_SCRIPT deploy_script
NO_UNSUPPORTED_PLATFORM_ERROR
)
install(SCRIPT ${deploy_script})
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
width: 1280
height: 800
title: qsTr("QYouRadio")
title: qsTr("QYouVideo")
Component.onCompleted: function() {
console.log(Player.stations)
}
flags: Qt.Dialog
modality: Qt.ApplicationModal
ColumnLayout {
anchors.fill: parent
header: Rectangle {
Layout.fillWidth: true
height: 43
Rectangle {
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;
}
color: Colors.surface0
RowLayout {
Layout.fillWidth: false
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 30
Layout.bottomMargin: 30
width: 220
anchors.fill: parent
anchors.margins: 3
Button {
Layout.rightMargin: 5
text: Player.loading ? "Loading" : (Player.playing ? "Pause" : "Play")
onClicked: function() {
if (Player.playing) {
Player.stopPlaying();
} else {
Player.startPlaying(tabbar.currentIndex);
}
}
// visible: stack.depth > 1
text: "Back"
onClicked: stack.popCurrentItem()
}
Slider {
Layout.fillWidth: true
Label {
id: logo
// visible: stack.currentItem.StackView.index == 0
Layout.leftMargin: 5
from: 0.1
to: 1.1
stepSize: 0.05
value: Player.volume + 0.1
onMoved: Player.volume = value - 0.1
text: "QYouVideo"
heading: "h1"
clip: true
}
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
TabButton.qml
YouAds.qml
QML_FILES BusyIndicator.qml
# SOURCES qyrcomponents.cpp qyrcomponents.h
)

View File

@@ -5,6 +5,8 @@ import QtQuick.Controls.Basic 6.8
Slider {
snapMode: Slider.SnapAlways
property real buffered
implicitWidth: 130
implicitHeight: 20
@@ -26,6 +28,17 @@ Slider {
background: Rectangle {
color: "#555"
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 {

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
}:
stdenv.mkDerivation {
pname = "qyouradio";
pname = "qyouvideo";
version = "1.0";
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": {
"locked": {
"lastModified": 1760315168,
"narHash": "sha256-qWDhFoiz6VSd+S+rVzC2m5u8xuAzxRsuDI8OojbPEZ4=",
"lastModified": 1760807995,
"narHash": "sha256-Xpg9h3/uNVMJXZq2LzBDaDM1u057ox0wnZGvxSogNY0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "160e3dfce302bb2fcd03761b84248c7534f3b948",
"rev": "a2975bcaf36af01a1279b84ab575fbc12d472b6a",
"type": "github"
},
"original": {

View File

@@ -1,9 +1,6 @@
#include <QIcon>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QDebug>
#include "buildFlags.h"
int main(int argc, char *argv[])
{
@@ -13,15 +10,9 @@ int main(int argc, char *argv[])
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
&app, []() { QCoreApplication::exit(-1); },
Qt::QueuedConnection);
engine.loadFromModule("qyouradio", "Main");
engine.loadFromModule("qyouvideo", "Main");
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();
}

View File

@@ -1,33 +1,65 @@
#include "player.h"
// Public
// HOLY GRAIL
// https://doc.qt.io/qt-6/qaudiobufferoutput.html
Player::Player(QObject *parent) : QObject(parent),
player(this), output(this), timer(this), m_currentIndex(0), m_stations({
Station{"Autoradio", "autoradio", "", 0},
Station{"Live Mix", "live", "", 0},
Station{"Bass Boosted", "bassboosted", "", 0}
})
player(this), output(this)
{
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] () {
qDebug("Player::Player()->Lambda: Playing got changed!");
qDebug("Player::Player() playingChanged");
emit this->playingChanged();
});
QObject::connect(&this->output, &QAudioOutput::volumeChanged, this, [this] () {
qDebug("Player::Player()->Lambda: Volume got changed!");
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->player.setAudioOutput(&this->output);
@@ -39,75 +71,111 @@ Player::~Player()
qDebug("Player::~Player(): Destructed");
}
// ********
// Read
// ********
bool Player::playing() const
{
return this->player.playbackState() == QMediaPlayer::PlayingState;
}
float Player::volume() const
{
return this->output.volume();
}
// *********
// Write
// *********
void Player::setVolume(float 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.");
return nullptr;
this->unloadVideo();
}
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::startPlaying(u_int8_t index)
{
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();
void Player::unloadVideo() {
qDebug("Player::unloadVideo(): Video unloaded");
this->player.setSource(QUrl("https://youradio.nonamesoft.xyz/youradio/api/" + this->m_stations[index].m_slug));
this->player.play();
qDebug("Player::startPlaying(): Starting playing");
}
this->m_id = "";
emit this->idChanged();
void Player::stopPlaying()
{
if (!this->playing())
{
qWarning("Player::stopPlaying(): Prevented redundant stopPlaying() call.");
return;
}
this->player.stop();
this->player.setSource(QUrl(""));
this->player.stop();
this->m_currentIndex = -1;
emit this->currentIndexChanged();
emit this->currentStationChanged();
this->m_title = "";
emit this->titleChanged();
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
#include <QtMultimedia>
#include <QtConcurrent/QtConcurrent>
#include <QDebug>
#include <QObject>
#include <QList>
#include <QTimer>
#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
{
Q_OBJECT
QML_SINGLETON
QML_ELEMENT
Q_PROPERTY(bool playing READ playing NOTIFY playingChanged FINAL)
Q_PROPERTY(float volume READ volume WRITE setVolume NOTIFY volumeChanged FINAL)
Q_PROPERTY(const QList<Station> stations READ stations NOTIFY stationsChanged FINAL)
Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentIndexChanged FINAL)
Q_PROPERTY(const Station* currentStation READ currentStation NOTIFY currentStationChanged FINAL)
Q_PROPERTY(QString title MEMBER m_title NOTIFY titleChanged FINAL)
Q_PROPERTY(QString id MEMBER m_id NOTIFY idChanged FINAL)
Q_PROPERTY(float position MEMBER m_position WRITE setPosition NOTIFY positionChanged FINAL)
Q_PROPERTY(float buffered MEMBER m_buffered NOTIFY bufferedChanged 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:
Player(QObject *parent = nullptr);
~Player();
bool playing() const;
bool playing() const;
float volume() const;
float volume() const;
void setPosition(float newPosition);
void setVolume(float newVolume);
const QList<Station> stations() const;
const int currentIndex() const;
const Station* currentStation() const;
public slots:
void startPlaying(u_int8_t index);
void stopPlaying();
void loadVideo(QString id, QString extension, QString title, float position);
void unloadVideo();
void play();
void pause();
signals:
void titleChanged();
void idChanged();
void positionChanged();
void bufferedChanged();
void loadingChanged();
void failedChanged();
void playingChanged();
void volumeChanged();
void stationsChanged();
void currentIndexChanged();
void currentStationChanged();
private:
QMediaPlayer player;
QAudioOutput output;
QTimer timer;
int m_currentIndex;
QList<Station> m_stations;
\
QString m_title = "";
QString m_id = "";
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"
BEGIN
VALUE "CompanyName", "LAMINAX CO."
VALUE "FileDescription", "QYouRadio"
VALUE "FileDescription", "QYouVideo"
VALUE "FileVersion", "1.0.0.1"
VALUE "InternalName", "qyouradio"
VALUE "InternalName", "qyouvideo"
VALUE "LegalCopyright", "Copyright (C) Ghostfox 2025"
//VALUE "OriginalFilename", "appqyouradio.exe"
VALUE "ProductName", "QYouRadio"
//VALUE "OriginalFilename", "appqyouvideo.exe"
VALUE "ProductName", "QYouVideo"
VALUE "ProductVersion", "1.0.0.1"
END
END