#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#
# SPDX-FileCopyrightText: 2011 Eike Hein <hein@kde.org>


# The overall format of this script's output. The format of the individual
# elements referenced here is specified further below.
output_format = "/me $intro $info [$player]"

# This is the '$intro' element in output_format.
intro_strings = {'audio' : 'is listening to', 'video' : 'is watching'}

# This is the '$info' element in output_format. You should not tinker with the
# case names on the left side of the colons.
format_strings = {
    'Title+SelfTitled'   : "$title by $artist (eponymous)",
    'SelfTitled'         : "${artist}'s self-titled album",
    'Title+Artist+Album' : "$title by $artist on $album",
    'Title+Artist'       : "$title by $artist",
    'Title+Album'        : "$title from $album",
    'Album+Artist'       : "$album by $artist",
    'Title'              : "$title",
    'Artist'             : "$artist",
    'Album'              : "$album"
}

# The lists below determine in which order the '/media', '/audio' and '/video'
# commands will query players if several of them are running. For '/audio'
# and '/video' they also determine which players are queried at all, however
# '/media' will query everything implementing the MPRIS2 standard, with un-
# listed players being queried last in alphabetical order.
# Entries should be the unique MPRIS2 bus names of players, i.e. the "amarok"
# part of "org.mpris.MediaPlayer2.amarok".
player_rankings = {
    'all'   : ['amarok', 'cantata', 'juk', 'tomahawk', 'rhythmbox', 'banshee', 'clementine',
               'audacious', 'spotify', 'dragonplayer', 'bangarang', 'vlc'],
    'audio' : ['amarok', 'cantata', 'juk', 'tomahawk', 'rhythmbox', 'banshee', 'audacious',
               'clementine', 'spotify', 'pragha', 'gogglesmm', 'qmmp', 'gmusicbrowser',
               'guayadeque', 'bangarang', 'dragonplayer', 'vlc'],
    'video' : ['dragonplayer', 'kaffeine', 'bangarang', 'vlc', 'smplayer', 'totem']
}

# When the generic '/media' command is used rather than '/audio' or '/video',
# the intro (see intro_strings) will be chosen based on the preferred media
# type of a player, specified here. If a player is not listed here, the
# 'audio' intro is used by default.
# Entries should be the unique MPRIS2 bus names of players, i.e. the "amarok"
# part of "org.mpris.MediaPlayer2.amarok".
preferred_media = {
    'dragonplayer' : 'video',
    'kaffeine'     : 'video',
    'smplayer'     : 'video',
    'totem'        : 'video',
    'vlc'          : 'video',
    'SMPlayer2'    : 'video'
}

# To list players and retrieve media metadata his script by default calls the
# 'qdbus' command installed by Qt. If you need to you can change this here,
# but beware that the output format of the alternate command has to match that
# of 'qdbus'. Pulled from the environment variable KONVERSATION_DBUS_BIN by default
#dbus_command = 'qdbus-qt6'

# If one of the title, album or artist metadata fields contains a character
# listed in FIXUP_CHARS, or if a string matching the regular expression given
# in REGEX_FIXUP is found in one of them, the respective field's text is
# surrounded by QUOTE_BEFORE and QUOTE_AFTER.
SIMPLE_FIXUP = '' # e.g. ' ' or '-'
REGEX_FIXUP = ''
QUOTE_BEFORE = '"'
QUOTE_AFTER = '"'


# ===== Do not change anything below this line. =====

import collections
import os.path
import re
import subprocess
import string
import sys

try:
    from urllib.parse import unquote_plus, urlsplit
except ImportError:
    from urllib import unquote_plus
    from urlparse import urlsplit

try:
    import konversation.dbus
    from konversation.dbus import dbus_command
    konversation.dbus.default_message_prefix = 'media: '

    import konversation.i18n
    konversation.i18n.init()
except ImportError:
    sys.exit("This script is intended to be run from within Konversation.")

if sys.hexversion < 0x02070000:
    err = i18n("The media script requires Python %1 or higher.", '2.7')
    konversation.dbus.error(err)
    sys.exit(err)

Player = collections.namedtuple('player', ('busname', 'name', 'identity', 'hastrack'))

class PlayerList(list):
    def insert(self, player, type='all'):
        try:
            index = player_rankings[type].index(player.name)
            list.insert(self, index, player)
        except ValueError:
            list.append(self, player)

def is_specific_player(kind):
    return kind is not None and kind not in ('audio', 'video')

def fetch_property(busname, attribute):
    output = subprocess.check_output((dbus_command, busname, '/org/mpris/MediaPlayer2', attribute))
    return output.decode(encoding='utf-8', errors='replace').strip()

def list_players():
    running = PlayerList()
    audio = PlayerList()
    video = PlayerList()
    eligible = PlayerList()

    try:
        output = subprocess.check_output((dbus_command, 'org.mpris.MediaPlayer2.*'))
    except subprocess.CalledProcessError:
        konversation.dbus.error(i18n("An error occurred while trying to list the running media players."), exit=True)

    for line in output.decode(errors='replace').splitlines():
        try:
            busname = line
            name = busname.split('.')[3]
            identity = fetch_property(busname, 'org.mpris.MediaPlayer2.Identity')
            hastrack = fetch_property(busname, 'org.mpris.MediaPlayer2.Player.PlaybackStatus')

            # If this is a player-specific invocation, consider both paused and playing states.
            if is_specific_player(requested):
                hastrack = hastrack in ('Playing', 'Paused')
            else:
                hastrack = hastrack == 'Playing'

            player = Player(busname, name, identity, hastrack)

            running.insert(player)

            if name in player_rankings['audio']:
                audio.insert(player, 'audio')
            if name in player_rankings['video']:
                video.insert(player, 'audio')
        except (subprocess.CalledProcessError, IndexError):
            pass

    if requested == 'audio':
        eligible = audio
    elif requested == 'video':
        eligible = video
    elif requested is not None:
        eligible = filter_by_name(running, requested)
    else:
        eligible = running

    eligible = [player for player in eligible if player.hastrack]

    return running, audio, video, eligible

