diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 71d8700..56d0447 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,4 @@ -# .github/workflows/sailfish-build.yml -name: Sailfish OS Package Build +name: Build SFOS 4.2 on: push: @@ -10,48 +9,29 @@ on: jobs: build: - runs-on: ubuntu-22.04 # Explizit Ubuntu 22.04 verwenden - container: - image: coderus/sailfishos-platform-sdk:4.4.0.58 - options: --privileged + runs-on: ubuntu-latest + container: coderus/sailfishos-platform-sdk:4.2.0.21 steps: - - name: Checkout code - uses: actions/checkout@v3 # Zurück zu v3, da es stabiler mit Containern arbeitet + - uses: actions/checkout@v2 + + - name: Build for aarch64 + run: mb2 -t SailfishOS-4.2.0.21-aarch64 build + + - name: Upload artifact + uses: actions/upload-artifact@v2 with: - fetch-depth: 0 + name: rpms + path: RPMS/*.rpm - - name: Prepare build environment - shell: bash - run: | - mkdir -p ~/rpmbuild/SOURCES - mkdir -p ~/rpmbuild/SPECS - cp rpm/harbour-tidalplayer.spec ~/rpmbuild/SPECS/ - cp rpm/harbour-tidalplayer.yaml ~/rpmbuild/SPECS/ - - - name: Create source tarball - shell: bash - run: | - VERSION=$(grep "Version:" rpm/harbour-tidalplayer.yaml | cut -d':' -f2 | tr -d ' ') - tar --transform "s,^,harbour-tidalplayer-$VERSION/," -czf ~/rpmbuild/SOURCES/harbour-tidalplayer-$VERSION.tar.gz * - - - name: Build RPM package - shell: bash - run: | - cd ~/rpmbuild/SPECS - mb2 -t SailfishOS-4.4.0.58 -s harbour-tidalplayer.spec build - - - name: Upload RPM artifacts - uses: actions/upload-artifact@v3 # Verwende v3 für bessere Container-Kompatibilität - with: - name: harbour-tidalplayer-rpm - path: ~/rpmbuild/RPMS/**/*.rpm - if-no-files-found: error - - - name: Create Release - if: startsWith(github.ref, 'refs/tags/') + - name: Create/Update Release uses: softprops/action-gh-release@v1 with: - files: ~/rpmbuild/RPMS/**/*.rpm - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + tag_name: nightly + name: Nightly Build + files: RPMS/*.rpm + prerelease: true + body: Automated nightly build + token: ${{ secrets.GITHUB_TOKEN }} + replace_artifacts: true + diff --git a/README.md b/README.md index 58ffd47..c0fdd81 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,21 @@ As v0.7.1 is not fully compatible with TIDAL anymore, the line 114 of tidalapi/u ### Usage of AI The current development is driven by Claude 3.5 Sonnet. The icon is made by Midjourney. + +## Future Features + +- Tidal account integration with OAuth authentication +- Browse and search Tidal's music library +- Create and manage playlists +- Play tracks, albums, and playlists +- Media controls (play, pause, next, previous) +- Track information display +- Album artwork display + +## Requirements + +- Python 3.x +- Qt/QML +- PyOtherSide +- Tidal API credentials + diff --git a/qml/components/AuthManager.qml b/qml/components/AuthManager.qml index 61377b8..a505993 100644 --- a/qml/components/AuthManager.qml +++ b/qml/components/AuthManager.qml @@ -3,7 +3,7 @@ import QtQuick 2.0 import Nemo.Configuration 1.0 Item { - id: root + id: authManager // Properties property bool isLoggedIn: false @@ -31,16 +31,32 @@ Item { key: "/expiry_time" } + ConfigurationValue { + id: mail + key: "/mail" + } + + ConfigurationValue { + id: audioQuality + key: "/audioQuality" + defaultValue: "HIGH" // Standardwert + } + // Funktionen zum Token-Management function updateTokens(type, token, rtoken, expiry) { + + var currentUnixTime = Math.floor(new Date().getTime() / 1000) + var oneWeekLater = currentUnixTime + 604800 + token_type.value = type access_token.value = token refresh_token.value = rtoken - expiry_time.value = expiry + expiry_time.value = oneWeekLater isLoggedIn = true } function checkAndLogin() { + pythonApi.quality = audioQuality.value if (token_type.value && access_token.value) { if (isTokenValid()) { console.log("old token valid"); @@ -60,10 +76,10 @@ Item { } function isTokenValid() { + + var currentUnixTime = Math.floor(new Date().getTime() / 1000) if (!expiry_time.value) return false - return Date.fromLocaleString(Qt.locale(), - expiry_time.value, - "yyyy-MM-ddThh:mm:ss") > currentDate + return expiry_time.value > currentUnixTime } function clearTokens() { diff --git a/qml/components/TidalApi.qml b/qml/components/TidalApi.qml index 8154d44..6d41d71 100644 --- a/qml/components/TidalApi.qml +++ b/qml/components/TidalApi.qml @@ -44,9 +44,14 @@ Item { property string playlist_artist: "" property string playlist_album: "" property string playlist_image: "" + + property string quality: "" + + property int playlist_duration: 0 property int playlist_track_id: 0 + property AuthManager authManager Python { id: pythonTidal @@ -67,6 +72,8 @@ Item { pythonApi.loginFailed() }) setHandler('get_token', function(type, token, rtoken, date) { + console.log("Got new token from session") + console.log(type, token, rtoken, date) pythonApi.oAuthSuccess(type, token, rtoken, date) }) @@ -151,9 +158,11 @@ Item { } onOAuthSuccess: { - if (authManager) { + console.log(type, token, rtoken, date) + //if (authManager) { authManager.updateTokens(type, token, rtoken, date) - } + loginSuccess() + //} } onLoginSuccess: { @@ -175,8 +184,10 @@ Item { function loginIn(tokenType, accessToken, refreshToken, expiryTime) { console.log(accessToken) + pythonTidal.call('tidal.Tidaler.initialize', [quality]) pythonTidal.call('tidal.Tidaler.login', [tokenType, accessToken, refreshToken, expiryTime]) + //pythonTidal.call('tidal.Tidaler.checkAndLogin', []) } // Search Funktionen diff --git a/qml/dialogs/OAuth.qml b/qml/dialogs/OAuth.qml index 9cf9cc5..0e0a13f 100644 --- a/qml/dialogs/OAuth.qml +++ b/qml/dialogs/OAuth.qml @@ -1,6 +1,5 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 - import io.thp.pyotherside 1.5 import Sailfish.WebView 1.0 import Sailfish.WebEngine 1.0 @@ -12,51 +11,63 @@ Dialog { allowedOrientations: Orientation.All canAccept: false + ConfigurationValue { + id: mail + key: "/mail" + } + WebView { - width: parent.width - anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + anchors.horizontalCenter: parent.horizontalCenter + id: webView + anchors.fill: parent - id: webView + url: "http://www.sailfishos.org" + httpUserAgent: "Mozilla/5.0 (Mobile; rv:78.0) Gecko/78.0 Firefox/78.0" - anchors.fill: parent + popupProvider: PopupProvider { } - url: "http://www.sailfishos.org" - //privateMode: true - httpUserAgent: "Mozilla/5.0 (Mobile; rv:78.0) Gecko/78.0" - + " Firefox/78.0" + onLoadingChanged: { + if (loadRequest.status === WebView.LoadSucceededStatus) { + var script = "function fillEmail() {" + + "var emailInput = document.querySelector('input[type=\"email\"]') || " + + "document.querySelector('input[name=\"email\"]') || " + + "document.querySelector('#email');" + + "if (emailInput) {" + + " emailInput.value = '" + mail.value + "';" + + " emailInput.dispatchEvent(new Event('input'));" + + " emailInput.dispatchEvent(new Event('change'));" + + "}" + + "};" + + "fillEmail();" + + "setTimeout(fillEmail, 500);"; - popupProvider: PopupProvider { - // Disable the Save Password dialog - //passwordManagerPopup: null - } - } - - Connections { - target: pythonApi - onAuthUrl: - { - console.log(url) - webView.load("https://" + url) + webView.runJavaScript(script); } + } + } - onLoginSuccess: - { - accountSettings.canAccept = true - accountSettings.accept() - loginTrue = true + Connections { + target: pythonApi + onAuthUrl: { + console.log(url) + webView.url = "https://" + url + } - pythonApi.logIn() - } + onLoginSuccess: { + accountSettings.canAccept = true + accountSettings.accept() + loginTrue = true + authManager.checkAndLogin() + } - onLoginFailed: - { - mainLabel.text = "Failed"; - loginTrue = false - } + onLoginFailed: { + mainLabel.text = "Failed" + loginTrue = false } + } - Component.onCompleted: { - pythonApi.getOAuth() - } + Component.onCompleted: { + pythonApi.getOAuth() + } } - diff --git a/qml/harbour-tidalplayer.qml b/qml/harbour-tidalplayer.qml index b893d7f..0b68e58 100644 --- a/qml/harbour-tidalplayer.qml +++ b/qml/harbour-tidalplayer.qml @@ -161,7 +161,7 @@ ApplicationWindow authManager.checkAndLogin() mprisPlayer.setCanControl(true) } - /* + Connections { target: pythonApi onOAuthSuccess: { @@ -171,7 +171,7 @@ ApplicationWindow authManager.clearTokens() } } -*/ + Connections { diff --git a/qml/pages/FirstPage.qml b/qml/pages/FirstPage.qml index d20e47d..97dcd85 100644 --- a/qml/pages/FirstPage.qml +++ b/qml/pages/FirstPage.qml @@ -24,7 +24,10 @@ Page { PullDownMenu { MenuItem { text: qsTr("Settings") - onClicked: pageStack.push(Qt.resolvedUrl("Settings.qml")) + onClicked: { + minPlayerPanel.open = false + pageStack.push(Qt.resolvedUrl("Settings.qml")) + } } diff --git a/qml/pages/Settings.qml b/qml/pages/Settings.qml index a0bcee2..8f4279c 100644 --- a/qml/pages/Settings.qml +++ b/qml/pages/Settings.qml @@ -4,59 +4,116 @@ import Nemo.Configuration 1.0 Page { id: page - - // The effective value will be restricted by ApplicationWindow.allowedOrientations allowedOrientations: Orientation.All - SilicaListView { - id: listView - //model: 20 - anchors.fill: parent - header: PageHeader { - title: qsTr("Settings") - } - delegate: BackgroundItem { - id: delegate + ConfigurationValue { + id: mail + key: "/mail" + } + ConfigurationValue { + id: audioQuality + key: "/audioQuality" + defaultValue: "HIGH" // Standardwert + } - } - //VerticalScrollDecorator {} + SilicaFlickable { + anchors.fill: parent + contentHeight: column.height - width: parent.width + Column { + id: column + width: parent.width + spacing: Theme.paddingLarge - spacing: Theme.paddingLarge + PageHeader { + title: qsTr("Settings") + } - Button { - id:loginButton - text: "Tidal Login via OAuth" - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - width: parent.width - visible: !loginTrue - onClicked: { - var dialog = pageStack.push(Qt.resolvedUrl("../dialogs/OAuth.qml")) + SectionHeader { + text: qsTr("Account") } - } - Button { - id:logoutButton + TextField { + id: emailField + width: parent.width + text: mail.value || "" + label: qsTr("Email address") + placeholderText: qsTr("Enter your email") + EnterKey.enabled: text.length > 0 + EnterKey.iconSource: "image://theme/icon-m-enter-next" + EnterKey.onClicked: focus = false + + onTextChanged: { + mail.value = text + } + } - text: "Remove Session" - anchors.horizontalCenter: loginButton.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - width: parent.width - visible: loginTrue + Item { + width: parent.width + height: Theme.paddingLarge + } - onClicked: { - token_type.value = "clear" - access_token.value = "clear" - loginTrue = false + TextSwitch { + visible: loginTrue + text: qsTr("Stay logged in") + description: qsTr("Keep your session active") + // Verbinde dies mit deiner Konfiguration + checked: false } - } + Button { + anchors { + left: parent.left + right: parent.right + margins: Theme.horizontalPageMargin + } + text: qsTr("Login with Tidal") + visible: !loginTrue + onClicked: { + pageStack.push(Qt.resolvedUrl("../dialogs/OAuth.qml")) + } + } + Button { + anchors { + left: parent.left + right: parent.right + margins: Theme.horizontalPageMargin + } + text: qsTr("Logout") + visible: loginTrue + onClicked: { + authManager.clearTokens() + token_type.value = "clear" + access_token.value = "clear" + loginTrue = false + } + } - } + SectionHeader { + text: qsTr("Playback") + visible: loginTrue + } + ComboBox { + visible: loginTrue + label: qsTr("Audio Quality") + currentIndex: 1 // Default auf HIGH + description: qsTr("Select streaming quality") + menu: ContextMenu { + MenuItem { text: qsTr("Low (96 kbps)") } + MenuItem { text: qsTr("High (320 kbps)") } + MenuItem { text: qsTr("Lossless (FLAC)") } + MenuItem { text: qsTr("Master (MQA)") } + } + onCurrentIndexChanged: { + var qualities = ["LOW", "HIGH", "LOSSLESS", "HI_RES"] + audioQuality.value = qualities[currentIndex] + } + } + } + VerticalScrollDecorator {} + } } diff --git a/qml/tidal.py b/qml/tidal.py index 473bcff..4b41040 100644 --- a/qml/tidal.py +++ b/qml/tidal.py @@ -16,7 +16,22 @@ class Tidal: def __init__(self): - self.session = tidalapi.Session() + self.session = None + self.config = None + + def initialize(self, quality="HIGH"): + if quality == "LOW": + selected_quality = tidalapi.Quality.low + elif quality == "HIGH": + selected_quality = tidalapi.Quality.high + elif quality == "LOSSLESS": + selected_quality = tidalapi.Quality.lossless + else: + # Fallback auf HIGH wenn unbekannte Qualität + selected_quality = tidalapi.Quality.high + + self.config = tidalapi.Config(quality=selected_quality, video_quality=tidalapi.VideoQuality.low) + self.session = tidalapi.Session(self.config) def login(self, token_type, access_token, refresh_token, expiry_time): if access_token == token_type: @@ -30,9 +45,16 @@ def login(self, token_type, access_token, refresh_token, expiry_time): pyotherside.send("oauth_updated", self.session.token_type, self.session.access_token, self.session.refresh_token, self.session.expiry_time) def request_oauth(self): + pyotherside.send("printConsole", "Start new session") self.login, self.future = self.session.login_oauth() + pyotherside.send("printConsole", "getting url") + pyotherside.send("get_url", self.login.verification_uri_complete) + pyotherside.send("printConsole", "waiting for done") + self.future.result() + pyotherside.send("printConsole", "Done", self.session.token_type, self.session.access_token) + if self.session.check_login() == True: pyotherside.send("get_token", self.session.token_type, self.session.access_token, self.session.refresh_token, self.session.expiry_time) else: