• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Статья Парсинг страницы с треками, формирование плейлистов и использование mutagen для чтения метаданных аудиофайлов в Python

Давайте сегодня займемся парсингом, а заодно посмотрим, как работает библиотека для извлечения IDv3 тегов из аудиофайлов. Парсить будем сайт, с которого можно бесплатно скачать музыкальные треки. Скажем так, совместим приятное с полезным. Попрактикуемся немного в парсинге, а заодно скачаем себе несколько треков, для прослушивания локально. Ну или не совсем. Давайте посмотрим, как это сделать с помощью Python.

000.png


Предыстория у создания этого скрипта простая. Захотелось послушать музыку. Слушать в браузере не хотелось. Тем более, что мне понравился консольный медиаплеер mpv. Шустрый и легкий. Я и подумал, а почему бы мне не найти сайт, забрать оттуда ссылки на треки, сделать плейлист, а потом слушать эти треки в плеере? Так я и сделал. Заодно добавил возможность скачивать эти треки и делать плейлист уже из загруженных локальных файлов.

На самом деле, простейший плейлист сделать совсем несложно. Нужно создать файл с расширением .m3u. Записать в него необходимые базовые теги и можно пользоваться практически в любом плеере. Базовые теги, это: #EXTM3U — заголовок плейлиста. Указывается так, как я и написал. Следующая строка, это уже расширенное описание медиафайла. Здесь может быть довольно большое количество параметров, но в данном случае я ограничился только указанием длительности композиции, группы и названия трека. Ну и еще одна строка — это путь к локальному файлу или ссылка на него в интернете. Ну, а дальше по кругу. Описание - ссылка, описание — ссылка.

Вот пример структуры файла:

Код:
#EXTM3U
#EXTINF:263, group-title="Brainstorm", Скользкие улицы
https://ru.hitmotop.com/get/music/20170907/Brainstorm_-_Skolzkie_ulicy_48344078.mp3
#EXTINF:263, group-title="Brainstorm", На заре
https://ru.hitmotop.com/get/music/20170907/Brainstorm_-_Na_zare_48344089.mp3
#EXTINF:263, group-title="Brainstorm", Выходные
https://ru.hitmotop.com/get/music/20170903/Brainstorm_-_Vykhodnye_48098235.mp3
#EXTINF:263, group-title="Brainstorm", Ветер
https://ru.hitmotop.com/get/music/20170903/Brainstorm_-_Veter_48058499.mp3

Ничего сложного. Сайт я выбрал, почти что, первый попавшийся на запрос: «Скачать музыку бесплатно». Это сайт: . Ну, а парсить ссылки и названия треков мы будем, примерно, вот с таких вот ссылок: . Здесь может быть как страница с музыкой по жанрам, так и страница отдельного исполнителя. Вот как это выглядит на скриншоте.

01.png


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


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

Для работы данного скрипта нужно установить пять библиотек. Это: requests, bs4, lxml, tqdm, colorama.

requests необходим для скачивания композиций и получения html-кода страницы. bs4 и lxml — для парсинга тегов и данных. tqdm нужен для создания прогресс-бара, ну, а colorama для раскрашивания вывода в терминале. Установить все это можно одной командой. Поэтому пишем в терминале:

pip install requests bs4 lxml tqdm colorama

и дожидаемся окончания процесса установки.

Следующим шагом будет импорт установленных и используемых библиотек в наш скрипт. Поэтому в блоке импорта пишем:

Python:
import getpass
import os
import sys
from pathlib import Path
from urllib.parse import urljoin

import mutagen
import requests
from bs4 import BeautifulSoup
from colorama import Fore
from colorama.initialise import init
from tqdm import tqdm

init()

А заодно инициализируем colorama.
Так как мы все же будем парсить страницы, нужно добавить словарь с заголовками для запроса. Особо ничего туда добавлять не нужно. Достаточно user-agent и accept. И то, последний не особо обязателен.

Python:
headers = {
    "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 "
                  "YaBrowser/22.11.3.838 Yowser/2.5 Safari/537.36",
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,"
              "application/signed-exchange;v=b3;q=0.9"
}


Функция загрузки html-кода

Данную функцию мы будем использовать для загрузки html-кода во всем скрипте. И при загрузке страниц с композициями, и для получения пагинации. Так как треков может быть на несколько страниц.
Создадим функцию get_html(url: str) -> (str, bool). На входе она получает ссылку на страницу и возвращает текст запроса или False в случае неудачи.

Делаем запрос к странице с передачей в него ссылки на страницу и определенных ранее заголовков. Также устанавливаем timeout, в течении которого запрос будет ждать ответа от сервера и загрузки контента. Сюда, помимо одного значения, можно передать кортеж, в котором указать разные значения для ожидания соединения и загрузки контента. Если же передано одно значение, оба параметра становятся равными ему. Проверяем статус-код. Если он равен 200, возвращаем html-код. Если же нет — False. И обрабатываем исключения, которые могут возникнуть. Отлавливать каждое я не стал. Хотя, основные исключения в данном случае, это ConnectionError и ReadTimeout, я ограничился общей заглушкой для исключений. Таким образом, если страница недоступна, возвращаем False.

Python:
def get_html(url: str) -> (str, bool):
    """
    Получаем html-код страницы.
    :param url: Ссылка на страницу.
    :return: HTML-код или False в случае неудачи.
    """
    try:
        rs = requests.get(url=url, headers=headers, timeout=5)
        return rs.text if rs.status_code == 200 else False
    except Exception:
        return False


Получение пагинации и имени исполнителя со страницы

Для того, чтобы получить ссылки на все треки представленные у исполнителя или все ссылки определенного жанра, нужно понимать, сколько страниц с информацией нужно распарсить. Для этого необходимо получить ссылки на эти страницы, или хотя бы примерно понимать, как выполняется переход на следующую станицу. Поэтому, нужно получить данные из блока пагинации. А у нас на странице он присутствует. Также нужно будет обработать тот случай, когда блока пагинации не окажется на странице. А такие здесь есть. Также здесь нужно будет, помимо пагинации, получить название категории из блока «хлебных крошек». Чтобы создать директорию для сохранения плейлистов и композиций.

Создадим функцию get_pagination_artist(url: str) -> tuple. На вход она получает ссылку на страницу с исполнителем или жанром, а возвращает кортеж из ссылки на последнюю страницу пагинации и имя исполнителя. Если же блока пагинации нет, возвращаем False и исполнителя.

Для начала запрашиваем html-код страницы из функции get_html, в которую передаем ссылку на страницу. Передаем полученные значения в объект BeautifulSoup с указанием парсера. Дальше получаем данные об исполнителе. На странице они располагаются в теге «ul» с классом «breadcrumb», который содержит теги «li», последний из которых и содержит нужные нам данные.

02.png


Затем нам нужно получить пагинацию. На странице она располагается в теге «section» с классом «pagination». Здесь мы ищем тег «ul» с классом «pagination__list», ищем все теги «li» и забираем последний, в котором и содержится нужная нам информация, в которой мы находим тег «a» и забираем ссылку.

03.png


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

Python:
def get_pagination_artist(url: str) -> tuple:
    """
    Получение пагинации и названия группы или имени артиста.
    :param url: Ссылка на страницу для получения пагинации.
    :return: Кортеж из ссылки на последнюю страницу пагинации и название артиста/группы.
    """
    if txt := get_html(url):
        soup = BeautifulSoup(txt, "lxml")
        artist = ""
        try:
            artist = soup.find('ul', class_="breadcrumb").find_all('li')[-1].text.strip()
            section = soup.find('section', class_="pagination").find('ul', class_="pagination__list").find_all('li')[-1].\
                find('a').get('href')
            return urljoin(url, section), artist
        except Exception:
            return False, artist
    return False, False


