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

'''
Name:           youplay.py
Description:    search, play and save audio files from Youtube
Usage:          start it with: './youplay.py --help'
Author:         Ralf Hersel - ralf.hersel@gmx.net
Licence:        GPL3
Repository:     https://codeberg.org/ralfhersel/youplay
More infos:     see constants and readme.md
'''

# === Todo =====================================================================

'''
- Table header alignment. Waiting for Python-Gtk4 documentation.
- Upload Flatpak on Flathub: not possible from Codeberg. Refuse.
'''

# === Dependencies =============================================================

'''
python3    - default on most GNU/Linux-Distros                                  # because it is Python :)
yt-dlp     - python3 -m pip install -U yt-dlp                                   # https://github.com/yt-dlp/yt-dlp
mpv        - sudo apt install mpv                                               # stand-alone player
python-mpv - pip3 install --upgrade python-mpv                                  # python bindings for the integrated MPV player
libmpv1    - mpv library
ffmpeg     - sudo apt install ffmpeg                                            # required to calculate mp3 duration
'''

# === Libraries ================================================================
import sys
import subprocess                                                               # for calling external commands
import os
import time                                                                     # for H:M:S convertion
import mpv                                                                      # for the music play in GUI mode
import glob                                                                     # for directory patterned file list (mp3)
import string                                                                   # for song name sanitizing
import getopt                                                                   # for parsing command line arguments

# 3rd party Lubraries
from mpv import MPV

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

PROG_NAME               = 'YouPlay'
VERSION                 = '0.46'
LAST_UPDATE             = '2023-05-30'
LICENCE                 = 'GPL3'
AUTHOR                  = 'Ralf Hersel'
CONTACT                 = 'https://social.anoxinon.de/@ralfhersel'
WIN_WIDTH               = 700
WIN_HEIGHT              = 410                                                   # optimized for 10 songs
LINE                    = '-' * 80
NUMBER_OF_SONGS         = 10
MAX_TITLE_LENGHT        = 80                                                    # Title lenght in general and for GUI
MAX_TITLE_LENCLI        = 65                                                    # Title lenght for CLI
MUSIC_FOLDER            = '/youplay/'                                           # subdirectory for downloaded songs
MUSIC_PATH              = ''                                                    # will be autom. set with full path for downloaded songs
MIN_SONGNAME_LENGTH     = 3                                                     # minimum lenght of song title

HELP = '''YouPlay Help:
================================================================================
-h    --help       show command line parameter
-v    --version    show version information
-a    --about      show general infos about the application
-g    --gui        start YouPlay in GUI-mode (otherwise CLI-mode)
-s    --songs      list already downloaded songs
[a song title]     song title, that you want to search for'''

WELCOME = f'''Welcome to {PROG_NAME}
================================================================================
'''

CONTROLS = '''Player controls:
    q       quit
    space   pause/continue
    right   fast forward
    left    fast backward'''

THANKS = f'''====================================================================
Thank you for using {PROG_NAME}!'''

ABOUT = f'''Reclaim Freedom
Suck it from Youtube
Fight against digital handcuffs

Version    : {VERSION}
Update    : {LAST_UPDATE}
Licence    : {LICENCE}
Author     : {AUTHOR}
Contact    : {CONTACT}

Stay free, play music, donate artists'''

# === Features =================================================================

def get_songlist(song):                                                         # get songlist from Youtube
    song_dict = {}
    if len(song) > MIN_SONGNAME_LENGTH:
        print(f"Retrieving song list from Youtube: {song}\nPlease wait ", end="")
        p = subprocess.Popen(
            [
                "yt-dlp",
                "--get-title",
                "--get-duration",
                f"ytsearch{NUMBER_OF_SONGS}:{song}",
            ],
            stdout=subprocess.PIPE,
            universal_newlines=True)
        p.wait()
        stdout = p.stdout
        count = 1                                                               # file line counter
        number = 1                                                              # list line counter
        for line in stdout:
            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(f'Warning: Song title is too short. Min {MIN_SONGNAME_LENGTH} chars.')
    return song_dict


