Native TIDAL client for Linux
from .player_object import playerObject
from .utils import *
from .secret_storage import SecretStore

src/lib/__init__.py
Nokse22/high-tide
# player_object.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

import random

from tidalapi.mix import Mix
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track

from gi.repository import GObject
from gi.repository import Gst, GLib

import random
import threading

class playerObject(GObject.GObject):
    """This class handles all the player logic, queue..."""

    shuffle_mode = GObject.Property(type=bool, default=False)
    current_song_index = GObject.Property(type=int, default=-1)
    is_playing = GObject.Property(type=bool, default=False)

    __gsignals__ = {
        'songs-list-changed': (GObject.SignalFlags.RUN_FIRST, None, (int,)),
        'update-slider': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'song-changed': (GObject.SignalFlags.RUN_FIRST, None, ()),
        'play-changed': (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
    }

    def __init__(self):
        GObject.GObject.__init__(self)
        self.queue = []  # List to store queued songs
        self.current_mix_album = None  # Information about the currently playing mix/album
        self.tracks_from_list_to_play = []
        self.shuffled_queue = []  # Shuffled version of the queue
        self.shuffle_mode = False
        self.is_playing = False
        self.playing_track = None
        self.song_album = None
        self.played_songs = []

        Gst.init()

        self.playbin = Gst.ElementFactory.make("playbin", "playbin")

        GLib.timeout_add(3000, self.print_queue_and_list)

    def play(self):
        self.is_playing = True
        self.notify("is_playing")

        self.emit("play-changed", self.is_playing)
        self.playbin.set_state(Gst.State.PLAYING)

        GLib.timeout_add(1000, self.update_slider_call)

    def play_shuffle(self):
        self.shuffle_mode = True
        self.shuffled_queue = self.tracks_from_list_to_play.copy()

        random.shuffle(self.shuffled_queue)

        self.play_track(self.shuffled_queue[0])
        self.play()

    def pause(self):
        self.is_playing = False
        self.notify("is_playing")

        self.emit("play-changed", self.is_playing)
        self.playbin.set_state(Gst.State.PAUSED)

    def play_track(self, track):
        th = threading.Thread(target=self._play_track, args=(track,))
        th.deamon = True
        th.start()

    def _play_track(self, track):
        print(f"play track: {track.name} by {track.artist.name}\n{track.media_metadata_tags}\n{track.audio_quality}\n{track.id}")
        music_url = track.get_url()
        self.playbin.set_state(Gst.State.NULL)

        self.playbin.set_property("uri", music_url)

        if self.is_playing:
            self.play()

        self.playing_track = track
        self.song_album = track.album
        self.emit("song-changed")

        GLib.timeout_add(1000, self.update_slider_call)

    def play_next(self):
        """Play the next song in the queue or from the currently playing album/mix/playlist."""

        if self.playing_track in self.tracks_from_list_to_play:
            self.played_songs.append(self.playing_track)

        if len(self.queue) != 0:
            track = self.queue[0]
            self.queue.pop(0)
        else:
            if self.shuffle_mode:
                # track = self.shuffled_queue[0]
                pass
            else:
                self.tracks_from_list_to_play.remove(self.playing_track)
                if self.tracks_from_list_to_play == []:
                    self.tracks_from_list_to_play = self.played_songs
                    self.played_songs = []
                track = self.tracks_from_list_to_play[0]
        self.play_track(track)

    def play_previous(self):
        """Play the previous song in the queue."""
        # if self.shuffle_mode:
        #     self.current_song_index = random.randint(0, len(self.current_mix_album.items()))
        # else:
        self.current_song_index += 1
        track = self.played_songs[len(self.played_songs) - 1]
        # self.played_songs.remove(self.playing_track)
        self.tracks_from_list_to_play.append(self.playing_track)
        self.play_track(track)

    def print_queue_and_list(self):
        return
        print("----------played songs----------")
        for track in self.played_songs:
            print(track.name)
        print("-------------queue--------------")
        for track in self.queue:
            print(track.name)
        print("---------songs to play----------")
        for track in self.tracks_from_list_to_play:
            print(track.name)

        return True

    def add_to_queue(self, track):
        self.queue.append(track)

    def add_next(self, track):
        self.queue.insert(0, track)

    def change_volume(self, value):
        self.playbin.set_property("volume", value)

    def shuffle(self, state):
        """Enable or disable shuffle mode."""
        print(f"shuffle toggled to {state}")
        self.shuffle_mode = state

        self.notify("shuffle_mode")

        # self.emit("songs-list-changed", self.shuffle_mode)

    def get_current_song(self):
        """Get information about the currently playing song."""
        return self.current_mix_album.items()[self.current_song_index]

    def clear_queue(self):
        """Clear the queue."""
        self.queue = []
        self.shuffled_queue = []
        self.current_song_index = -1

    def set_current_mix_album(self, mix_album):
        """Set information about the currently playing mix/album."""
        self.current_mix_album = mix_album

    def update_slider_call(self):
        self.emit("update-slider")
        if self.is_playing:
            return True
        return False

    def query_duration(self, time_format):
        return self.playbin.query_duration(Gst.Format.TIME)

    def query_position(self, time_format):
        return self.playbin.query_position(Gst.Format.TIME)

    def seek(self, time_format, something, seek_time):
        self.playbin.seek_simple(time_format, something, seek_time)

src/lib/player_object.py
Nokse22/high-tide
import gi
gi.require_version('Secret', '1')

from gi.repository import Secret

import json

class SecretStore():
    def __init__(self, _session):
        super().__init__()

        print("initializing secret store")

        self.version = "0.0"
        self.session = _session

        self.token_dictionary = {}
        self.attributes = {
            "version": Secret.SchemaAttributeType.STRING
        }

        self.schema = Secret.Schema.new(
            "io.github.nokse22.high-tide",
            Secret.SchemaFlags.NONE,
            self.attributes
        )

        self.key = "high-tide-login"

        password = Secret.password_lookup_sync(
            self.schema,
            {},
            None
        )

        try:
            if password:
                json_data = json.loads(password)
                self.token_dictionary = json_data

        except Exception as error:
            print("Failed to load secret store, resetting", error)

            self.token_dictionary = {}

    def get(self):
        return (self.token_dictionary["token-type"],
                self.token_dictionary["access-token"],
                self.token_dictionary["refresh-token"],
                self.token_dictionary["expiry-time"])

    def clear(self):
        self.token_dictionary.clear()
        self.save()

        Secret.password_clear_sync(
            self.schema,
            {},
            None
        )

    def save(self):
        token_type = self.session.token_type
        access_token = self.session.access_token
        refresh_token = self.session.refresh_token
        expiry_time = self.session.expiry_time

        self.token_dictionary = {
            "token-type" : token_type,
            "access-token" : access_token,
            "refresh-token" : refresh_token,
            "expiry-time" : str(expiry_time)
        }

        json_data = json.dumps(self.token_dictionary, indent=2)

        Secret.password_store_sync(
            self.schema,
            {},
            Secret.COLLECTION_DEFAULT,
            self.key,
            json_data,
            None
        )

src/lib/secret_storage.py
Nokse22/high-tide
# utils.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist
from tidalapi.user import Favorites

import requests

def pretty_duration(secs):
    if not secs:
        return
    hours = secs // 3600
    minutes = (secs % 3600) // 60
    seconds = secs % 60

    if hours > 0:
        return f"{int(hours):02}:{int(minutes):02}:{int(seconds):02}"
    else:
        return f"{int(minutes):02}:{int(seconds):02}"

def add_image(image_widget, item):
    try:
        image_url = item.image()
        response = requests.get(image_url)
    except Exception as e:
        print(str(e))
        return
    if response.status_code == 200:
        image_data = response.content
        file_path = f"tmp_img/{item.id}.jpg"
        with open(file_path, "wb") as file:
            file.write(image_data)

        GLib.idle_add(_add_image, image_widget, file_path)

def _add_image(image_widget, file_path):
    image_widget.set_from_file(file_path)

src/lib/utils.py
Nokse22/high-tide
# login.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
# from gi.repository import GObject

import tidalapi

@Gtk.Template(resource_path='/io/github/nokse22/high-tide/login.ui')
class LoginWindow(Adw.Window):
    __gtype_name__ = 'LoginWindow'

    link_button = Gtk.Template.Child()
    code_label = Gtk.Template.Child()

    def __init__(self, _win, _session):
        super().__init__()

        self.session = _session
        self.win = _win

        login, future = self.session.login_oauth()

        uri = login.verification_uri_complete

        link = f"https://{uri}"

        self.code_label.set_label(uri[-5:])
        self.link_button.set_label(link)
        self.link_button.set_uri(link)

        GLib.timeout_add(600, self.check_login)

    def check_login(self):
        if self.session.check_login():
            self.win.secret_store.save()
            self.win.on_logged_in()
            self.destroy()
            return False
        return True

    @Gtk.Template.Callback("on_copy_code_button_clicked")
    def foo(self, btn):
        pass

src/login.py
Nokse22/high-tide
# main.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

import sys
import gi

gi.require_version('Gtk', '4.0')
gi.require_version('Adw', '1')
gi.require_version('Gst', '1.0')

from gi.repository import Gtk, Gio, Adw, Gdk
from .window import TidalWindow

from tidalapi.media import Quality

import threading
import os
import shutil

class TidalApplication(Adw.Application):
    """The main application singleton class."""

    def __init__(self):
        super().__init__(application_id='io.github.nokse22.high-tide',
                         flags=Gio.ApplicationFlags.DEFAULT_FLAGS)
        self.create_action('quit', lambda *_: self.quit(), ['<primary>q'])
        self.create_action('about', self.on_about_action)
        self.create_action('preferences', self.on_preferences_action)
        self.create_action('log-in', self.on_login_action)
        self.create_action('log-out', self.on_logout_action)
        self.create_action('download', self.on_download, ['<primary>d'])

        css = '''
        .card-image{
            border-radius:10px;
            transition: opacity 0.1s ease-in-out;
        }

        .card-image:hover{
            opacity: 0.8;
        }

        .small-image{
            border-radius:4px;
        }

        .card-bg{
            transition: opacity 0.1s ease-in-out;
	        border-radius:14px;
        }

        .card-bg:hover{
            transition: background-color 0.1s ease-in-out;
            background-color:@card_shade_color;
	        border-radius:14px;
        }

        .link-text{
            color: #888;
            text-decoration: underline;
            transition: color 0.3s ease-in-out;
            background-color: transparent;
            font-weight: normal;
        }

        .link-text:hover{
            color: #444;
        }

        .card-label{
            padding:6px;
        }

        .card-button{
            padding:0px;
            border-radius:10px;
            background-color:black;
        }

        .artist-button{
            padding:0px;
        }

        .play-btn{
            padding-left:18px;
            padding-right:18px;
            padding-top:8px;
            padding-bottom:8px;
        }

        .artist-picture{
            border-radius:100px;
        }

        .lyrics{
            font-weight:bold;
            font-size:16pt;
            color:@theme_text_color;
        }

        .explicit-label{
	        padding:0px;
	        padding-left:3px;
	        padding-right:3px;
	        background-color:@popover_shade_color;
	        border-radius:4px;
	        font-size:11pt;
	        font-weight:bold;
        }
        '''
        css_provider = Gtk.CssProvider()
        css_provider.load_from_data(css, -1)
        Gtk.StyleContext.add_provider_for_display(
            Gdk.Display.get_default(),
            css_provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)

        folder_path = "tmp_img"

        if os.path.exists(folder_path):
            shutil.rmtree(folder_path)

        os.makedirs(folder_path)

    def on_download(self, *args):
        th = threading.Thread(target=self.win.download_song)
        th.deamon = True
        th.start()

    def on_login_action(self, *args):
        self.win.new_login()

    def on_logout_action(self, *args):
        self.win.logout()

    def do_activate(self):
        """Called when the application is activated.

        We raise the application's main window, creating it if
        necessary.
        """
        self.win = self.props.active_window
        if not self.win:
            self.win = TidalWindow(application=self)
        self.win.present()

    def on_about_action(self, widget, _):
        """Callback for the app.about action."""
        about = Adw.AboutWindow(transient_for=self.props.active_window,
                                application_name='High Tide',
                                application_icon='io.github.nokse22.high-tide',
                                developer_name='Nokse',
                                version='0.1.0',
                                developers=['Nokse'],
                                copyright='© 2023 Nokse')
        about.present()

    def on_preferences_action(self, widget, _):
        """Callback for the app.preferences action."""
        print('app.preferences action activated')

        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/preferences.ui")

        builder.get_object("_quality_row").connect("notify::selected", self.on_quality_changed)
        builder.get_object("_quality_row").set_selected(self.win.settings.get_int("quality"))

        builder.get_object("_preference_window").present()

    def on_quality_changed(self, widget, *args):
        self.win.select_quality(widget.get_selected())

    def create_action(self, name, callback, shortcuts=None):
        """Add an application action.

        Args:
            name: the name of the action
            callback: the function to be called when the action is
              activated
            shortcuts: an optional list of accelerators
        """
        action = Gio.SimpleAction.new(name, None)
        action.connect("activate", callback)
        self.add_action(action)
        if shortcuts:
            self.set_accels_for_action(f"app.{name}", shortcuts)

    def do_shutdown(self):
        track = self.win.player_object.playing_track
        list_ = self.win.player_object.current_mix_album
        if track and list_:
            track_id = track.id
            list_id = list_.id
            self.win.settings.set_int("last-playing-song-id", track_id)
            self.win.settings.set_string("last-playing-list-id", list_id)

        folder_path = "tmp_img"

        if os.path.exists(folder_path):
            shutil.rmtree(folder_path)

        os.makedirs(folder_path)

def main(version):
    """The application's entry point."""
    app = TidalApplication()
    return app.run(sys.argv)

src/main.py
Nokse22/high-tide
# new_playlist.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later


from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib

import tidalapi

@Gtk.Template(resource_path='/io/github/nokse22/high-tide/new_playlist.ui')
class NewPlaylistWindow(Adw.Window):
    __gtype_name__ = 'NewPlaylistWindow'

    playlist_name_entry = Gtk.Template.Child()
    playlist_description_entry = Gtk.Template.Child()
    create_button = Gtk.Template.Child()

    def __init__(self, _win, _session):
        super().__init__()

        self.window = _win
        self.session = _session
        self.playlist_name_entry.connect("notify::text", self.on_title_text_inserted_func)

    @Gtk.Template.Callback("on_create_button_clicked")
    def on_create_button_clicked_func(self, btn):
        playlist_title = self.playlist_name_entry.get_text()
        playlist_description = self.playlist_description_entry.get_text()
        self.session.user.create_playlist(playlist_title, playlist_description)
        self.window.add_favourite_playlists()

        self.destroy()

    # @Gtk.Template.Callback("on_title_text_inserted")
    def on_title_text_inserted_func(self, *args):
        playlist_title = self.playlist_name_entry.get_text()
        print(f"!{playlist_title}!")
        if playlist_title != "":
            self.create_button.set_sensitive(True)
            return
        self.create_button.set_sensitive(False)
            

src/new_playlist.py
Nokse22/high-tide
from .home_page import homePage
from .single_type_page import singleTypePage
from .explore_page import explorePage
from .artist_page import artistPage
from .search_page import searchPage
from .track_radio_page import trackRadioPage
from .playlist_page import playlistPage
from .start_up_page import startUpPage
from .not_logged_in_page import notLoggedInPage

src/pages/__init__.py
Nokse22/high-tide
# album_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist

from ..lib import utils

import threading
import requests
import random

from .page import Page

class albumPage(Page):
    __gtype_name__ = 'albumPage'

    def _load_page(self):
        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/tracks_list_template.ui")

        page_content = builder.get_object("_main")
        tracks_list_box = builder.get_object("_list_box")
        tracks_list_box.connect("row-activated", self.on_row_selected)

        builder.get_object("_title_label").set_label(self.item.name)
        builder.get_object("_first_subtitle_label").set_label(self.item.artist.name)
        builder.get_object("_second_subtitle_label").set_label(f"{self.item.num_tracks} tracks ({utils.pretty_duration(self.item.duration)})")

        builder.get_object("_play_button").connect("clicked", self.on_play_button_clicked)
        builder.get_object("_shuffle_button").connect("clicked", self.on_shuffle_button_clicked)
        builder.get_object("_add_to_my_collection_button").connect("clicked", self.on_add_to_my_collection_button_clicked)

        image = builder.get_object("_image")
        th = threading.Thread(target=self.add_image, args=(image, self.item))
        th.deamon = True
        th.start()

        for index, track in enumerate(self.item.items()):
            listing = self.get_album_track_listing(track)
            listing.set_name(str(index))
            tracks_list_box.append(listing)

        self.content.remove(self.spinner)
        self.content.append(page_content)

    def on_row_selected(self, list_box, row):
        index = int(row.get_name())

        self.window.player_object.current_mix_album = self.item
        self.window.player_object.tracks_from_list_to_play = self.item.items()
        track = self.window.player_object.tracks_from_list_to_play[index]
        self.window.player_object.play_track(track)
        self.window.player_object.play()
        self.window.player_object.current_song_index = index

src/pages/album_page.py
Nokse22/high-tide
# artist_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist

from ..lib import utils

import threading
import requests
import random
import copy

from .page import Page

class artistPage(Page):
    __gtype_name__ = 'artistPage'

    def __init__(self, _window, _item, _name):
        super().__init__(_window, _item, _name)

        self.top_tracks = []

    def _load_page(self):

        print(f"artist: {self.item.name}, id: {self.item.id}, {self.item.picture}")

        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/artist_page_template.ui")

        page_content = builder.get_object("_main")
        top_tracks_list_box = builder.get_object("_top_tracks_list_box")
        carousel_box = builder.get_object("_carousel_box")
        top_tracks_list_box.connect("row-activated", self.on_row_selected)

        builder.get_object("_name_label").set_label(self.item.name)
        # builder.get_object("_bio_label").set_label(self.item.roles)

        builder.get_object("_play_button").connect("clicked", self.on_play_button_clicked)
        builder.get_object("_shuffle_button").connect("clicked", self.on_shuffle_button_clicked)

        builder.get_object("_follow_button").connect("clicked", self.on_add_to_my_collection_button_clicked)
        builder.get_object("_radio_button").connect("clicked", self.on_artist_radio_button_clicked)

        image = builder.get_object("_image")

        try:
            self.top_tracks = self.item.get_top_tracks(10)
        except:
            pass
        else:
            for index, track in enumerate(self.top_tracks):
                listing = self.get_track_listing(track)
                listing.set_name(str(index))
                top_tracks_list_box.append(listing)

        carousel, cards_box = self.get_carousel("Albums")
        carousel_box.append(carousel)

        try:
            albums = self.item.get_albums()
        except:
            pass
        else:
            for album in albums:
                album_card = self.get_album_card(album)
                cards_box.append(album_card)

        carousel, cards_box = self.get_carousel("Similar Artists")
        carousel_box.append(carousel)

        this_artist = self.window.session.artist(self.item.id)
        try:
            artists = this_artist.get_similar()
        except:
            pass
        else:
            for artist in artists:
                artist_card = self.get_artist_card(artist)
                cards_box.append(artist_card)

        try:
            bio = self.item.get_bio()
        except:
            pass
        else:
            expander = Gtk.Expander(label="Bio", css_classes=["title-3"], margin_bottom=50)
            label = Gtk.Label(label=bio, wrap=True, css_classes=[])
            expander.set_child(label)
            carousel_box.append(expander)

        artist_picture = builder.get_object("_avatar")

        try:
            image = self.item.image()
        except:
            artist_picture.set_icon_name("emblem-music-symbolic")
        else:
            th = threading.Thread(target=self.add_image_to_avatar, args=(artist_picture, image))
            th.deamon = True
            th.start()

        self.content.remove(self.spinner)
        self.content.append(page_content)

    def on_row_selected(self, list_box, row):
        index = int(row.get_name())

        self.window.player_object.current_mix_album_list = self.top_tracks
        track = self.window.player_object.current_mix_album_list[index]
        self.window.player_object.current_mix_album = track.album
        self.window.player_object.play_track(track)
        self.window.player_object.current_song_index = index

    def on_play_button_clicked(self, btn):
        self.window.player_object.current_mix_album = self.item
        self.window.player_object.current_mix_album_list = self.top_tracks
        track = self.window.player_object.current_mix_album_list[0]
        self.window.player_object.play_track(track)
        self.window.player_object.current_song_index = 0

    def on_shuffle_button_clicked(self, btn):
        self.window.player_object.current_mix_album = None
        self.window.player_object.current_mix_album_list = self.top_tracks
        self.window.player_object.play_shuffle()

    def on_artist_radio_button_clicked(self, btn):
        from .track_radio_page import trackRadioPage
        page = trackRadioPage(self.window, self.item, f"Radio of {self.item.name}")
        page.load()
        self.window.navigation_view.push(page)

src/pages/artist_page.py
Nokse22/high-tide
# explore_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist

from ..lib import utils

import threading
import requests
import random

from .page import Page

class explorePage(Page):
    __gtype_name__ = 'explorePage'

    def _load_page(self):
        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/home_page_template.ui")

        page_content = builder.get_object("_main")
        explore_content = builder.get_object("_content")

        explore = self.window.session.explore()

        # print(explore.categories)

        for index, category in enumerate(explore.categories):
            items = []

            if isinstance(category.items[0], PageLink):
                carousel, flow_box_box = self.get_link_carousel(category.title if category.title else "More")

                flow_box = Gtk.FlowBox(homogeneous=True, height_request=100)
                flow_box_box.append(flow_box)
                explore_content.append(carousel)
            else:
                carousel, cards_box = self.get_carousel(category.title)
                explore_content.append(carousel)

            buttons_for_page = 0

            for index, item in enumerate(category.items):
                if isinstance(item, PageItem): # Featured
                    button = self.get_page_item_card(item)
                    cards_box.append(button)
                elif isinstance(item, PageLink): # Generes and moods
                    if buttons_for_page == 4:
                        flow_box = Gtk.FlowBox(homogeneous=True, height_request=100)
                        flow_box_box.append(flow_box)
                        buttons_for_page = 0
                    button = self.get_page_link_card(item)
                    flow_box.append(button)
                    buttons_for_page += 1
                elif isinstance(item, Mix): # Mixes and for you
                    button = self.get_mix_card(item)
                    cards_box.append(button)
                elif isinstance(item, Album):
                    album_card = self.get_album_card(item)
                    cards_box.append(album_card)
                elif isinstance(item, Artist):
                    button = self.get_artist_card(item)
                    cards_box.append(button)
                elif isinstance(item, Playlist):
                    button = self.get_playlist_card(item)
                    cards_box.append(button)

        self.content.remove(self.spinner)
        self.content.append(page_content)

src/pages/explore_page.py
Nokse22/high-tide
# generic_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist

from ..lib import utils

import threading
import requests
import random

from .page import Page

class genericPage(Page):
    __gtype_name__ = 'genericPage'

    def _load_page(self):
        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/home_page_template.ui")

        page_content = builder.get_object("_main")
        generic_page_content = builder.get_object("_content")

        generic_content = self.item.get()

        for index, category in enumerate(generic_content.categories):
            items = []

            carousel, cards_box = self.get_carousel(category.title)

            if not isinstance(category.items[0], Track):
                generic_page_content.append(carousel)
            else:
                continue

            for item in category.items:
                if isinstance(item, PageItem): # Featured
                    button = self.get_page_item_card(item)
                    cards_box.append(button)
                elif isinstance(item, PageLink): # Generes and moods
                    items.append("\t" + item.title)
                    button = self.get_page_link_card(item)
                    cards_box.append(button)
                elif isinstance(item, Mix): # Mixes and for you
                    button = self.get_mix_card(item)
                    cards_box.append(button)
                elif isinstance(item, Album):
                    album_card = self.get_album_card(item)
                    cards_box.append(album_card)
                elif isinstance(item, Artist):
                    button = self.get_artist_card(item)
                    cards_box.append(button)
                elif isinstance(item, Playlist):
                    button = self.get_playlist_card(item)
                    cards_box.append(button)

        self.content.remove(self.spinner)
        self.content.append(page_content)

src/pages/generic_page.py
Nokse22/high-tide
# home_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist
from tidalapi.user import Favorites

from ..lib import utils

import threading
import requests
import random

from .page import Page

class homePage(Page):
    __gtype_name__ = 'homePage'

    def _load_page(self):
        self.set_tag("home")

        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/home_page_template.ui")

        self.set_title("Home")
        page_content = builder.get_object("_main")
        home_content = builder.get_object("_content")

        home = self.window.session.home()

        for index, category in enumerate(home.categories):
            if index > 8:
                break
            items = []
            if isinstance(category.items[0], PageItem) or isinstance(category.items[0], PageLink):
                continue

            carousel, cards_box = self.get_carousel(category.title)

            if not isinstance(category.items[0], Track):
                home_content.append(carousel)
            else:
                continue

            for item in category.items:
                if isinstance(item, PageItem): # Featured
                    items.append("\t" + item.short_header)
                    items.append("\t" + item.short_sub_header[0:50])
                    button = self.get_page_item_card(item)
                    # cards_box.append(button)
                elif isinstance(item, Mix): # Mixes and for you
                    button = self.get_mix_card(item)
                    cards_box.append(button)
                elif isinstance(item, Album):
                    album_card = self.get_album_card(item)
                    cards_box.append(album_card)
                elif isinstance(item, Artist):
                    button = self.get_artist_card(item)
                    cards_box.append(button)
                elif isinstance(item, Playlist):
                    button = self.get_playlist_card(item)
                    cards_box.append(button)

        self.content.remove(self.spinner)
        self.content.append(page_content)

src/pages/home_page.py
Nokse22/high-tide
# mix_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist

from ..lib import utils

import threading
import requests
import random

from .page import Page

class mixPage(Page):
    __gtype_name__ = 'mixPage'

    def _load_page(self):
        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/tracks_list_template.ui")

        page_content = builder.get_object("_main")
        tracks_list_box = builder.get_object("_list_box")
        tracks_list_box.connect("row-activated", self.on_row_selected)

        builder.get_object("_title_label").set_label(self.item.title)
        builder.get_object("_first_subtitle_label").set_label(self.item.sub_title)

        builder.get_object("_play_button").connect("clicked", self.on_play_button_clicked)
        builder.get_object("_shuffle_button").connect("clicked", self.on_shuffle_button_clicked)
        builder.get_object("_add_to_my_collection_button").connect("clicked", self.on_add_to_my_collection_button_clicked)

        image = builder.get_object("_image")
        th = threading.Thread(target=self.add_image, args=(image, self.item))
        th.deamon = True
        th.start()

        for index, track in enumerate(self.item.items()):
            listing = self.get_track_listing(track)
            listing.set_name(str(index))
            tracks_list_box.append(listing)

        self.content.remove(self.spinner)
        self.content.append(page_content)

    def on_row_selected(self, list_box, row):
        index = int(row.get_name())

        self.window.player_object.current_mix_album = self.item
        self.window.player_object.tracks_from_list_to_play = self.item.items()
        track = self.window.player_object.tracks_from_list_to_play[index]
        self.window.player_object.play_track(track)
        self.window.player_object.play()
        self.window.player_object.current_song_index = index

src/pages/mix_page.py
Nokse22/high-tide
# single_type_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist
from tidalapi.user import Favorites

from ..lib import utils

import threading
import requests
import random

from .page import Page

class notLoggedInPage(Page):
    __gtype_name__ = 'notLoggedInPage'

    def _load_page(self):
        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/home_page_template.ui")

        self.set_title("Not Logged In")
        page_content = builder.get_object("_main")
        content = builder.get_object("_content")

        descr = '''To be able to use this app you need to login with your TIDAL account.
Open the hamburger menu on the left and click Login.'''

        status_page = Adw.StatusPage(title="Login first", description=descr, icon_name="face-wink-symbolic")
        status_page.set_vexpand(True)
        status_page.set_hexpand(True)

        self.content.remove(self.spinner)
        self.content.append(status_page)

src/pages/not_logged_in_page.py
Nokse22/high-tide
# mix_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist
from tidalapi.user import Favorites

from ..lib import utils

import threading
import requests
import random
import os

from ..widgets.generic_track_widget import GenericTrackWidget
from ..widgets.card_widget import CardWidget

class Page(Adw.NavigationPage):
    __gtype_name__ = 'Page'

    def __init__(self, _window, _item=None, _name=None):
        super().__init__()

        self.window = _window
        if _name:
            self.set_title(_name)

        self.builder = Gtk.Builder.new_from_resource('/io/github/nokse22/high-tide/ui/pages_ui/page_template.ui')
        self.item = _item

        self.content = self.builder.get_object("_content")
        self.object = self.builder.get_object("_main")
        self.spinner = self.builder.get_object("_spinner")

        self.set_child(self.object)

    def load(self):
        th = threading.Thread(target=self._load_page)
        th.deamon = True
        th.start()

    def _load_page(self):
        return

    def get_album_card(self, item):
        card = CardWidget(item, self.window)
        return card

    def get_track_listing(self, track):
        track_listing = GenericTrackWidget(track, self.window, False)
        return track_listing

    def get_mix_card(self, item):
        card = CardWidget(item, self.window)
        return card

    def on_mix_button_clicked(self, btn, mix):
        self.window.sidebar_list.select_row(None)

        from .mix_page import mixPage

        page = mixPage(self.window, mix, mix.title)
        page.load()
        self.window.navigation_view.push(page)

    def get_album_track_listing(self, track):
        track_listing = GenericTrackWidget(track, self.window, True)
        return track_listing

    def on_play_button_clicked(self, btn):
        self.window.player_object.current_mix_album = self.item
        self.window.player_object.tracks_from_list_to_play = self.item.items()
        track = self.window.player_object.tracks_from_list_to_play[0]
        self.window.player_object.play_track(track)
        self.play()
        self.window.player_object.current_song_index = 0

    def on_shuffle_button_clicked(self, btn):
        self.window.player_object.current_mix_album = self.item
        self.window.player_object.tracks_from_list_to_play = self.item.items()
        self.window.player_object.play_shuffle()

    def on_artist_button_clicked(self, btn, artist):
        print(artist)
        self.window.sidebar_list.select_row(None)

        from .artist_page import artistPage

        page = artistPage(self.window, artist, artist.name)
        page.load()
        self.window.navigation_view.push(page)

    def add_image(self, image_widget, item):
        try:
            image_url = item.image()
            response = requests.get(image_url)
        except Exception as e:
            print(str(e))
            return
        if response.status_code == 200:
            image_data = response.content
            file_path = f"tmp_img/{item.id}.jpg"
            with open(file_path, "wb") as file:
                file.write(image_data)

            GLib.idle_add(self._add_image, image_widget, file_path)

    def _add_image(self, image_widget, file_path):
        image_widget.set_from_file(file_path)

    def add_image_to_avatar(self, avatar_widget, image_url):
        try:
            response = requests.get(image_url)
        except:
            return

        if response.status_code == 200:
            image_data = response.content
            file_path = f"tmp_img/{random.randint(0, 100)}.jpg"
            with open(file_path, "wb") as file:
                file.write(image_data)

            GLib.idle_add(self._add_image_to_avatar, avatar_widget, file_path)

    def _add_image_to_avatar(self, avatar_widget, file_path):
        file = Gio.File.new_for_path(file_path)
        image = Gdk.Texture.new_from_file(file)
        avatar_widget.set_custom_image(image)

    def get_carousel(self, title):
        cards_box = Gtk.Box()
        box = Gtk.Box(orientation=1, margin_bottom=12, margin_start=12, margin_end=12, overflow=Gtk.Overflow.HIDDEN)
        title_box = Gtk.Box(margin_top=12, margin_bottom=6)
        title_box.append(Gtk.Label(label=title, xalign=0, css_classes=["title-3"], ellipsize=3))
        prev_button = Gtk.Button(icon_name="go-next-symbolic", margin_start=6, halign=Gtk.Align.END, css_classes=["circular"])
        next_button = Gtk.Button(icon_name="go-previous-symbolic", hexpand=True, halign=Gtk.Align.END, css_classes=["circular"])
        title_box.append(next_button)
        title_box.append(prev_button)

        box.append(title_box)
        cards_box = Adw.Carousel(halign=Gtk.Align.START, allow_scroll_wheel=False, allow_long_swipes=True)
        cards_box.set_overflow(Gtk.Overflow.VISIBLE)
        box.append(cards_box)

        prev_button.connect("clicked", self.carousel_go_prev, cards_box)
        next_button.connect("clicked", self.carousel_go_next, cards_box)

        return box, cards_box

    def get_link_carousel(self, title):
        cards_box = Gtk.Box()
        box = Gtk.Box(orientation=1, margin_bottom=12, margin_start=12, margin_end=12, overflow=Gtk.Overflow.HIDDEN)
        title_box = Gtk.Box(margin_top=12, margin_bottom=6)
        title_box.append(Gtk.Label(label=title, xalign=0, css_classes=["title-3"], ellipsize=3))
        prev_button = Gtk.Button(icon_name="go-next-symbolic", margin_start=6, halign=Gtk.Align.END, css_classes=["circular"])
        next_button = Gtk.Button(icon_name="go-previous-symbolic", hexpand=True, halign=Gtk.Align.END, css_classes=["circular"])
        title_box.append(next_button)
        title_box.append(prev_button)

        box.append(title_box)
        cards_box = Adw.Carousel(halign=Gtk.Align.FILL, allow_scroll_wheel=False, allow_long_swipes=True)
        cards_box.set_overflow(Gtk.Overflow.VISIBLE)
        box.append(cards_box)

        prev_button.connect("clicked", self.carousel_go_prev, cards_box, 1)
        next_button.connect("clicked", self.carousel_go_next, cards_box, 1)

        return box, cards_box

    def carousel_go_prev(self, btn, carousel, jump=2):
        pos = carousel.get_position()
        if pos + 2 >= carousel.get_n_pages():
            if pos + 1 == carousel.get_n_pages():
                return
            else:
                next_page = carousel.get_nth_page(pos + 1)
        else:
            next_page = carousel.get_nth_page(pos + jump)
        if next_page != None:
            carousel.scroll_to(next_page, True)

    def carousel_go_next(self, btn, carousel, jump=2):
        pos = carousel.get_position()
        if pos - 2 < 0:
            if pos - 1 < 0:
                return
            else:
                next_page = carousel.get_nth_page(0)
        else:
            next_page = carousel.get_nth_page(pos - jump)

        carousel.scroll_to(next_page, True)

    def get_playlist_card(self, playlist):
        card = CardWidget(playlist, self.window)
        return card

    def on_playlist_button_clicked(self, btn, playlist):
        self.window.sidebar_list.select_row(None)

        from .playlist_page import playlistPage

        page = playlistPage(self.window, playlist, playlist.name)
        page.load()
        self.window.navigation_view.push(page)

    def on_album_button_clicked(self, btn, album):
        self.window.sidebar_list.select_row(None)
        self.window.player_object.current_mix_album = album

        from .album_page import albumPage

        page = albumPage(self.window, album, album.name)
        page.load()
        self.window.navigation_view.push(page)

    def get_artist_page(self, artist):
        from .artist_page import artistPage

        page = artistPage(self.window, artist, artist.name)
        page.load()
        self.window.navigation_view.push(page)

    def get_artist_card(self, item):
        card = CardWidget(item, self.window)
        return card

    def get_page_item_card(self, page_item):
        card = CardWidget(page_item, self.window)
        return card

    def get_page_link_card(self, page_link):
        button = Gtk.Button(label=page_link.title, margin_start=12, margin_end=12,
                hexpand=True, width_request=200, vexpand=True)
        button.connect("clicked", self.on_page_link_clicked, page_link)
        return button

    def on_page_link_clicked(self, btn, page_link):
        from .generic_page import genericPage

        page = genericPage(self.window, page_link, page_link.title)
        page.load()
        self.window.navigation_view.push(page)

    def add_to_my_collection(self, btn, item):
        print(item)
        if isinstance(item, Track):
            result = self.window.session.user.favorites.add_track(item.id)
        elif isinstance(item, Mix):
            return # still not supported
            result = self.window.session.user.favorites.add_mix(item.id)
        elif isinstance(item, Album):
            result = self.window.session.user.favorites.add_album(item.id)
        elif isinstance(item, Artist):
            result = self.window.session.user.favorites.add_artist(item.id)
        elif isinstance(item, Playlist):
            result = self.window.session.user.favorites.add_playlist(item.id)
        else:
            result = False

        if result:
            print("item successfully added to my collection")
            btn.set_icon_name("heart-filled-symbolic")
        else:
            print("failed to add item to my collection")

    def remove_from_my_collection(self, btn, item):
        if isinstance(item, Track):
            result = self.window.session.user.favorites.remove_track(item.id)
        elif isinstance(item, Mix):
            return # still not supported
            result = self.window.session.user.favorites.remove_mix(item.id)
        elif isinstance(item, Album):
            result = self.window.session.user.favorites.remove_album(item.id)
            print("adding album")
        elif isinstance(item, Artist):
            result = self.window.session.user.favorites.remove_artist(item.id)
        elif isinstance(item, Playlist):
            result = self.window.session.user.favorites.remove_playlist(item.id)
        else:
            result = False

        if result:
            print("item successfully removed from my collection")
            btn.set_icon_name("heart-outline-thick-symbolic")

    def on_add_to_my_collection_button_clicked(self, btn):
        if btn.get_icon_name() == "heart-outline-thick-symbolic":
            th = threading.Thread(target=self.add_to_my_collection, args=(btn, self.item,))
        else:
            th = threading.Thread(target=self.remove_from_my_collection, args=(btn, self.item,))
        th.deamon = True
        th.start()

src/pages/page.py
Nokse22/high-tide
# playlist_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist

from ..lib import utils


import threading
import requests
import random

from .page import Page

class playlistPage(Page):
    __gtype_name__ = 'playlistPage'

    def _load_page(self):
        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/tracks_list_template.ui")

        page_content = builder.get_object("_main")
        tracks_list_box = builder.get_object("_list_box")
        tracks_list_box.connect("row-activated", self.on_row_selected)

        builder.get_object("_play_button").connect("clicked", self.on_play_button_clicked)
        builder.get_object("_shuffle_button").connect("clicked", self.on_shuffle_button_clicked)

        builder.get_object("_title_label").set_label(self.item.name)
        creator = self.item.creator
        if creator:
            creator = creator.name
        else:
            creator = "TIDAL"
        builder.get_object("_first_subtitle_label").set_label(f"by {creator}")
        builder.get_object("_second_subtitle_label").set_label(f"{self.item.num_tracks} tracks ({utils.pretty_duration(self.item.duration)})")

        image = builder.get_object("_image")
        th = threading.Thread(target=self.add_image, args=(image, self.item))
        th.deamon = True
        th.start()

        for index, track in enumerate(self.item.items()):
            listing = self.get_track_listing(track)
            listing.set_name(str(index))
            tracks_list_box.append(listing)

        self.content.remove(self.spinner)
        self.content.append(page_content)

    def on_row_selected(self, list_box, row):
        index = int(row.get_name())

        self.window.player_object.current_mix_album = self.item
        self.window.player_object.tracks_from_list_to_play = self.item.items()
        track = self.window.player_object.tracks_from_list_to_play[index]
        self.window.player_object.play_track(track)
        self.window.player_object.play()
        self.window.player_object.current_song_index = index

src/pages/playlist_page.py
Nokse22/high-tide
# search_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist

from ..lib import utils


import threading
import requests
import random

from .page import Page

class searchPage(Page):
    __gtype_name__ = 'searchPage'

    def _load_page(self):
        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/home_page_template.ui")

        page_content = builder.get_object("_main")
        results_box = builder.get_object("_content")

        query = self.window.search_entry.get_text()

        results = self.window.session.search(query, [Artist, Album, Playlist, Track], 10)

        print(query, results)

        top_hit = results["top_hit"]
        # results_box.append(Gtk.Label(label=top_hit.name))

        carousel, cards_box = self.get_carousel("Artists")
        artists = results["artists"]

        if len(artists) > 0:
            results_box.append(carousel)
            for artist in artists:
                artist_card = self.get_artist_card(artist)
                cards_box.append(artist_card)

        carousel, cards_box = self.get_carousel("Albums")
        albums = results["albums"]
        if len(albums) > 0:
            results_box.append(carousel)
            for album in albums:
                album_card = self.get_album_card(album)
                cards_box.append(album_card)

        scrolled_window = Gtk.ScrolledWindow(vexpand=True, hscrollbar_policy=Gtk.PolicyType.NEVER)

        self.content.remove(self.spinner)
        self.content.append(page_content)


src/pages/search_page.py
Nokse22/high-tide
# single_type_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist
from tidalapi.user import Favorites

from ..lib import utils

import threading
import requests
import random

from .page import Page

class singleTypePage(Page):
    __gtype_name__ = 'singleTypePage'

    def _load_page(self):
        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/home_page_template.ui")

        page_content = builder.get_object("_main")
        content = builder.get_object("_content")

        if self.item == "track":
            self.add_tracks(content)
        elif self.item == "mix":
            self.add_mixes(content)
        elif self.item == "album":
            self.add_albums(content)
        elif self.item == "artist":
            self.add_artists(content)
        elif self.item == "playlist":
            self.add_playlists(content)

        self.content.remove(self.spinner)
        self.content.append(page_content)

    def add_tracks(self, content):
        tracks_list_box = Gtk.ListBox(css_classes=["boxed-list"], margin_bottom=12, margin_start=12, margin_end=12, margin_top=12)
        content.append(tracks_list_box)

        favourite_tracks = Favorites(self.window.session, self.window.session.user.id).tracks(50)
        tracks_list_box.connect("row-activated", self.on_tracks_row_selected, favourite_tracks)

        for index, track in enumerate(favourite_tracks):
            listing = self.get_track_listing(track)
            listing.set_name(str(index))
            tracks_list_box.append(listing)

    def add_mixes(self, content):
        flow_box = Gtk.FlowBox(selection_mode=0)
        content.append(flow_box)

        favourites = Favorites(self.window.session, self.window.session.user.id).mix()

        for index, mix in enumerate(favourites):
            card = self.get_mix_card(mix)
            flow_box.append(card)

    def add_artists(self, content):
        flow_box = Gtk.FlowBox(selection_mode=0)
        content.append(flow_box)

        favourites = Favorites(self.window.session, self.window.session.user.id).artists()

        for index, artist in enumerate(favourites):
            card = self.get_artist_card(artist)
            flow_box.append(card)

    def add_playlists(self, content):
        flow_box = Gtk.FlowBox(selection_mode=0)
        content.append(flow_box)

        favourites = Favorites(self.window.session, self.window.session.user.id).playlists()

        for index, playlist in enumerate(favourites):
            card = self.get_playlist_card(playlist)
            flow_box.append(card)

    def add_albums(self, content):
        flow_box = Gtk.FlowBox(selection_mode=0)
        content.append(flow_box)

        favourites = Favorites(self.window.session, self.window.session.user.id).albums()

        for index, album in enumerate(favourites):
            card = self.get_album_card(album)
            flow_box.append(card)

    def on_tracks_row_selected(self, list_box, row, favourite_tracks):
        index = int(row.get_name())

        self.window.player_object.current_mix_album_list = favourite_tracks
        track = favourite_tracks[index]
        print(track)
        self.window.player_object.song_album = track.album
        self.window.player_object.play_track(track)
        self.window.player_object.current_song_index = index

src/pages/single_type_page.py
Nokse22/high-tide
# single_type_page.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist
from tidalapi.user import Favorites

from ..lib import utils

import threading
import requests
import random

from .page import Page

class startUpPage(Page):
    __gtype_name__ = 'startUpPage'

    def _load_page(self):
        self.set_tag("loading-page")

        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/home_page_template.ui")

        page_content = builder.get_object("_main")
        content = builder.get_object("_content")

src/pages/start_up_page.py
Nokse22/high-tide
# track_radio.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import Gdk

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist

from ..lib import utils

import threading
import requests
import random

from .page import Page

class trackRadioPage(Page):
    __gtype_name__ = 'trackRadioPage'

    def __init__(self, _window, _item, _name):
        super().__init__(_window, _item, _name)

        self.radio_tracks = []

    def _load_page(self):
        builder = Gtk.Builder.new_from_resource("/io/github/nokse22/high-tide/ui/pages_ui/tracks_list_template.ui")

        page_content = builder.get_object("_main")
        tracks_list_box = builder.get_object("_list_box")
        tracks_list_box.connect("row-activated", self.on_row_selected)

        builder.get_object("_title_label").set_label(f"Radio of {self.item.name}")

        if isinstance(self.item, Track):
            builder.get_object("_first_subtitle_label").set_label(f"by {self.item.artist.name}")

        builder.get_object("_play_button").connect("clicked", self.on_play_button_clicked)
        builder.get_object("_shuffle_button").connect("clicked", self.on_shuffle_button_clicked)
        builder.get_object("_add_to_my_collection_button").connect("clicked", self.on_add_to_my_collection_button_clicked)

        image = builder.get_object("_image")
        if isinstance(self.item, Track):
            th = threading.Thread(target=self.add_image, args=(image, self.item.album))
        else:
            th = threading.Thread(target=self.add_image, args=(image, self.item))
        th.deamon = True
        th.start()

        if isinstance(self.item, Track):
            self.radio_tracks = self.item.get_track_radio()
        else:
            self.radio_tracks = self.item.get_radio()

        for index, track in enumerate(self.radio_tracks):
            listing = self.get_track_listing(track)
            listing.set_name(str(index))
            tracks_list_box.append(listing)

        self.content.remove(self.spinner)
        self.content.append(page_content)

    def on_row_selected(self, list_box, row):
        index = int(row.get_name())

        self.window.player_object.current_mix_album_list = self.radio_tracks
        track = self.window.player_object.current_mix_album_list[index]
        self.window.player_object.current_mix_album = track.album
        self.window.player_object.play_track(track)
        self.window.player_object.current_song_index = index

    def on_add_to_my_collection_button_clicked(self):
        pass

src/pages/track_radio_page.py
Nokse22/high-tide
# card_widget.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio

import tidalapi
import threading

from ..lib import utils

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track
from tidalapi.playlist import Playlist
from tidalapi.user import Favorites

@Gtk.Template(resource_path='/io/github/nokse22/high-tide/ui/widgets/card_widget.ui')
class CardWidget(Gtk.Box):
    __gtype_name__ = 'CardWidget'

    image = Gtk.Template.Child()
    button = Gtk.Template.Child()
    title_label = Gtk.Template.Child()
    detail_label = Gtk.Template.Child()
    track_artist_label = Gtk.Template.Child()
    track_artist_button = Gtk.Template.Child()

    def __init__(self, _item, _win):
        super().__init__()

        self.item = _item
        self.win = _win

        if isinstance(_item, Mix):
            self.make_mix_card()
        elif isinstance(_item, Album):
            self.make_album_card()
        elif isinstance(_item, Playlist):
            self.make_playlist_card()
        elif isinstance(_item, Artist):
            self.make_artist_card()
        elif isinstance(_item, PageItem):
            self.make_page_item_card()

    def make_mix_card(self):
        self.title_label.set_text(self.item.title)
        self.detail_label.set_text(self.item.sub_title)
        self.track_artist_label.set_visible(False)

        th = threading.Thread(target=utils.add_image, args=(self.image, self.item))
        th.deamon = True
        th.start()

    def make_album_card(self):
        self.title_label.set_text(self.item.name)
        self.track_artist_label.set_text(self.item.artist.name)
        self.detail_label.set_visible(False)

        th = threading.Thread(target=utils.add_image, args=(self.image, self.item))
        th.deamon = True
        th.start()

    def make_playlist_card(self):
        self.title_label.set_text(self.item.name)
        self.track_artist_label.set_visible(False)

        creator = self.item.creator
        if creator:
            creator = creator.name
        else:
            creator = "TIDAL"
        self.detail_label.set_text(f"by {creator}")

        th = threading.Thread(target=utils.add_image, args=(self.image, self.item))
        th.deamon = True
        th.start()

    def make_artist_card(self):
        self.title_label.set_text(self.item.name)
        self.detail_label.set_text("Artist")
        self.track_artist_label.set_visible(False)

        th = threading.Thread(target=utils.add_image, args=(self.image, self.item))
        th.deamon = True
        th.start()

    def make_page_item_card(self):
        self.title_label.set_text(self.item.short_header)
        self.detail_label.set_text(self.item.short_sub_header)
        self.track_artist_label.set_visible(False)

        th = threading.Thread(target=utils.add_image, args=(self.image, self.item))
        th.deamon = True
        th.start()

    @Gtk.Template.Callback("on_artist_button_clicked")
    def _on_artist_button_clicked(self, *args):
        from ..pages.artist_page import artistPage
        page = artistPage(self.win, self.item.artist, f"{self.item.artist.name}")
        page.load()
        self.win.navigation_view.push(page)

    @Gtk.Template.Callback("on_image_button_clicked")
    def _on_image_button_clicked(self, *args):
        if isinstance(self.item, Mix):
            from ..pages.mix_page import mixPage
            page = mixPage(self.win, self.item, f"{self.item.title}")
            page.load()
            self.win.navigation_view.push(page)

        elif isinstance(self.item, Album):
            from ..pages.album_page import albumPage
            page = albumPage(self.win, self.item, f"{self.item.name}")
            page.load()
            self.win.navigation_view.push(page)

        elif isinstance(self.item, Playlist):
            from ..pages.playlist_page import playlistPage
            page = playlistPage(self.win, self.item, f"{self.item.name}")
            page.load()
            self.win.navigation_view.push(page)

        elif isinstance(self.item, Artist):
            from ..pages.artist_page import artistPage
            page = artistPage(self.win, self.item, f"{self.item.name}")
            page.load()
            self.win.navigation_view.push(page)

src/widgets/card_widget.py
Nokse22/high-tide
# generic_track_widget.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later


from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gio

import tidalapi
import threading

from ..lib import utils

from tidalapi.user import Favorites

@Gtk.Template(resource_path='/io/github/nokse22/high-tide/ui/widgets/generic_track_widget.ui')
class GenericTrackWidget(Gtk.ListBoxRow):
    __gtype_name__ = 'GenericTrackWidget'

    image = Gtk.Template.Child()
    track_album_label = Gtk.Template.Child()
    track_title_label = Gtk.Template.Child()
    track_duration_label = Gtk.Template.Child()
    artist_label = Gtk.Template.Child()
    playlists_submenu = Gtk.Template.Child()
    track_album_button = Gtk.Template.Child()
    _grid = Gtk.Template.Child()
    explicit_label = Gtk.Template.Child()

    def __init__(self, _track, _win, is_album):
        super().__init__()

        if is_album:
            self._grid.remove(self.track_album_button)
            self.image.set_visible(False)
            self.track_title_label.set_margin_start(12)

        self.track = _track
        self.win = _win

        self.track_album_label.set_label(self.track.album.name)
        self.track_title_label.set_label(self.track.name)
        self.artist_label.set_label(self.track.artist.name)

        self.explicit_label.set_visible(self.track.explicit)

        self.track_duration_label.set_label(utils.pretty_duration(self.track.duration))

        action_group = Gio.SimpleActionGroup()
        action_entries = [
            ("radio", self._get_radio),
            ("play-next", self._play_next),
            ("add-to-queue", self._add_to_queue),
            ("add-to-my-collection", self._add_to_my_collection)
        ]

        # action = Gio.SimpleAction.new("add-to-playlist", GLib.Variant("s"))
        # action.connect("activate", self.add_to_playlist)
        # action_group.add_action(action)

        # for index, playlist in enumerate(self.win.favourite_playlists):
        #     if index > 10:
        #         break
        #     item = Gio.MenuItem.new()
        #     item.set_label(playlist.name)
        #     item.set_action_and_target_value("trackwidget.add-to-playlist", GLib.Variant.new_string(playlist.id))
        #     self.playlists_submenu.insert_item(index, item)

        for index, playlist in enumerate(self.win.favourite_playlists):
            if index > 10:
                break
            item = Gio.MenuItem.new()
            item.set_label(playlist.name)
            action_name = f"add-to-playlist-{index}"  # Unique action name for each playlist
            action = Gio.SimpleAction.new(action_name, None)
            action.connect("activate", self._add_to_playlist, index)
            action_group.add_action(action)
            item.set_action_and_target_value(action_name, GLib.Variant.new_string(playlist.id))
            self.playlists_submenu.insert_item(index, item)

        for name, callback in action_entries:
            action = Gio.SimpleAction.new(name, None)
            action.connect("activate", callback)
            action_group.add_action(action)

        self.insert_action_group("trackwidget", action_group)

        th = threading.Thread(target=utils.add_image, args=(self.image, self.track.album))
        th.deamon = True
        th.start()

    def _get_radio(self, *args):
        from ..pages.track_radio_page import trackRadioPage
        page = trackRadioPage(self.win, self.track, f"{self.track.name} Radio")
        page.load()
        self.win.navigation_view.push(page)

    def _play_next(self, *args):
        self.win.player_object.add_next(self.track)

    def _add_to_queue(self, *args):
        self.win.player_object.add_to_queue(self.track)

    def _add_to_my_collection(self, *args):
        th = threading.Thread(target=self.th_add_to_my_collection, args=())
        th.deamon = True
        th.start()

    def th_add_to_my_collection(self):
        self.win.session.user.favorites.add_track(self.track.id)

    def _add_to_playlist(self, action, parameter, playlist_index):
        playlist_id = parameter.get_string()
        selected_playlist = self.win.favourite_playlists[playlist_index]

        print(f"Added to playlist: {selected_playlist.name}, ID: {playlist_id}")

    @Gtk.Template.Callback("on_artist_button_clicked")
    def _on_artist_button_clicked(self, *args):
        from ..pages.artist_page import artistPage
        page = artistPage(self.win, self.track.artist, f"{self.track.artist.name}")
        page.load()
        self.win.navigation_view.push(page)

    @Gtk.Template.Callback("on_album_button_clicked")
    def _on_album_button_clicked(self, *args):
        from ..pages.album_page import albumPage
        page = albumPage(self.win, self.track.album, f"{self.track.album.name}")
        page.load()
        self.win.navigation_view.push(page)

src/widgets/generic_track_widget.py
Nokse22/high-tide
# window.py
#
# Copyright 2023 Nokse
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later

from gi.repository import Adw
from gi.repository import Gtk
from gi.repository import Gio
from gi.repository import GObject
# from gi.repository import Secret

from gi.repository import Gst, GLib

import base64
import json

import tidalapi
from tidalapi.page import PageItem, PageLink
from tidalapi.mix import Mix, MixType
from tidalapi.artist import Artist
from tidalapi.album import Album
from tidalapi.media import Track, Quality
from tidalapi.playlist import Playlist

from .lib import playerObject
from .lib import utils

import os
import sys

from .login import LoginWindow
from .new_playlist import NewPlaylistWindow

from .pages import homePage, singleTypePage, explorePage, artistPage, notLoggedInPage
from .pages import searchPage, trackRadioPage, playlistPage, startUpPage

import threading
import requests
import random

from .lib import SecretStore

@Gtk.Template(resource_path='/io/github/nokse22/high-tide/window.ui')
class TidalWindow(Adw.ApplicationWindow):
    __gtype_name__ = 'TidalWindow'

    # homepage_box = Gtk.Template.Child()
    split_view = Gtk.Template.Child()
    progress_bar = Gtk.Template.Child()
    sidebar_list = Gtk.Template.Child()
    dutation_label = Gtk.Template.Child()
    time_played_label = Gtk.Template.Child()
    shuffle_button = Gtk.Template.Child()
    navigation_view = Gtk.Template.Child()
    play_button = Gtk.Template.Child()
    search_entry = Gtk.Template.Child()
    small_progress_bar = Gtk.Template.Child()
    song_title_label = Gtk.Template.Child()
    artist_button = Gtk.Template.Child()
    playing_track_image = Gtk.Template.Child()
    artist_label = Gtk.Template.Child()
    sidebar_collection = Gtk.Template.Child()
    lyrics_split_view = Gtk.Template.Child()
    lyrics_box = Gtk.Template.Child()
    sidebar_playlists = Gtk.Template.Child()
    volume_button = Gtk.Template.Child()
    in_my_collection_button = Gtk.Template.Child()
    explicit_label = Gtk.Template.Child()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.settings = Gio.Settings.new('io.github.nokse22.high-tide')

        self.settings.bind("window-width", self, "default-width", Gio.SettingsBindFlags.DEFAULT)
        self.settings.bind("window-height", self, "default-height", Gio.SettingsBindFlags.DEFAULT)

        self.player_object = playerObject()

        self.volume_button.get_adjustment().set_value(self.settings.get_int("last-volume")/10)

        # self.shuffle_button.connect("toggled", self.on_shuffle_button_toggled)

        self.player_object.bind_property("shuffle_mode", self.shuffle_button, "active", GObject.BindingFlags.DEFAULT)
        self.player_object.connect("update-slider", self.update_slider)
        self.player_object.connect("song-changed", self.on_song_changed)
        self.player_object.connect("play-changed", self.update_controls)
        self.artist_button.connect("clicked", self.on_toolbar_artist_button_clicked)

        self.search_entry.connect("activate", self.on_search_activated)

        self.session = tidalapi.Session()

        self.user = self.session.user

        self.select_quality(self.settings.get_int("quality"))

        self.current_mix = None
        self.player_object.current_song_index = 0
        self.previous_time = 0
        self.favourite_playlists = []
        self.favourite_tracks = []

        self.secret_store = SecretStore(self.session)

        # TO REMOVE UNSECURED TOKENS set in early development
        self.settings.set_string("token-type", "")
        self.settings.set_string("access-token", "")
        self.settings.set_string("refresh-token", "")
        self.settings.set_string("expiry-time", "")

        page = startUpPage(self, None, "Loading")
        page.load()
        self.navigation_view.push(page)

        th = threading.Thread(target=self.login, args=())
        th.deamon = True
        th.start()

    def on_logged_in(self):
        print("on logged in")
        self.favourite_tracks = self.session.user.favorites.tracks()

        self.search_entry.set_sensitive(True)

        page = homePage(self)
        page.load()
        self.navigation_view.replace([page])

        th = threading.Thread(target=self._set_last_playing_song, args=())
        th.deamon = True
        th.start()

        self.add_favourite_playlists()

    def on_login_failed(self):
        print("login failed")

        page = notLoggedInPage(self)
        page.load()
        self.navigation_view.replace([page])

    def add_favourite_playlists(self):
        playlists = self.session.user.favorites.playlists()

        child = self.sidebar_playlists.get_first_child()
        while child != None:
            self.sidebar_playlists.remove(child)
            child = self.sidebar_playlists.get_first_child()

        for index, playlist in enumerate(playlists):
            label = Gtk.Label(xalign=0, label=playlist.name, name=index)
            self.sidebar_playlists.append(label)

        self.favourite_playlists = playlists

    def _set_last_playing_song(self):
        track_id = self.settings.get_int("last-playing-song-id")
        list_id = self.settings.get_string("last-playing-list-id")

        if track_id == '':
            return

        track = self.session.track(track_id)
        self.player_object.play_track(track)

    def on_song_changed(self, *args):
        print("song changed")
        album = self.player_object.song_album
        track = self.player_object.playing_track
        self.song_title_label.set_label(track.name)
        self.artist_label.set_label(track.artist.name)
        self.in_my_collection_button.set_icon_name("heart-outline-thick-symbolic")
        self.explicit_label.set_visible(track.explicit)

        self.settings.set_int("last-playing-song-id", track.id)

        album_image_link = album.image()
        if album_image_link:
            th = threading.Thread(target=self.add_image, args=(self.playing_track_image, album_image_link))
            th.deamon = True
            th.start()

        if self.player_object.is_playing:
            self.play_button.set_icon_name("media-playback-pause-symbolic")
        else:
            self.play_button.set_icon_name("media-playback-start-symbolic")

        self.control_bar_artist = track.artist
        self.update_slider()

    def on_toolbar_artist_button_clicked(self, btn):
        self.sidebar_list.select_row(None)
        artist = self.player_object.playing_track.artist
        page = artistPage(self, artist, artist.name)
        page.load()
        self.navigation_view.push(page)
        print("clicked on artist")

    def on_search_activated(self, *args):
        page = searchPage(self, None, "Search")
        page.load()
        self.navigation_view.push(page)

    def update_controls(self, is_playing, *arg):
        if not is_playing:
            self.play_button.set_icon_name("media-playback-pause-symbolic")
            print("pause")
        else:
            self.play_button.set_icon_name("media-playback-start-symbolic")
            print("play")

    def new_login(self):
        login_window = LoginWindow(self, self.session)
        login_window.set_transient_for(self)
        login_window.set_modal(self)
        login_window.present()

    def login(self):
        try:
            result = self.session.load_oauth_session(
                self.secret_store.token_dictionary["token-type"],
                self.secret_store.token_dictionary["access-token"],
                self.secret_store.token_dictionary["refresh-token"],
                self.secret_store.token_dictionary["expiry-time"])
        except Exception as e:
            print(f"error! {e}")
            GLib.idle_add(self.on_login_failed)
        else:
            self.on_logged_in()

    def logout(self):
        self.secret_store.clear()

        self.search_entry.set_sensitive(False)

        page = notLoggedInPage(self)
        page.load()
        self.navigation_view.replace([page])

    def explore_page(self):
        explore = session.explore()

    def add_image(self, image_widget, image_url):
        try:
            response = requests.get(image_url)
        except:
            return
        if response.status_code == 200:
            image_data = response.content
            file_path = f"tmp_img/{random.randint(0, 100)}.jpg"
            with open(file_path, "wb") as file:
                file.write(image_data)

            GLib.idle_add(self._add_image, image_widget, file_path)

    def _add_image(self, image_widget, file_path):
        image_widget.set_from_file(file_path)

    @Gtk.Template.Callback("on_play_button_clicked")
    def on_play_button_clicked(self, btn):
        if not self.player_object.is_playing:
            self.player_object.play()
            btn.set_icon_name("media-playback-pause-symbolic")
            print("pause")
        else:
            self.player_object.pause()
            btn.set_icon_name("media-playback-start-symbolic")
            print("play")

    def update_slider(self, *args):
        if not self.player_object.is_playing:
            return False # cancel timeout
        else:
            success, self.duration = self.player_object.query_duration(Gst.Format.TIME)
            if not success:
                end_value = 100
                pass
            else:
                end_value = self.duration / Gst.SECOND
                self.progress_bar.set_range(0, end_value)

                self.dutation_label.set_label(utils.pretty_duration(end_value))

            success, position = self.player_object.query_position(Gst.Format.TIME)
            if not success:
                pass
            else:
                position = position / Gst.SECOND
                self.progress_bar.get_adjustment().set_value(position)
                self.small_progress_bar.set_fraction(position/end_value)
                self.previous_time = position

                self.time_played_label.set_label(utils.pretty_duration(position))
            if position >= end_value - 0.2:
                print("ended")
                self.player_object.play_next()
        return True


    @Gtk.Template.Callback("on_lyrics_button_clicked")
    def on_lyrics_button_clicked_func(self, widget):
        # self.lyrics_split_view.set_collapsed(False)
        self.lyrics_split_view.set_show_sidebar(not self.lyrics_split_view.get_show_sidebar())

        th = threading.Thread(target=self.add_lyrics_to_page, args=())
        th.deamon = True
        th.start()

    def add_lyrics_to_page(self):
        lyrics = self.player_object.playing_track.lyrics()
        label = Gtk.Label(label=lyrics.text, wrap=True, justify=2, opacity=0.6,
                margin_start=12, margin_end=12, css_classes=["lyrics"])
        self.lyrics_box.set_child(label)

    def download_song(self):
        song = self.player_object.playing_track
        song_url = song.get_url()
        try:
            response = requests.get(song_url)
        except:
            return
        if response.status_code == 200:
            image_data = response.content
            file_path = f"{song.id}.flac"
            with open(file_path, "wb") as file:
                file.write(image_data)

    def select_quality(self, pos):
        match pos:
            case 0:
                self.session.audio_quality = Quality.low_96k
            case 1:
                self.session.audio_quality = Quality.low_320k
            case 2:
                self.session.audio_quality = Quality.high_lossless
            case 3:
                self.session.audio_quality = Quality.hi_res
            case 4:
                self.session.audio_quality = Quality.hi_res_lossless

        self.settings.set_int("quality", pos)

    @Gtk.Template.Callback("on_in_my_collection_button_clicked")
    def on_in_my_collection_button_clicked(self, btn):
        if self.in_my_collection_button.get_icon_name() == "heart-outline-thick-symbolic":
            th = threading.Thread(target=self.add_track_to_my_collection, args=(self.player_object.playing_track.id,))
            self.in_my_collection_button.set_icon_name("heart-filled-symbolic")
        else:
            th = threading.Thread(target=self.remove_track_from_my_collection, args=(self.player_object.playing_track.id,))
            self.in_my_collection_button.set_icon_name("heart-outline-thick-symbolic")
        th.deamon = True
        th.start()

    def add_track_to_my_collection(self, track_id):
        result = self.session.user.favorites.add_track(track_id)
        if result:
            print("successfully added to my collection")

    def remove_track_from_my_collection(self, track_id):
        result = self.session.user.favorites.remove_track(track_id)
        if result:
            print("successfully removed from my collection")

    @Gtk.Template.Callback("on_volume_changed")
    def on_volume_changed_func(self, widget, value):
        print(f"volume changed to {value}")
        self.player_object.change_volume(value)
        self.settings.set_int("last-volume", int(value*10))

    @Gtk.Template.Callback("on_new_playlist_button_clicked")
    def on_new_playlist_button_clicked_func(self, btn):
        new_playlist_win = NewPlaylistWindow(self, self.session)
        new_playlist_win.set_transient_for(self)
        new_playlist_win.set_modal(self)
        new_playlist_win.present()

    @Gtk.Template.Callback("on_track_radio_button_clicked")
    def on_track_radio_button_clicked_func(self, widget):
        track = self.player_object.playing_track
        page = trackRadioPage(self, track, f"{track.name} Radio")
        page.load()
        self.navigation_view.push(page)

    @Gtk.Template.Callback("on_slider_seek")
    def on_slider_seek(self, *args):
        seek_time_secs = self.progress_bar.get_value()
        if abs(seek_time_secs - self.previous_time) > 6:
            print("seeking")
            print(abs(seek_time_secs - self.previous_time))
            self.player_object.seek(Gst.Format.TIME,  Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, seek_time_secs * Gst.SECOND)
            self.previous_time = seek_time_secs

    @Gtk.Template.Callback("on_skip_forward_button_clicked")
    def on_skip_forward_button_clicked_func(self, widget):
        print("skip forward")
        self.player_object.play_next()

    @Gtk.Template.Callback("on_skip_backward_button_clicked")
    def on_skip_backward_button_clicked_func(self, widget):
        print("skip backward")
        self.player_object.play_previous()

    @Gtk.Template.Callback("on_playlists_sidebar_row_activated")
    def on_playlists_sidebar_row_activated_func(self, list_box, row):
        if row == None:
            return
        index = row.get_child().get_name()

        playlist = self.favourite_playlists[int(index)]

        page = playlistPage(self, playlist, playlist.name)
        page.load()
        self.navigation_view.push(page)

    @Gtk.Template.Callback("on_sidebar_row_selected_clicked")
    def on_sidebar_row_selected_clicked_func(self, list_box, row):
        if row == None:
            return
        if row.get_child().get_name() == "HOME":
            self.navigation_view.pop_to_tag("home")
        elif row.get_child().get_name() == "EXPLORE":
            page = explorePage(self, None, "Explore")
            page.load()
            self.navigation_view.push(page)
        elif row.get_child().get_name() == "F-TRACK":
            page = singleTypePage(self, "track", "Favourite Tracks")
            page.load()
            self.navigation_view.push(page)
        elif row.get_child().get_name() == "F-MIX":
            page = singleTypePage(self, "mix", "Favourite Mixes")
            page.load()
            self.navigation_view.push(page)
        elif row.get_child().get_name() == "F-ARTIST":
            page = singleTypePage(self, "artist", "Favourite Artists")
            page.load()
            self.navigation_view.push(page)
        elif row.get_child().get_name() == "F-PLAYLIST":
            page = singleTypePage(self, "playlist", "Favourite Playlists")
            page.load()
            self.navigation_view.push(page)
        elif row.get_child().get_name() == "F-ALBUM":
            page = singleTypePage(self, "album", "Favourite Albums")
            page.load()
            self.navigation_view.push(page)

    @Gtk.Template.Callback("show_sidebar")
    def func(self, btn):
        self.split_view.set_show_sidebar(True)

src/window.py
Nokse22/high-tide