Функция для парсинга данных о треке

Создадим функцию get_track(txt: str) -> list, которая получает html-код, а возвращает список с найденными значениями. Определим список, куда и будем складывать найденные значения, я назвал его temp. Создаем объект супа, куда передаем html-код и парсер. Получаем все блоки с информацией о треках. Информация о треке содержится в теге «div» с классом «track__info». Итерируемся по полученным объектам с данными. Забираем название трека, которое содержится в теге «div» с классом «track__title», ссылку на трек из блока «div» с классом «track__info-r», продолжительность композиции из тега «div» с классом «track__time», а также исполнителя конкретной композиции из тега «div» с классом «track__desc». Вот как это выглядит в инструментах разработчика.

04.png


После того, как получены все данные со страницы, возвращаем список с ними из функции.

Python:
def get_track(txt: str) -> list:
    """
    Получение данных о музыкальной композиции (треке).
    :param txt: HTML-код страницы.
    :return: Список с полученными данными.
    """
    temp = []
    soup = BeautifulSoup(txt, "lxml")
    all_muz = soup.find_all('div', class_="track__info")
    for muz in all_muz:
        title = muz.find('div', class_="track__title").text.strip()
        link = muz.find('div', class_="track__info-r").find('a').get('href')
        artist_song = soup.find('div', class_="track__desc").text.strip()
        duration = soup.find('div', class_="track__time").find('div', class_='track__fulltime').text.strip()
        time_track = f'{int(duration.split(":")[0]) * 60 + int(duration.split(":")[1])}'
        temp.append(f'#EXTINF:{time_track}, group-title="{artist_song}", {title}\n{link}\n')
        print(f'{artist_song} - {title}: {link}')
    return temp


Итерируемся по страницам из пагинации

Создадим функцию get_links(path: Path, iter_num: int, iter_link: str) -> (list, bool). На входе она получает объект Path, в котором содержится путь к директории для сохранения данных, количество треков из пагинации, для диапазона, в котором нужно итерироваться, и ссылку из пагинации, в которую будут поставляться нужные номера страниц. Если же у нас не было блока пагинации, то в функцию будет передана ссылка на страницу и номер итерации равный 0, который мы проверяем. Если он равен нулю, то получаем html-код по ссылке на страницу без пагинации. Передаем в функцию парсинга данных и добавляем полученные значения в список temp.

Если же пагинация была, запускаем цикл для итерации по страницам с шагом 48, который равен количеству композиций на странице. Формируем ссылку, и далее повторяем то же самое, что делали без пагинации. Получаем данные со страницы и добавляем полученное в список.

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

Python:
def get_links(path: Path, iter_num: int, iter_link: str) -> (list, bool):
    """
    Получение ссылок на mp3-файлы и запись в m3u-файл (плейлист).
    :param iter_link: Ссылка для перехода по страницам пагинации.
    :param iter_num: Количество страниц (треков) в категории.
    :param path: Путь к директории для сохранения.
    :return: Кортеж из ссылок на аудио-треки + количество
    полученных ссылок или False, если список пуст.
    """
    temp = []

    if iter_num == 0:
        if txt := get_html(f'{iter_link}0'):
            temp.extend(get_track(txt))
    else:
        for i in range(0, iter_num+1, 48):
            link = f'{iter_link}{i}'
            if txt := get_html(link):
                temp.extend(get_track(txt))

    if temp:
        path.mkdir(exist_ok=True)
        with open(path / f'{path.name}_web.m3u', mode='w', encoding='utf-8') as file:
            file.write("#EXTM3U\n")
            for item in temp:
                file.write(item)
        return [x.split("\n")[1].strip() for x in temp], len(temp)
    return False


Скачивание треков на диск

