Skip to content

Instantly share code, notes, and snippets.

@HeroBrine1st
Last active July 18, 2022 11:19
Show Gist options
  • Save HeroBrine1st/e3bff1fa5a3a9d6029550d6d45370150 to your computer and use it in GitHub Desktop.
Save HeroBrine1st/e3bff1fa5a3a9d6029550d6d45370150 to your computer and use it in GitHub Desktop.
Скачиваем музыку со спотифая без выноса мозга

P.s. если с линуксом "на вы", то лучше найти какой-нибудь другой способ

В чём заключается проблема

  1. Если внутри freyr происходит ошибка (однажды выбило переполнение стека, в другой раз оно просто зависло) - работа до конца не завершится даже у уже загруженных файлов, а точнее не будут записаны метаданные
  2. Оно может не скачать трек по причине 403 или отсутствия источников, и в статистике не выведет, какие конкретно треки это были
  3. Голым беш скриптом автоматизация невозможна из-за того, что freyr пишет напрямую в tty 😕

Вам понадобится

  1. Линукс (wsl сойдёт, но не тестировался)
  2. Докер

Докер нужен для того, чтобы перенаправить вывод с freyr в переменную bash (других способов в голову не пришло)

Подготовка

Настройка докера

Создаём Dockerfile:

FROM freyrcli/freyrjs
# Не знаю нужно ли, но мне лень тестировать без рута
USER root
ENTRYPOINT ["sh"]

(стоит уточнить спустя несколько месяцев, что можно использоовать параметр --entrypoint, но мне лень переписывать и тестировать) В той же папке делаем docker build . -t herobrine1st/freyr Делаем docker run --rm -ti herobrine1st/freyr freyr test. Если он говорит, что не нашёл сервис - всё хорошо, интернет есть. Если интернета нет (он так и напишет), то команда sysctl -w net.ipv4.ip_forward=1 c последующим рестартом докера мне всегда помогали. Если не поможет - гугл

Получаем список треков

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

Идём в репозиторий с утилитой, сохраняем свои треки в текстовый формат. Затем делаем grep track "ваш файл" | awk '{print $NF}' | sed 's/\r//' | sort | uniq > tracks.txt В файле tracks.txt должно появиться много строк вида spotify:track:5nkzM2Pcoml4C8in9ozjwA

Настройка скрипта

#!/bin/bash
path="/home/herobrine1st/Music" # указать путь

container_id=$(docker container run -d -v $path:/data herobrine1st/freyr -c "while true; do sleep 1; done")
echo "Running in $container_id"
if [[ -f failed.txt ]]; then
    cp failed.txt "failed-$(date -Iseconds).txt"
fi
if [[ ! -f tracks.txt ]]; then
    mv failed.txt tracks.txt
fi # else resume
for line in $(<tracks.txt); do
    out=$(docker exec -t $container_id freyr $line)
    if [[ "${out,,}" =~ failed|error ]]; then
        echo $line failed
        echo $line >> failed.txt
    else
        echo $line success
    fi
done
mv tracks.txt "tracks-$(date -Iseconds).txt"
docker container stop $container_id
docker container rm $container_id

Копируем в download.sh, указываем путь, куда произойдёт скачивание.

Скачивание

Выполнить download.sh (bash download.sh). Скрипт будет вызывать freyr, чтобы вызвать музыку, а если не сможет - сохранит в файл failed.txt, который использует на следующем запуске. Треки, которые не удалось скачать, могут скачаться через впн или через некоторое время (нужно вызвать скрипт ещё раз). Если треки не скачиваются - значит freyr не может найти их на ютубе. Чтобы убедиться в том, что треки не могут скачаться по причине отсутствия источника, можно вызвать cat failed.txt | xargs freyr при установленном в PATH freyr.

Решение проблем

Номера треков в альбоме дублируются

Это происходит из-за того, что freyr не учитывает номер диска. Можно починить вручную, скрипт для поиска альбомов с такими проблемами:

for album in */*; do
    if [[ -f $album ]]; then
        continue
    fi
    track_numbers=()
    for file in "$album"/*.m4a; do
        file=$(basename "$file")
        track_numbers+=(${file:0:2})
    done
    seen=()
    for i in "${track_numbers[@]}"; do
        i=$((10#$i))
        if [ -z "${seen[i]}" ]; then
            seen[i]=1
        else
            echo $album ${track_numbers[*]}
            break
        fi
    done
done

Затем переименовать файлы и поправить номер диска и номер трека с помощью atomicparsley (если ваш софт использует метаданные)

Не все треки скачались

Ахтунг: здесь надо быть программистом, мне лень делать полную инструкцию

Пропущенные треки окажутся в файле failed.txt. Делаем запросы в апи спотифая, чтобы получить объекты треков, склеиваем все ответы в один json-файл (он будет похож на {"tracks": [{...}, {...}, ...]}) с названием failed.json и выполняем скрипт

import json

tracks = []


with open("failed.json") as f:
    tracks = json.load(f)["tracks"]

for track in tracks:
    album = track["album"]

    album_name = album["name"]
    album_art = sorted(
        album["images"],
        key=lambda x: x["width"],
        reverse=True)[0]["url"]
    tracks_in_album = album["total_tracks"]

    track_number = track["track_number"]
    disc_number = track["disc_number"]
    track_name = track["name"]
    explicit = track["explicit"]

    artists = [artist["name"] for artist in track["artists"]]

    link = track["external_urls"]["spotify"]

    tracks.append({
        "album": {
            "name": album_name,
            "cover": album_art,
            "total_tracks": tracks_in_album
        },
        "name": track_name,
        "track_number": track_number,
        "disc_number": disc_number,
        "artists": artists,
        "link": link
    })

print("Writing..")
with open("tracks.json", "w") as f:
    json.dump(tracks, f)

Затем копируем tracks.json в какую-нибудь пустую папку или делаем бекап уже скачанной музыки (на всякий пожарный; второе предпочтительнее), копируем add_metadata.sh из этого гиста в ту же папку. Не забудьте сделать chmod +x на файле.

Затем смотрим в файл tracks.json (лучше всего браузером, там видно индексы объектов), ищем на ютубе трек (можно в любом месте, лишь бы yt-dlp умел оттуда скачивать файлы), получаем ссылку и запускаем с аргументами: индекс из массива в tracks.json и ссылка, которую вы получили в кавычках или без символа & и всего, что после него. Пример: ./add_metadata.sh индекс "ссылка". Если и этот скрипт не сможет скачать, то он положит индекс и ссылку в yt-dlp-failed.txt

#!/bin/bash
index=$1
link=$2
info=$(cat tracks.json | jq ".[$index]")
# Album
album=$(echo $info | jq ".album")
cover_url=$(echo $album | jq ".cover" -r)
album_title=$(echo $album | jq ".name" -r)
total_tracks=$(echo $album | jq ".total_tracks")
# Song
song_title=$(echo $info | jq ".name" -r)
artists=$(echo $info | jq ".artists | join(\", \")" -r)
artist=$(echo $info | jq ".artists | .[0]" -r)
track_number=$(echo $info | jq ".track_number")
disc_number=$(echo $info | jq ".disc_number")
#File
name=download_$(openssl rand -hex 8)
while true; do
read -p "Are you sure your link points to $song_title - $artists ($album_title)? " yn
case $yn in
[Yy]* ) break;;
[Nn]* ) exit;;
* ) echo "Please answer yes or no.";;
esac
done
function normalize_name {
echo $1 | sed "s/\//_/"
}
song_path="$(normalize_name "$artist")/$(normalize_name "$album_title")/$(printf "%02d" $track_number) $(normalize_name $song_title).m4a"
echo "Downloading to $song_path"
if [[ -f "$song_path" ]]; then
echo "$song_path: file exists"
exit
fi
yt-dlp -f bestaudio $link --extract-audio --audio-format m4a -o "$name.%(ext)s"
if [[ $? != 0 ]]; then
rm "$name.*"
echo "$index $link" > yt-dlp-failed.txt
exit
fi
file=$name.m4a
echo "Downloading cover"
wget $cover_url -O $name.jpg
atomicparsley $file --artist "$artist" --title "$song_title" --album "$album_title" --tracknum "$track_number/$total_tracks" --disk $disc_number --artwork $name.jpg --comment "Artists: $artists" --overWrite
if [[ $? != 0 ]]; then
echo "Failed"
rm "$name*"
echo "$index $link" > yt-dlp-failed.txt
exit
fi
mkdir -p "$(normalize_name "$artist")/$(normalize_name "$album_title")"
mv "$file" "$song_path"
if [[ ! -f "$(normalize_name "$artist")/$(normalize_name "$album_title")/cover.png" ]]; then
ffmpeg -i $name.jpg "$(normalize_name "$artist")/$(normalize_name "$album_title")/cover.png"
fi
rm $name*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment