4 Commits

Author SHA1 Message Date
Dark Steveneq
902693bd40 heugh jazz 2025-10-19 02:58:12 +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
24 changed files with 627 additions and 720 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,42 @@
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
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
) )
# 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 +44,25 @@ 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)
project(qyouvideo VERSION 0.1 LANGUAGES CXX)

134
Main.qml
View File

@@ -9,11 +9,7 @@ ApplicationWindow {
id: root id: root
width: 1280 width: 1280
height: 800 height: 800
title: qsTr("QYouRadio") title: qsTr("QYouVideo")
Component.onCompleted: function() {
console.log(Player.stations)
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@@ -30,76 +26,8 @@ ApplicationWindow {
Label { Label {
Layout.leftMargin: 5 Layout.leftMargin: 5
text: "QYouRadio" text: "QYouVideo"
heading: "h1" 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 { Item {
@@ -113,16 +41,17 @@ ApplicationWindow {
background: Item{} background: Item{}
TabButton { TabButton {
text: qsTr("Autoradio") text: qsTr("Videos")
outlined: true
implicitWidth: 80
} }
TabButton { TabButton {
text: qsTr("Live Mix") text: qsTr("About")
outlined: true
implicitWidth: 80
} }
TabButton { TabButton {
text: qsTr("Deep Bass") text: qsTr("Upload")
}
TabButton {
text: qsTr("Settings")
} }
} }
} }
@@ -141,61 +70,28 @@ ApplicationWindow {
active: true active: true
asynchronous: true asynchronous: true
visible: status == Loader.Ready visible: status == Loader.Ready
sourceComponent: ViewPlayer { source: "ViewVideoList.qml"
index: 0
}
} }
Loader { Loader {
// active: tabbar.currentIndex == 1 // active: tabbar.currentIndex == 1
active: true active: true
asynchronous: true asynchronous: true
visible: status == Loader.Ready visible: status == Loader.Ready
sourceComponent: ViewPlayer { source: "ViewAbout.qml"
index: 1
}
} }
Loader { Loader {
// active: tabbar.currentIndex == 2 // active: tabbar.currentIndex == 2
active: true active: true
asynchronous: true asynchronous: true
visible: status == Loader.Ready visible: status == Loader.Ready
sourceComponent: ViewPlayer { sourceComponent: Label {
index: 2 heading: "h1"
text: "No idea if I'll ever implement this so don't worry >w<"
font.bold: true
} }
} }
Component.onCompleted: contentItem.highlightMoveDuration = 160; Component.onCompleted: contentItem.highlightMoveDuration = 160;
} }
RowLayout {
Layout.fillWidth: false
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 30
Layout.bottomMargin: 30
width: 220
Button {
Layout.rightMargin: 5
text: Player.loading ? "Loading" : (Player.playing ? "Pause" : "Play")
onClicked: function() {
if (Player.playing) {
Player.stopPlaying();
} else {
Player.startPlaying(tabbar.currentIndex);
}
}
}
Slider {
Layout.fillWidth: true
Layout.leftMargin: 5
from: 0.1
to: 1.1
stepSize: 0.05
value: Player.volume + 0.1
onMoved: Player.volume = value - 0.1
}
}
} }
} }

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,32 @@
import QtQuick 6.8
import QtQuick.Controls 6.8
import QtQuick.Controls.Basic 6.8
BusyIndicator {
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: parent
// running: parent.parent.visible && parent.parent.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: Player.loadVideo(parent.id)
}
}

109
VideoPlayer.qml Normal file
View File

