Compare commits

...

16 Commits

Author SHA1 Message Date
Dark Steveneq
63d9c21e98 Mostly reimplement Player, begin implementing ViewPlayer UI design 2025-10-17 22:29:12 +02:00
Dark Steveneq
c99efcf9e3 Reimplement streaming 2025-10-16 23:20:10 +02:00
Dark Steveneq
01a3775ac7 Begin rewriting Player in C++ 2025-10-16 19:21:35 +02:00
1f821d58cc Update .gitea/workflows/build-git.yaml 2025-10-15 00:18:26 +02:00
0847ca9ffd Update .gitea/workflows/build-git.yaml 2025-10-15 00:16:28 +02:00
07e33ebb35 Update .gitea/workflows/build-git.yaml 2025-10-15 00:14:35 +02:00
00b267e902 Update .gitea/workflows/build-git.yaml 2025-10-15 00:14:05 +02:00
Dark Steveneq
6d0689718d Improve title pan animation, add slideout animation
Some checks failed
Build / build-linux (push) Failing after 18s
Build / build-windows (push) Has been cancelled
2025-10-14 23:03:38 +02:00
Dark Steveneq
18ad69ba5d Begin implementing UI design, implement metadata fetching
Some checks failed
Build / build-linux (push) Failing after 17s
Build / build-windows (push) Has been cancelled
2025-10-14 21:02:58 +02:00
Dark Steveneq
d46d37c465 Refactor Player, fix playing indicator
Some checks failed
Build / build-linux (push) Failing after 17s
Build / build-windows (push) Has been cancelled
2025-10-14 13:36:21 +02:00
Dark Steveneq
def26f8fda Fix theme on native light mode, implement volume slider
Some checks failed
Build / build-linux (push) Failing after 18s
Build / build-windows (push) Has been cancelled
2025-10-14 10:06:06 +02:00
5ffbede5a0 Update .gitea/workflows/build-git.yaml
Some checks failed
Build / build-linux (push) Failing after 1m3s
Build / build-windows (push) Has been cancelled
2025-10-14 02:46:56 +02:00
6fe5061898 Update .gitea/workflows/build-git.yaml 2025-10-14 02:44:29 +02:00
3dd897d576 Add .gitea/workflows/build-git.yaml
Some checks failed
Build / build-linux (push) Failing after 3m58s
Build / build-windows (push) Has been cancelled
2025-10-14 02:37:03 +02:00
0672391721 Delete .gitea/actions/build-git.yml 2025-10-14 02:36:34 +02:00
f0559753f3 Add .gitea/actions/build-git.yml 2025-10-13 23:58:11 +02:00
19 changed files with 785 additions and 180 deletions

View File

