Все мы погрязли в мире музыкальных сервисов. Вечная реклама, подписки, запрет на загрузку музыки для прослушивания локально, слежка и остальная хрень которая в них присутствует... В этой статье я хочу вам рассказать про крутой self-hosted проект Navidrome, который позволяет загружать музыку на свой сервер и прослушивать ее с любых устройств. Также я дам вам бонус от себя, который улучшит ваш user experience. 1. Подготовка сервера Вам для этого понадобится сервер Ubuntu/Debilian. Требуется установить Docker при помощи этой команды sudo apt update && \ sudo apt install -y ca-certificates curl gnupg lsb-release && \ sudo mkdir -p /etc/apt/keyrings && \ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \ echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null && \ sudo apt update && \ sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin && \ sudo systemctl enable docker && \ sudo systemctl start docker Code sudo apt update && \ sudo apt install -y ca-certificates curl gnupg lsb-release && \ sudo mkdir -p /etc/apt/keyrings && \ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \ echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null && \ sudo apt update && \ sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin && \ sudo systemctl enable docker && \ sudo systemctl start docker 2. Установка Navidrome Для установки самого Navidrome можно использовать Docker Compose, который мы установили. Теперь можем создать отдельную аудиторию mkdir navidrome Code mkdir navidrome Потом зайти в файл docker-compose.yml при помощи nano nano docker-compose.yml Code nano docker-compose.yml В этот файл мы вставим наш docker-compose конфиг, при помощи которого Navidrome сразу запуститься и будет работать на сервере в отдельном контейнере # docker-compose.yml version: "3" services: navidrome: image: deluan/navidrome:latest container_name: navidrome ports: - "4533:4533" volumes: - ~/navidrome/music:/music - ~/navidrome/data:/data environment: - ND_LOGLEVEL=info - ND_SESSIONTIMEOUT=72h - ND_TRANSCODINGENABLED=false # чтобы не грузить процессор restart: unless-stopped Code # docker-compose.yml version: "3" services: navidrome: image: deluan/navidrome:latest container_name: navidrome ports: - "4533:4533" volumes: - ~/navidrome/music:/music - ~/navidrome/data:/data environment: - ND_LOGLEVEL=info - ND_SESSIONTIMEOUT=72h - ND_TRANSCODINGENABLED=false # чтобы не грузить процессор restart: unless-stopped Дальше для запуска нашего контейнера надо написать команду docker compose up -d Code docker compose up -d Флаг -d говорит о том, что код должен работать в фоне Теперь для продолжения работы вам надо перейти по адресу http://<IP ВАШЕГО СЕРВЕРА>:4533/ На сайте вам предложат ввести login и password для доступа к сервису. 3. Мой подарок Так как, скорее всего вы не захотите закидывать ваши песни на сервер и настраивать там метаданные файла для того, чтобы это все красиво отображалось, я сделал скрипт который работает в формате Telegram бота для загрузки песен из SoundCloud и Yandex.Music import os import shutil import re import requests import asyncio import subprocess from aiogram import Bot, Dispatcher, types from aiogram.types import FSInputFile from yt_dlp import YoutubeDL from mutagen.easyid3 import EasyID3 from mutagen.id3 import ID3, APIC from shutil import copyfile TOKEN = "Сюда надо вставить ваш Telegram Bot Token" NAVIDROME_MUSIC_PATH = os.path.expanduser("~/navidrome/music") YANDEX_TOKEN = "Сюда надо вставить токен Yandex Music" bot = Bot(token=TOKEN) dp = Dispatcher() os.makedirs("downloads", exist_ok=True) def clean_downloads(): for item in os.listdir("downloads"): full_path = os.path.join("downloads", item) if os.path.isfile(full_path): os.remove(full_path) elif os.path.isdir(full_path): shutil.rmtree(full_path) def safe_filename(name: str) -> str: return re.sub(r'[\\/*?:"<>|]', "", name).strip() def resolve_redirect(url: str) -> str: response = requests.head(url, allow_redirects=True) return response.url def download_soundcloud(url: str): resolved_url = resolve_redirect(url) ydl_opts = { 'format': 'bestaudio/best', 'outtmpl': 'downloads/%(uploader)s - %(title)s.%(ext)s', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', }], 'writethumbnail': True, 'prefer_ffmpeg': True, 'quiet': True, 'noplaylist': False, } with YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(resolved_url, download=True) entries = info.get('entries', [info]) results = [] for entry in entries: title = entry.get('title') or 'Unknown Title' artist = entry.get('uploader') or 'Unknown Artist' album = entry.get('album') or 'SoundCloud' final_name = safe_filename(f"{artist} - {title}") mp3_path = f"downloads/{final_name}.mp3" cover_path = None for ext in ['jpg', 'png', 'webp']: test = f"downloads/{final_name}.{ext}" if os.path.exists(test): cover_path = test break results.append({ 'mp3': mp3_path, 'cover': cover_path, 'artist': artist, 'title': title, 'album': album }) return results def download_yandex_music(url: str): os.makedirs("downloads/yandex", exist_ok=True) subprocess.run([ "yandex-music-downloader", "-u", url, "--dir", "downloads/yandex", "--token", YANDEX_TOKEN ], check=True) tracks = [] for root, dirs, files in os.walk("downloads/yandex"): cover_path = None for file in files: if file.lower().startswith("cover") and file.lower().endswith(('.png', '.jpg', '.jpeg')): cover_path = os.path.join(root, file) for file in files: if file.endswith((".mp3", ".m4a")): full_path = os.path.join(root, file) parts = full_path.split(os.sep) try: artist = parts[2] album = parts[3] except IndexError: artist = "Unknown Artist" album = "Yandex.Music" filename = os.path.splitext(os.path.basename(file))[0] title = re.sub(r'^\d+\s*[-–—]\s*', '', filename).strip() tracks.append({ 'mp3': full_path, 'cover': cover_path, 'artist': artist, 'title': title, 'album': album }) return tracks def tag_and_convert_audio(file_path: str, artist: str, title: str, album: str, cover_path: str = None): if file_path.endswith(".m4a"): mp3_path = file_path.replace(".m4a", ".mp3") subprocess.run([ "ffmpeg", "-y", "-i", file_path, "-vn", "-ar", "44100", "-ac", "2", "-b:a", "192k", mp3_path ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) os.remove(file_path) file_path = mp3_path audio = EasyID3(file_path) audio['artist'] = artist audio['title'] = title audio['album'] = album audio.save() if cover_path: audio = ID3(file_path) with open(cover_path, 'rb') as img: audio['APIC'] = APIC( encoding=3, mime='image/jpeg', type=3, desc='Cover', data=img.read() ) audio.save() return file_path @dp.message() async def handle_message(message: types.Message): if 'soundcloud.com' in message.text: await message.reply(" Загружаю с SoundCloud...") try: tracks = download_soundcloud(message.text) for track in tracks: final_path = tag_and_convert_audio(track['mp3'], track['artist'], track['title'], track['album'], track['cover']) dest = os.path.join(NAVIDROME_MUSIC_PATH, os.path.basename(final_path)) copyfile(final_path, dest) await bot.send_audio( chat_id=message.chat.id, audio=FSInputFile(final_path), title=track['title'], performer=track['artist'], thumbnail=FSInputFile(track['cover']) if track['cover'] else None ) except Exception as e: await message.reply(f" Ошибка: {e}") finally: clean_downloads() elif 'music.yandex.ru' in message.text: await message.reply(" Загружаю с Яндекс Музыки...") try: tracks = download_yandex_music(message.text) for track in tracks: final_path = tag_and_convert_audio(track['mp3'], track['artist'], track['title'], track['album'], track['cover']) dest = os.path.join(NAVIDROME_MUSIC_PATH, os.path.basename(final_path)) copyfile(final_path, dest) await bot.send_audio( chat_id=message.chat.id, audio=FSInputFile(final_path), title=track['title'], performer=track['artist'] ) except Exception as e: await message.reply(f" Ошибка: {e}") finally: clean_downloads() else: await message.reply("Отправь ссылку на SoundCloud или Яндекс Музыку") asyncio.run(dp.start_polling(bot)) Python import os import shutil import re import requests import asyncio import subprocess from aiogram import Bot, Dispatcher, types from aiogram.types import FSInputFile from yt_dlp import YoutubeDL from mutagen.easyid3 import EasyID3 from mutagen.id3 import ID3, APIC from shutil import copyfile TOKEN = "Сюда надо вставить ваш Telegram Bot Token" NAVIDROME_MUSIC_PATH = os.path.expanduser("~/navidrome/music") YANDEX_TOKEN = "Сюда надо вставить токен Yandex Music" bot = Bot(token=TOKEN) dp = Dispatcher() os.makedirs("downloads", exist_ok=True) def clean_downloads(): for item in os.listdir("downloads"): full_path = os.path.join("downloads", item) if os.path.isfile(full_path): os.remove(full_path) elif os.path.isdir(full_path): shutil.rmtree(full_path) def safe_filename(name: str) -> str: return re.sub(r'[\\/*?:"<>|]', "", name).strip() def resolve_redirect(url: str) -> str: response = requests.head(url, allow_redirects=True) return response.url def download_soundcloud(url: str): resolved_url = resolve_redirect(url) ydl_opts = { 'format': 'bestaudio/best', 'outtmpl': 'downloads/%(uploader)s - %(title)s.%(ext)s', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', }], 'writethumbnail': True, 'prefer_ffmpeg': True, 'quiet': True, 'noplaylist': False, } with YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(resolved_url, download=True) entries = info.get('entries', [info]) results = [] for entry in entries: title = entry.get('title') or 'Unknown Title' artist = entry.get('uploader') or 'Unknown Artist' album = entry.get('album') or 'SoundCloud' final_name = safe_filename(f"{artist} - {title}") mp3_path = f"downloads/{final_name}.mp3" cover_path = None for ext in ['jpg', 'png', 'webp']: test = f"downloads/{final_name}.{ext}" if os.path.exists(test): cover_path = test break results.append({ 'mp3': mp3_path, 'cover': cover_path, 'artist': artist, 'title': title, 'album': album }) return results def download_yandex_music(url: str): os.makedirs("downloads/yandex", exist_ok=True) subprocess.run([ "yandex-music-downloader", "-u", url, "--dir", "downloads/yandex", "--token", YANDEX_TOKEN ], check=True) tracks = [] for root, dirs, files in os.walk("downloads/yandex"): cover_path = None for file in files: if file.lower().startswith("cover") and file.lower().endswith(('.png', '.jpg', '.jpeg')): cover_path = os.path.join(root, file) for file in files: if file.endswith((".mp3", ".m4a")): full_path = os.path.join(root, file) parts = full_path.split(os.sep) try: artist = parts[2] album = parts[3] except IndexError: artist = "Unknown Artist" album = "Yandex.Music" filename = os.path.splitext(os.path.basename(file))[0] title = re.sub(r'^\d+\s*[-–—]\s*', '', filename).strip() tracks.append({ 'mp3': full_path, 'cover': cover_path, 'artist': artist, 'title': title, 'album': album }) return tracks def tag_and_convert_audio(file_path: str, artist: str, title: str, album: str, cover_path: str = None): if file_path.endswith(".m4a"): mp3_path = file_path.replace(".m4a", ".mp3") subprocess.run([ "ffmpeg", "-y", "-i", file_path, "-vn", "-ar", "44100", "-ac", "2", "-b:a", "192k", mp3_path ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) os.remove(file_path) file_path = mp3_path audio = EasyID3(file_path) audio['artist'] = artist audio['title'] = title audio['album'] = album audio.save() if cover_path: audio = ID3(file_path) with open(cover_path, 'rb') as img: audio['APIC'] = APIC( encoding=3, mime='image/jpeg', type=3, desc='Cover', data=img.read() ) audio.save() return file_path @dp.message() async def handle_message(message: types.Message): if 'soundcloud.com' in message.text: await message.reply(" Загружаю с SoundCloud...") try: tracks = download_soundcloud(message.text) for track in tracks: final_path = tag_and_convert_audio(track['mp3'], track['artist'], track['title'], track['album'], track['cover']) dest = os.path.join(NAVIDROME_MUSIC_PATH, os.path.basename(final_path)) copyfile(final_path, dest) await bot.send_audio( chat_id=message.chat.id, audio=FSInputFile(final_path), title=track['title'], performer=track['artist'], thumbnail=FSInputFile(track['cover']) if track['cover'] else None ) except Exception as e: await message.reply(f" Ошибка: {e}") finally: clean_downloads() elif 'music.yandex.ru' in message.text: await message.reply(" Загружаю с Яндекс Музыки...") try: tracks = download_yandex_music(message.text) for track in tracks: final_path = tag_and_convert_audio(track['mp3'], track['artist'], track['title'], track['album'], track['cover']) dest = os.path.join(NAVIDROME_MUSIC_PATH, os.path.basename(final_path)) copyfile(final_path, dest) await bot.send_audio( chat_id=message.chat.id, audio=FSInputFile(final_path), title=track['title'], performer=track['artist'] ) except Exception as e: await message.reply(f" Ошибка: {e}") finally: clean_downloads() else: await message.reply("Отправь ссылку на SoundCloud или Яндекс Музыку") asyncio.run(dp.start_polling(bot)) После заполнения всех данных вам нужно установить нужные пакеты pip install aiogram requests yt-dlp mutagen sudo apt install ffmpeg pip install -U https://github.com/llistochek/yandex-music-downloader/archive/main.zip Code pip install aiogram requests yt-dlp mutagen sudo apt install ffmpeg pip install -U https://github.com/llistochek/yandex-music-downloader/archive/main.zip Для получения токена воспользуйтесь этой документацией Чтобы ваш скрипт всегда работал, нужно сделать systemd сервис, который будет запускаться вместе с запуском системы и работать бесперебойно sudo nano /etc/systemd/system/navidrome-bot.service Code sudo nano /etc/systemd/system/navidrome-bot.service Туда вставьте [Unit] Description=Navidrome Music Downloader Bot After=network.target [Service] ExecStart=/usr/bin/python3 /root/scripts/navidrome_music_downloader/bot.py WorkingDirectory=/root/scripts/navidrome_music_downloader Restart=always User=root Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target Code [Unit] Description=Navidrome Music Downloader Bot After=network.target [Service] ExecStart=/usr/bin/python3 /root/scripts/navidrome_music_downloader/bot.py WorkingDirectory=/root/scripts/navidrome_music_downloader Restart=always User=root Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target Дальше надо перезагрузить все daemon sudo systemctl daemon-reexec sudo systemctl daemon-reload Code sudo systemctl daemon-reexec sudo systemctl daemon-reload И запустить наш сервис [CODE=code]sudo systemctl start navidrome-bot sudo systemctl enable navidrome-bot[/CODE]Вуа-ля! Ваш личный музыкальный сервис готов! 4. Клиенты для разных устройств Android: Symfonium iOS substreamer iSub Вот все клиенты, которые я сам лично прочекал. На компе слушаю прям с браузера =================================== Спасибо за прочтение статьи! Надеюсь данный материал будет полезен именно guest
бля парни покупайте себе вместо блядского яблока элитных зеленых роботов и пиратьте всё что душе угодно
круто конечно, но к сожалению там не будет рекомендаций да и гораздо проще настроить AIMP с облачным хранилищем (если self-hosted то nextcloud) The post was merged to previous Jun 27, 2025 а посмотри про funkwhale, как тебе оно?
MALWARE, да кстати funkwhale крутая темка, но бля. мне моя тема больше нравится тк один докер и все готово
Было бы заебись если бы еще оттуда можно было треки в дискорд ботов грузить. Просто ссылкой в очередь в условного Rhytm