#!/usr/bin/env python3
# -*- coding: utf-8 -*-

'''
Name:           youplay_qt.py
Description:    search, play and save audio files from Youtube
Usage:          start it with: './youplay.py --help'
Author:         Ralf Hersel - ralf.hersel@gmx.net
Author Qt Port: Prof.P - @prof.p:matrix.sp-codes.de
Licence:        GPL3
GUI-Library:	PySide6 (Qt)
Repository:		https://codeberg.org/ralfhersel/youplay
More infos:     see constants and readme.md
'''

# === Libraries ================================================================
import os
from youplay import *

# Third party imports                                                             # for the music play in GUI mode
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QDialog, QLabel, QVBoxLayout, QHBoxLayout,
                 QPushButton, QLineEdit, QMessageBox, QTreeView, QStyle, QAbstractItemView, QProgressBar)
from PySide6.QtGui import (QGuiApplication, QIcon, QStandardItem, QStandardItemModel)
from PySide6.QtCore import (QThread, Signal, Slot, Qt, QSize, QModelIndex)

# === Constants ================================================================

SEARCH_ICON             = os.path.dirname(__file__) + '/img/icons8-search-50.png'                                   # Free icon used from https://icons8.com/
PROG_ICON               = os.path.dirname(__file__) + '/youplay.svg'

# === Classes ==================================================================

class Get_song_from_yt(QThread):
    """Get song from Downloads or Youtube and download it (as thread)."""

    audio_path = Signal(str, str)

    def __init__(self, song):
        super().__init__()
        
        self.song = song

    def run(self):
        MUSIC_PATH = get_music_dir()
        if not os.path.exists(MUSIC_PATH):                                          # create sub-dir MUSIC_FOLDER if not exists
            os.mkdir(MUSIC_PATH)
        self.song = sanitize_song(self.song)													# remove unwanted chars from song title
        audio_path = MUSIC_PATH + self.song + '.mp3'
        if os.path.exists(audio_path):												# check if mp3-file exists (already downloaded)
            # return audio_path
            self.audio_path.emit(audio_path, self.song)
        else:																		# file does not exist
            print('\nDownloading song from Youtube:', self.song,
                '\nPlease wait until the song is downloaded ', end='')
            p = subprocess.Popen(['yt-dlp', '-q', '-f bestaudio', '--max-downloads',
                '1', '--yes-playlist', '--default-search', 'ytsearch', self.song,
                '--audio-format', 'mp3', '-o', audio_path, '--extract-audio',
                '--audio-quality', '0'])
            p.wait()
            p.terminate()
        # return audio_path
        self.audio_path.emit(audio_path, self.song)

class Dl_songs(QThread):
    """Get all downloaded files in a thread."""

    count_dl_songs = Signal(dict)

    def __init__(self):
        super().__init__()

    def run(self):
        music_path = get_music_dir()
        song_dict = {}
        files=glob.glob(music_path + '*.mp3')
        try: filename = files[0]
        except IndexError:
            print('No songs in music folder: ' + music_path)
            self.count_dl_songs.emit(song_dict)

        number = 0
        for filepath in files:
            title = os.path.basename(filepath)                                  # get filename from path
            title = title[:-4]                                                  # cut '.mp3' from filename
            title = title[:MAX_TITLE_LENGHT].ljust(MAX_TITLE_LENGHT, ' ')    	# cut and fit title length
            number += 1
            duration = get_mp3_duration(filepath)
            song_dict[str(number)] = [title, duration]

        self.count_dl_songs.emit(song_dict)

class Yt_songs(QThread):
    """Get songlist from yt in a thread."""

    count_yt_songs = Signal(dict)

    def __init__(self, song):
        super().__init__()
        self.song = song

    def run(self):
        song_dict = {}
        if len(self.song) > 3:
            print('Retrieving song list from Youtube:', self.song, '\nPlease wait ', end='')
            command = 'yt-dlp --get-title --get-duration "ytsearch{}:{}"'.format(str(NUMBER_OF_SONGS), self.song)
            p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)

            stdout = p.stdout
            count = 1                                                               # file line counter
            number = 1                                                              # list line counter
            for line in stdout:
                line = line.decode()
                striped_line = line.strip()
                if count % 2 == 0:                                                  # even = duration
                    duration = striped_line
                    if duration != '0':                                             # no stream
                        song_dict[str(number)] = [title, duration]
                        number += 1
                else:                                                               # not even = titel
                    title = striped_line[:MAX_TITLE_LENGHT].ljust(MAX_TITLE_LENGHT, ' ') # fit title lenght
                count += 1
        else:
            print('Warning: Song title is too short. Min 5 chars.')

        self.count_yt_songs.emit(song_dict)

