diff --git a/qml/components/TidalApi.qml b/qml/components/TidalApi.qml index b8e3f74..2cab971 100644 --- a/qml/components/TidalApi.qml +++ b/qml/components/TidalApi.qml @@ -38,6 +38,10 @@ Item { signal cacheTrack(var track_info) signal cacheAlbum(var album_info) signal cacheArtist(var artist_info) + signal albumofArtist(var album_info) + signal topTracksofArtist(var track_info) + signal similarArtist(var artist_info) + signal noSimilarArtists() signal playlistTrackAdded(var track_info) signal albumTrackAdded(var track_info) @@ -117,6 +121,21 @@ Item { tidalApi.cacheAlbum(album_info) }) + setHandler('TopTrackofArtist', function(track_info) { + tidalApi.topTracksofArtist(track_info) + }) + + setHandler('AlbumofArtist', function(album_info) { + tidalApi.albumofArtist(album_info) + }) + + setHandler('SimilarArtist', function(artist_info) { + tidalApi.similarArtist(artist_info) + }) + + setHandler('noSimilarArtists', function() { + tidalApi.noSimilarArtists() + }) // Search Handler setHandler('addTrack', function(id, title, album, artist, image, duration) { @@ -374,7 +393,17 @@ Item { pythonTidal.call('tidal.Tidaler.get_favorite_tracks', []) } + function getAlbumsofArtist(artistid) { + pythonTidal.call('tidal.Tidaler.getAlbumsofArtist', [artistid]) + } + function getTopTracksofArtist(artistid) { + pythonTidal.call('tidal.Tidaler.getTopTracksofArtist', [artistid]) + } + + function getSimiliarArtist(artistid) { + pythonTidal.call('tidal.Tidaler.getSimiliarArtist', [artistid]) + } } diff --git a/qml/components/TidalCache.qml b/qml/components/TidalCache.qml index 49693c0..29f37e7 100644 --- a/qml/components/TidalCache.qml +++ b/qml/components/TidalCache.qml @@ -19,10 +19,13 @@ id: root target: tidalApi // Bestehende Connections + /* onTrackChanged: { // id, title, album, artist, image, duration saveTrackToCache({ - id: id, + trackid: trackid, + albumid: albumid, + artistid: artistid, title: title, album: album, artist: artist, @@ -35,9 +38,10 @@ id: root onAlbumChanged: { // id, title, artist, image saveAlbumToCache({ - id: id, + albumid: albumid, title: title, artist: artist, + artistid: artistid, image: image, timestamp: Date.now() }) @@ -46,25 +50,27 @@ id: root onArtistChanged: { // id, name, img saveArtistToCache({ - id: id, + artistid: artistid, name: name, image: img, timestamp: Date.now() }) } - + */ // Neue Connections für Suchergebnisse onCacheTrack: { //track_info saveTrackToCache({ - id: track_info.id, + trackid: track_info.trackid, title: track_info.title, - album: track_info.album, artist: track_info.artist, - image: track_info.image, - duration: track_info.duration, + artistid:track_info.artistid, + album: track_info.album, albumid: track_info.albumid, + duration: track_info.duration, + image: track_info.image, + track_num : track_info.track_num, timestamp: Date.now(), fromSearch: true // Optional: markiert Einträge aus der Suche }) @@ -73,10 +79,10 @@ id: root onCacheArtist: { //artist_info saveArtistToCache({ - id: artist_info.id, + artistid: artist_info.artistid, name: artist_info.name, - bio: artist_info.bio, image: artist_info.image, + bio: artist_info.bio, timestamp: Date.now(), fromSearch: true // Optional: markiert Einträge aus der Suche }) @@ -85,19 +91,25 @@ id: root onCacheAlbum: { //album_info saveAlbumToCache({ - id: album_info.id, + albumid: album_info.albumid, title: album_info.title, artist: album_info.artist, + artistid: album_info.artistid, image: album_info.image, duration: album_info.duration, + num_tracks : album_info.num_tracks, + year : album_info.year, timestamp: Date.now(), fromSearch: true // Optional: markiert Einträge aus der Suche }) } + /* onTrackAdded: { // id, title, album, artist, image, duration saveTrackToCache({ - id: id, + trackid: trackid, + albumid: albumid, + artistid: artistid, title: title, album: album, artist: artist, @@ -111,7 +123,8 @@ id: root onAlbumAdded: { // id, title, artist, image, duration saveAlbumToCache({ - id: id, + albumid: albumid, + artistid: artistid, title: title, artist: artist, image: image, @@ -130,7 +143,7 @@ id: root timestamp: Date.now(), fromSearch: true }) - } + }*/ } // Optional: Erweiterte Such-spezifische Funktionen @@ -150,7 +163,7 @@ id: root function addSearchTrack(id) { if (!searchResults.tracks.includes(id)) { searchResults.tracks.push(id) - }d, title, artist, image, duration + } } function addSearchAlbum(id) { @@ -207,7 +220,9 @@ id: root var result = tidalApi.getTrackInfo(id) if (result) { var trackData = { - id: id, + trackid: id, + albumid: result.albumid, + artistid: result.artistid, title: result.title, artist: result.artist, album: result.album, @@ -222,9 +237,73 @@ id: root return null } + function getAlbumInfo(id) { + // Erst im Cache nachsehen + var cachedTrack = getAlbum(id) + if (cachedTrack) { + if (Date.now() - cachedTrack.timestamp < maxCacheAge) { + return cachedTrack + } else { + console.log("Cache entry too old, refreshing...") + } + } + + // Wenn nicht im Cache oder zu alt, von Python holen + + var result = tidalApi.getAlbumInfo(id) + if (result) { + var trackData = { + albumid: id, + title: result.title, + artist: result.artist, + artistid: result.artistid, + image : result.image, + duration: result.duration, + num_tracks : result.num_tracks, + year : result.year, + timestamp: Date.now() + } + + saveAlbumToCache(trackData) + return trackData + } + + return null + } + + function getArtistInfo(id) { + // Erst im Cache nachsehen + var cachedTrack = getArtist(id) + if (cachedTrack) { + if (Date.now() - cachedTrack.timestamp < maxCacheAge) { + return cachedTrack + } else { + console.log("Cache entry too old, refreshing...") + } + } + + // Wenn nicht im Cache oder zu alt, von Python holen + + var result = tidalApi.getArtistInfo(id) + if (result) { + var trackData = { + artistid: id, + name: result.name, + image : result.image, + bio : result.bio, + timestamp: Date.now() + } + + saveAlbumToCache(trackData) + return trackData + } + + return null + } + // Datenbank initialisieren function initDatabase() { - db = LocalStorage.openDatabaseSync("TidalCache", "1.1", "Cache for Tidal data", 1000000) + db = LocalStorage.openDatabaseSync("TidalCache", "1.2", "Cache for Tidal data", 1000000) db.transaction(function(tx) { // Tracks Tabelle tx.executeSql('CREATE TABLE IF NOT EXISTS tracks(id TEXT PRIMARY KEY, data TEXT, timestamp INTEGER)') @@ -268,6 +347,7 @@ id: root for(i = 0; i < rs.rows.length; i++) { try { artistCache[rs.rows.item(i).id] = JSON.parse(rs.rows.item(i).data) + //console.log(artistCache[rs.rows.item(i).id].artistid, artistCache[rs.rows.item(i).id].name) } catch(e) { console.error("Error parsing artist data:", e) } @@ -278,26 +358,26 @@ id: root // Cache-Speicherfunktionen function saveTrackToCache(trackData) { - trackCache[trackData.id] = trackData + trackCache[trackData.trackid] = trackData db.transaction(function(tx) { tx.executeSql('INSERT OR REPLACE INTO tracks(id, data, timestamp) VALUES(?, ?, ?)', - [trackData.id, JSON.stringify(trackData), trackData.timestamp]) + [trackData.trackid, JSON.stringify(trackData), trackData.timestamp]) }) } function saveAlbumToCache(albumData) { - albumCache[albumData.id] = albumData + albumCache[albumData.albumid] = albumData db.transaction(function(tx) { tx.executeSql('INSERT OR REPLACE INTO albums(id, data, timestamp) VALUES(?, ?, ?)', - [albumData.id, JSON.stringify(albumData), albumData.timestamp]) + [albumData.albumid, JSON.stringify(albumData), albumData.timestamp]) }) } function saveArtistToCache(artistData) { - artistCache[artistData.id] = artistData + artistCache[artistData.artistidid] = artistData db.transaction(function(tx) { tx.executeSql('INSERT OR REPLACE INTO artists(id, data, timestamp) VALUES(?, ?, ?)', - [artistData.id, JSON.stringify(artistData), artistData.timestamp]) + [artistData.artistid, JSON.stringify(artistData), artistData.timestamp]) }) } diff --git a/qml/pages/AlbumPage.qml b/qml/pages/AlbumPage.qml index 987aa16..b3271e9 100644 --- a/qml/pages/AlbumPage.qml +++ b/qml/pages/AlbumPage.qml @@ -84,14 +84,45 @@ Page { spacing: Theme.paddingSmall anchors.verticalCenter: parent.verticalCenter - Label { - id: artistName - width: parent.width - text: albumData ? albumData.artist : "" - truncationMode: TruncationMode.Fade - color: Theme.highlightColor - font.pixelSize: Theme.fontSizeLarge - } +BackgroundItem { + width: parent.width + height: artistRow.height + Theme.paddingMedium * 2 + + Rectangle { + anchors.fill: parent + color: Theme.rgba(Theme.highlightBackgroundColor, parent.pressed ? 0.3 : 0.1) + radius: Theme.paddingSmall + } + + Row { + id: artistRow + anchors.centerIn: parent + spacing: Theme.paddingMedium + + Image { + id: artistIcon + source: "image://theme/icon-s-person" + width: Theme.iconSizeSmall + height: width + anchors.verticalCenter: parent.verticalCenter + } + + Label { + id: artistName + text: albumData ? albumData.artist : "" + truncationMode: TruncationMode.Fade + color: parent.parent.pressed ? Theme.highlightColor : Theme.primaryColor + font.pixelSize: Theme.fontSizeLarge + } + } + + onClicked: { + if (albumData && albumData.artistid) { + pageStack.push(Qt.resolvedUrl("ArtistPage.qml"), + { artistId: albumData.artistid }) + } + } +} Label { width: parent.width @@ -102,7 +133,7 @@ Page { Label { width: parent.width - text: albumData ? qsTr("Released: ") + albumData.releaseDate : "" + text: albumData ? qsTr("Released: ") + albumData.year : "" color: Theme.secondaryColor font.pixelSize: Theme.fontSizeSmall opacity: isHeaderCollapsed ? 0.0 : 1.0 @@ -112,7 +143,7 @@ Page { Label { width: parent.width - text: albumData ? qsTr("Tracks: ") + albumData.numberOfTracks : "" + text: albumData ? qsTr("Tracks: ") + albumData.num_tracks : "" color: Theme.secondaryColor font.pixelSize: Theme.fontSizeSmall opacity: isHeaderCollapsed ? 0.0 : 1.0 @@ -172,7 +203,7 @@ Page { } Label { - text: albumData ? qsTr("%n tracks", "", albumData.numberOfTracks) : "" + text: albumData ? qsTr("%n tracks", "", albumData.num_tracks) : "" color: Theme.secondaryColor font.pixelSize: Theme.fontSizeExtraSmall anchors.verticalCenter: parent.verticalCenter diff --git a/qml/pages/ArtistPage.qml b/qml/pages/ArtistPage.qml index d661336..0813597 100644 --- a/qml/pages/ArtistPage.qml +++ b/qml/pages/ArtistPage.qml @@ -2,103 +2,395 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import QtMultimedia 5.6 import Sailfish.Media 1.0 - import "widgets" - Page { id: artistPage - property int track_id + property int artistId : -1 + property var artistData: null + property bool isHeaderCollapsed: false + +function processWimpLinks(text) { + if (!text) return "" + + // Text in Teile zerlegen + var parts = text.split("[wimpLink") + var result = parts[0] // Start mit dem ersten Teil ohne Link + + // Durch alle weiteren Teile gehen + for (var i = 1; i < parts.length; i++) { + var part = parts[i] + try { + // Artist Link + if (part.indexOf('artistId="') >= 0) { + var idMatch = part.match(/artistId="(\d+)"/) + var textMatch = part.match(/](.*?)\[/) + if (idMatch && textMatch) { + result += '' + textMatch[1] + '' + result += part.split("[/wimpLink]")[1] || "" + } + } + // Album Link + else if (part.indexOf('albumId="') >= 0) { + var idMatch = part.match(/albumId="(\d+)"/) + var textMatch = part.match(/](.*?)\[/) + if (idMatch && textMatch) { + result += '' + textMatch[1] + '' + result += part.split("[/wimpLink]")[1] || "" + } + } + else { + // Falls kein Match, original Text behalten + result += "[wimpLink" + part + } + } catch (e) { + console.log("Fehler beim Verarbeiten eines Links:", e) + // Bei Fehler original Text behalten + result += "[wimpLink" + part + } + } + return result +} - // The effective value will be restricted by ApplicationWindow.allowedOrientations allowedOrientations: Orientation.All - // To enable PullDownMenu, place our content in a SilicaFlickable SilicaFlickable { - - width: parent.width + id: flickable anchors { fill: parent bottomMargin: minPlayerPanel.margin } + contentHeight: mainColumn.height + PullDownMenu { - MenuItem { - text: qsTr("Show Playlist") - onClicked: - { - onClicked: pageStack.push(Qt.resolvedUrl("PlaylistPage.qml")) - } - } MenuItem { text: minPlayerPanel.open ? "Hide player" : "Show player" onClicked: minPlayerPanel.open = !minPlayerPanel.open - anchors.horizontalCenter: parent.horizontalCenter } } + Column { - id: infoCoulumn + id: mainColumn + width: parent.width + spacing: Theme.paddingMedium + PageHeader { id: header - title: qsTr("Artist Info") + title: qsTr("Artist Info") } - spacing: 10 // Abstand zwischen den Elementen in der Column - width: parent.width // Die Column nimmt die volle Breite des Eltern-Elements (Item) ein - Image { - id: coverImage - anchors { - top: header.bottom - horizontalCenter: albumPage.isPortrait ? parent.horizontalCenter : undefined + // Artist Info Section + Item { + id: artistInfoContainer + width: parent.width + height: isHeaderCollapsed ? Theme.itemSizeLarge : width * 0.4 + clip: true + + Behavior on height { + NumberAnimation { duration: 200 } } - sourceSize.width: { - var maxImageWidth = Screen.width - var leftMargin = Theme.horizontalPageMargin - var rightMargin = artistPage.isPortrait ? Theme.horizontalPageMargin : 0 - return (maxImageWidth - leftMargin - rightMargin)*3/2 + Row { + width: parent.width + height: parent.height + spacing: Theme.paddingMedium + anchors.margins: Theme.paddingMedium + + Image { + id: coverImage + width: parent.height + height: width + fillMode: Image.PreserveAspectFit + + Rectangle { + color: Theme.rgba(Theme.highlightBackgroundColor, 0.1) + anchors.fill: parent + visible: coverImage.status !== Image.Ready + } + } + + Column { + width: parent.width - coverImage.width - parent.spacing - Theme.paddingLarge * 2 + height: parent.height + spacing: Theme.paddingSmall + anchors.verticalCenter: parent.verticalCenter + + Label { + id: artistName + width: parent.width + truncationMode: TruncationMode.Fade + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeLarge + } + + Item { + width: parent.width + height: parent.height - artistName.height - parent.spacing + clip: true + + Flickable { + id: bioFlickable + anchors.fill: parent + contentHeight: bioText.height + clip: true + + Label { + id: bioText + width: parent.width + wrapMode: Text.WordWrap + textFormat: Text.RichText // Wichtig für HTML-Links + color: Theme.secondaryColor + font.pixelSize: Theme.fontSizeSmall + + onLinkActivated: { + var parts = link.split(":") + if (parts.length === 2) { + if (parts[0] === "artist") { + pageStack.push(Qt.resolvedUrl("ArtistPage.qml"), + { artistId: parseInt(parts[1]) }) + } else if (parts[0] === "album") { + pageStack.push(Qt.resolvedUrl("AlbumPage.qml"), + { albumId: parseInt(parts[1]) }) + } + } + } + } + } + + // Scrollbar für die Biografie + VerticalScrollDecorator { + flickable: bioFlickable + } + } + } + } + } - fillMode: Image.PreserveAspectFit + // Albums Section + SectionHeader { + text: qsTr("Albums") } - Label - { - id: artistName - anchors { - top : coverImage.bottom + + // Ersetze den ScrollDecorator mit diesem angepassten horizontalen Scroll-Indikator + SilicaListView { + id: albumsView + width: parent.width + height: Theme.itemSizeLarge * 2.5 // Höhe vergrößert + orientation: ListView.Horizontal + clip: true + spacing: Theme.paddingMedium // Abstand zwischen den Items + + model: ListModel {} + + delegate: BackgroundItem { + width: Theme.itemSizeLarge * 2 // Breite vergrößert + height: albumsView.height + + Column { + anchors { + fill: parent + margins: Theme.paddingSmall + } + spacing: Theme.paddingMedium // Mehr Abstand zwischen Bild und Text + + Image { + width: parent.width + height: width // Quadratisches Cover + source: model.cover + fillMode: Image.PreserveAspectCrop + } + + Label { + width: parent.width + text: model.title + truncationMode: TruncationMode.Fade + font.pixelSize: Theme.fontSizeSmall // Größere Schrift + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap // Text kann umbrechen + maximumLineCount: 2 // Maximal zwei Zeilen + } } - truncationMode: TruncationMode.Fade + + onClicked: pageStack.push(Qt.resolvedUrl("AlbumPage.qml"), + { albumId: model.albumId }) + } + + // Horizontaler Scroll-Indikator + Rectangle { + visible: albumsView.contentWidth > albumsView.width + height: 2 color: Theme.highlightColor + opacity: 0.4 + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + + Rectangle { + height: parent.height + color: Theme.highlightColor + width: Math.max(parent.width * (albumsView.width / albumsView.contentWidth), Theme.paddingLarge) + x: (parent.width - width) * (albumsView.contentX / (albumsView.contentWidth - albumsView.width)) + visible: albumsView.contentWidth > albumsView.width + } } } + + // Top Tracks Section + SectionHeader { + text: qsTr("Popular Tracks") + } + TrackList { id: topTracks - title : "Popular Tracks" + width: parent.width + height: artistPage.height - y - (minPlayerPanel.open ? minPlayerPanel.height : 0) + type: "tracklist" + } + + // Similiar Artists Section + SectionHeader { + id:similarArtistsSection + text: qsTr("Similiar Artists") + } + + // Ersetze den ScrollDecorator mit diesem angepassten horizontalen Scroll-Indikator + SilicaListView { + id: simartistView + width: parent.width + height: Theme.itemSizeLarge * 2.5 // Höhe vergrößert + orientation: ListView.Horizontal + clip: true + spacing: Theme.paddingMedium // Abstand zwischen den Items + + model: ListModel {} + + delegate: BackgroundItem { + width: Theme.itemSizeLarge * 2 // Breite vergrößert + height: simartistView.height + + Column { + anchors { + fill: parent + margins: Theme.paddingSmall + } + spacing: Theme.paddingMedium // Mehr Abstand zwischen Bild und Text + + Image { + width: parent.width + height: width // Quadratisches Cover + source: model.cover + fillMode: Image.PreserveAspectCrop + } + + Label { + width: parent.width + text: model.name + truncationMode: TruncationMode.Fade + font.pixelSize: Theme.fontSizeSmall // Größere Schrift + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap // Text kann umbrechen + maximumLineCount: 2 // Maximal zwei Zeilen + } + } + + onClicked: pageStack.push(Qt.resolvedUrl("ArtistPage.qml"), + { artistId: model.artistId }) + } + } + // Horizontaler Scroll-Indikator + Rectangle { + visible: simartistView.contentWidth > simartistView.width + height: 2 + color: Theme.highlightColor + opacity: 0.4 anchors { - top: infoCoulumn.bottom// Anker oben an den unteren Rand der Column - topMargin: 650 // Abstand zwischen der Column und dem ListView - left: parent.left // Anker links am linken Rand des Eltern-Elements (Page) - right: parent.right // Anker rechts am rechten Rand des Eltern-Elements (Page) - leftMargin: Theme.horizontalPageMargin - rightMargin: Theme.horizontalPageMargin - //bottom: parent.bottom// Anker unten am unteren Rand des Eltern-Elements (Page) - } - height: 600 + left: parent.left + right: parent.right + bottom: parent.bottom + } + + Rectangle { + height: parent.height + color: Theme.highlightColor + width: Math.max(parent.width * (simartistView.width / simartistView.contentWidth), Theme.paddingLarge) + x: (parent.width - width) * (simartistView.contentX / (simartistView.contentWidth - simartistView.width)) + visible: simartistView.contentWidth > simartistView.width + } } + } + + VerticalScrollDecorator {} + onContentYChanged: { + if (contentY > Theme.paddingLarge) { + isHeaderCollapsed = true + } else { + isHeaderCollapsed = false + } } + } + + Component.onCompleted: { + if (artistId > 0) { + artistData = cacheManager.getArtist(artistId) + if (!artistData) { + console.log("Artist nicht im Cache gefunden:", artistId) + } + header.title = artistData.name + artistName.text = artistData.name + coverImage.source = artistData.image + if (artistData.bio) { + console.log("Verarbeite Bio...") + var processedBio = processWimpLinks(artistData.bio) + bioText.text = processedBio + } + tidalApi.getAlbumsofArtist(artistData.artistid) + tidalApi.getTopTracksofArtist(artistData.artistid) + tidalApi.getSimiliarArtist(artistData.artistid) + } + } Connections { target: tidalApi - onArtistChanged: - { + onArtistChanged: { header.title = name + artistName.text = name coverImage.source = img + bioText.text = "" } - onTrackAdded: - { + + onTrackAdded: { topTracks.addTrack(title, artist, album, id, duration) } + // Neues Signal für Alben + onAlbumofArtist: { + + albumsView.model.append({ + title: album_info.title, + cover: album_info.image, + albumId: album_info.albumid + }) + } + + onSimilarArtist: { + + simartistView.model.append({ + name: artist_info.name, + cover: artist_info.image, + artistId: artist_info.artistid + }) + } + + onNoSimilarArtists: { + // Optional: Section Header ausblenden + similarArtistsSection.visible = false + simartistView.visible = false + } + } } diff --git a/qml/pages/Search.qml b/qml/pages/Search.qml index 059a293..c82cbee 100644 --- a/qml/pages/Search.qml +++ b/qml/pages/Search.qml @@ -35,7 +35,7 @@ Item { id: searchField width: parent.width placeholderText: qsTr("Type and Search") - text: "" + text: "Corvus Corax" label: qsTr("Please wait for login ...") enabled: tidalApi.loginTrue @@ -150,7 +150,9 @@ Item { name: track.title, artist: track.artist, album: track.album, - id: track.id, + trackid: track.trackid, + albumid: track.albumid, + artistid: track.artistid, type: typeTrack, image: track.image, duration: track.duration, @@ -159,9 +161,11 @@ Item { } function createAlbumItem(album) { + console.log(album.title, album.albumid) return { name: album.title, - id: album.id, + albumid: album.albumid, + artistid: album.artistid, type: typeAlbum, image: album.image, duration: album.duration @@ -171,7 +175,8 @@ Item { function createArtistItem(artist) { return { name: artist.name, - id: artist.id, + artistid: artist.artistid, + albumid:artist.albumid, type: typeArtist, image: artist.image } diff --git a/qml/pages/TrackList.qml b/qml/pages/TrackList.qml index 1722a75..9a0973e 100644 --- a/qml/pages/TrackList.qml +++ b/qml/pages/TrackList.qml @@ -8,7 +8,7 @@ Item { property string title: "" property string playlistId: "" property int albumId: -1 - property string type: "current" // "playlist" oder "current" oder "album" + property string type: "current" // "playlist" oder "current" oder "album" oder "tracklist" Timer { id: updateTimer @@ -138,13 +138,13 @@ Item { MenuItem { text: qsTr("Play Now") onClicked: { - playlistManager.playTrack(model.id) + playlistManager.playTrack(model.trackid) } } MenuItem { text: qsTr("Add to Queue") onClicked: { - playlistManager.appendTrack(model.id) + playlistManager.appendTrack(model.trackid) } } } @@ -154,7 +154,7 @@ Item { playlistManager.playPosition(model.index) } else { - playlistManager.playTrack(model.id) + playlistManager.playTrack(model.trackid) } } } @@ -192,7 +192,7 @@ Item { "title": track_info.title, "artist": track_info.artist, "album": track_info.album, - "id": track_info.id, + "trackid": track_info.trackid, "duration": track_info.duration, "image": track_info.image }) @@ -205,7 +205,20 @@ Item { "title": track_info.title, "artist": track_info.artist, "album": track_info.album, - "id": track_info.id, + "trackid": track_info.trackid, + "duration": track_info.duration, + "image": track_info.image + }) + } + } + + onTopTracksofArtist: { + if (type === "tracklist") { + listModel.append({ + "title": track_info.title, + "artist": track_info.artist, + "album": track_info.album, + "trackid": track_info.trackid, "duration": track_info.duration, "image": track_info.image }) @@ -221,7 +234,7 @@ Item { "title": title, "artist": artist, "album": album, - "id": id, + "trackid": trackid, "duration": duration, "image": image, "index": index diff --git a/qml/pages/stuff/SearchResultDelegate.qml b/qml/pages/stuff/SearchResultDelegate.qml index caccfb3..a8b55ad 100644 --- a/qml/pages/stuff/SearchResultDelegate.qml +++ b/qml/pages/stuff/SearchResultDelegate.qml @@ -56,12 +56,35 @@ ListItem { MenuItem { text: qsTr("Play Album") visible: itemData.type === 1 // typeTrack - onClicked: playlistManager.playAlbumFromTrack(itemData.id) + onClicked: playlistManager.playAlbumFromTrack(itemData.trackid) } MenuItem { text: qsTr("Queue") - onClicked: playlistManager.appendTrack(itemData.id) + onClicked: playlistManager.appendTrack(itemData.trackid) + } + + MenuItem { + text: qsTr("Album Info") + onClicked: + { + pageStack.push(Qt.resolvedUrl("../AlbumPage.qml"), + { + "albumId" :itemData.albumid + }) + } + } + + MenuItem { + text: qsTr("Artist Info") + onClicked: + { + console.log(itemData.trackid) + pageStack.push(Qt.resolvedUrl("../ArtistPage.qml"), + { + "artistId" :itemData.artistid + }) + } } MenuItem { @@ -69,7 +92,7 @@ ListItem { onClicked: delegate.remorseAction(qsTr("Deleting"), function() { listModel.remove(model.index) }) - } + } } onClicked: handleItemClick(itemData) @@ -109,10 +132,10 @@ ListItem { function handlePlay(item) { switch(item.type) { case 1: // Track - playlistManager.playTrack(item.id) + playlistManager.playTrack(item.trackid) break case 2: // Album - playlistManager.playAlbum(item.id) + playlistManager.playAlbum(item.albumid) break case 4: // Playlist tidalApi.playPlaylist(item.uid) @@ -128,16 +151,21 @@ ListItem { { "albumId": item.albumid }) + console.log(item.albumid) + break case 2: // Album pageStack.push(Qt.resolvedUrl("../AlbumPage.qml"), { - "albumId" :item.id + "albumId" :item.albumid }) + break case 3: // Artist - pageStack.push(Qt.resolvedUrl("../ArtistPage.qml")) - tidalApi.getArtistInfo(item.id) + pageStack.push(Qt.resolvedUrl("../ArtistPage.qml"), + { + "artistId" :item.artistid + }) break } } diff --git a/qml/playlistmanager.py b/qml/playlistmanager.py index c59cb43..6ec21b3 100644 --- a/qml/playlistmanager.py +++ b/qml/playlistmanager.py @@ -1,5 +1,6 @@ import pyotherside + class PlaylistManager: def __init__(self): self.current_index = -1 @@ -109,4 +110,5 @@ def clearList(self): self.playlist = [] self._notify_playlist_state() + PL = PlaylistManager() diff --git a/qml/tidal.py b/qml/tidal.py index b87bb29..bfbb579 100644 --- a/qml/tidal.py +++ b/qml/tidal.py @@ -9,10 +9,18 @@ import tidalapi import pyotherside +from requests.exceptions import HTTPError + + class Tidal: def __init__(self): self.session = None self.config = None + self.top_tracks = 20 + self.album_search = 20 + self.track_search = 20 + self.artist_search = 20 + pyotherside.send('loadingStarted') def initialize(self, quality="HIGH"): @@ -31,6 +39,12 @@ def initialize(self, quality="HIGH"): self.config = tidalapi.Config(quality=selected_quality, video_quality=tidalapi.VideoQuality.low) self.session = tidalapi.Session(self.config) + def setconfig(self, top_tracks, album_search, track_search, artist_search): + self.top_tracks = top_tracks + self.album_search = album_search + self.track_search = track_search + self.artist_search = artist_search + def login(self, token_type, access_token, refresh_token, expiry_time): if access_token == token_type: pyotherside.send("oauth_login_failed") @@ -63,10 +77,12 @@ def request_oauth(self): def handle_track(self, track): try: return { - "id": str(track.id), + "trackid": str(track.id), "title": str(track.name), "artist": str(track.artist.name), + "artistid": str(track.artist.id), "album": str(track.album.name), + "albumid": int(track.album.id), "duration": int(track.duration), "image": track.album.image(320) if hasattr(track.album, 'image') else "", "track_num" : track.track_num, @@ -80,7 +96,7 @@ def handle_track(self, track): def handle_artist(self, artist): try: return { - "id": str(artist.id), + "artistid": str(artist.id), "name": str(artist.name), "image": artist.image(320) if hasattr(artist, 'image') else "", "type": "artist", @@ -90,14 +106,29 @@ def handle_artist(self, artist): print(f"Error handling artist: {e}") return None + except HTTPError as e: + if e.response.status_code == 404: + return { + "artistid": str(artist.id), + "name": str(artist.name), + "image": artist.image(320) if hasattr(artist, 'image') else "", + "type": "artist", + "bio" : "" + } + else: + return f"Error fetching biography: {str(e)}" + def handle_album(self, album): try: return { - "id": str(album.id), + "albumid": int(album.id), "title": str(album.name), "artist": str(album.artist.name), + "artistid" : str(album.artist.id), "image": album.image(320) if hasattr(album, 'image') else "", "duration": int(album.duration) if hasattr(album, 'duration') else 0, + "num_tracks": int(album.num_tracks), + "year": int(album.year), "type": "album" } except AttributeError as e: @@ -129,7 +160,7 @@ def handle_playlist(self, playlist): def genericSearch(self, text): pyotherside.send('loadingStarted') - result = self.session.search(text) + result = self.session.search(text,limit=self.top_tracks) search_results = { "tracks": [], "artists": [], @@ -282,7 +313,7 @@ def playAlbumTracks(self, id): track_info = self.handle_track(track) if track_info: pyotherside.send("cacheTrack", track_info) - pyotherside.send("addTracktoPL", track_info['id']) + pyotherside.send("addTracktoPL", track_info['trackid']) pyotherside.send("fillFinished") def playAlbumfromTrack(self, id): @@ -290,7 +321,7 @@ def playAlbumfromTrack(self, id): track_info = self.handle_track(track) if track_info: pyotherside.send("cacheTrack", track_info) - pyotherside.send("addTracktoPL", track_info['id']) + pyotherside.send("addTracktoPL", track_info['trackid']) pyotherside.send("fillFinished") def getTopTracks(self, id, max): @@ -300,7 +331,7 @@ def getTopTracks(self, id, max): if track_info: pyotherside.send("cacheTrack", track_info) pyotherside.send("addTrack", - track_info['id'], + track_info['trackid'], track_info['title'], track_info['album'], track_info['artist'], @@ -343,7 +374,7 @@ def playPlaylist(self, id): track_info = self.handle_track(track) if track_info: pyotherside.send("cacheTrack", track_info) - pyotherside.send("addTracktoPL", track_info['id']) + pyotherside.send("addTracktoPL", track_info['trackid']) #if i == 0: # pyotherside.send("fillStarted") @@ -363,4 +394,48 @@ def getPlaylistTracks(self, playlist_id): finally: pyotherside.send('loadingFinished') + def getAlbumsofArtist(self, id): + pyotherside.send('loadingStarted') + albums = self.session.artist(int(id)).get_albums() + for ti in albums: + i = self.handle_album(ti) + pyotherside.send("cacheAlbum", i) + pyotherside.send("AlbumofArtist", i) + + pyotherside.send('loadingFinished') + + def getTopTracksofArtist(self, id): + pyotherside.send('loadingStarted') + tracks = self.session.artist(int(id)).get_top_tracks(self.top_tracks) + for ti in tracks: + i = self.handle_track(ti) + pyotherside.send("cacheTrack", i) + pyotherside.send("TopTrackofArtist", i) + + pyotherside.send('loadingFinished') + + def getSimiliarArtist(self, id): + pyotherside.send('loadingStarted') + try: + artists = self.session.artist(int(id)).get_similar() + if artists: # Wenn Artists zurückgegeben wurden + for ti in artists: + i = self.handle_artist(ti) + pyotherside.send("cacheArtist", i) + pyotherside.send("SimilarArtist", i) + else: + pyotherside.send("noSimilarArtists") # Signal wenn keine ähnlichen Künstler gefunden + except requests.exceptions.HTTPError as e: + if e.response.status_code == 404: + print(f"Keine ähnlichen Künstler gefunden für ID: {id}") + pyotherside.send("noSimilarArtists") + else: + print(f"HTTP Fehler beim Abrufen ähnlicher Künstler: {e}") + pyotherside.send("apiError", str(e)) + except Exception as e: + print(f"Allgemeiner Fehler beim Abrufen ähnlicher Künstler: {e}") + pyotherside.send("apiError", str(e)) + finally: + pyotherside.send('loadingFinished') + Tidaler = Tidal()