def show_list(song_dict):                                                       # show the list of songs
    print(LINE)
    for key, item in song_dict.items():
        number = str(key)
        title = item[0]
        duration = item[1]
        print(number.ljust(3), title[:MAX_TITLE_LENCLI].ljust(MAX_TITLE_LENCLI, ' '), '-', duration.rjust(8))
    print(LINE)
    keep_asking = True
    while keep_asking:
        answer = input('\nSong number or RETURN for first song or (q)uit: ')
        if answer == 'q':                                                       # quit
            keep_asking = False
        elif len(answer) == 0:                                                  # return
            answer = '1'                                                        # play first song
            keep_asking = False
        elif not answer.isdigit():                                              # wrong letter
            print('Warning: wrong entry, try again.')
        elif int(answer) in range(1, NUMBER_OF_SONGS + 1):                      # check number
            keep_asking = False
        else:
            print('Warning: wrong number, try again.')
    return answer


def get_song(song):                                                             # Get song from Downloads or Youtube and download it
    music_path = get_music_dir()
    if not os.path.exists(music_path):                                          # create sub-dir MUSIC_FOLDER if not exists
        os.mkdir(music_path)
    song = sanitize_song(song)                                                  # remove unwanted chars from song title
    audio_path = music_path + song + '.mp3'
    if os.path.exists(audio_path):                                              # check if mp3-file exists (already downloaded)
        return audio_path
    else:                                                                       # file does not exist
        print('\nDownloading song from Youtube:', 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', song,
            '--audio-format', 'mp3', '-o', audio_path, '--extract-audio',
            '--audio-quality', '0'])
        p.wait()
    return audio_path

def play_song(audio_path):
    """Play song from CLI."""
    print(CONTROLS)
    p = subprocess.Popen(['mpv', audio_path])                                   # play the song with MPV
    p.wait()
    p.terminate()

def show_downloaded_songs():
    """Show list of downloaded songs."""
    music_path = get_music_dir()
    files=glob.glob(music_path + '*.mp3')
    try: filename = files[0]
    except IndexError:
        print('Warning: No songs in music folder:', music_path)
        answer = 'q'
        return answer
    number = 0
    song_dict = {}
    for filepath in files:
        title = os.path.basename(filepath)                                      # get filename from path
        title = title[:-4]                                                      # cut '.mp3' from filename
        title = title[:MAX_TITLE_LENCLI].ljust(MAX_TITLE_LENCLI, ' ')           # cut and fit title length
        number += 1
        duration = get_mp3_duration(filepath)
        song_dict[str(number)] = [title, duration]
    keep_asking = True
    while keep_asking:
        print('Downloaded songs')
        print(LINE)
        for key, item in song_dict.items():
            number = str(key)
            title = item[0]
            duration = item[1]
            print(number.ljust(3), title, '-', duration)
        print(LINE)
        answer = input('\nSong number or RETURN for first song or (q)uit: ')
        if answer == 'q':                                                       # quit
            return answer
        elif len(answer) == 0:                                                  # return
            answer = '1'                                                        # play first song
            song = song_dict[answer][0]                                         # get song name
            audio_path = get_song(song)                                         # download song
            play_song(audio_path)                                               # play song
        elif not answer.isdigit():                                              # wrong letter
            print('Warning: wrong entry, try again.')
        elif int(answer) in range(1, len(song_dict)+1):                         # check number
            song = song_dict[answer][0]                                         # get song name
            audio_path = get_song(song)                                         # download song
            play_song(audio_path)                                               # play song
        else:
            print('Warning: wrong number, try again.')
    return answer

# === Misc =====================================================================

def sanitize_song(song):                                                        # remove special chars from songname
    song = song.strip()                                                         # remove leading/trailing whitespace
    song = song.replace('/', '-')                                               # avoid that song name creates sub-directories
    song = song.replace('\\', '-')                                              # avoid other bullshit
    song = song.replace('"', '')                                               # avoid code injection
    song = song.replace('--', '')                                              # avoid code injection
    song = song.replace(';', '')                                               # avoid code injection
    return song

def cli_progress(p):                                                            # show progress
    print('.', end='')                                                          # CLI progress bar
    sys.stdout.flush()                                                          # update display
    return True