# === GUI ======================================================================

class SearchDialog(QDialog):
    search_text = Signal(str)  # Define the custom signal

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Search Dialog")
        self.setWindowFlags(Qt.WindowType.Dialog|Qt.WindowType.WindowTitleHint)
        self.setFixedSize(100,50)

        layout = QVBoxLayout()

        self.search_input = QLineEdit()
        self.search_input.setClearButtonEnabled(True)
        self.search_input.setPlaceholderText("Song nr.")
        layout.addWidget(self.search_input)

        self.setLayout(layout)

    def keyPressEvent(self, event):
        key = event.key()

        if key == Qt.Key.Key_Return:
            self.search()

        if key == Qt.Key.Key_Escape:
            self.close()

    def search(self):
        search_text = self.search_input.text()
        self.accept()  # Close the dialog
        self.search_text.emit(search_text)  # Emit the custom signal with the search text
        self.search_input.clear()

class MainWindow(QMainWindow):
    """Implement Main Window class."""
    def __init__(self):
        super().__init__()

        self.prev_index = QModelIndex()

        # === Variables ========================================================
        import locale
        locale.setlocale(locale.LC_NUMERIC, "C")
        from mpv import MPV
        self.player = MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True) # initialize mpv internal player
        @self.player.property_observer('time-pos')
        def time_observer(_name, value):										# get playback timer
            if value is not None:                                               # avoid Type Error when started
                sec = int(value)                                                # get seconds from flaot
                sec_time_object = time.gmtime(sec)                              # convert to time object
                hms = time.strftime("%H:%M:%S",sec_time_object)                 # convert seconds to hour:minute:second
                title_time_text = self.status_text.strip() + '  -  ' + hms		# song titel - play time
                self.show_status_text(title_time_text)      					# update status bar

        self.search_dialog = SearchDialog(self)
        self.search_dialog.search_text.connect(self.perform_search)  # Connect the custom signal

        self.initUI()
        self.connect_signals()

    def initUI(self):
        self.setWindowTitle(PROG_NAME + " it from Youtube")
        self.resize(QSize(WIN_WIDTH, WIN_HEIGHT))
        self.center_window()

        # create a (master) vertical layout
        vBox = QVBoxLayout()

        # create a horizontal layout (for the buttons)
        hBox = QHBoxLayout()

        # Create a progress bar and set its alignment to the right
        self.progressbar = QProgressBar(self)
        self.progressbar.setAlignment(Qt.AlignmentFlag.AlignRight)
        self.progressbar.hide()
        self.progressbar.setMaximum(0)
        self.progressbar.setMinimum(0)

        # add the progress bar to the status bar
        self.statusBar().addPermanentWidget(self.progressbar)

        # create an input line-edit
        self.entry = QLineEdit()
        self.entry.setClearButtonEnabled(True)
        self.entry.addAction(QIcon(SEARCH_ICON), QLineEdit.ActionPosition.LeadingPosition)
        self.entry.setPlaceholderText("Search")

        # create a group of push buttons
        self.button_play = QPushButton("&Play")
        self.button_pause = QPushButton("Pause")
        self.button_back = QPushButton("&Back")
        self.button_forward = QPushButton("&Forward")
        self.button_songs = QPushButton("S&ongs")
        self.button_about = QPushButton("&About")
        self.button_quit = QPushButton("&Quit")

        # create a model for the results
        self.liststore = QStandardItemModel(0,3, self)
        self.liststore.setHorizontalHeaderLabels(['Nr', 'Title'])
        item = QStandardItem("Duration")
        item.setTextAlignment(Qt.AlignmentFlag.AlignRight)
        self.liststore.setHorizontalHeaderItem(2, item)

        # create a view for the results
        self.treeview = QTreeView()
        self.treeview.setModel(self.liststore)
        self.treeview.setRootIsDecorated(False)
        self.treeview.setSortingEnabled(True)
        self.treeview.setAlternatingRowColors(True)
        self.treeview.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        self.format_treeview()

        # create the general layout
        vBox.addWidget(self.entry)
        vBox.addWidget(self.treeview)
        vBox.addLayout(hBox)

        # add buttons to the horizontal layout
        hBox.addWidget(self.button_play)
        hBox.addWidget(self.button_pause)
        hBox.addWidget(self.button_back)
        hBox.addWidget(self.button_forward)
        hBox.addWidget(self.button_songs)
        hBox.addWidget(self.button_about)
        hBox.addWidget(self.button_quit)

        # set the central widget of the Window
        widget = QWidget()
        widget.setLayout(vBox)
        self.setCentralWidget(widget)
        self.entry.setFocus()

    def center_window(self):
        self.setGeometry(QStyle.alignedRect(
            Qt.LayoutDirection.LeftToRight,
            Qt.AlignmentFlag.AlignCenter,
            self.size(),
            QGuiApplication.primaryScreen().availableGeometry(),
            ),
        )

    def format_treeview(self):
        self.treeview.setColumnWidth(0, 20)
        self.treeview.setColumnWidth(1, 550)
        self.treeview.setColumnWidth(2, 30)
        self.treeview.sortByColumn(0, Qt.SortOrder.AscendingOrder)

    def connect_signals(self):
        self.button_pause.clicked.connect(self.on_button_pause_clicked)
        self.button_play.clicked.connect(self.on_button_play_clicked)
        self.button_back.clicked.connect(self.on_button_back_clicked)
        self.button_forward.clicked.connect(self.on_button_forward_clicked)
        self.button_songs.clicked.connect(self.on_button_songs_clicked)
        self.button_about.clicked.connect(self.on_button_about_clicked)
        self.button_quit.clicked.connect(self.on_button_quit_clicked)
        self.treeview.doubleClicked.connect(self.on_treeView_doubleClicked)

    @Slot(QModelIndex)
    def on_treeView_doubleClicked(self, index):
        self.player.stop()
        self.button_play.setText('&Play')
        self.on_button_play_clicked()

    def perform_search(self, search_text):
        if search_text:
            self.treeview.setFocus()
            model = self.treeview.model()
            flags = Qt.MatchFlag.MatchRecursive | Qt.MatchFlag.MatchCaseSensitive
            try:
                matching_indexes = model.match(model.index(0, 0), Qt.ItemDataRole.DisplayRole, int(search_text), -1, flags)
            except Exception as e:
                message =f"Please type in a song nr."
                print(message)
                self.show_status_text(message)
            else:
                if matching_indexes:
                    index = matching_indexes[0]
                    self.treeview.scrollTo(index)
                    self.treeview.setCurrentIndex(index)
                self.on_treeView_doubleClicked(index)