@@ -0,0 +1,109 @@
import QtQuick 6.8
import QtMultimedia 6.8
import QtQuick.Controls 6.8
import QtQuick.Layouts 6.8
import QYRComponents 1.0
Rectangle {
visible: Player.active
color: Colors.surface1
radius: 10
property bool unrolled: true
// width: childrenRect.width
height: unrolled ? 560 : 96
MouseArea {
anchors.fill: parent
height: 20
hoverEnabled: true
propagateComposedEvents: true
onClicked: unrolled = true;
onContainsMouseChanged: function() {
if (!containsMouse) {
unrolled = false;
}
}
}
ColumnLayout {
anchors.fill: parent
Item {
visible: unrolled
Layout.fillHeight: true
}
RowLayout {
Button {
text: Player.loading ? "Loading" : (Player.playing ? "Pause" : "Play")
enabled: !Player.loading
onClicked: function() {
if (Player.playing) {
Player.pause()
} else {
Player.play();
}
}
}
Button {
text: "Stop"
onClicked: Player.stop()
}
Label {
heading: "h4"
text: new Date(Player.position).toISOString().slice(14, 19)
}
Slider {
Layout.fillWidth: true
from: 0
to: Player.duration
value: Player.position
buffered: Player.buffered
onMoved: Player.position = value
}
Label {
heading: "h4"
text: new Date(Player.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: Player.title
heading: "h2"
font.bold: true
}
Label {
visible: unrolled
Layout.fillWidth: true
wrapMode: Text.WordWrap
clip: true
text: Player.description
heading: "h4"
font.bold: true
}
}
}

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

104
ViewVideoList.qml Normal file
View File

@@ -0,0 +1,104 @@
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 {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Player.active ? 96 : 0
visible: !loading || !failed
anchors.fill: parent
model: ListModel { id: model }
clip: true
cellWidth: 384
cellHeight: 224
delegate: VideoEntry {}
}
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.fetchData()
}
}
Timer {
interval: 1000
running: true
onTriggered: parent.fetchData()
}
function fetchData() {
model.clear();
loading = true;
failed = false;
const xhr = new XMLHttpRequest;
xhr.open("GET", "http://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();
}
VideoPlayer {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
Layout.fillWidth: true
}
}

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,66 @@
#include "player.h" #include "player.h"
// Public // HOLY GRAIL
// https://doc.qt.io/qt-6/qaudiobufferoutput.htmli t
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), manager(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->stop();
}
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
{
this->m_loading = false;
emit this->loadingChanged();
if (!this->m_initialLoadFinished)
{
this->m_initialLoadFinished = true;
this->play();
}
}
});
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 +72,132 @@ 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
// ***********
void Player::loadVideo(QString id)
{ {
if (this->m_currentIndex < 0 || this->m_currentIndex >= this->m_stations.length()) if (this->m_active)
{ {
qWarning("Player::currentStation(): Tried accessing out of bounds."); this->unloadVideo();
return nullptr;
} }
return &this->m_stations[this->m_currentIndex]; QNetworkReply* reply = this->manager.get(QNetworkRequest(QUrl("https://youvideo.nonamesoft.xyz/youvideo/video/" + id)));
}
// Public slots QEventLoop loop(this);
void Player::startPlaying(u_int8_t index) QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
{ loop.exec();
if (index < 0 || index >= this->m_stations.length())
if (reply->error() != QNetworkReply::NoError)
{ {
qWarning("Player::startPlaying(): Tried accessing out of bounds."); qDebug() << "Player::loadVideo(): Video " << id << " doesn't exist!\n";
delete reply;
return; 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)); const QJsonDocument data = QJsonDocument::fromJson(reply->readAll());
this->player.play();
qDebug("Player::startPlaying(): Starting playing"); this->player.setSource(QUrl("https://youvideo.nonamesoft.xyz/youvideo/api/videofile/with_extension/" + id + data ["extension"].toString()));
this->m_title = data["name"].toString();
emit this->titleChanged();
this->m_description = data["description"].toString();
emit this->descriptionChanged();
this->m_id = id;
emit this->idChanged();
this->m_initialLoadFinished = false;
this->m_active = true;
emit this->activeChanged();
this->m_duration = data["metadata"]["duration"].toDouble() * 1000;
emit this->durationChanged();
this->m_position = 0;
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 + data ["extension"].toString();
delete reply;
} }
void Player::stopPlaying() void Player::unloadVideo() {
qDebug("Player::unloadVideo(): Video unloaded");
this->player.setSource(QUrl(""));
this->player.stop();
this->m_failed = false;
emit this->activeChanged();
this->m_active = false;
emit this->activeChanged();
}
void Player::play()
{ {
if (!this->playing()) if (!this->m_active || this->m_loading)
{
return;
}
this->player.play();
}
void Player::pause()
{
if (!this->m_active || this->m_loading)
{
return;
}
this->player.pause();
}
void Player::stop()
{
if (!this->m_active || this->m_loading)
{ {
qWarning("Player::stopPlaying(): Prevented redundant stopPlaying() call.");
return; return;
} }
this->player.stop(); this->player.stop();
this->player.setSource(QUrl("")); this->unloadVideo();
this->m_currentIndex = -1;
emit this->currentIndexChanged();
emit this->currentStationChanged();
qDebug("Player::stopPlaying(): Stopping playback...");
} }

View File

@@ -1,73 +1,73 @@
#pragma once #pragma once
#include <QtMultimedia> #include <QtMultimedia>
#include <QtConcurrent/QtConcurrent>
#include <QDebug> #include <QDebug>
#include <QObject> #include <QObject>
#include <QList> #include <QNetworkAccessManager>
#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 description MEMBER m_description NOTIFY descriptionChanged FINAL)
Q_PROPERTY(const QList<Station> stations READ stations NOTIFY stationsChanged FINAL) Q_PROPERTY(QString id MEMBER m_id NOTIFY idChanged FINAL)
Q_PROPERTY(int currentIndex READ currentIndex NOTIFY currentIndexChanged FINAL) Q_PROPERTY(bool active MEMBER m_active NOTIFY activeChanged FINAL)
Q_PROPERTY(const Station* currentStation READ currentStation NOTIFY currentStationChanged FINAL) Q_PROPERTY(float duration MEMBER m_duration NOTIFY durationChanged 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: 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);
void stopPlaying(); void unloadVideo();
void play();
void pause();
void stop();
signals: signals:
void titleChanged();
void descriptionChanged();
void idChanged();
void activeChanged();
void durationChanged();
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; QNetworkAccessManager manager;
int m_currentIndex; QString m_title = "";
QList<Station> m_stations; QString m_description = "";
QString m_id = "";
float m_duration = 0;
float m_position = 0;
float m_buffered = 0;
bool m_initialLoadFinished = false;
bool m_active = false;
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