Статья Создание меню выбора клавишами в терминале с помощью Python

Когда мы пишем какое-то консольное приложение подразумевающее пользовательский выбор, хорошо было бы иметь такой инструмент, который бы создавал нужное нам меню и позволял перемещаться по нему с помощью стрелок на клавиатуре. Ведь это гораздо удобнее, чем просить пользователя ввести какую-либо цифру, соответствующую тому или иному пункту. И такой инструмент есть. Давайте посмотрим, как его реализовать в скрипте Python.

000.png



К сожалению, все, что будет описано далее не работает в ОС Windows. Поэтому, речи о кросплатформенности тут не идет. Однако, в Linux данный код работает прекрасно. Давайте создадим небольшой скрипт, который будет использовать меню для пользовательского выбора и запускать те или иные файлы или ссылки в консольном медиаплеере mpv.

Следует сразу же сказать, что данный код не будет работать в терминале IDE PyCharm. В VSCode не знаю, не проверял. В терминале все работает в лучшем виде.


Что потребуется?

Для начала, если вы хотите протестировать код в том виде, в котором он будет представлен в данной статье, нужно установить медиаплеер. В ОС Linux использующих кодовую базу Debian делается это командой:

sudo apt install mpv

Также нам потребуется библиотека, с помощью которой и будет создаваться меню. Установить ее можно набрав команду:

pip install simple-term-menu


Импорт модулей в скрипт

После того, как необходимые библиотеки будут установлены, импортируем нужные модули в скрипт.

Python:
import os
import subprocess
import sys
import time
from pathlib import Path

from simple_term_menu import TerminalMenu


Создание списков и словарей нужных в работе

Следующим шагом, который будет необходим, создадим словарь и список. В словаре мы будем хранить, так как это оболочка над плеером, названия файлов или каналов, если это плейлист. И ссылки на эти файлы или каналы.
Также необходимо создать список, в котором будут храниться названия файлов или каналов в текстовом виде. Этот список будет необходим при создании меню. Добавляем необходимый код в скрипт.

Python:
media = dict()
options = []


Определение функций оболочки

Теперь давайте определимся с тем, что будет уметь делать оболочка. Не особо много, но это и не главное. Ведь мы не создаем полноценный проигрыватель, а только лишь пишем скрипт для демонстрации возможности создания меню. Поэтому, оболочка будет уметь парсить плейлисты формата m3u, сканировать указанные директории на наличие в них медиафайлов. Ну и пожалуй — все.

Напишем функцию main, в которой будем осуществлять выбор пользователя для выполнения тех или иных действий. Заключим код в блок try — except, чтобы иметь возможность обрабатывать исключения. Выведем информацию для пользователя, что это Главное меню. Затем создадим список, в который запишем названия пунктов будущего меню. Как видите, их в списке три.

Инициализируем класс TerminalMenu в который передадим созданный список с пунктами меню, а также вызовем у данного класса метод show для его отображения. В ответ возвращается индекс пункта меню в используемом списке. Поэтому, если мы хотим обработать действие, нужно по данному индексу забрать из списка его значение. И далее, в зависимости от выбора пользователя запускаем парсинг плейлиста, сканирование директории или инициализируем исключение, с помощью которого будем выходить из функции.

В обработке исключений, которые могут возникнуть при работе с функцией с помощью subprocess запустим код очистки данных в терминале, выведем пользователю сообщение и завершим работу скрипта.

Python:
def main():
    """
    Главная функция. Выбор пользователя. Запуск сканирования директории
    или парсинга плейлиста. Запуск меню после сканирования.
    """
    try:
        print(f"\nГлавное меню\n{'-'*25}")
        opt = ["1. Ввести путь к плейлисту", "2. Открыть директорию", "3. Выход"]
        ch = TerminalMenu(opt).show()
        if opt[ch] == "1. Ввести путь к плейлисту":
            path = input("Путь к плейлисту >>> ")
            if not Path(path).exists() or not Path(path).is_file() or Path(path).suffix != ".m3u" or path == "":
                print("Неверная ссылка")
                main()
            open_playlist(path)
            menu_run()
        elif opt[ch] == "2. Открыть директорию":
            path = input("Путь к директории >>> ")
            if not Path(path).exists() or not Path(path).is_dir() or path == "":
                print("Неверная ссылка")
                main()
            dir_scan(path)
            menu_run()
        elif opt[ch] == "3. Выход":
            raise KeyboardInterrupt
    except (KeyboardInterrupt, TypeError):
        subprocess.call("clear", shell=True)
        print("\nGood By!\n")
        sys.exit(0)