# === Event Handler ============================================================
    def closeEvent(self, _event):
        self.on_window_delete()

    def on_button_play_clicked(self):
        label = self.button_play.text()
        if label == '&Play':
            selected = self.get_tree_selection()
            self.do_play(selected)
        if label == '&Stop':
            self.button_play.setText('&Play')
            self.player.stop()
            self.show_status_text('Stopped playing')

    def on_window_delete(self):
        """Close the window by red cross."""
        self.player.stop()
        del self.player
        QApplication.quit()

    def on_button_about_clicked(self):
        """ABOUT clicked."""
        msg = QMessageBox()

        msg.setText(ABOUT)
        msg.setWindowTitle(QApplication.applicationName())
        
        button_about_qt = QPushButton("About Qt")
        button_about_qt.clicked.connect(lambda: QApplication.aboutQt())
        msg.addButton(button_about_qt, QMessageBox.HelpRole)

        msg.setStandardButtons(QMessageBox.StandardButton.Ok)

        msg.exec()

    def on_button_quit_clicked(self):
        """QUIT clicked."""
        self.close()

    def on_button_pause_clicked(self, widget):
        """PAUSE/CONTINUE clicked."""
        label = self.button_pause.text()
        if label == 'Pause':
            self.button_pause.setText('Continue')
            self.player.command('cycle', 'pause')
        if label == 'Continue':
            self.button_pause.setText('Pause')
            self.player.command('cycle', 'pause')

    def on_button_back_clicked(self):
        """BACK clicked."""
        try:
            self.player.command('seek', '-5')
        except SystemError as error:
            print(error)

    def on_button_forward_clicked(self):
        """FORWARD clicked."""
        try:
            self.player.command('seek', '5')
        except SystemError as error:
            print(error)

    def keyPressEvent(self, event):
        key = event.key()

        if QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier:
            if key == Qt.Key.Key_Q:
                self.on_button_quit_clicked()
            elif key == Qt.Key.Key_F:
                self.search_dialog.exec()  # Show the search dialog as a modal dialog
        else:
            if key == Qt.Key.Key_Return:
                self.on_entry_return()

    def on_button_songs_clicked(self):
        """Show list of downloaded songs."""
        self.songlist = Dl_songs()
        self.songlist.count_dl_songs.connect(self.on_dl_songs_finished)

        self.show_status_text('Retrieving downloaded songs...')
        self.progressbar.show()

        self.songlist.start()

    def on_dl_songs_finished(self, song_dict):
        self.liststore.clear()
        self.liststore.setHorizontalHeaderLabels(['Nr', 'Title', 'Duration'])
        for key, item in song_dict.items():
            number = QStandardItem(key.zfill(2)) # apply leading 0 for the track number
            # number = QStandardItem(key)
            title = QStandardItem(item[0])
            duration = QStandardItem(item[1])
            self.liststore.appendRow( [number, title, duration] )

        self.format_treeview()

        self.show_status_text('Select a song and click Play')

        self.progressbar.hide()

    def on_entry_return(self):
        """Enable new search when song is playing."""
        label = self.button_play.text()
        if label == '&Stop':
            self.player.stop()
            self.show_status_text('Stopped playing')
        self.button_play.setText('&Play')

        song = self.entry.text()

        self.songlist = Yt_songs(song)
        self.songlist.count_yt_songs.connect(self.on_yt_songs_finished)

        self.show_status_text('Searching on YouTube...')
        self.progressbar.show()

        self.songlist.start()

    def on_yt_songs_finished(self, song_dict):
        song_list = song_dict.items()                                           # convert dict to list
        if len(song_list) == 0:                                                 # nothing found
            self.show_status_text('Nothing found on Youtube, try another search')
        else:
            self.liststore.clear()
            self.liststore.setHorizontalHeaderLabels(['Nr', 'Title', 'Duration'])
            for key, item in song_dict.items():
                number = QStandardItem(key.zfill(2)) # apply leading 0 for the track number
                # number = QStandardItem(key)
                title = QStandardItem(item[0])
                duration = QStandardItem(item[1])
                self.liststore.appendRow( [number, title, duration] )

            self.format_treeview()

            self.show_status_text('Select a song and click Play')

        self.progressbar.hide()