Создадим функцию track_download(url, path) -> bool. На входе она получает ссылку на трек и путь для его сохранения. Ну, а возвращает False, если загрузка не удалась. Решил не использовать потоки, так как торопиться особо некуда. Скачивание происходит с нормальной скоростью. Тем более, если у вас 100-битный канал, то загрузка у вас произойдет итак, достаточно быстро. А вот если у вас не особо скоростное соединение, то потоки помогут, но не особо, так как ширина канала от использования потоков не увеличиться.

Создаем запрос, в который передаем ссылку для загрузки, заголовки и ставим параметр stream в значение True, для того, чтобы иметь возможность итерироваться по контенту. Проверяем статус-код. Если он 200, двигаемся дальше, нет — возвращаем False. Пытаемся получить из заголовков размер скачиваемого трека. Если его нет, ставим значение по умолчанию 0. Формируем имя для сохраняемого файла, которое берем из ссылки на трек. Создаем переменную progress, в которую будет возвращаться значение из tqdm, оборачиваем в него итерацию по контенту, который загружаем частями. Передаем в tqdm длину трека, чтобы он мог сформировать длину прогресс-бара, передаем суффикс, который будет подставляться к полученным значениям. Итерируемся по контенту из progress и сохраняем полученный контент в файл. После чего обновляем значение индикатора загрузки.

Python:
def track_download(url, path) -> bool:
    """
    Скачивание треков по полученным ранее ссылкам.
    :param url: Ссылка на трек.
    :param path: Путь для сохранения трека.
    :return: False, если не удалось загрузить трек.
    """
    filename = ""
    try:
        rs = requests.get(url=url, headers=headers, stream=True)
        if rs.status_code == 200:
            file_size = int(rs.headers.get("Content-Length", 0))
            filename = Path(url).name
            progress = tqdm(rs.iter_content(1024), f"{Fore.GREEN}Downloading: {Fore.RESET}"
                                                   f"{filename}", total=file_size, unit="B",
                            unit_scale=True, unit_divisor=1024)
            with open(path / filename, "wb") as f:
                for data in progress.iterable:
                    f.write(data)
                    progress.update(len(data))
        return False
    except KeyboardInterrupt:
        (path / filename).unlink()
        print(f"\n{Fore.GREEN}До свидания: {Fore.RESET}{getpass.getuser()}\n")
        sys.exit(0)
    except Exception:
        print(f"Не удалось загрузить: {url}")
        return False


Обработка пользовательского выбора и использование mutagen для получения информации о треке

Создадим функцию main, в которой все, собственно и будет начинаться. Запросим у пользователя ссылку на страницу исполнителя или жанра. Сделаем минимальную проверку, что ссылка — это ссылка. Хотя, по большому счету, наверное, надо было бы проверить, чтобы хост был именно тот, с которого и будем скачивать треки и формировать плейлисты. Передаем полученную ссылку в функцию получения пагинации. Проверяем, если оба возвращенные значения False, выходим из скрипта. Если получено значение с именем исполнителя, формируем путь к папке для загрузки. Проверяем, не является ли значение пагинации False. Если нет, забираем количество композиций из ссылки, формируем ссылку для перехода по страницам и передаем в функцию получения данных о треках. Если же значение ссылки пагинации False, значит ее на странице нет, а значит, она только одна. Поэтому передаем в функцию для получения данных о треках путь для сохранения, количество композиций для итерации 0, и ссылку на страницу, с которой нужно будет получить данные.

Python:
def main():
    """
    Получение ссылки на страницу для загрузки.
    Запрос у пользователя дополнительных действий.
    Запуск функций для получения плейлистов и загрузки треков.
    """
    try:
        link = input("\nВведите ссылку на альбом исполнителя (ru.hitmotop.com): ")
        if not link.startswith("http"):
            print("\n[!] Введите ссылку")
            main()
            return
        pag_link, artist = get_pagination_artist(link)
        print("")
        if not pag_link and not artist:
            raise KeyboardInterrupt
        path = Path.cwd() / artist
        if (path / f'{path.name}_web.m3u').exists():
            print(f'{Fore.RED}Плейлист: "{path.name}_web.m3u" существует. И будет перезаписан.{Fore.RESET}')
            (path / f'{path.name}_web.m3u').unlink()
        if pag_link:
            iter_num = int(pag_link.split("/")[-1])
            iter_link = f'{"/".join(pag_link.split("/")[:-1])}/'
            links, count = get_links(path, iter_num, iter_link)
        else:
            links, count = get_links(path, 0, f'{link}/start/')

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