Парсинг плейлиста

Как вы помните, первый пункт меню у нас ввод пути к плейлисту. Для того, чтобы передать данные в меню, его нужно для начала распарсить. А именно, получить названия каналов, а также получить ссылки на них. Тем более, что в плейлисте не обязательно могут быть ссылки на каналы в интернете, а вполне себе ссылки на локальные файлы. Плейлист состоит из трех основных частей. Первая часть, это заголовок плейлиста: #EXTM3U; вторая часть, расширенная информация о медиафайле: #EXTINF; ссылка на веб-ресурс или локальный файл.

В контексте нашего скрипта нам нужно только последние два параметра. Расширенная информация, из которой мы заберем название медиафайла для меню, и ссылка на медиафайл.

Создадим функцию open_playlist(path: str), которая принимает ссылку на файл. Определим переменные: key и num. В переменной key будет содержаться название канала или файла. В переменной num просто порядковый номер ресурса. Чтобы в меню содержались пронумерованные названия.

Python:
key = ""
num = 0

Откроем плейлист на чтение. Так как это, по сути, обычный текстовый файл, то ничего особого придумывать не нужно. В цикле проитерируемся по строкам файла. Если строка начинается на #EXTINF, значит это описание медиафайла. Забираем из него название и присваиваем в переменную key. С помощью continue итерируемся дальше. Если строка начинается со ссылки, увеличиваем счетчик на 1, Добавляем номер к названию медиафайла, добавляем пару ключ значение (название, ссылка) в словарь. А также добавляем название файла в глобальный список options, в котором и хранятся элементы меню.

Заключим код в блок try — except, для того, чтобы обработать ошибку декодирования данных, так как плейлисты могут быть сохранены в кодировке, которая отличается от кодировки utf-8.

Python:
def open_playlist(path: str):
    """
    Функция для парсинга плейлиста.

    :param path: Путь к плейлисту.
    """
    key = ""
    num = 0
    try:
        with open(path, 'r', encoding='utf-8') as file:
            for item in file.readlines():
                if item.startswith("#EXTINF"):
                    key = item.split(',')[-1].replace('#EXTGRP:', '').strip()
                    continue
                elif item.startswith("http"):
                    num += 1
                    key = f'{num}. {key}'
                    media.update({key: item.strip()})
                    options.append(key)
    except UnicodeDecodeError:
        print("Не могу декодировать данные")
        sys.exit(0)


Функция чтения содержимого директории

Как вы помните, второй пункт в меню выбора, это открытие директории с медиафалами. Создадим функцию dir_scan(path: str), которая на вход принимает ссылку на директорию. Создадим список с расширениями медиафайлов. Я назвал его suf. Конечно же, это далеко не все форматы, которые поддерживает mpv. Поэтому, если вам нужны какие-то дополнительные форматы, можете добавить их самостоятельно. Подробнее о поддерживаемых форматах можно почитать в документации плеера: . Для нашего скрипта добавленных форматов достаточно.

Создадим список файлов с нужными расширениями в директории с помощью os.listdir. И теперь проитерируемся в цикле по данному списку. Заберем названия файлов и добавим к ним порядковый номер. После этого, добавим пару ключ — значение в словарь. А также в глобальный список options добавим названия файлов для меню.

Python:
def dir_scan(path: str):
    """
    Функция для сканирования файлов в директории.

    :param path: Путь к директории.
    """
    # Добавьте нужные форматы. Подробнее о поддерживаемых mpv
    # форматах см. в документации: https://mpv.io/manual/master/
    suf = [".mp3", ".wav", ".mp4", ".avi"]
    files = [x for x in os.listdir(path) if Path(x).suffix in suf]

    for num, file in enumerate(files):
        name = f'{num+1}.{Path(file).name.split(Path(file).suffix)[0]}'
        media.update({name: str(Path(path) / file)})
        options.append(name)