# === Methods ==================================================================
    def do_play(self, song):
        """Play song from GUI."""
        if song == None:
            self.show_status_text('Nothing selected')
        else:
            self.progressbar.show()
            self.show_status_text('Downloading song from Youtube ... please wait')

            self.get_songs = Get_song_from_yt(song)
            self.get_songs.audio_path.connect(self.on_get_song_from_yt_finished)

            self.get_songs.start()

    def on_get_song_from_yt_finished(self, audio_path, song):
        self.progressbar.hide()
        self.status_text = song
        self.button_play.setText('&Stop')
        self.player.play(audio_path)

    def get_tree_selection(self):
        """Get selected tree item."""
        selModel = self.treeview.selectionModel()
        model = self.treeview.model()

        if selModel:
            count_selected = len(selModel.selectedRows())

            index = selModel.currentIndex()
            # ix = model.index(index.row(), 0) #  column which contains the id
            ix = model.index(index.row(), 1) #  column which contains the name

        if count_selected > 0:
            result = ix.data()
        else:
            result = ix.data()
        return result

    def show_status_text(self, text):
        """Show status text."""
        self.statusBar().showMessage(text)

def main():
    global app
    app = QApplication()
    app.setApplicationName(PROG_NAME)
    app.setApplicationVersion(VERSION)
    app.setWindowIcon(QIcon(PROG_ICON))

    gui = MainWindow()
    gui.show()

    app.exec()

# === Main =====================================================================
if __name__ == '__main__':    
    main()
