трансфер с гитхаба
This commit is contained in:
+49
-7
@@ -1,7 +1,6 @@
|
|||||||
# ---> Python
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[codz]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
@@ -47,7 +46,7 @@ htmlcov/
|
|||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
*.cover
|
*.cover
|
||||||
*.py,cover
|
*.py.cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
cover/
|
cover/
|
||||||
@@ -107,17 +106,24 @@ ipython_config.py
|
|||||||
# commonly ignored for libraries.
|
# commonly ignored for libraries.
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
#poetry.lock
|
#poetry.lock
|
||||||
|
#poetry.toml
|
||||||
|
|
||||||
# pdm
|
# pdm
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
# 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.lock
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
#pdm.toml
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
||||||
.pdm.toml
|
|
||||||
.pdm-python
|
.pdm-python
|
||||||
.pdm-build/
|
.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
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
|
|
||||||
@@ -130,6 +136,7 @@ celerybeat.pid
|
|||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
.env
|
.env
|
||||||
|
.envrc
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
@@ -168,9 +175,44 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.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 stuff:
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
|
|
||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.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
|
||||||
@@ -2,17 +2,20 @@ MIT License
|
|||||||
|
|
||||||
Copyright (c) 2026 s13bby
|
Copyright (c) 2026 s13bby
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
in the Software without restriction, including without limitation the rights
|
||||||
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
following conditions:
|
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
|
The above copyright notice and this permission notice shall be included in all
|
||||||
portions of the Software.
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
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.
|
||||||
|
|||||||
@@ -1,3 +1,207 @@
|
|||||||
# music_loader
|
# 🎵 Music Loader
|
||||||
|
|
||||||
Загрузчик музыки из онлайн-источников с поддержкой интерактивного поиска, пакетной загрузки и импорта плейлистов.
|
Загрузчик музыки из онлайн-источников с поддержкой интерактивного поиска, пакетной загрузки и импорта плейлистов.
|
||||||
|
|
||||||
|
## 📋 Возможности
|
||||||
|
|
||||||
|
- 🔍 **Интерактивный поиск** — выбор трека из списка найденных результатов
|
||||||
|
- 📜 **Пакетная загрузка** — скачивание треков из плейлиста (файл `.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
|
||||||
|
|||||||
+117
@@ -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}' не найден ни в одном источнике.")
|
||||||
@@ -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
|
||||||
+100
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user