Создание и отображение меню из медиафайлов

Теперь нам понадобиться небольшая функция, основной задачей которой будет создание меню из переданного в нее списка, а также его отображение в терминале. Данный код вам уже знаком по отображению меню пользовательского выбора. Создадим функцию menu. На вход она ничего не принимает, а возвращает индекс значения из списка, который соответствует пункту меню. Данная функция буквально в одну строку. Создаем экземпляр класса TerminalMenu, передаем в него составленный список с пунктами и отображаем с помощью метода show.

Python:
def menu():
    """
    Функция создания меню.

    :return: Возвращает индекс выбранного пункта меню в списке.
    """
    return TerminalMenu(options).show()


Перемещение по меню, выбор пункта и передача для воспроизведения

Для воспроизведения медиафайла или ресурса по ссылке будет служить другая функция. В данной же функции мы будем получать пользовательский выбор, когда он нажмет клавишу Enter на нужном пункте, запускать воспроизведение и после его окончания снова отображать меню со списком файлов.

Создадим функцию menu_run(). На вход она ничего не принимает. Для начала очищаем терминал от постороннего текста с помощью системной команды clear. Выводим сообщение для пользователя о том, что он перед собой видит, а именно плейлист. Получаем при нажатии на пункт меню значение индекса из функции menu и передаем полученное значение в функцию play, которая и будет проигрывать файлы. Ну и после окончания проигрывания запускаем рекурсивно саму себя.

Также обработаем исключения. В частности, TypeError и KeyboardInterrupt. И та, и та ошибки возникают при прерывании функции с помощью комбинации клавиш «Ctrl+C». В обработке ошибок очистим терминал, очистим список и словарь, после чего вызовем функцию main для отображения Главного меню.

Python:
def menu_run():
    """
    Функция запуска проигрывания плейлиста.
    """
    try:
        subprocess.call("clear", shell=True)
        print(f"ПЛЕЙЛИСТ:\n(для возврата в 'Главное меню' нажмите 'Ctrl+C')\n{'-' * 25}")
        m = menu()
        play(m)
        menu_run()
    except (KeyboardInterrupt, TypeError):
        subprocess.call("clear", shell=True)
        options.clear()
        media.clear()
        main()


Проигрывание композиций

В данном скрипте проигрывание композиций осуществляется с помощью медиаплеера mpv. Так как медиаплеер консольный, то при проигрывании, например аудио, управлять им можно только с помощью горячих клавиш. Подробнее о них можно почитать в документации плеера. Я опишу лишь некоторые. Клавиши стрелок. Вверх — вниз: перемотка композиции на минуту. Клавиши вперед — назад: перемотка на секунды. Клавиши 0 и 9, увеличение/уменьшение громкости. Клавиша q — выход из плеера.

Создадим функцию play(n: int). На вход она принимает индекс элемента списка меню, являющийся целым числом. В переменную m получаем из словаря путь к медиафайлу по ключу, который берется, в свою очередь, из списка по индексу. С помощью subprocess.check_call запускаем воспроизведение медиафайла. Здесь же устанавливаем процент громкости, который по умолчанию равен 100. subprocess.check_call здесь используется для того, чтобы по окончании композиции или после нажатия клавиши выхода q поймать возвращаемое значение, которое равно 0. Проверяем возвращаемое значение. Если оно равно 0, увеличиваем переданный в функцию индекс на единицу пишем сообщение в терминал для пользователя, если он желает выйти. Ждем две секунды и рекурсивно запускаем функцию с новым значением индекса, что будет означать проигрывание следующей композиции из списка.