Python:
        if links:
            ch = input(f"\n{Fore.YELLOW}Плейлист сохранен. Получено ссылок: {Fore.RESET}{count}. "
                       f"{Fore.YELLOW}Загрузить треки? y/n: ")
            if ch.lower() in ['y', 'yes']:
                path.mkdir(exist_ok=True)
                for url in links:
                    if (path / Path(url).name).exists():
                        print(f"{Fore.YELLOW}Пропуск: {Fore.RESET}{Path(url).name} | {Fore.YELLOW}Существует")
                        continue
                    track_download(url, path)
            else:
                raise KeyboardInterrupt
        else:
            print(f"{Fore.RED}Не удалось получить ссылки")
            raise KeyboardInterrupt
        print(f"\n{Fore.GREEN}Все файлы загружены в папку: {Fore.RESET}{path}\n")

И, наконец, работа с mutagen. После того, как файлы будут загружены, сообщаем об этом пользователю. И далее запрашиваем у него, не желает ли он создать плейлист из файлов в директории. Если он согласен, создаем список для объектов плейлиста. Итерируемся в директории по файлам с помощью os.listdir. Выбор данного способа обусловлен тем, что в этом случае файлы в директории возвращаются в том порядке, в котором они в нем находятся. А не так как в случае с Path. Затем проверяем расширение файла. Если оно .mp3, получаем с помощью mutagen информацию о файле. Затем добавляем полученную информацию в список с объектами плейлиста. После всего, проверяем, не является ли список пустым и сохраняем полученные значения в файл.

Здесь нужно сказать, что у IDv3 тегов, которые возвращает mutagen, есть свои значения. Например, вот некоторые из них:

TPE1 - основной исполнитель;
TIT2 - название произведения;
TALB - название альбома, фильма и т. д.;
TCON — жанр;
COMM — комментарий;
TDRC - отметка времени записи;
WCOP - авторское право/юридическая информация.

Таким образом, указав конкретный тег мы может получить его значение из возвращаемого словаря.
Также, можно получить длину композиции с помощью функции mutagen — info.length.

Python:
        mkpls = input(f"{Fore.YELLOW}Желаете создать плейлист из загруженных файлов? y/n: ")
        if mkpls.lower() in ['y', 'yes']:
            temp = []
            for file in os.listdir(path):
                if Path(file).suffix == ".mp3":
                    audio = mutagen.File(path / file)
                    temp.append(f'#EXTINF:{audio.info.length:.2f}, group-title="{audio.get("TPE1")}", '
                                f'{audio.get("TIT2")}\n{path / file}\n')
            if temp:
                with open(path / f'{path.name}_loc.m3u', mode='w', encoding='utf-8') as file:
                    file.write("#EXTM3U\n")
                    for item in temp:
                        file.write(item)
            else:
                print(f"{Fore.RED}Не удалось получить список файлов в папке")
                raise KeyboardInterrupt
            print(f"{Fore.GREEN}Плейлист создан и сохранен в папку: {Fore.RESET}{path}\n")
        else:
            raise KeyboardInterrupt
    except KeyboardInterrupt:
        print(f"{Fore.GREEN}До свидания: {Fore.RESET}{getpass.getuser()}")
        sys.exit(0)

Как видите, на самом деле работа с этой полезной библиотекой достаточно проста.

Python:
def main():
    """
    Получение ссылки на страницу для загрузки.
    Запрос у пользователя дополнительных действий.
    Запуск функций для получения плейлистов и загрузки треков.
    """
    try:
        link = input("\nВведите ссылку на альбом исполнителя (ru.hitmotop.com): ")
        if not link.startswith("http"):
            print("\n[!] Введите ссылку")
            main()
            return
        pag_link, artist = get_pagination_artist(link)
        print("")
        if not pag_link and not artist:
            raise KeyboardInterrupt
        path = Path.cwd() / artist
        if (path / f'{path.name}_web.m3u').exists():
            print(f'{Fore.RED}Плейлист: "{path.name}_web.m3u" существует. И будет перезаписан.{Fore.RESET}')
            (path / f'{path.name}_web.m3u').unlink()
        if pag_link:
            iter_num = int(pag_link.split("/")[-1])
            iter_link = f'{"/".join(pag_link.split("/")[:-1])}/'
            links, count = get_links(path, iter_num, iter_link)
        else:
            links, count = get_links(path, 0, f'{link}/start/')

        if links:
            ch = input(f"\n{Fore.YELLOW}Плейлист сохранен. Получено ссылок: {Fore.RESET}{count}. "
                       f"{Fore.YELLOW}Загрузить треки? y/n: ")
            if ch.lower() in ['y', 'yes']:
                path.mkdir(exist_ok=True)
                for url in links:
                    if (path / Path(url).name).exists():
                        print(f"{Fore.YELLOW}Пропуск: {Fore.RESET}{Path(url).name} | {Fore.YELLOW}Существует")
                        continue
                    track_download(url, path)
            else:
                raise KeyboardInterrupt
        else:
            print(f"{Fore.RED}Не удалось получить ссылки")
            raise KeyboardInterrupt
        print(f"\n{Fore.GREEN}Все файлы загружены в папку: {Fore.RESET}{path}\n")

        mkpls = input(f"{Fore.YELLOW}Желаете создать плейлист из загруженных файлов? y/n: ")
        if mkpls.lower() in ['y', 'yes']:
            temp = []
            for file in os.listdir(path):
                if Path(file).suffix == ".mp3":
                    audio = mutagen.File(path / file)
                    temp.append(f'#EXTINF:{audio.info.length:.2f}, group-title="{audio.get("TPE1")}", '
                                f'{audio.get("TIT2")}\n{path / file}\n')
            if temp:
                with open(path / f'{path.name}_loc.m3u', mode='w', encoding='utf-8') as file:
                    file.write("#EXTM3U\n")
                    for item in temp:
                        file.write(item)
            else:
                print(f"{Fore.RED}Не удалось получить список файлов в папке")
                raise KeyboardInterrupt
            print(f"{Fore.GREEN}Плейлист создан и сохранен в папку: {Fore.RESET}{path}\n")
        else:
            raise KeyboardInterrupt
    except KeyboardInterrupt:
        print(f"{Fore.GREEN}До свидания: {Fore.RESET}{getpass.getuser()}")
        sys.exit(0)


if __name__ == "__main__":
    main()

Таким образом, мы получим загрузчик плейлистов и треков с сайта ru.hitmotop.com. Ну и несколько скриншотов, демонстрирующих работу скрипта.

05.png

06.png

После того, как вы получите плейлисты и треки, можно отправить их, к примеру, в mpv с помощью команды в терминале:

mpv --volume=50 --no-audio-display 'путь к плейлисту'

И да, чтобы долго не искать, если вы все же решите использовать mpv. Для переключения композиций используются следующие комбинации клавиш: «Shift+“.“» - на следующую композицию; «Shift+“,“» - на предыдущую композицию. Выход из плеера с помощью клавиши «q». Пауза: «p». Увеличение громкости: 0, уменьшение: 9.

Python:
# pip install requests bs4 lxml tqdm colorama

import getpass
import os
import sys
from pathlib import Path
from urllib.parse import urljoin

import mutagen
import requests
from bs4 import BeautifulSoup
from colorama import Fore
from colorama.initialise import init
from tqdm import tqdm

init()

headers = {
    "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 "
                  "YaBrowser/22.11.3.838 Yowser/2.5 Safari/537.36",
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,"
              "application/signed-exchange;v=b3;q=0.9"
}


def get_html(url: str) -> (str, bool):
    """
    Получаем html-код страницы.
    :param url: Ссылка на страницу.
    :return: HTML-код или False в случае неудачи.
    """
    try:
        rs = requests.get(url=url, headers=headers, timeout=5)
        return rs.text if rs.status_code == 200 else False
    except Exception:
        return False


def get_track(txt: str) -> list:
    """
    Получение данных о музыкальной композиции (треке).
    :param txt: HTML-код страницы.
    :return: Список с полученными данными.
    """
    temp = []
    soup = BeautifulSoup(txt, "lxml")
    all_muz = soup.find_all('div', class_="track__info")
    for muz in all_muz:
        title = muz.find('div', class_="track__title").text.strip()
        link = muz.find('div', class_="track__info-r").find('a').get('href')
        artist_song = soup.find('div', class_="track__desc").text.strip()
        duration = soup.find('div', class_="track__time").find('div', class_='track__fulltime').text.strip()
        time_track = f'{int(duration.split(":")[0]) * 60 + int(duration.split(":")[1])}'
        temp.append(f'#EXTINF:{time_track}, group-title="{artist_song}", {title}\n{link}\n')
        print(f'{artist_song} - {title}\n{link}\n')
    return temp


def get_links(path: Path, iter_num: int, iter_link: str) -> (list, bool):
    """
    Получение ссылок на mp3-файлы и запись в m3u-файл (плейлист).
    :param iter_link: Ссылка для перехода по страницам пагинации.
    :param iter_num: Количество страниц (треков) в категории.
    :param path: Путь к директории для сохранения.
    :return: Кортеж из ссылок на аудио-треки + количество
    полученных ссылок или False, если список пуст.
    """
    temp = []

    if iter_num == 0:
        if txt := get_html(f'{iter_link}0'):
            temp.extend(get_track(txt))
    else:
        for i in range(0, iter_num+1, 48):
            link = f'{iter_link}{i}'
            if txt := get_html(link):
                temp.extend(get_track(txt))

    if temp:
        path.mkdir(exist_ok=True)
        with open(path / f'{path.name}_web.m3u', mode='w', encoding='utf-8') as file:
            file.write("#EXTM3U\n")
            for item in temp:
                file.write(item)
        return [x.split("\n")[1].strip() for x in temp], len(temp)
    return False


def get_pagination_artist(url: str) -> tuple:
    """
    Получение пагинации и названия группы или имени артиста.
    :param url: Ссылка на страницу для получения пагинации.
    :return: Кортеж из ссылки на последнюю страницу пагинации и название артиста/группы.
    """
    if txt := get_html(url):
        soup = BeautifulSoup(txt, "lxml")
        artist = ""
        try:
            artist = soup.find('ul', class_="breadcrumb").find_all('li')[-1].text.strip()
            section = soup.find('section', class_="pagination").find('ul', class_="pagination__list").find_all('li')[-1].\
                find('a').get('href')
            return urljoin(url, section), artist
        except Exception:
            return False, artist
    return False, False


def track_download(url, path) -> bool:
    """
    Скачивание треков по полученным ранее ссылкам.
    :param url: Ссылка на трек.
    :param path: Путь для сохранения трека.
    :return: False, если не удалось загрузить трек.
    """
    filename = ""
    try:
        rs = requests.get(url=url, headers=headers, stream=True)
        if rs.status_code == 200:
            file_size = int(rs.headers.get("Content-Length", 0))
            filename = Path(url).name
            progress = tqdm(rs.iter_content(1024), f"{Fore.GREEN}Downloading: {Fore.RESET}"
                                                   f"{filename}", total=file_size, unit="B",
                            unit_scale=True, unit_divisor=1024)
            with open(path / filename, "wb") as f:
                for data in progress.iterable:
                    f.write(data)
                    progress.update(len(data))
        return False
    except KeyboardInterrupt:
        (path / filename).unlink()
        print(f"\n{Fore.GREEN}До свидания: {Fore.RESET}{getpass.getuser()}\n")
        sys.exit(0)
    except Exception:
        print(f"Не удалось загрузить: {url}")
        return False