@@ -0,0 +1,127 @@
name: CI
on:
workflow_dispatch
push # when to trigger this. Here, on every push
jobs:
build_linux:
name: "Build and test"
runs-on: ubuntu-latest
steps:
- name: Install dependencies (linux)
run: sudo apt install ninja-build
if: matrix.os == 'ubuntu-latest' # conditional, runs this step only on the Ubuntu runner
- name: Install Qt
uses: jurplel/install-qt-action@v3
with:
version: '6.9.3'
- uses: https://github.com/actions/checkout@v3
- name: Build
# We don't need to set up the environment variable for CMake to see Qt because the install-qt-action
# sets up the necessary variables automatically
run: cmake -S . -B build -G "Ninja Multi-Config" && cmake --build build --config Release
#
# Create the AppImage
#
- name: Create AppImage
run: |
make INSTALL_ROOT=appdir install
wget -c -nv "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage" -O linuxdeployqt
chmod a+x linuxdeployqt
./linuxdeployqt appdir/usr/share/applications/*.desktop -appimage -bundle-non-qt-libs -extra-plugins=imageformats/libqsvg.so -qmldir="${{env.QML_DIR_NIX}}"
#
# Rename AppImage to match "%AppName%-%Version%-Linux.AppImage" format
#
- name: Rename AppImage
run: mv *.AppImage ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux.AppImage
#
# Upload AppImage to build artifacts
#
- name: Upload AppImage
uses: actions/upload-artifact@v2
with:
name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux.AppImage
path: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux.AppImage
#
# Windows build
#
build-windows:
runs-on: windows-latest
steps:
#
# Checkout the repository
#
- name: Checkout repository and submodules
uses: actions/checkout@v2
with:
submodules: recursive
#
# Configure MSVC
#
- name: Configure MSVC
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
spectre: true
- name: Install Qt
uses: jurplel/install-qt-action@v3
with:
aqtversion: '==3.1.*'
version: '6.10.0'
host: 'windows'
target: 'desktop'
arch: 'win64_msvc2022_64'
archives: 'qtdeclarative qtbase opengl32sw d3dcompiler_47'
#
# Install NSIS
#
- name: Install NSIS
run: |
Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
scoop bucket add extras
scoop install nsis
#
# Compile application
#
- name: Compile
run: |
qmake ${{env.QMAKE_PROJECT}} CONFIG+=release
nmake
#
# Copy Qt DLLs, compiler runtime & application icon
#
- name: Deploy
run: |
mkdir bin
move release/${{env.EXECUTABLE}}.exe bin
windeployqt bin/${{env.EXECUTABLE}}.exe -qmldir="${{env.QML_DIR_WIN}}" --compiler-runtime
mkdir "${{env.APPLICATION}}"
move bin "${{env.APPLICATION}}"
xcopy deploy\windows\resources\icon.ico "${{env.APPLICATION}}"
#
# Create NSIS installer
#
- name: Make NSIS installer
run: |
move "${{env.APPLICATION}}" deploy\windows\nsis\
cd deploy\windows\nsis
makensis /X"SetCompressor /FINAL lzma" setup.nsi
ren *.exe ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.exe
#
# Upload installer to build artifacts
#
- name: Upload NSIS installer
uses: actions/upload-artifact@v2
with:
name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.exe
path: deploy/windows/nsis/${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.exe

5
.gitignore vendored
View File

@@ -85,6 +85,11 @@ CMakeLists.txt.user*
.vs/ .vs/
out/ out/
.cache
.direnv .direnv
dist dist
CMakePresets.json
CMakeUserPresets.json CMakeUserPresets.json
ext/*
!ext/.gitkeep

11
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"C_Cpp.workspaceParsingPriority": "medium",
"files.associations": {
"flake.lock": "json",
"qobject": "cpp",
"chrono": "cpp",
"variant": "cpp",
"qtmultimedia": "cpp",
"qdebug": "cpp"
}
}

17
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "cmake",
"label": "CMake: build",
"command": "build",
"options": {"cwd": "${workspaceFolder}/build"},
"targets": [
"all"
],
"group": "build",
"problemMatcher": [],
"detail": "CMake template build task"
}
]
}

View File

@@ -1,15 +1,22 @@
cmake_minimum_required(VERSION 3.16) 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(qyouradio VERSION 0.1 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(Qt6 REQUIRED COMPONENTS 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(appqyouradio qt_add_executable(appqyouradio
main.cpp main.cpp
buildFlags.h
player.cpp player.h
resources/qyouradio.rc resources/qyouradio.rc
resources/resource.h resources/resource.h
) )
@@ -28,7 +35,7 @@ qt_add_qml_module(appqyouradio
VERSION 1.0 VERSION 1.0
QML_FILES QML_FILES
Main.qml Main.qml
Player.qml # Player.qml
ViewPlayer.qml ViewPlayer.qml
ViewSettings.qml ViewSettings.qml
) )
@@ -44,10 +51,18 @@ set_target_properties(appqyouradio PROPERTIES
WIN32_EXECUTABLE TRUE WIN32_EXECUTABLE TRUE
) )
target_include_directories(appqyouradio PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
target_link_libraries(appqyouradio 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 appqyouradio
BUNDLE DESTINATION . BUNDLE DESTINATION .

View File

@@ -1,22 +0,0 @@
{
"version": 3,
"configurePresets": [
{
"hidden": true,
"name": "Qt",
"cacheVariables": {
"CMAKE_PREFIX_PATH": "$env{QTDIR}"
},
"vendor": {
"qt-project.org/Qt": {
"checksum": "wVa86FgEkvdCTVp1/nxvrkaemJc="
}
}
}
],
"vendor": {
"qt-project.org/Presets": {
"checksum": "67SmY24ZeVbebyKD0fGfIzb/bGI="
}
}
}

155
Main.qml
View File

@@ -11,28 +11,95 @@ ApplicationWindow {
height: 800 height: 800
title: qsTr("QYouRadio") title: qsTr("QYouRadio")
Component.onCompleted: function() {
console.log(Player.stations)
}
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: 10
Rectangle {
Layout.fillWidth: true
height: 43
color: Colors.surface0
RowLayout { RowLayout {
Layout.fillWidth: true anchors.fill: parent
anchors.margins: 3
Label { Label {
text: "QYR" Layout.leftMargin: 5
heading: "h3" text: "QYouRadio"
font.bold: true heading: "h1"
// font.bold: true
} }
Item { Item {
Layout.fillWidth: true 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 { Label {
visible: Player.playing anchors.top: parent.top
text: "Playback active" anchors.left: parent.left
heading: "h4" anchors.right: parent.right
font.bold: true 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 {
@@ -42,6 +109,9 @@ ApplicationWindow {
TabBar { TabBar {
id: tabbar id: tabbar
spacing: 10 spacing: 10
background: Item{}
TabButton { TabButton {
text: qsTr("Autoradio") text: qsTr("Autoradio")
} }
@@ -51,58 +121,81 @@ ApplicationWindow {
TabButton { TabButton {
text: qsTr("Deep Bass") text: qsTr("Deep Bass")
} }
} TabButton {
Button { text: qsTr("Settings")
text: "S" }
onClicked: function() {
var component = Qt.createComponent("ViewSettings.qml")
var window = component.createObject(root)
window.show()
} }
} }
} }
SwipeView { SwipeView {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.margins: 10 Layout.margins: 20
interactive: false interactive: false
currentIndex: tabbar.currentIndex currentIndex: tabbar.currentIndex
Loader { Loader {
active: tabbar.currentIndex == 0 // active: tabbar.currentIndex == 0
active: true
asynchronous: true asynchronous: true
visible: status == Loader.Ready visible: status == Loader.Ready
sourceComponent: ViewPlayer { sourceComponent: ViewPlayer {
title: qsTr("Autoradio") index: 0
streamURL: "https://youradio.nonamesoft.xyz/api/autoradio"
} }
} }
Loader { Loader {
active: tabbar.currentIndex == 1 // active: tabbar.currentIndex == 1
active: true
asynchronous: true asynchronous: true
visible: status == Loader.Ready visible: status == Loader.Ready
sourceComponent: ViewPlayer { sourceComponent: ViewPlayer {
title: qsTr("Live Mix") index: 1
streamURL: "https://youradio.nonamesoft.xyz/api/live"
} }
} }
Loader { Loader {
active: tabbar.currentIndex == 2 // active: tabbar.currentIndex == 2
active: true
asynchronous: true asynchronous: true
visible: status == Loader.Ready visible: status == Loader.Ready
sourceComponent: ViewPlayer { sourceComponent: ViewPlayer {
title: qsTr("Deep Bass") index: 2
streamURL: "https://youradio.nonamesoft.xyz/api/deepbass" }
}
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);
} }
} }
} }
YouAds { Slider {
Layout.fillWidth: false Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter 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,30 +0,0 @@
import QtMultimedia 6.8
pragma Singleton
MediaPlayer {
source: ""
function startPlaying(url) {
if (playing) {
return;
}
console.log("Starting playback from " + url);
source = url;
play();
}
function stopPlaying() {
console.log("Stopping playback...");
source = "";
stop();
}
audioOutput: AudioOutput {
volume: 0.4
}
onErrorOccurred: function(error, errorString) {
stopPlaying();
}
}

122
Player.qml.prev Normal file
View File

@@ -0,0 +1,122 @@
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

@@ -23,22 +23,22 @@ Item {
primaryAlt: "#0056b3", primaryAlt: "#0056b3",
secondary: "#009eff", secondary: "#009eff",
secondaryAlt: "#0076b3", secondaryAlt: "#0076b3",
surface1: "#323232", surface1: "#373737",
surface0: "#282828", surface0: "#2a2a2a",
background: "#1f1f1f" background: "#1f1f1f"
}) })
} }
readonly property string fontFamily: "Arial" readonly property string fontFamily: "Arial"
readonly property var fontSize: ({ readonly property var fontSize: ({
h1: 32, h1: 26,
h2: 24, h2: 24,
h3: 18, h3: 18,
h4: 16, h4: 16,
h5: 13, h5: 12,
h6: 10, // h6: 10,
p: 14, p: 12,
base: 14, base: 12,
button: 12 button: 12
}) })

View File

@@ -5,6 +5,7 @@ import QtQuick.Controls.Basic 6.8
Label { Label {
property string heading: "base" property string heading: "base"
color: Colors.text
font.family: Colors.fontFamily font.family: Colors.fontFamily
font.pixelSize: Colors.fontSize[heading] font.pixelSize: Colors.fontSize[heading]
} }

View File

@@ -5,85 +5,47 @@ import QtQuick.Layouts 6.8
import QYRComponents 1.0 import QYRComponents 1.0
ColumnLayout { ColumnLayout {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
property string title: "" property var index: null
property string streamURL: ""
property string metaURL: ""
Label { Label {
Layout.fillWidth: true Layout.fillWidth: true
Layout.bottomMargin: 20 Layout.bottomMargin: 20
text: title text: qsTr(Player.stations[index].name)
font.bold: true
heading: "h2" heading: "h2"
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
Label {
Layout.fillWidth: true
text: "Title: "
heading: "h3"
horizontalAlignment: Text.AlignHCenter
}
Label {
Layout.fillWidth: true
text: "Artist: "
heading: "h3"
horizontalAlignment: Text.AlignHCenter
}
Label {
Layout.fillWidth: true
text: "Genre: "
heading: "h3"
horizontalAlignment: Text.AlignHCenter
}
Label {
Layout.fillWidth: true
text: "Bitrate: " + " kbps"
heading: "h3"
horizontalAlignment: Text.AlignHCenter
}
Label {
Layout.fillWidth: true
text: "Listeners: "
heading: "h3"
horizontalAlignment: Text.AlignHCenter
}
RowLayout {
Layout.fillWidth: false
Layout.topMargin: 20
Layout.alignment: Qt.AlignHCenter
width: 220
Button {
Layout.rightMargin: 5
text: Player.loading ? "Loading" : (Player.playing ? "Pause" : "Play")
onClicked: function() {
if (Player.playing) {
Player.stopPlaying();
} else {
Player.startPlaying(parent.parent.streamURL);
}
}
}
Slider {
Layout.fillWidth: true
Layout.leftMargin: 5
from: 0.1
to: 1.1
stepSize: 0.1
}
}
Item { Item {
Layout.fillHeight: true 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,5 +1,7 @@
{ stdenv { stdenv
, lib , lib
, alsa-lib
, xorg
, clang , clang
, qtbase , qtbase
, qtdeclarative , qtdeclarative
@@ -18,6 +20,10 @@ stdenv.mkDerivation {
src = ./.; src = ./.;
buildInputs = [ buildInputs = [
# alsa-lib
# xorg.libX11
# xorg.libXext
qtbase qtbase
qtdeclarative qtdeclarative
qtlocation qtlocation

1
buildFlags.h.in Normal file
View File

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

103
cmake/FindDiscordSDK.cmake Normal file
View File

@@ -0,0 +1,103 @@
# 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()

0
ext/.gitkeep Normal file
View File

View File

@@ -1,7 +1,9 @@
#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[])
{ {
@@ -15,5 +17,11 @@ int main(int argc, char *argv[])
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();
} }

113
player.cpp Normal file
View File

@@ -0,0 +1,113 @@
#include "player.h"
// Public
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}
})
{
QObject::connect(&this->player, &QMediaPlayer::playbackStateChanged, this, [this] () {
qDebug("Player::Player()->Lambda: Playing got changed!");
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);
qDebug("Player::Player(): Constructed");
}
Player::~Player()
{
qDebug("Player::~Player(): Destructed");
}
bool Player::playing() const
{
return this->player.playbackState() == QMediaPlayer::PlayingState;
}
float Player::volume() const
{
return this->output.volume();
}
void Player::setVolume(float newVolume)
{
this->output.setVolume(newVolume);
}
const QList<Station> Player::stations() const
{
return this->m_stations;
}
const int Player::currentIndex() const
{
return this->m_currentIndex;
}
const Station* Player::currentStation() const
{
if (this->m_currentIndex < 0 || this->m_currentIndex >= this->m_stations.length())
{
qWarning("Player::currentStation(): Tried accessing out of bounds.");
return nullptr;
}
return &this->m_stations[this->m_currentIndex];
}
// 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();
this->player.setSource(QUrl("https://youradio.nonamesoft.xyz/youradio/api/" + this->m_stations[index].m_slug));
this->player.play();
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->m_currentIndex = -1;
emit this->currentIndexChanged();
emit this->currentStationChanged();
qDebug("Player::stopPlaying(): Stopping playback...");
}

73
player.h Normal file
View File

@@ -0,0 +1,73 @@
#pragma once
#include <QtMultimedia>
#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)
public:
Player(QObject *parent = nullptr);
~Player();
bool playing() const;
float volume() const;
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();
signals:
void playingChanged();
void volumeChanged();
void stationsChanged();
void currentIndexChanged();
void currentStationChanged();
private:
QMediaPlayer player;
QAudioOutput output;
QTimer timer;
int m_currentIndex;
QList<Station> m_stations;
};