mandrock-files / music

audio-downloader

Одна функція в шелі, яка тягне аудіо з SoundCloud, YouTube і ще тисячі сайтів у ~/Music/unsorted_downloads. Встановлення під Windows/macOS/Linux, вибір формату, cookies і чому Spotify тут не буде.

Завантажувач музики на yt-dlp

Окремий «SoundCloud downloader» шукати не треба: yt-dlp вміє і SoundCloud, і YouTube, і ще близько тисячі сайтів. Нижче — обгортка на одну команду, яка складає все в ~/Music/unsorted_downloads, і чесна межа: де воно працює, а де DRM.


Рамка рішення

Що на вході Дія
Лінк на трек, сет, плейлист, профіль gt <url> — обгортка сама розгортає список
Потрібен оригінальний файл із SoundCloud -Original / -o — бере формат download, якщо аплоадер його відкрив
Стрім із платного тарифу, приватний або віковий лінк -Cookies / -c — сесія береться з браузера
Архів, а не плеєр у машині -Format best / -f best — без перекодування
Spotify, Apple Music, Tidal, Deezer ніяк — DRM; лишається експорт списку і пошук треків деінде
Сумнів gt <url> без прапорців: mp3 320, теги й обкладинка на місці

Встановлення і функція

Потрібні два бінарники: yt-dlp і ffmpeg. ffmpeg тут не опційний — через нього йде і витяг аудіо, і теги, і вшивання обкладинки. Повний перелік способів — у офіційному гайді зі встановлення.

1. Бінарники

winget install --id yt-dlp.yt-dlp

winget тягне Gyan.FFmpeg як залежність і сам прописує PATH — окремо ставити ffmpeg не треба. Термінал після цього перезапустити. Альтернатива, якщо в системі вже є Scoop:

scoop install yt-dlp ffmpeg

Перевірка: yt-dlp --version і ffmpeg -version.

2. Функція

function Get-Track {
    [CmdletBinding()]
    [Alias('gt')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromRemainingArguments)]
        [string[]]$Url,

        [string]$Dest = (Join-Path $HOME 'Music\unsorted_downloads'),

        [ValidateSet('mp3', 'opus', 'flac', 'best')]
        [string]$Format = 'mp3',

        [switch]$Original,
        [switch]$Cookies
    )

    begin {
        foreach ($bin in 'yt-dlp', 'ffmpeg') {
            if (-not (Get-Command $bin -ErrorAction SilentlyContinue)) {
                throw "$bin not found in PATH"
            }
        }
        if (-not (Test-Path -LiteralPath $Dest)) {
            New-Item -ItemType Directory -Path $Dest -Force | Out-Null
        }

        $ytArgs = @(
            '-f', $(if ($Original) { 'download/bestaudio/best' } else { 'bestaudio/best' })
            '--extract-audio'
            '--audio-quality', '0'
            '--embed-thumbnail'
            '--embed-metadata'
            '--no-overwrites'
            '--ignore-errors'
            '--download-archive', (Join-Path $Dest '.archive.txt')
            '-P', $Dest
            '-o', '%(uploader)s - %(title)s.%(ext)s'
        )
        if ($Format -ne 'best') { $ytArgs += @('--audio-format', $Format) }
        if ($Cookies)           { $ytArgs += @('--cookies-from-browser', 'edge') }

        $before = @(Get-ChildItem -LiteralPath $Dest -File -ErrorAction SilentlyContinue).Name
    }

    process {
        foreach ($u in $Url) {
            Write-Host "-> $u" -ForegroundColor Cyan
            & yt-dlp @ytArgs -- $u
        }
    }

    end {
        Get-ChildItem -LiteralPath $Dest -File |
            Where-Object { $_.Name -notin $before -and $_.Extension -ne '.txt' } |
            Select-Object Name, @{n = 'MB'; e = { [math]::Round($_.Length / 1MB, 1) } }
    }
}

3. Прописати в профіль