Также необходимо обработать исключения, которые будут возникать при нажатии сочетания клавиш «Ctrl+C» или невозможности открыть медиаресурс. Если возникает IndexError, это означает, что достигнут конец плейлиста. Сюда можно добавить простое обнуление индекса и рекурсивный запуск функции по плейлисту с самого начала. Я же просто сообщаю об этом пользователю и возвращаюсь в меню со списком медиафайлов. Если произошло клавиатурное прерывание, просто возвращаемся в меню со списком. Если же возникла ошибка открытия медиаресурса, делаем то же самое, но с другим сообщением. По большому счету можно все эти исключение объединить в один блок Exception, выводить усредненное сообщение и просто выходить в меню со списком медиафайлов.

Python:
def play(n: int):
    """
    Функция проигрывания плейлиста с указанной позиции.

    :param n: Индекс пункта меню в списке.
    """
    try:
        m = media[options[n]]
        s = subprocess.check_call(f"mpv --volume=50 '{m}'", shell=True)
        if s == 0:
            n += 1
            print("\nДля выхода в плейлист нажмите 'Ctrl+C'\n")
            time.sleep(2)
            play(n)
    except subprocess.CalledProcessError:
        print("\nОШИБКА ОТКРЫТИЯ ССЫЛКИ\n")
        time.sleep(2)
        menu_run()
    except IndexError:
        print("Конец плейлиста")
        time.sleep(2)
        menu_run()
    except KeyboardInterrupt:
        menu_run()


Заключение

Вот, собственно и все. Напомню, что данный код не будет работать в IDE PyCharm из за особенностей встроенного терминала. В VSCode не проверял. В терминале же все работает отлично.

Python:
"""
Данный код не является полноценным плеером, а служит лишь для
иллюстрации возможности использования терминального меню.

Для работы с меню необходимо установить пакет: simple-term-menu.
Установка пакета: pip install simple-term-menu

Для работы данного кода требуется установка медиа-плеера mpv.
Установка в Linux Mint/Debian/Ubuntu: sudo apt install mpv

В ОС Windows библиотека использующаяся в скрипте не работает.

Меню не работает при запуске в IDE, по крайней мере в PyCharm. VSCode не тестировался.
Для корректной работы код необходимо запускать в терминале.
"""

import os
import subprocess
import sys
import time
from pathlib import Path

from simple_term_menu import TerminalMenu

media = dict()
options = []


def open_playlist(path: str):
    """
    Функция для парсинга плейлиста.

    :param path: Путь к плейлисту.
    """
    key = ""
    num = 0
    try:
        with open(path, 'r', encoding='utf-8') as file:
            for item in file.readlines():
                if item.startswith("#EXTINF"):
                    key = item.split(',')[-1].replace('#EXTGRP:', '').strip()
                    continue
                elif item.startswith("http"):
                    num += 1
                    key = f'{num}. {key}'
                    media.update({key: item.strip()})
                    options.append(key)
    except UnicodeDecodeError:
        print("Не могу декодировать данные")
        sys.exit(0)


def dir_scan(path: str):
    """
    Функция для сканирования файлов в директории.

    :param path: Путь к директории.
    """
    # Добавьте нужные форматы. Подробнее о поддерживаемых mpv
    # форматах см. в документации: https://mpv.io/manual/master/
    suf = [".mp3", ".wav", ".mp4", ".avi"]
    files = [x for x in os.listdir(path) if Path(x).suffix in suf]

    for num, file in enumerate(files):
        name = f'{num+1}.{Path(file).name.split(Path(file).suffix)[0]}'
        media.update({name: str(Path(path) / file)})
        options.append(name)


def menu():
    """
    Функция создания меню.

    :return: Возвращает индекс выбранного пункта меню в списке.
    """
    return TerminalMenu(options).show()


def play(n: int):
    """
    Функция проигрывания плейлиста с указанной позиции.

    :param n: Индекс пункта меню в списке.
    """
    try:
        m = media[options[n]]
        s = subprocess.check_call(f"mpv --volume=50 '{m}'", shell=True)
        print(s)
        if s == 0:
            n += 1
            print("\nДля выхода в плейлист нажмите 'Ctrl+C'\n")
            time.sleep(2)
            play(n)
    except subprocess.CalledProcessError:
        print("\nОШИБКА ОТКРЫТИЯ ССЫЛКИ\n")
        time.sleep(2)
        menu_run()
    except IndexError:
        print("Конец плейлиста")
        time.sleep(2)
        menu_run()
    except KeyboardInterrupt:
        menu_run()


