From a6514cc14f078fe5ac1d5ff3d5b2414f6f7da53f Mon Sep 17 00:00:00 2001 From: s13bby Date: Fri, 15 May 2026 04:03:13 +0600 Subject: [PATCH] =?UTF-8?q?=D1=82=D1=80=D0=B0=D0=BD=D1=81=D1=84=D0=B5?= =?UTF-8?q?=D1=80=20=D1=81=20=D0=B3=D0=B8=D1=82=D1=85=D0=B0=D0=B1=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 56 +++++++++-- LICENSE | 27 +++--- README.md | 208 +++++++++++++++++++++++++++++++++++++++- core/album.py | 117 ++++++++++++++++++++++ core/cover.py | 75 +++++++++++++++ core/hitmo.py | 100 +++++++++++++++++++ core/import_playlist.py | 71 ++++++++++++++ core/youtube.py | 53 ++++++++++ main.py | 82 ++++++++++++++++ 9 files changed, 768 insertions(+), 21 deletions(-) create mode 100644 core/album.py create mode 100644 core/cover.py create mode 100644 core/hitmo.py create mode 100644 core/import_playlist.py create mode 100644 core/youtube.py create mode 100644 main.py diff --git a/.gitignore b/.gitignore index 36b13f1..c8f06af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ -# ---> Python # Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] +*.py[codz] *$py.class # C extensions @@ -47,7 +46,7 @@ htmlcov/ nosetests.xml coverage.xml *.cover -*.py,cover +*.py.cover .hypothesis/ .pytest_cache/ cover/ @@ -107,17 +106,24 @@ ipython_config.py # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock +#poetry.toml # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control #pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml +#pdm.toml .pdm-python .pdm-build/ +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ @@ -130,6 +136,7 @@ celerybeat.pid # Environments .env +.envrc .venv env/ venv/ @@ -168,9 +175,44 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + # Ruff stuff: .ruff_cache/ # PyPI configuration file .pypirc +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +venv/ +*.txt +*.mp3 +downloads/ +temp/ +*.png +*.jpg +*.webp +test.py +*.json \ No newline at end of file diff --git a/LICENSE b/LICENSE index e65c97f..ae1a066 100644 --- a/LICENSE +++ b/LICENSE @@ -2,17 +2,20 @@ MIT License Copyright (c) 2026 s13bby -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index c1f320d..aecb8b6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,207 @@ -# music_loader +# 🎵 Music Loader -Загрузчик музыки из онлайн-источников с поддержкой интерактивного поиска, пакетной загрузки и импорта плейлистов. \ No newline at end of file +Загрузчик музыки из онлайн-источников с поддержкой интерактивного поиска, пакетной загрузки и импорта плейлистов. + +## 📋 Возможности + +- 🔍 **Интерактивный поиск** — выбор трека из списка найденных результатов +- 📜 **Пакетная загрузка** — скачивание треков из плейлиста (файл `.txt`) +- 🌐 **Несколько источников** — поддержка hitmo и youtube +- 🎨 **Обложки треков** — автоматическое добавление обложек из Яндекс Музыки и iTunes +- 📥 **Импорт плейлистов** — загрузка списков треков из плейлистов Яндекс Музыки и Spotify +- 📀 **Поиск альбомов** — поиск и экспорт альбомов в плейлист из iTunes и Яндекс Музыки +- ⏱️ **Автоматическая задержка** — пауза между загрузками для избежания блокировок +- 📁 **Гибкое хранение** — настройка папки для сохранения файлов + +## 🚀 Требования + +- Python 3.13+ +- Библиотеки: + ```bash + pip install requests beautifulsoup4 tqdm mutagen pillow yandex_music yt_dlp + ``` + +## 📖 Использование + +### Базовый синтаксис + +```bash +python main.py [опции] +``` + +### Опции + +| Опция | Описание | +|-------|----------| +| `-h`, `--help` | Показать справку | +| `-a`, `--album` | Импортировать альбом (itunes, yandex) | +| `-i`, `--import_playlist` | Импортировать плейлист (yandex, spotify) | +| `-p`, `--playlist` | Путь к файлу плейлиста (`.txt`) | +| `-s`, `--source` | Источник: `hitmo`, `youtube` (по умолчанию: `hitmo`) | + +### Примеры + +**Интерактивный режим** (поиск и выбор трека): +```bash +python main.py +python main.py -s youtube +``` + +**Пакетная загрузка** (из плейлиста): +```bash +python main.py -p playlist.txt +python main.py -s youtube -p playlist.txt +``` + +**Импорт плейлиста**: +```bash +python main.py -i yandex +python main.py -i spotify +``` + +**Поиск альбома**: +```bash +python main.py -a +``` + +## 🎵 Импорт плейлиста из Яндекс Музыки + +Для импорта плейлиста из Яндекс Музыки: + +1. Откройте нужный плейлист на [music.yandex.ru](https://music.yandex.ru) +2. Нажмите на **три точки** (⋮) в верхней части плейлиста +3. Выберите **«Поделиться»** → **«HTML-код»** +4. Скопируйте ссылку из открывшегося окна +5. Запустите: + ```bash + python main.py -i yandex + ``` +6. Вставьте скопированный HTML-код + +После этого в текущей папке появится файл `<название_плейлиста>.txt` с треками. + +## 🎵 Импорт плейлиста из Spotify + +Для импорта плейлиста из Spotify: + +1. Открой https://open.spotify.com/ в браузере +2. Нажми **F12** (откроются DevTools) +3. Перейди во вкладку **Network** +4. Останови запись (кнопка ⏹️) и очисти историю (кнопка 🚫) +5. **Перед открытием плейлиста** нажми запись (кнопка ⏺️) +6. Перейди на интересующий плейлист +7. Дождись полной загрузки и отключи запись +8. Найди элементы с названием **query** в начале списка (или прокликай первые 10–15 запросов) +9. Нажми на один из элементов **query** +10. Перейди во вкладку **Response** — если видишь длинный JSON, это нужный запрос +11. ПКМ по элементу → **Copy** → **Copy response** +12. Создай файл с расширением `.json` и сохрани в него скопированный ответ +13. Запусти: + ```bash + python main.py -i spotify + ``` +14. Укажи путь к созданному JSON-файлу + +После этого в текущей папке появится файл `<случайное_имя>.txt` с треками. + +## 📀 Поиск и импорт альбома + +Для поиска и импорта альбома: + +1. Запустите: + ```bash + python main.py -a + ``` +2. Введите артиста и название альбома в формате: `артист название` +3. Альбом будет найден в iTunes или Яндекс Музыке +4. В текущей папке появится файл `<название_альбома>.txt` с треками + +## 📝 Формат плейлиста + +Создайте текстовый файл (например, `playlist.txt`) в формате: +``` +артист название_трека +еще артист еще трек +booker джанк +``` + +Каждая строка — отдельный запрос на поиск и загрузку. + +## ⏱️ Задержка между загрузками + +При использовании плейлиста между скачиваниями треков автоматически добавляется задержка, указанная в `settings.json` (по умолчанию: **1 секунда**) для предотвращения временной блокировки со стороны сайта. + +## ⚙️ Настройки + +Создайте файл `settings.json` в корне проекта: + +```json +{ + "delay": 1, + "save to": "downloads/", + "cover": true, + "source": "hitmo", + "proxy": "http://ip:port" +} +``` + +| Параметр | Описание | +|----------|----------| +| `delay` | Задержка между запросами (сек) | +| `save to` | Папка для сохранения треков | +| `cover` | Автоматическая установка обложек | +| `source` | Источник по умолчанию (`hitmo` или `youtube`) | +| `proxy` | Прокси для YouTube (требуется для работы в РФ) | + +## 🌐 Источники + +| Источник | Описание | +|----------|----------| +| **hitmo** (ru, eu) | Основной источник | +| **youtube** | Вспомогательный источник (требуется прокси для работы в РФ) | + +## 🎨 Обложки и метаданные + +| Сервис | Обложки | Альбомы | Плейлисты | +|--------|---------|---------|-----------| +| Яндекс Музыка | ✅ | ✅ | ✅ | +| iTunes | ✅ | ✅ | ❌ | +| Spotify | ❌ | ❌ | ✅ | + +## 📂 Структура проекта + +``` +music_loader/ +├── main.py +├── core/ +│ ├── album.py # Поиск альбома, экспортирование в плейлист +│ ├── cover.py # Поиск, скачивание и установка обложек +│ ├── hitmo.py # Парсинг сайта, поиск и скачивание треков +│ ├── import_playlist.py # Импорт плейлистов из Яндекс и Spotify +│ └── youtube.py # Поиск и скачивание треков с YouTube +├── settings.json +├── LICENSE +├── README.md +└── .gitignore +``` + +--- + +> ## Дисклеймер | Disclaimer +> Данный проект предназначен исключительно для личного и образовательного использования. +> +> Программа использует сторонние источники данных для поиска треков, обложек и метаданных. В отдельных случаях возможны неточности в результатах поиска, особенно при работе с плейлистами. +> +> Пользователь несет полную ответственность за соблюдение авторских прав и условий использования контента при загрузке или использовании материалов, полученных с помощью данного проекта. +> +> Автор проекта не связан с сервисами, используемыми программой, и не несет ответственности за возможные ограничения, блокировки или иные действия со стороны сторонних сервисов. +> +> This project is intended for personal and educational use only. +> +> The software uses third-party sources to search for tracks, artwork, and metadata. In some cases, inaccuracies may occur in search results, especially when processing playlists. +> +> Users are solely responsible for complying with copyright laws and the terms of service of the platforms when downloading or using content obtained through this project. +> +> The project author is not affiliated with the services used by the software and is not responsible for any restrictions, bans, or other actions imposed by third-party platforms. +> +> **сделано:** s13bby diff --git a/core/album.py b/core/album.py new file mode 100644 index 0000000..08d6bcc --- /dev/null +++ b/core/album.py @@ -0,0 +1,117 @@ +import requests +import os +from yandex_music import Client + +def get_itunes_search_data(query): + url = "https://itunes.apple.com/search" + params = {"term": query, "entity": "album", "limit": 1} + try: + response = requests.get(url, params=params) + return response.json() + except Exception as e: + print(f"iTunes API Error: {e}") + return None + +def extract_itunes_album(data): + if not data or data.get("resultCount") == 0: + return None + return data["results"][0] + +def get_itunes_tracks(album_id): + params = {"id": album_id, "entity": "song"} + response = requests.get("https://itunes.apple.com/lookup", params=params) + results = response.json().get("results", []) + return results[1:] if len(results) > 1 else [] + +def format_itunes_track(item, artist_name): + ms = item.get('trackTimeMillis', 0) + duration = f"{ms // 60000}:{(ms // 1000) % 60:02d}" + return { + "no": item.get("trackNumber"), + "artist": artist_name, + "title": item.get("trackName"), + "duration": duration + } + + +def get_yandex_client(): + try: + return Client().init() + except: + return None + +def find_yandex_album(client, query): + search = client.search(query, type_='album') + if not search.albums or not search.albums.results: + return None + return search.albums.results[0] + +def format_yandex_track(track): + sec = track.duration_ms // 1000 + duration = f"{sec // 60}:{sec % 60:02d}" + artists = ", ".join([a.name for a in track.artists]) + return { + "no": getattr(track, 'number', 0), + "artist": artists, + "title": track.title, + "duration": duration + } + + +def process_itunes(query): + data = get_itunes_search_data(query) + album = extract_itunes_album(data) + if not album: + return None + + raw_tracks = get_itunes_tracks(album["collectionId"]) + artist = album.get("artistName", "Unknown") + + return { + "title": album["collectionName"], + "year": album["releaseDate"][:4], + "tracks": [format_itunes_track(t, artist) for t in raw_tracks] + } + +def process_yandex(query): + client = get_yandex_client() + if not client: return None + + album_info = find_yandex_album(client, query) + if not album_info: return None + + full_album = client.albums_with_tracks(album_info.id) + tracks = [] + for vol in full_album.volumes: + for t in vol: + tracks.append(format_yandex_track(t)) + + return { + "title": album_info.title, + "year": album_info.year or "Unknown", + "tracks": tracks + } + +def save_to_file(album_data): + filename = f"{album_data['title']}.txt" + with open(filename, "w", encoding="utf-8") as f: + for t in album_data['tracks']: + line = f"{t['no']}. {t['artist']} - {t['title']} ({t['duration']})" + print(line) + f.write(f"{t['artist']} {t['title']}\n") + print(f"\nГотово! Сохранено в {filename}") + + +def main(request): + print(f"Поиск '{request}' в iTunes...") + result = process_itunes(request) + + if not result: + print("В iTunes не найдено. Ищем в Яндекс.Музыке...") + result = process_yandex(request) + + if result: + print(f"Найдено: {result['title']} ({result['year']})") + save_to_file(result) + else: + print(f"Альбом '{request}' не найден ни в одном источнике.") \ No newline at end of file diff --git a/core/cover.py b/core/cover.py new file mode 100644 index 0000000..f40903e --- /dev/null +++ b/core/cover.py @@ -0,0 +1,75 @@ +import io +import requests +import os +import time +import json +from yandex_music import Client +from PIL import Image +from mutagen.mp3 import MP3 +from mutagen.id3 import ID3, APIC + +def get_yandex_cover(query, settings): + try: + client = Client().init() + time.sleep(settings["delay"]) + search_result = client.search(query, type_='track') + if search_result.tracks and search_result.tracks.results: + track_obj = search_result.tracks.results[0] + if track_obj.og_image: + return "https://" + track_obj.og_image.replace('%%', '1000x1000') + return None + except: + return None + +def get_itunes_cover(query, settings): + url = "https://itunes.apple.com/search" + params = {"term": query, "entity": "song", "limit": 1} + try: + time.sleep(settings["delay"]) + response = requests.get(url, params=params, timeout=5) + data = response.json() + if data.get("resultCount", 0) > 0: + return data["results"][0]["artworkUrl100"].replace("100x100bb.jpg", "1000x1000bb.jpg") + return None + except: + return None + +def get_best_cover(query, settings): + cover_url = get_itunes_cover(query, settings) + if not cover_url: + cover_url = get_yandex_cover(query, settings) + return cover_url + +def set_cover(track_path, request): + with open("settings.json", "r", encoding="utf-8") as file: + settings = json.load(file) + cover_url = get_best_cover(request, settings) + if not cover_url: + return + + try: + response = requests.get(cover_url, timeout=10) + img = Image.open(io.BytesIO(response.content)).convert("RGB") + + buffer = io.BytesIO() + img.save(buffer, format="JPEG", quality=90) + img_data = buffer.getvalue() + + audio = MP3(track_path, ID3=ID3) + if audio.tags is None: + audio.add_tags() + + audio.tags.add( + APIC( + encoding=3, + mime='image/jpeg', + type=3, + desc='Cover', + data=img_data + ) + ) + audio.save() + return True + except Exception as e: + print(f"Ошибка: {e}") + return False \ No newline at end of file diff --git a/core/hitmo.py b/core/hitmo.py new file mode 100644 index 0000000..450098e --- /dev/null +++ b/core/hitmo.py @@ -0,0 +1,100 @@ +import requests +import sys +import re +import os +import time +import json +from bs4 import BeautifulSoup +from tqdm import tqdm + +def ping(): + pages = ["https://rus.hitmotop.com/", "https://eu.hitmo-top.com/"] + for page in pages: + status = requests.get(page) + if status.status_code == 200: + return page + print("Ресурсы не доступны") + sys.exit() + +def parse(request, page, settings): + url = page + "search?q=" + request.replace(" ", "%20") + time.sleep(settings["delay"]) + content = BeautifulSoup(requests.get(url).text, "html.parser") + return content + +def get_artists(content): + return [artists.get_text(strip=True) for artists in content.find_all("div", class_="track__desc")] + +def get_titles(content): + return [titles.get_text(strip=True) for titles in content.find_all("div", class_="track__title")] + +def get_links(content): + return [links.get("href") for links in content.find_all("a", class_="track__download-btn")] + +def get_data_dict(content): + dictionary = { + i: [artist, title, link] + for i, (artist, title, link) in enumerate(zip(get_artists(content), get_titles(content), get_links(content))) + } + return dictionary + +def get_url(index, dictionary): + return dictionary[index][2] + +def handle(): + selected_index = int(input("Скачать трек: ")) + return selected_index + +def playlist(): + return 0 + +def get_filename(index, dictionary): + filename = f"{dictionary[index][0]} - {dictionary[index][1]}.mp3" + filename = re.sub(r'[*"?<>|:$&()\[\]!/]+', "", filename) + return filename + +def get_path_to_file(filename, settings): + os.makedirs(settings["save to"], exist_ok=True) + path_to_file = os.path.join(settings["save to"], filename) + return path_to_file + +def print_content(dictionary): + for i in range(len(dictionary)): + print(f"{i}. {dictionary[i][0]} - {dictionary[i][1]}") + return None + +def download(url, path_to_file, filename, settings): + time.sleep(settings["delay"]) + with requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"}) as r: + r.raise_for_status() + size = int(r.headers.get('content-length', 0)) + + with open(path_to_file, 'wb') as f, tqdm(total=size, unit='B', unit_scale=True) as pbar: + for chunk in r.iter_content(8192): + pbar.update(f.write(chunk)) + print(f"Скачано: {filename}") + +def main(request, mode): + try: + with open("settings.json", "r", encoding="utf-8") as file: + settings = json.load(file) + except Exception as e: + print(f"Ошибка открытия файла: {e}") + sys.exit() + page = ping() + content = parse(request, page, settings) + dictionary = get_data_dict(content) + if not dictionary: + print(f"По запросу {request} ничего не найдено.") + return + if mode == "handle": + print_content(dictionary) + index = handle() + else: + index = playlist() + url = get_url(index, dictionary) + filename = get_filename(index, dictionary) + path_to_file = get_path_to_file(filename, settings) + download(url, path_to_file, filename, settings) + + return path_to_file \ No newline at end of file diff --git a/core/import_playlist.py b/core/import_playlist.py new file mode 100644 index 0000000..7fa57fd --- /dev/null +++ b/core/import_playlist.py @@ -0,0 +1,71 @@ +import json +import sys +import re +import random +from yandex_music import Client + +def yandex(html_): + pattern = r"playlist/([\w\.-]+)/(\d+)" + + match = re.search(pattern, html_) + + if match: + owner = match.group(1) + kind = match.group(2) + else: + sys.exit() + + client = Client().init() + + try: + playlist = client.users_playlists(kind, owner) + + print(f"\nПлейлист: {playlist.title}") + print(f"Всего треков: {playlist.track_count}") + print("-" * 30) + + file = open(f"{playlist.title}.txt", "w", encoding="utf-8") + + for item in playlist.tracks: + track = item.track + artists = ", ".join([a.name for a in track.artists]) + title = track.title + file.write(f"{artists} {title}\n") + print(f"{artists} — {title}") + + print(f"\n{playlist.title}.txt плейлист сформирован") + file.close() + + sys.exit() + + except Exception: + print(f"Ошибка при чтении плейлиста: {Exception}") + sys.exit() + +def spotify(playlist): + try: + with open(playlist, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception as e: + print(f"Ошибка открытия файла: {e}") + sys.exit() + + temp_name = str(random.randint(0, 2**32 -1))[:5] + + items = data["data"]["playlistV2"]["content"]["items"] + + file = open(f"{temp_name}.txt", "w", encoding="utf-8") + + for item in items: + track = item["itemV2"]["data"] + + title = track["name"] + artist = track["artists"]["items"][0]["profile"]["name"] + file.write(f"{artist} {title}\n") + print(f"{artist} — {title}") + + + file.close() + print(f"\n{temp_name}.txt плейлист сформирован") + + sys.exit() \ No newline at end of file diff --git a/core/youtube.py b/core/youtube.py new file mode 100644 index 0000000..3e75880 --- /dev/null +++ b/core/youtube.py @@ -0,0 +1,53 @@ +import yt_dlp +import os +import json + +def download_track(input_): + with open ("settings.json", "r", encoding="utf-8") as data: + settings = json.load(data) + + query = f"ytsearch1:{input_}" + + if not os.path.exists(settings["save to"]): + os.makedirs(settings["save to"]) + + ydl_opts = { + "format": "bestaudio/best", + "postprocessors": [{ + "key": "FFmpegExtractAudio", + "preferredcodec": "mp3", + "preferredquality": "320", + }], + + "proxy": settings["proxy"], + + "extractor_args": { + "youtube": { + "player_client": ["android"], + "skip": ["webpage"], + } + }, + + "nocheckcertificate": True, + "outtmpl": settings["save to"] + "%(title)s.%(ext)s", + "quiet": False, + } + + try: + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + info_dict = ydl.extract_info(query, download=True) + + if 'entries' in info_dict: + video_info = info_dict['entries'][0] + else: + video_info = info_dict + + file_path = ydl.prepare_filename(video_info) + final_path = os.path.splitext(file_path)[0] + ".mp3" + + print(f"Скачано: {final_path}") + return final_path + + except Exception as e: + print(f"Ошибка при скачивании: {e}") + return False diff --git a/main.py b/main.py new file mode 100644 index 0000000..0154465 --- /dev/null +++ b/main.py @@ -0,0 +1,82 @@ +import argparse +import sys +import json +import time +from core import album, cover, hitmo, import_playlist, youtube + +try: + with open("settings.json", "r", encoding="utf-8") as data: + settings = json.load(data) +except Exception: + print("\nФайл с настройками settings.json не найден.\nВосстановите этот файл и перезапустите скрипт") + sys.exit() + +def arguments(): + parser = argparse.ArgumentParser(description="Загрузчик музыки из онлайн-источников с поддержкой интерактивного поиска, пакетной загрузки и импорта плейлистов.") + parser.add_argument("-a", "--album", action="store_true", help="Импортировать альбом") + parser.add_argument("-i", "--import_playlist", type=str, default="", help="Импортировать плейлист") + parser.add_argument("-p", "--playlist", type=str, default="", help="Скачать список из txt") + parser.add_argument("-s", "--source", type=str, default=f"{settings['source']}", help="Источник (hitmo/youtube)") + + args = parser.parse_args() + return args.album, settings["cover"], args.import_playlist, args.playlist, args.source + +def main(): + arg_album, cfg_cover, arg_import, arg_playlist, arg_source = arguments() + + if arg_album: + request = input("Введите артиста и название альбома (синтаксис: артист название)\n") + album.main(request) + sys.exit() + + elif arg_import == "yandex": + request = input("Введите HTML-код плейлиста\n") + import_playlist.yandex(request) + sys.exit() + elif arg_import == "spotify": + request = input("Введите путь до плейлиста в формате json\n") + import_playlist.spotify(request) + sys.exit() + + elif arg_playlist: + try: + with open(arg_playlist, "r", encoding="utf-8") as f: + lines = [l.strip() for l in f.readlines() if l.strip()] + except Exception as e: + print(f"Ошибка открытия файла: {e}") + sys.exit() + + for i, line in enumerate(lines, 1): + if arg_source == "hitmo": + result = hitmo.main(line, "playlist") + else: + result = youtube.download_track(line) + + if result: + cover.set_cover(result, line) + print(f"[{i}/{len(lines)}] Обработан: {line}") + else: + print(f"Трек {line} не найден, пропускаем") + + if i < len(lines): + time.sleep(settings["delay"]) + sys.exit() + + elif arg_source == "youtube": + request = input("Введите артиста и название трека\n") + result = youtube.download_track(request) + if result: + cover.set_cover(result, request) + else: + print(f"Трек {request} не найден") + sys.exit() + + else: + request = input("Введите артиста и название трека\n") + result = hitmo.main(request, "handle") + if result: + cover.set_cover(result, request) + sys.exit() + +if __name__ == "__main__": + main() \ No newline at end of file