if (-not (Test-Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force }
notepad $PROFILE     # вставити функцію, зберегти
. $PROFILE           # перечитати без перезапуску

4. Виклик

gt https://soundcloud.com/artist/track      # один трек
gt https://soundcloud.com/artist/likes      # цілий розділ або плейлист
gt url1, url2, url3                         # пачкою
gt url -Original                            # оригінал, якщо аплоадер дозволив
gt url -Format best                         # без перекодування
gt url -Cookies                             # платні стріми, приватні лінки
gt url -Dest 'D:\library\Music\inbox'       # інша тека разово
Get-Content urls.txt | gt                   # зі списку у файлі

1. Бінарники

Через Homebrew:

brew install yt-dlp ffmpeg

Перевірка: yt-dlp --version і ffmpeg -version.

2. Функція

gt() {
  local dest="${MUSIC_DEST:-$HOME/Music/unsorted_downloads}"
  local format="mp3" fmt_sel="bestaudio/best"
  local urls=() cookies=()

  for bin in yt-dlp ffmpeg; do
    command -v "$bin" >/dev/null 2>&1 || { echo "$bin not found in PATH" >&2; return 1; }
  done

  while (( $# )); do
    case "$1" in
      -f|--format)   format="$2"; shift 2 ;;
      -o|--original) fmt_sel="download/bestaudio/best"; shift ;;
      -c|--cookies)  cookies=(--cookies-from-browser safari); shift ;;
      -d|--dest)     dest="$2"; shift 2 ;;
      -h|--help)     echo "gt [-f mp3|opus|flac|best] [-o] [-c] [-d DIR] URL..."; return 0 ;;
      -*)            echo "unknown flag: $1" >&2; return 1 ;;
      *)             urls+=("$1"); shift ;;
    esac
  done

  (( ${#urls[@]} )) || { echo "no URLs given" >&2; return 1; }
  mkdir -p "$dest"

  local args=(
    -f "$fmt_sel"
    --extract-audio
    --audio-quality 0
    --embed-thumbnail
    --embed-metadata
    --no-overwrites
    --ignore-errors
    --download-archive "$dest/.archive.txt"
    -P "$dest"
    -o "%(uploader)s - %(title)s.%(ext)s"
  )
  [[ "$format" != "best" ]] && args+=(--audio-format "$format")

  local u
  for u in "${urls[@]}"; do
    printf '\033[36m-> %s\033[0m\n' "$u"
    yt-dlp "${args[@]}" "${cookies[@]}" -- "$u"
  done
}

3. Прописати в профіль

Функція кладеться в окремий файл, профіль його підтягує:

echo 'source ~/.gt.sh' >> ~/.zshrc   # zsh — дефолт у macOS
source ~/.zshrc

4. Виклик

gt https://soundcloud.com/artist/track      # один трек
gt https://soundcloud.com/artist/likes      # цілий розділ або плейлист
gt url1 url2 url3                           # пачкою
gt -o url                                   # оригінал, якщо аплоадер дозволив
gt -f best url                              # без перекодування
gt -c url                                   # платні стріми, приватні лінки
gt -d ~/Desktop/inbox url                   # інша тека разово
xargs gt < urls.txt                         # зі списку у файлі

1. Бінарники

Пакет із репозиторію дистрибутива зазвичай відстає на кілька версій, а yt-dlp ламається щоразу, коли сайти міняють віддачу. Тому pipx:

sudo apt install pipx ffmpeg      # або dnf / pacman
pipx install yt-dlp

Оновлення — pipx upgrade yt-dlp. Альтернативи: python3 -m pip install -U yt-dlp або окремий бінарник зі сторінки релізів.

2. Функція

Та сама, що в macOS-вкладці, з однією заміною — браузер для cookies:

      -c|--cookies)  cookies=(--cookies-from-browser firefox); shift ;;

--cookies-from-browser приймає: brave, chrome, chromium, edge, firefox, opera, safari, vivaldi, whale. Chromium-браузери поза списком (Arc, Floorp) підключаються через шлях до профілю: --cookies-from-browser "chromium:/шлях/до/профілю".

3. Прописати в профіль

echo 'source ~/.gt.sh' >> ~/.bashrc
source ~/.bashrc

4. Виклик

Ідентичний macOS-варіанту.


Що реально віддає джерело

Джерело Що приходить Що ставити
SoundCloud, аплоадер увімкнув download оригінал (wav / flac / mp3 320) -Original
SoundCloud звичайний транскод mp3 128 або opus, HLS -Format best
SoundCloud із сесією Go+ AAC-пресет 256 kbps -Cookies
YouTube / YouTube Music opus ~160 kbps або m4a 128 -Format best

-Format mp3 на будь-якому з транскодів — це перекодування вже стисненого в ще раз стиснене. Виправдано, коли файл поїде в машину, на флешку чи в старий плеєр; для архіву — best.

Дефолт при сумніві: best для колекції, mp3 для того, що має грати будь-де.


YouTube: два прапорці зверху

Обгортка працює з YouTube безмін, але для музики варто дописати у власну копію:

--sponsorblock-remove music_offtopic
--parse-metadata "%(title)s:%(artist)s - %(title)s"

Перший зрізає нем'юзичні вступи й кінцівки за розміткою SponsorBlock. Другий розбирає звичне Виконавець - Назва із заголовка в окремі теги, замість того щоб лишати все одним рядком.


Spotify і решта DRM

Spotify не завантажується — і це не пробіл у yt-dlp. Потік зашифрований Widevine, тобто файлу, який можна витягти, там просто нема, а обхід DRM проєкт не реалізує принципово. Apple Music, Tidal і Deezer — та сама історія.

Робочий обхідний шлях полягає не в тому, щоб «зламати» Spotify, а в тому, щоб використати його як каталог: вивантажити список треків плейлиста (Web API або сторонній експортер дає виконавця, назву й альбом у CSV), а далі шукати ті самі треки там, де файл віддають — Bandcamp, Qobuz, офіційні завантаження артиста, YouTube-канал лейбла. Результат той самий, файли кращі, DRM ніде не заважає.


Антипатерни


Чекліст

P.S. — про папку unsorted

Тека називається unsorted_downloads із наміром колись розсортувати. Статистика по всіх подібних теках на планеті свідчить, що момент сортування настає рівно тоді, коли закінчується місце на диску.

Сортування, до речі, теж можна віддати yt-dlp: шаблон -o приймає %(uploader)s/%(title)s.%(ext)s і розкладе по теках виконавців одразу. Це переносить проблему на рівень вище, де вона зветься «сорок тек по одному треку».