def main():
    """
    Получение ссылки на страницу для загрузки.
    Запрос у пользователя дополнительных действий.
    Запуск функций для получения плейлистов и загрузки треков.
    """
    try:
        link = input("\nВведите ссылку на альбом исполнителя (ru.hitmotop.com): ")
        if not link.startswith("http"):
            print("\n[!] Введите ссылку")
            main()
            return
        pag_link, artist = get_pagination_artist(link)
        print("")
        if not pag_link and not artist:
            raise KeyboardInterrupt
        path = Path.cwd() / artist
        if (path / f'{path.name}_web.m3u').exists():
            print(f'{Fore.RED}Плейлист: "{path.name}_web.m3u" существует. И будет перезаписан.{Fore.RESET}')
            (path / f'{path.name}_web.m3u').unlink()
        if pag_link:
            iter_num = int(pag_link.split("/")[-1])
            iter_link = f'{"/".join(pag_link.split("/")[:-1])}/'
            links, count = get_links(path, iter_num, iter_link)
        else:
            links, count = get_links(path, 0, f'{link}/start/')

        if links:
            ch = input(f"\n{Fore.YELLOW}Плейлист сохранен. Получено ссылок: {Fore.RESET}{count}. "
                       f"{Fore.YELLOW}Загрузить треки? y/n: ")
            if ch.lower() in ['y', 'yes']:
                path.mkdir(exist_ok=True)
                for url in links:
                    if (path / Path(url).name).exists():
                        print(f"{Fore.YELLOW}Пропуск: {Fore.RESET}{Path(url).name} | {Fore.YELLOW}Существует")
                        continue
                    track_download(url, path)
            else:
                raise KeyboardInterrupt
        else:
            print(f"{Fore.RED}Не удалось получить ссылки")
            raise KeyboardInterrupt
        print(f"\n{Fore.GREEN}Все файлы загружены в папку: {Fore.RESET}{path}\n")

        mkpls = input(f"{Fore.YELLOW}Желаете создать плейлист из загруженных файлов? y/n: ")
        if mkpls.lower() in ['y', 'yes']:
            temp = []
            for file in os.listdir(path):
                if Path(file).suffix == ".mp3":
                    audio = mutagen.File(path / file)
                    temp.append(f'#EXTINF:{audio.info.length:.2f}, group-title="{audio.get("TPE1")}", '
                                f'{audio.get("TIT2")}\n{path / file}\n')
            if temp:
                with open(path / f'{path.name}_loc.m3u', mode='w', encoding='utf-8') as file:
                    file.write("#EXTM3U\n")
                    for item in temp:
                        file.write(item)
            else:
                print(f"{Fore.RED}Не удалось получить список файлов в папке")
                raise KeyboardInterrupt
            print(f"{Fore.GREEN}Плейлист создан и сохранен в папку: {Fore.RESET}{path}\n")
        else:
            raise KeyboardInterrupt
    except KeyboardInterrupt:
        print(f"\n{Fore.GREEN}До свидания: {Fore.RESET}{getpass.getuser()}\n")
        sys.exit(0)


if __name__ == "__main__":
    main()


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

Вложения

  • mutagen_parsing.zip
    3,1 КБ · Просмотры: 106
Последнее редактирование:

satfan

Green Team
26.06.2022
77
1
BIT
52
Спасибо.
Испытал скрипт - просто супер.
Можно ли его настроить для работы с сайтами, где лежит музыка высокого качества (.flac)
Например:
 
Последнее редактирование:
Мы в соцсетях:

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