def get_mp3_duration(filepath):                                                 # calculate duration of mp3 file
    result = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
        "format=duration", "-of",
        "default=noprint_wrappers=1:nokey=1", filepath],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT)
    duration = float(result.stdout)
    sec_time_object = time.gmtime(duration)                                     # convert to time object
    hms = time.strftime("%H:%M:%S",sec_time_object)                             # convert seconds to hour:minute:second
    return hms

def get_music_dir():                                                            # get standard music directory
    result = subprocess.run(['xdg-user-dir', 'MUSIC'], stdout=subprocess.PIPE, text=True)
    music_path = result.stdout.strip()
    youplay_music_path = music_path + MUSIC_FOLDER
    return youplay_music_path

def read_commandline_args(argv):
    """Parse the given command line arguments."""

    try:
        opts, args = getopt.getopt(argv, "hvag", ["help", "version", "about", "gui="])

    except getopt.GetoptError as err:
        print(f"GetoptError: {err}\n")
        print(HELP)
        sys.exit(2)

    for opt, arg in opts:
        if opt in ("-h", "--help"):
            print(HELP)
            sys.exit()
        else:

            if opt in ("-v", "--version"):
                print(f"{PROG_NAME} {VERSION}")
                sys.exit()

            if opt in ("-a", "--about"):
                print(ABOUT)
                sys.exit()

            if opt in ("-g", "--gui"):
                match arg.lower():
                    case "" | "gtk" | "gtk4":
                        import youplay_gtk4 as youplay_gui
                    case "gtk3":
                        import youplay_gtk3 as youplay_gui
                    case "qt" | "qt6":
                        import youplay_qt6 as youplay_gui
                    case "pyqt6":
                        import youplay_pyqt6 as youplay_gui
                    case _:
                        print(f"GetoptError: argument '{arg}' for option '--gui' not recognized")
                        sys.exit(2)
                youplay_gui.main()
                sys.exit()

def main(argv):
    read_commandline_args(argv)

    print(WELCOME)

    song_dict = {}
    global MUSIC_PATH
    MUSIC_PATH = get_music_dir()                                                # get standard XDG music path 

    question = 'Enter (q)uit, (l)ist, (s)ongs or song title: '
    answer = ""
    finish = False
    while finish == False:

        if answer == '':
            answer = input(question)                                            # no song from command line
            if answer == '':
                print('Error: wrong entry')
                finish = True
            elif answer == 'q':
                finish = True
            elif answer == 's':
                show_downloaded_songs()
                answer = input(question)
            elif answer == 'l':
                print('Warning: there are no search results')
                answer = input(question)
            elif answer[0] != '-' and answer[:2] != '--':                       # song title entered but no param
                song = answer
                song = sanitize_song(song)                                      # remove unwanted chars from song title
                song_dict = get_songlist(song)
                if len(song_dict) > 0:                                          # list not empty
                    answer = show_list(song_dict)
                else:
                    print('Warning: nothing found')
                    answer = input(question)
            else:
                print('Warning: invalid input')
                finish = True
        elif answer == 'q':
            finish = True
        elif answer == 's' or answer == '-s' or answer == '--songs':            # show list of downloaded songs
            show_downloaded_songs()
            answer = input(question)
        elif answer == 'l':
            if len(song_dict) > 0:                                              # list not empty
                answer = show_list(song_dict)
            else:
                print('Warning: There is no search result')
                answer = input(question)
        elif answer.isnumeric():                                                # song number selected
            if len(song_dict) == 0:                                             # nothing found
                print('Warning: Youtube found nothing')
                answer = input(question)
            else:
                song = song_dict[answer][0]                                     # get song name
                audio_path = get_song(song)                                     # download song
                play_song(audio_path)                                           # play song
                answer = input(question)
        elif answer[0] != '-' and answer[:2] != '--':                           # expect song title from cli
            song = answer
            song_dict = get_songlist(song)
            if len(song_dict) > 0:                                              # list not empty
                answer = show_list(song_dict)
            else:
                print('Warning: nothing found')
                answer = input(question)
        else:
            print('Error: invalid input')
            finish = True
 

    print(THANKS)
    return 0
    
if __name__ == '__main__':
    sys.exit(main(sys.argv[1:]))