def menu_run():
    """
    Функция запуска проигрывания плейлиста.
    """
    try:
        subprocess.call("clear", shell=True)
        print(f"ПЛЕЙЛИСТ:\n(для возврата в 'Главное меню' нажмите 'Ctrl+C')\n{'-' * 25}")
        m = menu()
        play(m)
        menu_run()
    except (KeyboardInterrupt, TypeError):
        subprocess.call("clear", shell=True)
        options.clear()
        media.clear()
        main()


def main():
    """
    Главная функция. Выбор пользователя. Запуск сканирования директории
    или парсинга плейлиста. Запуск меню после сканирования.
    """
    try:
        print(f"\nГлавное меню\n{'-'*25}")
        opt = ["1. Ввести путь к плейлисту", "2. Открыть директорию", "3. Выход"]
        ch = TerminalMenu(opt).show()
        if opt[ch] == "1. Ввести путь к плейлисту":
            path = input("Путь к плейлисту >>> ")
            if not Path(path).exists() or not Path(path).is_file() or Path(path).suffix != ".m3u" or path == "":
                print("Неверная ссылка")
                main()
            open_playlist(path)
            menu_run()
        elif opt[ch] == "2. Открыть директорию":
            path = input("Путь к директории >>> ")
            if not Path(path).exists() or not Path(path).is_dir() or path == "":
                print("Неверная ссылка")
                main()
            dir_scan(path)
            menu_run()
        elif opt[ch] == "3. Выход":
            raise KeyboardInterrupt
    except (KeyboardInterrupt, TypeError):
        subprocess.call("clear", shell=True)
        print("\nGood By!\n")
        sys.exit(0)


if __name__ == "__main__":
    main()

Вот собственно немного скриншотов:

001.png

002.png

003.png

004.png

005.png

006.png

Видео воспроизводиться в стандартном окне, в котором уже присутствуют элементы управления.

А на этом, пожалуй, все.

Спасибо за внимание. Надеюсь, данная информация будет вам полезна
 

Вложения

  • menu.py.zip
    2,1 КБ · Просмотры: 158
Последнее редактирование модератором:
02.03.2021
562
404
BIT
274
Благодарю, отличная статья. Вопрос а насколько сложно будет сделать подобное для запуска из скрипта минуя терминал, к примеру подобное сделать для домашнего кинотеатра?
 

Johan Van

Green Team
13.06.2020
363
691
BIT
395
Благодарю, отличная статья. Вопрос а насколько сложно будет сделать подобное для запуска из скрипта минуя терминал, к примеру подобное сделать для домашнего кинотеатра?

Ну, если минуя терминал... Если я правильно понимаю, то домашний кинотеатр в теории сам должен уметь ходить по плейлистам? А вообще, если речь об mpv, то он работает на разных ОС, включая Android. То есть, можно установить версию для ведроида и пользоваться. Хотя, может я не совсем понял вопрос...
 
  • Нравится
Реакции: Сергей Сталь
02.03.2021
562
404
BIT
274
Ну, если минуя терминал... Если я правильно понимаю, то домашний кинотеатр в теории сам должен уметь ходить по плейлистам? А вообще, если речь об mpv, то он работает на разных ОС, включая Android. То есть, можно установить версию для ведроида и пользоваться. Хотя, может я не совсем понял вопрос...
я думаю где можно еще использовать подобную наработку
 

Johan Van

Green Team
13.06.2020
363
691
BIT
395
я думаю где можно еще использовать подобную наработку

То есть меню? Если да, то хоть где. Единственное, что немного печально, это неспособность этой библиотеки работать под Виндой. А вообще, в любых меню, там где требуется просто выбрать вариант, чтобы не вводить его руками.

Наверное, если немного пошаманить, то попробовать можно даже на андроиде запускать вместе с плеером. Ведь там тоже Линукс. Но это надо тестировать.
 
  • Нравится
Реакции: Сергей Сталь
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!