def filter_by_name(player_list, name):
    return [player for player in player_list if player.name.lower() == name or player.identity.lower() == name]

def check_running():
    if requested == 'audio' and not players_audio:
        konversation.dbus.error(i18n("No running audio players found.", requested), exit=True)
    elif requested == 'video' and not players_video:
        konversation.dbus.error(i18n("No running video players found.", requested), exit=True)
    elif is_specific_player(requested) and not filter_by_name(players_running, requested):
        konversation.dbus.error(i18n("\"%1\" is not running.", requested), exit=True)
    elif not players_running:
        konversation.dbus.error(i18n("No running media players found."), exit=True)

def check_playing():
    if not players_eligible:
        if requested == 'audio':
            players = players_audio
        elif requested == 'video':
            players = players_video
        elif requested is not None:
            players = filter_by_name(players_running, requested)
        else:
            players = players_running

        if len(players) == 1:
                konversation.dbus.error(i18n("Nothing is playing in %1.", players[0].identity), exit=True)
        else:
            if requested == 'audio':
                konversation.dbus.error(i18nc("1 = Comma-separated list of audio players.",
                                              "None of the running audio players (%1) are playing anything.",
                                              ', '.join(sorted([player.identity for player in players_audio]))),
                                        exit=True)
            elif requested == 'video':
                konversation.dbus.error(i18nc("1 = Comma-separated list of video players.",
                                              "None of the running video players (%1) are playing anything.",
                                              ', '.join(sorted([player.identity for player in players_video]))),
                                        exit=True)
            else:
                konversation.dbus.error(i18nc("1 = Comma-separated list of media players.",
                                              "None of the running media players (%1) are playing anything.",
                                              ', '.join(sorted([player.identity for player in players_running]))),
                                        exit=True)

def get_metadata(player):
    try:
        output = fetch_property(player.busname, 'org.mpris.MediaPlayer2.Player.Metadata')
    except subprocess.CalledProcessError:
        konversation.dbus.error(i18n("An error occurred while trying to retrieve media metadata from %1.", player.identity), exit=True)

    keys = ('xesam:title:', 'xesam:artist:', 'xesam:albumArtist:', 'xesam:album:', 'xesam:url:')
    metadata = dict()

    for line in output.splitlines():
        if line.startswith(keys):
            key = line.split(':', 2)[1]
            value = line.split(':', 2)[-1].strip()

            if value:
                metadata[key] = value

    if not 'title' in metadata and 'url' in metadata:
        url = urlsplit(metadata['url'])

        if url.scheme == 'file' and url.path:
            metadata['title'] = os.path.basename(unquote_plus(url.path))

    if not 'artist' in metadata and 'albumArtist' in metadata:
        metadata['artist'] = metadata['albumArtist']

    return metadata

def fixup(metadata):
    for key, value in metadata.items():
        try:
            quote = False

            if len([c for c in value if c not in SIMPLE_FIXUP]) != len(value):
                quote = True
            elif REGEX_FIXUP and re.search(REGEX_FIXUP, value):
                quote = True

            if quote:
                metadata[key] = QUOTE_BEFORE + value + QUOTE_AFTER
        except re.error:
            konversation.dbus.error(i18nc("REGEX_FIXUP is the name of a config key in a config file.",
                                          "There is a problem with the regular expression specified in REGEX_FIXUP."),
                                    exit=True)

def format(player, metadata):
    fixup(metadata)

    format_key = [x for x in list(metadata) if x in ('album', 'artist', 'title')]

    if not format_key:
        konversation.dbus.error(i18n("%1 did not report the metadata needed to output a message.", player.identity), exit=True)

    try:
        if metadata['album'] == metadata['artist']:
            format_key.remove('album')
            format_key.remove('artist')
            format_key.append('selftitled')
    except KeyError:
        pass

    format_key = '+'.join(sorted(format_key))
    format = {'+'.join(sorted(k.lower().split('+'))): v for (k, v) in format_strings.items()}[format_key]

    if format:
        info = string.Template(format).safe_substitute(metadata)
    else:
        konversation.dbus.error(i18n("There is a problem in the output format configuration."), exit=True)

    if requested in ('audio', 'video'):
        intro = intro_strings[requested]
    else:
        try:
            intro = intro_strings[preferred_media[player.name]]
        except KeyError:
            intro = intro_strings['audio']

    output = dict(intro=intro, info=info, player=player.identity)

    return string.Template(output_format).safe_substitute(output)

if __name__ == '__main__':
    if not konversation.dbus.target:
        # No target usually means we were called from a server tab.
        version = '3'
        indent = '    '
        i = konversation.dbus.info
        i(i18n("media v%1 for Konversation.", version))
        i(i18n("Usage:"))
        i(indent + i18n("\"/media\" - report what the first player found is playing."))
        i(indent + i18n("\"/media [ 'audio' | 'video' ]\" - report what is playing in a known audio or video player."))
        i(indent + i18n("\"/media { Player }\" - report what is playing in the specified player if it is found."))
    else:
        try:
            requested = sys.argv[3].lower().strip()
        except IndexError:
            requested = None

        players_running, players_audio, players_video, players_eligible = list_players()

        check_running()
        check_playing()

        player = players_eligible[0]
        metadata = get_metadata(player)

        konversation.dbus.say(format(player, metadata), '')
