diff --git a/.gitignore b/.gitignore index 30bc162..63b25ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -/node_modules \ No newline at end of file +/node_modules +.env +/uploads/media +/uploads/songs.json +/_bmad +/.claude +/_bmad-output \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 6417e93..c5cfb17 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "workbench.editor.decorations.colors": true + "workbench.editor.decorations.colors": true, + "liveServer.settings.port": 5501 } \ No newline at end of file diff --git a/README.md b/README.md index abdafca..de54b01 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ Плеер, чтобы слушать свою музыку -Готовность: ~10% +Готовность: ~16% + +На данный момент актуальная и рабочая версия. diff --git a/app old.js b/app old.js deleted file mode 100644 index f89ac16..0000000 --- a/app old.js +++ /dev/null @@ -1,167 +0,0 @@ -let songs = [] - -const getSongs = async () => { - try { - const response = await fetch("http://localhost:7999/songs.json") - songs = await response.json() - renderSongs(songs) - renderPlayer(songs) - } catch (error) { - console.error(error) - } -} - -const renderSongs = (songs) => { - console.log(songs); - - const songsWrapper = document.querySelector(".songsWrapper"); - songsWrapper.innerHTML = songs.map(song => { - return `
-
- Постер музыкальной композиции -
-
-

${song.name}

-

${song.artist}

-
-
- `; - }).join(""); -} - -const renderPlayer = () => { - const myAudio = new Audio(); - - let currentSongIndex = 0; - - myAudio.volume = 0.2; - - // Находим ползунок громкости - const volumeSlider = document.getElementById("volumeSlider"); - // Находим ползунок длительности - const progressSlider = document.getElementById("progressSlider"); - - const playButton = document.querySelector(".player__control__play"); - const prevButton = document.querySelector(".player__control__prev"); - const nextButton = document.querySelector(".player__control__next"); - const coverImage = document.querySelector(".player__cover__img"); - const titleText = document.querySelector(".player__info h3"); - const artistText = document.querySelector(".player__info h4"); - const durationText = document.querySelector(".player__control__duration__2"); - - // Проверка на загрузку песен - if (!songs || songs.length === 0) { - console.error("Список песен пуст или не загружен."); - return; - } - - // Обновление информации о треке - const updatePlayerInfo = (song) =>{ - if (!song) return; - console.log("Обновление информации о треке:", song); - coverImage.src = song.cover; - titleText.textContent = song.name; - artistText.textContent = song.artist; - console.log(titleText); - - myAudio.src = song.link; - } - - // Запуск текущей песни - const playSong = () => { - myAudio.play(); - playButton.textContent = "Пауза"; - } - - // Пауза текущей песни - const pauseSong = () => { - myAudio.pause(); - playButton.textContent = "Играть"; - } - - // Переключение на другую песню - const playNextSong = () => { - currentSongIndex = (currentSongIndex + 1) % songs.length; - updatePlayerInfo(songs[currentSongIndex]); - playSong(); - } - - // Переключение на новую песню - const playPrevSong = () => { - currentSongIndex = (currentSongIndex - 1 + songs.length) % songs.length; - updatePlayerInfo(songs[currentSongIndex]); - playSong(); - } - - // Обработчик событий для кнопки - playButton.addEventListener("click", () => { - if (myAudio.pause){ - playSong(); - } else { - playSong(); - } - }) - - - prevButton.addEventListener("click", playPrevSong); - nextButton.addEventListener("click", playNextSong); - - // Обновление прогресса песни - myAudio.addEventListener("timeupdate", () => { - const progress = (myAudio.currentTime / myAudio.duration) * 100; - progressSlider.value = progress; - - // Обновление отображения длительности - const minute = Math.floor(myAudio.currentTime / 60); - const second = Math.floor(myAudio.currentTime % 60).toString().padStart(2,"0"); - durationText.textContent = `${minute}:${second}` - }); - - - // Перемотка песни - progressSlider.addEventListener('input', (event) => { - const seekTime = (event.target.value / 100) * myAudio.duration; - myAudio.currentTime = seekTime; - }); - - // Загрузка первой песни - updatePlayerInfo(songs[currentSongIndex]); - - // Отладка: проверим, что кнопки рендерятся и клики работают - document.querySelectorAll('.js-music-btn').forEach(songElement => { - songElement.addEventListener('click', (event) => { - const audioUrl = event.currentTarget.getAttribute('data-url'); - console.log("Воспроизведение трека с URL:", audioUrl); - - if (myAudio.src !== audioUrl) { - myAudio.src = audioUrl; - } - myAudio.play().catch(error => console.error("Ошибка воспроизведения:", error)); - }); - }); - - // // Обрабатываем изменение ползунка - // volumeSlider.addEventListener("input", (event) => { - // myAudio.volume = event.target.value; - // console.log("Громкость:", myAudio.volume); // Отладка - // }); - - // // Обновление прогресса ползунка в зависимости от времени воспроизведения - // myAudio.addEventListener("timeupdate", () => { - // const progress = (myAudio.currentTime / myAudio.duration) * 100; - // progressSlider.value = progress; - // }) - - - - // Обновляем ползунок для загрузки новой песни - myAudio.addEventListener("loadedmetadata", () => { - progressSlider.max = 100; - }) -}; - -document.addEventListener('DOMContentLoaded', () => { - getSongs(); -}); - - diff --git a/app.js b/app.js deleted file mode 100644 index 7a68e94..0000000 --- a/app.js +++ /dev/null @@ -1,221 +0,0 @@ -let songs = []; -let currentSongIndex = 0; -let myAudio = new Audio(); -myAudio.volume = localStorage.getItem('volume'); - -const getSongs = async () => { - try { - const response = await fetch("http://localhost:7999/songs.json"); - songs = await response.json(); - console.log("Загруженные песни:", songs); // Проверка загрузки данных - - if (songs && songs.length > 0) { - renderSongs(songs); - updatePlayerInfo(songs[currentSongIndex]); - } else { - console.error("Файл песен пуст или не содержит данных."); - } - } catch (error) { - console.error("Ошибка загрузки песен:", error); - } -}; - -const renderSongs = (songs) => { - const songsWrapper = document.querySelector(".songsWrapper"); - if (!songsWrapper) { - console.error("Контейнер .songsWrapper не найден на странице."); - return; - } - - songsWrapper.innerHTML = songs.map((song, index) => ` -
-
Cover
-
-

${song.name}

${song.artist}

-
-
- `).join(""); - - // Обработчики кликов на каждой песне - document.querySelectorAll(".js-music-btn").forEach(button => { - button.addEventListener("click", (event) => { - currentSongIndex = parseInt(event.currentTarget.dataset.index); - updatePlayerInfo(songs[currentSongIndex]); - playSong(); - console.log("Песня выбрана:", songs[currentSongIndex]); // Проверка выбранной песни - }); - }); -}; - - - - -// Сохранение плеера при переходе между вкладками -// -// -// - -// Сохраняем состояние плеера -const savePlayerState = () => { - const state = { - currentSongIndex, - currentTime: myAudio.currentTime, - isPlaying: !myAudio.paused - }; - localStorage.setItem("playerState", JSON.stringify(state)); -}; - -// Загрузка состояния плеера -const loadPlayerState = () => { - const state = JSON.parse(localStorage.getItem("playerState")); - if (state) { - currentSongIndex = state.currentSongIndex; - updatePlayerInfo(songs[currentSongIndex]); // Обновляем данные плеера - myAudio.currentTime = state.currentTime; - if (state.isPlaying) playSong(); - } -}; - -window.addEventListener("beforeunload", savePlayerState); - -document.addEventListener("DOMContentLoaded", () => { - getSongs().then(() => loadPlayerState()); -}); - -myAudio.addEventListener("timeupdate", savePlayerState); -document.querySelector(".player__control__next").addEventListener("click", () => { - playNextSong(); - savePlayerState(); -}); -document.querySelector(".player__control__prev").addEventListener("click", () => { - playPrevSong(); - savePlayerState(); -}); - -// -// -// -// Конец этой части когда - - -// Функция для сортировки -const sortSongs = (criterion) => { - songs.sort((a, b) => { - if (criterion === "name") { - return a.name.localeCompare(b.name); - } else if (criterion === "artist") { - return a.artist.localeCompare(b.artist); - } else if (criterion === "id") { - return a.id.localeCompare(b.id) // Предполагается, что длительность в секундах или миллисекундах - } - }); - renderSongs(songs); // Перерисовываем список -}; - -// Обработчик изменения выбора сортировки -document.getElementById("sort").addEventListener("change", (event) => { - const selectedCriterion = event.target.value; - sortSongs(selectedCriterion); -}); - - -// Функция для фильтрации песен по запросу -const searchSongs = (query) => { - const filteredSongs = songs.filter(song => - song.name.toLowerCase().includes(query.toLowerCase()) || - song.artist.toLowerCase().includes(query.toLowerCase()) - ); - renderSongs(filteredSongs); // Перерисовываем список с отфильтрованными песнями -}; - -document.getElementById("searchInput").addEventListener("input", (event) => { - const query = event.target.value; - searchSongs(query); -}); - - -// Обновление информации у плеера -const updatePlayerInfo = (song) => { - - document.querySelector(".player__cover_img").src = song.cover; - document.querySelector(".player__info__title").textContent = song.name; - document.querySelector(".player__info__artist").textContent = song.artist; - - console.log('Максимальное время воспроизведения в секундах', myAudio.duration); - - myAudio.src = song.link; - console.log("Информация о текущем треке обновлена:", song); - console.log("Текущий аудиофайл:", song.audio); // Проверка пути к файлу -}; - -const playSong = () => { - myAudio.play(); - document.querySelector(".player__control__play").innerHTML = ``; -}; - -const pauseSong = () => { - myAudio.pause(); - document.querySelector(".player__control__play").innerHTML = ``; -}; - -const playNextSong = () => { - currentSongIndex = (currentSongIndex + 1) % songs.length; - updatePlayerInfo(songs[currentSongIndex]); - playSong(); -}; - -const playPrevSong = () => { - currentSongIndex = (currentSongIndex - 1 + songs.length) % songs.length; - updatePlayerInfo(songs[currentSongIndex]); - playSong(); -}; - -const autoNextSong = () => { - if (myAudio.currentTime == myAudio.duration) { - playNextSong(); - } -} - - -document.querySelector(".player__control__play").addEventListener("click", () => { - if (myAudio.paused) { - playSong(); - } else { - pauseSong(); - } -}); - -// Кнопки переключения песни -document.querySelector(".player__control__next").addEventListener("click", playNextSong); -document.querySelector(".player__control__prev").addEventListener("click", playPrevSong); - -// Ползунок регулеровки громкости -document.getElementById("volumeSlider").addEventListener("input", (event) => { - myAudio.volume = event.target.value; - localStorage.setItem('volume', myAudio.volume); -}); - - -// Цифровое отображение времени песни -myAudio.addEventListener("timeupdate", () => { - const progress = (myAudio.currentTime / myAudio.duration) * 100; - document.getElementById("progressSlider").value = progress; - console.log('Текущее время воспроизведения в секундах', myAudio.currentTime); - - const minutes = Math.floor(myAudio.currentTime / 60); - const seconds = Math.floor(myAudio.currentTime % 60).toString().padStart(2, "0"); - - const minute_2 = Math.floor(myAudio.duration / 60); - const seconds_2 = Math.floor(myAudio.duration % 60).toString().padStart(2,"0"); - - document.querySelector(".player__control__duration__2").textContent = `${minute_2}:${seconds_2}`; - document.querySelector(".player__control__duration__1").textContent = `${minutes}:${seconds}`; - autoNextSong(); -}); - -// Отображение прогресса песни в виде полосы -document.getElementById("progressSlider").addEventListener("input", (event) => { - myAudio.currentTime = (event.target.value / 100) * myAudio.duration; -}); - -document.addEventListener("DOMContentLoaded", getSongs); \ No newline at end of file diff --git a/catalog.html b/catalog.html deleted file mode 100644 index 1439914..0000000 --- a/catalog.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - Заголовок страницы - - - - - - - - -
-
-

- Personal Music -

- -
-
- - - -
-
- -
- - -
-
-
- -
- -
-
-
- Cover трека -
-
-

Название трека

-

Автор трека

-
-
-
-
- - - - -
-
-

Длительность трека

- -

Длительность трека

-
-
-
- -
-
- -
- - - - \ No newline at end of file diff --git a/client/css/style.css b/client/css/style.css new file mode 100644 index 0000000..08d4612 --- /dev/null +++ b/client/css/style.css @@ -0,0 +1,577 @@ +/* ──────────────────────────────────────────────── + VARIABLES +──────────────────────────────────────────────── */ +:root { + --bg: #07070F; + --accent: #C0392B; + --accent-hover: #E74C3C; + --accent-2: #A855F7; + --glass-bg: rgba(255, 255, 255, 0.06); + --glass-border: rgba(255, 255, 255, 0.10); + --glass-hover: rgba(255, 255, 255, 0.10); + --text-1: #F1F5F9; + --text-2: #94A3B8; + --header-h: 64px; + --player-h: 80px; + --radius: 14px; +} + +/* ──────────────────────────────────────────────── + RESET & BASE +──────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { height: 100%; } + +body { + min-height: 100%; + background: var(--bg); + background-image: + radial-gradient(ellipse 60% 40% at 80% 10%, rgba(192,57,43,0.12) 0%, transparent 60%), + radial-gradient(ellipse 50% 50% at 10% 90%, rgba(168,85,247,0.08) 0%, transparent 60%); + color: var(--text-1); + font-family: 'Segoe UI', system-ui, sans-serif; + font-size: 14px; + line-height: 1.5; + display: flex; + flex-direction: column; +} + +/* custom scrollbar */ +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.12); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.22); } + +/* ──────────────────────────────────────────────── + GLASS UTILITY +──────────────────────────────────────────────── */ +.glass { + background: var(--glass-bg); + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); + border: 1px solid var(--glass-border); +} + +/* ──────────────────────────────────────────────── + HEADER +──────────────────────────────────────────────── */ +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + height: var(--header-h); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 28px; + border-radius: 0; + border-top: none; + border-left: none; + border-right: none; +} + +.header__logo { + font-size: 1.1rem; + font-weight: 700; + letter-spacing: 0.04em; + color: var(--text-1); + text-transform: uppercase; +} + +/* ──────────────────────────────────────────────── + NAV +──────────────────────────────────────────────── */ +.nav { + display: flex; + gap: 4px; +} + +.nav-link { + color: var(--text-2); + text-decoration: none; + padding: 6px 16px; + border-radius: 20px; + font-size: 0.875rem; + font-weight: 500; + transition: background 0.2s, color 0.2s; +} + +.nav-link:hover { + background: var(--glass-hover); + color: var(--text-1); +} + +.nav-link.active { + background: rgba(192, 57, 43, 0.25); + color: var(--accent-hover); +} + +/* ──────────────────────────────────────────────── + MAIN CONTENT +──────────────────────────────────────────────── */ +#content { + margin-top: var(--header-h); + margin-bottom: var(--player-h); + flex: 1; + overflow-y: auto; + padding: 24px 0; + width: 100%; + max-width: 860px; + align-self: center; +} + +/* Градиентное затухание контента перед плеером */ +body::after { + content: ''; + position: fixed; + bottom: var(--player-h); + left: 0; + right: 0; + height: 72px; + background: linear-gradient(to top, var(--bg) 0%, transparent 100%); + pointer-events: none; + z-index: 50; +} + +/* ──────────────────────────────────────────────── + SEARCH BAR +──────────────────────────────────────────────── */ +.search-bar { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + border-radius: var(--radius); + margin-bottom: 16px; + color: var(--text-2); +} + +.search-bar input { + flex: 1; + background: none; + border: none; + outline: none; + color: var(--text-1); + font-size: 0.9rem; +} + +.search-bar input::placeholder { color: var(--text-2); } + +/* ──────────────────────────────────────────────── + TRACKS LIST +──────────────────────────────────────────────── */ +.tracks-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 6px; +} + +.track-item { + display: flex; + align-items: center; + gap: 14px; + padding: 10px 14px; + border-radius: var(--radius); + cursor: pointer; + border: 1px solid transparent; + transition: background 0.18s, border-color 0.18s, box-shadow 0.18s; + user-select: none; +} + +.track-item:hover { + background: rgba(192, 57, 43, 0.10); + border-color: rgba(192, 57, 43, 0.25); +} + +.track-item.active { + background: rgba(192, 57, 43, 0.18); + border-color: rgba(192, 57, 43, 0.45); + box-shadow: 0 0 16px rgba(192, 57, 43, 0.12); +} + +.track-item.active .track-title { color: var(--accent-hover); } + +.track-num { + width: 24px; + text-align: right; + color: var(--text-2); + font-size: 0.8rem; + flex-shrink: 0; +} + +.track-item.active .track-num { color: var(--accent); } + +.track-cover { + width: 48px; + height: 48px; + border-radius: 8px; + object-fit: cover; + flex-shrink: 0; + background: rgba(255,255,255,0.05); +} + +.track-info { + flex: 1; + min-width: 0; +} + +.track-title { + display: block; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text-1); +} + +.track-artist { + display: block; + font-size: 0.8rem; + color: var(--text-2); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.track-album { + color: var(--text-2); + opacity: 0.65; +} + +.track-duration { + font-size: 0.8rem; + color: var(--text-2); + flex-shrink: 0; + margin-left: auto; + padding-left: 12px; + font-variant-numeric: tabular-nums; +} + +/* ──────────────────────────────────────────────── + AUDIO PLAYER BAR +──────────────────────────────────────────────── */ +.player { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: var(--player-h); + display: grid; + grid-template-columns: 1fr 2fr 1fr; + align-items: center; + padding: 0 24px; + border-radius: 0; + border-bottom: none; + border-left: none; + border-right: none; + z-index: 100; +} + +/* left — cover + info */ +.player__left { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; +} + +.player__cover { + width: 52px; + height: 52px; + border-radius: 8px; + object-fit: cover; + background: rgba(255,255,255,0.05); + flex-shrink: 0; +} + +.player__info { min-width: 0; } + +.player__title { + font-size: 0.875rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text-1); +} + +.player__artist { + font-size: 0.78rem; + color: var(--text-2); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* center — controls + progress */ +.player__center { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} + +.player__controls { + display: flex; + align-items: center; + gap: 8px; +} + +.btn-icon { + background: none; + border: none; + color: var(--text-2); + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 0.9rem; + transition: color 0.15s, background 0.15s; +} + +.btn-icon:hover { color: var(--text-1); background: var(--glass-hover); } + +.btn-icon.active { color: var(--accent-hover); } + +.btn-play { + width: 40px; + height: 40px; + font-size: 1rem; + background: var(--accent); + color: #fff; + border-radius: 50%; +} + +.btn-play:hover { background: var(--accent-hover); color: #fff; } + +/* progress bar */ +.player__progress { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + max-width: 480px; +} + +.player__progress span { + font-size: 0.72rem; + color: var(--text-2); + width: 32px; + text-align: center; + flex-shrink: 0; +} + +/* right — volume */ +.player__right { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + color: var(--text-2); +} + +/* ──────────────────────────────────────────────── + RANGE SLIDERS +──────────────────────────────────────────────── */ +input[type=range] { + -webkit-appearance: none; + appearance: none; + height: 4px; + border-radius: 2px; + outline: none; + cursor: pointer; + background: linear-gradient( + to right, + var(--accent) 0%, + var(--accent) var(--value, 0%), + rgba(255,255,255,0.18) var(--value, 0%), + rgba(255,255,255,0.18) 100% + ); +} + +#progressSlider { flex: 1; } +#volumeSlider { width: 80px; } + +input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 0; + height: 12px; + background: #fff; + border-radius: 50%; + transition: width 0.15s; +} + +input[type=range]:hover::-webkit-slider-thumb, +input[type=range]:active::-webkit-slider-thumb { width: 12px; } + +/* ──────────────────────────────────────────────── + UPLOAD FORM +──────────────────────────────────────────────── */ +.upload-card { + border-radius: var(--radius); + padding: 32px; + max-width: 480px; + margin: 0 auto; +} + +.upload-card h2 { + font-size: 1.2rem; + font-weight: 600; + margin-bottom: 24px; + color: var(--text-1); +} + +.form-group { + display: flex; + flex-direction: column; + gap: 12px; +} + +.form-input { + background: rgba(255,255,255,0.06); + border: 1px solid var(--glass-border); + border-radius: 8px; + padding: 10px 14px; + color: var(--text-1); + font-size: 0.875rem; + outline: none; + transition: border-color 0.2s; + width: 100%; +} + +.form-input::placeholder { color: var(--text-2); } +.form-input:focus { border-color: rgba(192,57,43,0.5); } + +.file-label { + display: flex; + align-items: center; + gap: 10px; + background: rgba(255,255,255,0.06); + border: 1px dashed var(--glass-border); + border-radius: 8px; + padding: 12px 14px; + cursor: pointer; + color: var(--text-2); + transition: border-color 0.2s, background 0.2s; + font-size: 0.875rem; +} + +.file-label:hover { + border-color: rgba(192,57,43,0.4); + background: rgba(192,57,43,0.06); + color: var(--text-1); +} + +.file-label input[type=file] { display: none; } + +.btn-submit { + margin-top: 8px; + background: var(--accent); + border: none; + border-radius: 8px; + color: #fff; + padding: 10px 20px; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: background 0.2s; + width: 100%; +} + +.btn-submit:hover { background: var(--accent-hover); } +.btn-submit:disabled { opacity: 0.5; cursor: not-allowed; } + +.upload-message { + margin-top: 12px; + font-size: 0.85rem; + text-align: center; +} + +.upload-message.success { color: #4ade80; } +.upload-message.error { color: var(--accent-hover); } + +/* ──────────────────────────────────────────────── + PEERS PAGE +──────────────────────────────────────────────── */ +.peers-card { + border-radius: var(--radius); + padding: 28px; +} + +.peers-card h2 { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 20px; +} + +.peers-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 24px; +} + +.peer-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-radius: 10px; + border: 1px solid var(--glass-border); + background: rgba(255,255,255,0.04); +} + +.peer-info strong { display: block; font-size: 0.9rem; } +.peer-info span { font-size: 0.78rem; color: var(--text-2); } + +.btn-remove { + background: none; + border: 1px solid rgba(192,57,43,0.3); + color: var(--accent); + border-radius: 6px; + padding: 4px 10px; + cursor: pointer; + font-size: 0.78rem; + transition: background 0.2s; +} + +.btn-remove:hover { background: rgba(192,57,43,0.15); } + +.peers-empty { color: var(--text-2); font-size: 0.875rem; margin-bottom: 20px; } + +.peers-add h3 { font-size: 0.95rem; margin-bottom: 12px; color: var(--text-2); } +.peers-add .form-group { flex-direction: row; gap: 8px; } +.peers-add .form-input { flex: 1; } + +/* ──────────────────────────────────────────────── + EMPTY / LOADING STATES +──────────────────────────────────────────────── */ +.state-empty, .state-loading { + text-align: center; + color: var(--text-2); + padding: 48px 0; + font-size: 0.9rem; +} + +.state-loading::after { + content: ''; + display: inline-block; + width: 18px; + height: 18px; + border: 2px solid var(--glass-border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.7s linear infinite; + vertical-align: middle; + margin-left: 8px; +} + +@keyframes spin { to { transform: rotate(360deg); } } diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..8fa3721 --- /dev/null +++ b/client/index.html @@ -0,0 +1,65 @@ + + + + + + Personal Music + + + + + +
+ + +
+ +
+ +
+
+ +
+

Выберите трек

+

+
+
+ +
+
+ + + + + +
+
+ 0:00 + + 0:00 +
+
+ +
+ + +
+
+ + + + diff --git a/client/js/api.js b/client/js/api.js new file mode 100644 index 0000000..3f32d7b --- /dev/null +++ b/client/js/api.js @@ -0,0 +1,40 @@ +// Единственное место для всех запросов к серверу + +const json = async (url, opts = {}) => { + const res = await fetch(url, opts); + if (!res.ok) throw new Error(`API ${res.status}: ${url}`); + if (res.status === 204) return null; + return res.json(); +}; + +export const api = { + getTracks: () => json('/api/tracks'), + getLibrary: () => json('/api/library'), + getPeers: () => json('/api/peers'), + + addPeer: (url, name) => json('/api/peers', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url, name }), + }), + + removePeer: (url) => json('/api/peers', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }), + }), + + uploadTrack: (formData) => fetch('/api/upload', { + method: 'POST', + body: formData, + }).then(async res => { + if (!res.ok) { + const err = await res.json().catch(() => ({ error: 'Upload failed' })); + throw new Error(err.error || 'Upload failed'); + } + return res.json(); + }), + + // URL для стриминга конкретного трека + streamUrl: (id) => `/api/tracks/${id}/stream`, +}; diff --git a/client/js/main.js b/client/js/main.js new file mode 100644 index 0000000..fb49a9e --- /dev/null +++ b/client/js/main.js @@ -0,0 +1,18 @@ +import { initPlayer } from './player.js'; +import { navigate } from './navigation.js'; + +document.addEventListener('DOMContentLoaded', () => { + // Инициализируем плеер (DOM уже есть в index.html) + initPlayer(); + + // SPA-навигация + document.addEventListener('click', (e) => { + const link = e.target.closest('.nav-link'); + if (!link) return; + e.preventDefault(); + navigate(link.dataset.page); + }); + + // Начальная страница + navigate('tracks'); +}); diff --git a/client/js/navigation.js b/client/js/navigation.js new file mode 100644 index 0000000..2138a11 --- /dev/null +++ b/client/js/navigation.js @@ -0,0 +1,37 @@ +import { setState } from './state.js'; +import { loadTracks } from './tracks.js'; +import { initUploadPage } from './upload.js'; +import { initPeersPage } from './peers.js'; + +const content = document.getElementById('content'); + +const pages = { + tracks: { template: '/pages/tracks.html', init: loadTracks }, + 'add-songs': { template: '/pages/add-songs.html', init: initUploadPage }, + peers: { template: '/pages/peers.html', init: initPeersPage }, +}; + +export const navigate = async (page) => { + const cfg = pages[page]; + if (!cfg) return; + + setState({ page }); + + // Подсветить активную ссылку + document.querySelectorAll('.nav-link').forEach(a => { + a.classList.toggle('active', a.dataset.page === page); + }); + + // Загружаем шаблон + try { + const res = await fetch(cfg.template); + if (!res.ok) throw new Error(res.status); + content.innerHTML = await res.text(); + } catch { + content.innerHTML = '

Не удалось загрузить страницу

'; + return; + } + + // Инициализируем логику страницы + await cfg.init?.(); +}; diff --git a/client/js/peers.js b/client/js/peers.js new file mode 100644 index 0000000..9ad3ab5 --- /dev/null +++ b/client/js/peers.js @@ -0,0 +1,62 @@ +import { api } from './api.js'; + +export const initPeersPage = async () => { + await renderPeers(); + + const form = document.getElementById('addPeerForm'); + const msgEl = document.getElementById('peerMessage'); + const submitBtn = form?.querySelector('.btn-submit'); + + form?.addEventListener('submit', async (e) => { + e.preventDefault(); + const url = document.getElementById('peerUrl').value.trim(); + const name = document.getElementById('peerName').value.trim(); + + msgEl.textContent = ''; + submitBtn.disabled = true; + submitBtn.textContent = 'Подключение...'; + + try { + await api.addPeer(url, name); + msgEl.textContent = 'Узел добавлен'; + msgEl.className = 'upload-message success'; + form.reset(); + await renderPeers(); + } catch (err) { + msgEl.textContent = `Ошибка: ${err.message}`; + msgEl.className = 'upload-message error'; + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Добавить'; + } + }); +}; + +const renderPeers = async () => { + const list = document.getElementById('peersList'); + if (!list) return; + + const peers = await api.getPeers().catch(() => []); + + if (!peers.length) { + list.innerHTML = '

Нет подключённых узлов

'; + return; + } + + list.innerHTML = peers.map(p => ` +
  • +
    + ${p.name} + ${p.url} · ${p.trackCount ?? '?'} треков +
    + +
  • + `).join(''); + + list.querySelectorAll('.btn-remove').forEach(btn => { + btn.addEventListener('click', async () => { + await api.removePeer(btn.dataset.url).catch(() => {}); + await renderPeers(); + }); + }); +}; diff --git a/client/js/player.js b/client/js/player.js new file mode 100644 index 0000000..7f8d3ad --- /dev/null +++ b/client/js/player.js @@ -0,0 +1,168 @@ +import { state, setState, on } from './state.js'; +import { api } from './api.js'; +import { formatTime, syncSliderFill } from './utils.js'; + +const audio = new Audio(); + +// ── Приватные ссылки на DOM ────────────────────────────────────────────────── +const $ = (sel) => document.querySelector(sel); + +const dom = { + get cover() { return $('.player__cover'); }, + get title() { return $('.player__title'); }, + get artist() { return $('.player__artist'); }, + get playBtn() { return $('#play-pause'); }, + get progress() { return $('#progressSlider'); }, + get curTime() { return $('#currentTime'); }, + get totTime() { return $('#totalTime'); }, + get volume() { return $('#volumeSlider'); }, + get shuffle() { return $('#shuffle'); }, + get repeat() { return $('#repeat'); }, +}; + +// ── Вспомогательные функции ────────────────────────────────────────────────── + +const updatePlayerUI = (track) => { + if (!track) return; + dom.cover.src = track.cover; + dom.title.textContent = track.title; + dom.artist.textContent = track.artist; +}; + +const setPlayIcon = (playing) => { + dom.playBtn.innerHTML = playing + ? '' + : ''; +}; + +// ── Реакции на state ───────────────────────────────────────────────────────── + +on('currentTrack', (track) => { + if (!track) return; + audio.src = api.streamUrl(track.id); + updatePlayerUI(track); + // state.isPlaying уже применён через Object.assign до emit + if (state.isPlaying) { + audio.play().catch(() => {}); + } +}); + +on('isPlaying', (playing) => { + setPlayIcon(playing); + if (playing) { + audio.play().catch(() => {}); + } else { + audio.pause(); + } + persistState(); +}); + +on('volume', (vol) => { + audio.volume = vol; + localStorage.setItem('volume', String(vol)); + syncSliderFill(dom.volume); +}); + +// ── События audio-элемента ─────────────────────────────────────────────────── + +audio.addEventListener('timeupdate', () => { + const { currentTime, duration } = audio; + if (!duration) return; + const pct = (currentTime / duration) * 100; + dom.progress.value = pct; + syncSliderFill(dom.progress); + dom.curTime.textContent = formatTime(currentTime); + dom.totTime.textContent = formatTime(duration); +}); + +// Фикс: используем событие 'ended', а не сравнение float +audio.addEventListener('ended', () => { + if (state.isRepeat) { + audio.currentTime = 0; + audio.play().catch(() => {}); + return; + } + playNext(); +}); + +// ── Публичный API плеера ───────────────────────────────────────────────────── + +export const playSong = (track, queue, index) => { + setState({ queue: queue ?? state.queue, currentIndex: index ?? 0, currentTrack: track, isPlaying: true }); +}; + +export const togglePlayPause = () => { + if (!state.currentTrack) return; + setState({ isPlaying: !state.isPlaying }); +}; + +export const playNext = () => { + const { queue, currentIndex, isShuffle } = state; + if (!queue.length) return; + const next = isShuffle + ? Math.floor(Math.random() * queue.length) + : (currentIndex + 1) % queue.length; + setState({ currentIndex: next, currentTrack: queue[next], isPlaying: true }); +}; + +export const playPrev = () => { + const { queue, currentIndex } = state; + if (!queue.length) return; + // Если больше 3 секунд — перемотать в начало, иначе предыдущий трек + if (audio.currentTime > 3) { + audio.currentTime = 0; + return; + } + const prev = (currentIndex - 1 + queue.length) % queue.length; + setState({ currentIndex: prev, currentTrack: queue[prev], isPlaying: true }); +}; + +// ── Инициализация ──────────────────────────────────────────────────────────── + +const persistState = () => { + localStorage.setItem('playerState', JSON.stringify({ currentTrack: state.currentTrack })); +}; + +export const initPlayer = () => { + // Восстанавливаем громкость + const savedVol = parseFloat(localStorage.getItem('volume') ?? '0.5'); + audio.volume = savedVol; + dom.volume.value = savedVol; + syncSliderFill(dom.volume); + setState({ volume: savedVol }); + + // Восстанавливаем последний трек (без автовоспроизведения) + const saved = JSON.parse(localStorage.getItem('playerState') ?? 'null'); + if (saved?.currentTrack) { + updatePlayerUI(saved.currentTrack); + setState({ currentTrack: saved.currentTrack, isPlaying: false }); + } + + // Управление плеером + dom.playBtn.addEventListener('click', togglePlayPause); + document.getElementById('next').addEventListener('click', playNext); + document.getElementById('prev').addEventListener('click', playPrev); + + document.getElementById('shuffle').addEventListener('click', () => { + const s = !state.isShuffle; + setState({ isShuffle: s }); + dom.shuffle.classList.toggle('active', s); + }); + + document.getElementById('repeat').addEventListener('click', () => { + const r = !state.isRepeat; + setState({ isRepeat: r }); + dom.repeat.classList.toggle('active', r); + }); + + dom.progress.addEventListener('input', (e) => { + if (audio.duration) { + audio.currentTime = (e.target.value / 100) * audio.duration; + } + syncSliderFill(e.target); + }); + + dom.volume.addEventListener('input', (e) => { + setState({ volume: parseFloat(e.target.value) }); + }); +}; diff --git a/client/js/state.js b/client/js/state.js new file mode 100644 index 0000000..2910782 --- /dev/null +++ b/client/js/state.js @@ -0,0 +1,34 @@ +// Единственный источник правды для всего приложения + +const _listeners = new Map(); + +export const state = { + queue: [], + currentIndex: 0, + currentTrack: null, + isPlaying: false, + volume: 0.5, + isShuffle: false, + isRepeat: false, + page: 'tracks', +}; + +const emit = (key, value) => { + (_listeners.get(key) || []).forEach(fn => fn(value)); +}; + +export const setState = (patch) => { + Object.assign(state, patch); + Object.keys(patch).forEach(key => emit(key, state[key])); +}; + +export const on = (event, handler) => { + if (!_listeners.has(event)) _listeners.set(event, []); + _listeners.get(event).push(handler); +}; + +export const off = (event, handler) => { + const fns = _listeners.get(event); + if (!fns) return; + _listeners.set(event, fns.filter(f => f !== handler)); +}; diff --git a/client/js/tracks.js b/client/js/tracks.js new file mode 100644 index 0000000..0f524f7 --- /dev/null +++ b/client/js/tracks.js @@ -0,0 +1,81 @@ +import { api } from './api.js'; +import { state, setState, on, off } from './state.js'; +import { playSong } from './player.js'; +import { formatTime } from './utils.js'; + +let allTracks = []; +let displayed = []; + +// Подсветить активный трек в списке +const syncActive = () => { + document.querySelectorAll('.track-item').forEach((el, i) => { + el.classList.toggle('active', displayed[i]?.id === state.currentTrack?.id); + }); +}; + +export const loadTracks = async () => { + const wrapper = document.getElementById('songsWrapper'); + if (!wrapper) return; + + wrapper.innerHTML = '
  • Загрузка
  • '; + + allTracks = await api.getTracks(); + displayed = allTracks; + + renderTracks(displayed); + + // Если трека ещё нет — предзагрузить первый (без воспроизведения) + if (allTracks.length && !state.currentTrack) { + setState({ queue: allTracks, currentIndex: 0, currentTrack: allTracks[0] }); + } + + // Поиск + const search = document.getElementById('searchInput'); + if (search) { + search.addEventListener('input', (e) => filterTracks(e.target.value)); + } + + // Обновлять активный трек при смене (off перед on — чтобы не дублировать) + off('currentTrack', syncActive); + on('currentTrack', syncActive); +}; + +export const renderTracks = (tracks) => { + const wrapper = document.getElementById('songsWrapper'); + if (!wrapper) return; + + if (!tracks.length) { + wrapper.innerHTML = '
  • Ничего не найдено
  • '; + return; + } + + wrapper.innerHTML = tracks.map((t, i) => ` +
  • + ${i + 1} + ${t.title} +
    + ${t.title} + + ${t.artist}${t.album ? ` · ${t.album}` : ''} + +
    + ${t.duration ? `${formatTime(t.duration)}` : ''} +
  • + `).join(''); + + wrapper.querySelectorAll('.track-item').forEach((el, i) => { + el.addEventListener('click', () => playSong(tracks[i], tracks, i)); + }); + + syncActive(); +}; + +const filterTracks = (query) => { + const q = query.toLowerCase().trim(); + displayed = q + ? allTracks.filter(t => + t.title.toLowerCase().includes(q) || + t.artist.toLowerCase().includes(q)) + : allTracks; + renderTracks(displayed); +}; diff --git a/client/js/upload.js b/client/js/upload.js new file mode 100644 index 0000000..5433258 --- /dev/null +++ b/client/js/upload.js @@ -0,0 +1,44 @@ +import { api } from './api.js'; + +export const initUploadPage = () => { + const form = document.getElementById('uploadForm'); + const fileInput = document.getElementById('audio'); + const fileName = document.getElementById('fileName'); + const msgEl = document.getElementById('uploadMessage'); + const submitBtn = document.querySelector('.btn-submit'); + + if (!form) return; + + // Показываем имя выбранного файла + fileInput?.addEventListener('change', () => { + fileName.textContent = fileInput.files[0]?.name || 'Выберите аудиофайл'; + }); + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + msgEl.textContent = ''; + msgEl.className = 'upload-message'; + submitBtn.disabled = true; + submitBtn.textContent = 'Загрузка...'; + + const formData = new FormData(); + formData.append('title', document.getElementById('title').value.trim()); + formData.append('artist', document.getElementById('artist').value.trim()); + formData.append('cover', document.getElementById('cover').value.trim()); + formData.append('audio', fileInput.files[0]); + + try { + const track = await api.uploadTrack(formData); + msgEl.textContent = `Загружено: «${track.title}» — ${track.artist}`; + msgEl.className = 'upload-message success'; + form.reset(); + fileName.textContent = 'Выберите аудиофайл'; + } catch (err) { + msgEl.textContent = `Ошибка: ${err.message}`; + msgEl.className = 'upload-message error'; + } finally { + submitBtn.disabled = false; + submitBtn.textContent = 'Загрузить'; + } + }); +}; diff --git a/client/js/utils.js b/client/js/utils.js new file mode 100644 index 0000000..9e0a5da --- /dev/null +++ b/client/js/utils.js @@ -0,0 +1,24 @@ +export const formatTime = (time) => { + if (!time || isNaN(time)) return '0:00'; + const m = Math.floor(time / 60); + const s = Math.floor(time % 60).toString().padStart(2, '0'); + return `${m}:${s}`; +}; + +export const shuffle = (array) => { + const arr = [...array]; + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; +}; + +// Обновить заполнение ползунка через CSS custom property +export const syncSliderFill = (input) => { + const min = parseFloat(input.min) || 0; + const max = parseFloat(input.max) || 100; + const val = parseFloat(input.value) || 0; + const pct = ((val - min) / (max - min)) * 100; + input.style.setProperty('--value', `${pct}%`); +}; diff --git a/client/pages/add-songs.html b/client/pages/add-songs.html new file mode 100644 index 0000000..98e0f68 --- /dev/null +++ b/client/pages/add-songs.html @@ -0,0 +1,15 @@ +
    +

    Добавить трек

    +
    + + + + + +
    +

    +
    diff --git a/client/pages/peers.html b/client/pages/peers.html new file mode 100644 index 0000000..4cd3a70 --- /dev/null +++ b/client/pages/peers.html @@ -0,0 +1,16 @@ +
    +

    Подключённые узлы

    + + +
    +

    Добавить узел

    +
    +
    + + +
    + +
    +

    +
    +
    diff --git a/client/pages/tracks.html b/client/pages/tracks.html new file mode 100644 index 0000000..e15ff73 --- /dev/null +++ b/client/pages/tracks.html @@ -0,0 +1,5 @@ + + diff --git a/css/add_songs.css b/css/add_songs.css new file mode 100644 index 0000000..e94b240 --- /dev/null +++ b/css/add_songs.css @@ -0,0 +1,60 @@ +.all { + justify-items: center; + background: rgba(255, 255, 255, 0.3); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(10px); + border-radius: 8px; + padding-bottom: 5vh; + padding-top: 4vh; + color: white; + text-shadow: 0 0 5px rgba(0, 0, 0, 0.8); +} + +.mainForm { + margin: auto; + width: 400px; + display: flex; + flex-direction: column; +} + +.input__text { + -webkit-appearance: none; + appearance: none; + border-radius: 4px; + background: rgba(255, 255, 255, 0.5); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); + border: none; + height: 2vh; + margin-top: 1vh; +} + +.input__upload { + border-radius: 4px; + background: none; + border: none; + height: 2vh; + margin-top: 1vh; +} + +.input__upload::-webkit-file-upload-button { + -webkit-appearance: none; + appearance: none; + background-color: rgba(255, 255, 255, 0.5); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); + border-radius: 4; + height: 2vh; + border: none; + margin-right: 2vh; +} + +.btn__submit { + margin-top: 1vh; + -webkit-appearance: none; + appearance: none; + background: rgba(255, 255, 255, 0.5); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); + border: none; + border-radius: 4PX; + height: 2.2vh; + color: rgb(99, 97, 97); +} \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..49afa49 --- /dev/null +++ b/css/style.css @@ -0,0 +1,310 @@ +/* Общие стили */ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background: rgba(0, 0, 0, 0.87); + color: #fff; + display: flex; + flex-direction: column; + align-items: center; +} + +.header { + min-width: none; + max-width: 80vw; + width: 100%; + margin-top: 1vh; + margin-bottom: 1vh; + border-radius: 20px; + padding: 15px; + background: rgba(255, 69, 0, 0.8); /* Красноватый оттенок с прозрачностью */ + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(10px); + text-align: center; +} + +.header h2 { + font-size: 1.8em; + font-weight: bold; + margin: 0; +} + +.menu a { + color: #fff; + margin: 0 15px; + font-weight: 500; + text-decoration: none; + padding: 5px 10px; + border-radius: 20px; + transition: background 0.3s; +} + +.menu a:hover { + background: rgba(255, 255, 255, 0.2); +} + +#content { + margin: auto; + width: 100%; + max-width: 800px; +} + +/* Стиль для списка треков */ +.songsWrapper { + width: 100%; + margin: 20px 0; + margin-bottom: 130px; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 15px; +} + +.song { + display: flex; + flex: 1; + align-items: center; + padding: 10px; + border-radius: 10px; + cursor: pointer; + transition: background 0.3s; +} + +.song:hover { + background: rgba(255, 255, 255, 0.2); +} + +.song img { + width: 70px; + height: 70px; + border-radius: 10px; + object-fit: cover; + margin-right: 15px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +.song h3 { + font-size: 1.1em; + margin: 0; + color: #ffd700; +} + +.song h4 { + font-size: 0.9em; + margin: 0; + color: #fff; + opacity: 0.8; +} + +/* Стиль аудиоплеера */ +.audio-player { + position: fixed; + bottom: 20px; + width: 100%; + min-width: 40vw; + max-width: 80vw; + padding: 15px; + background: rgba(255, 255, 255, 0.2); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(10px); + border-radius: 20px; + display: flex; + align-items: center; + justify-content: space-between; + color: #fff; +} + +/* Обложка трека в плеере */ +.player__cover_img { + width: 60px; + height: 60px; + border-radius: 10px; + object-fit: cover; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); + margin-right: 15px; +} + +.player__left { + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + min-width: 0; + margin-right: 20px; +} + +/* Информация о треке */ +.player__info { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; + margin-right: 20px; +} + +.player__info__title { + font-size: 1.2em; + font-weight: bold; + color: #ffd700; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.player__info__artist { + font-size: 0.9em; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Общий блок кнопок и полосы прогресса */ +.player__center { + display: flex; + flex: 1; + flex-direction: column; + justify-content: space-between; +} + +/* Полоса прогресса и время */ +.player__progress { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + margin-top: 0.5vh; +} + +#progressSlider { + -webkit-appearance: none; + appearance: none; + height: 6px; + background: rgba(255, 255, 255, 0.5); + border-radius: 5px; + outline: none; + width: 100%; + margin: 0 10px; + cursor: pointer; + transition: border 0.3s; +} + +#progressSlider::-webkit-slider-runnable-track { + background: linear-gradient(to right, #FF5733 0%, #FF5733 var(--value, 50%), #ddd var(--value, 50%), #ddd 100%); + border-radius: 5px; +} + + +#progressSlider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 0px; + height: 8px; + background: #FF5733; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); + transition: width 0.3s, height 0.3s; +} + +.player__progress span { + font-size: 0.8em; + min-width: 35px; + text-align: center; +} + +/* Управление громкостью */ +.player__volume { + display: flex; + justify-content: flex-end; + align-items: center; + flex: 1; +} + +.player__right { + right: 0; +} + +#volumeSlider { + -webkit-appearance: none; + appearance: none; + height: 6px; + background: rgba(255, 255, 255, 0.5); + border-radius: 5px; + outline: none; + width: 80px; + cursor: pointer; +} + +#volumeSlider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 12px; + height: 12px; + background: #FF5733; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); +} + +/* Кнопки управления плеером */ +.player__control { + margin: auto; + display: flex; + align-items: center; + gap: 12px; +} + +.player__control button { + background: rgba(255, 87, 51, 0.8); + border: none; + color: #fff; + padding: 8px 15px; + margin: 0 5px; + border-radius: 16px; + cursor: pointer; + font-size: 0.9em; + transition: background 0.3s; +} + +.player__control button:hover { + background: rgba(255, 87, 51, 1); +} + +.player__control button:focus { + outline: none; +} + +.active { + background: rgba(255, 248, 55, 0.5) !important; + border: 1px solid black; +} + +.active:hover { + background: rgba(255, 248, 55, 0.2) !important; +} + +/* -------------------------------------------------- Дальше файл tracks --------------------------------- */ + + +.search { + width: 100%; /* Растягиваем input на всю ширину контейнера */ + height: 30px; + box-sizing: border-box; /* Учитываем отступы и границы в ширину */ + border-radius: 4px; /* Закругленные углы */ + padding: 4px; + background: rgba(255, 255, 255, 0.1); + margin-bottom: 1vh; + transition: background 0.3s; +} + + +.search input{ + appearance: none; + width: 100%; + background: none; + border: none; + outline: none; +} \ No newline at end of file diff --git a/index.html b/index.html index 7182942..d409e85 100644 --- a/index.html +++ b/index.html @@ -1,31 +1,66 @@ - - + + - Заголовок страницы - - - - - + Мой музыкальный плеер + + + +
    -
    -

    - Personal Music -

    - -
    +

    Personal Music

    +
    - - -
    - - + + +
    +

    Добро пожаловать на страницу с музыкой!

    - \ No newline at end of file + + +
    +
    +
    + Обложка трека +
    +
    +

    Название трека

    +

    Автор трека

    +
    +
    +
    + +
    + + + + + +
    + + +
    + 0:00 + + 0:00 +
    +
    + + +
    +
    + + +
    +
    +
    + + + + diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..392dc0c --- /dev/null +++ b/js/main.js @@ -0,0 +1,31 @@ +import { loadTracksPage, loadVisualizerPage, loadAddSongsPage } from './navigation.js'; +import { togglePlayPause, playNextSong, playPreviousSong, } from './player.js'; + + + +// Основная инициализация +document.addEventListener("DOMContentLoaded", () => { + loadTracksPage(); // Загружаем страницу треков при первой загрузке + + // Навигация по страницам + document.addEventListener("click", (e) => { + if (e.target.classList.contains("nav-link-tracks")) { + e.preventDefault(); + console.log('Загрузка страницы с треками'); + loadTracksPage(); + } + else if (e.target.classList.contains("nav-link-visualizer")) { + e.preventDefault(); + loadVisualizerPage(); + } + else if (e.target.classList.contains("nav-link-add-songs")) { + e.preventDefault(); + loadAddSongsPage(); + } + }); + + // Управление плеером + document.getElementById("play-pause").addEventListener("click", togglePlayPause); + document.getElementById("next").addEventListener("click", playNextSong); + document.getElementById("prev").addEventListener("click", playPreviousSong); +}); diff --git a/js/navigation.js b/js/navigation.js new file mode 100644 index 0000000..fc95977 --- /dev/null +++ b/js/navigation.js @@ -0,0 +1,59 @@ +import { getSongs, searchSongs } from './tracks.js'; +import { AddNewSongs } from './upload.js'; +import { startVisualizer } from './visualizer.js'; + +// Загружаем страницу треков +export const loadTracksPage = async () => { + try { + const response = await fetch("pages/tracks.html"); + if (!response.ok) throw new Error(`Ошибка: ${response.status}`); + const html = await response.text(); + document.getElementById("content").innerHTML = html; + + // Загружаем и отображаем треки + await getSongs(); + console.log("Страница треков загружена"); + + // Привязываем обработчик для поиска после загрузки track.html + const searchInput = document.getElementById("searchInput"); + if (searchInput) { + searchInput.addEventListener("input", (event) => { + searchSongs(event.target.value); + }); + } + } catch (error) { + console.error("Ошибка загрузки страницы треков:", error); + } +}; + +// Загружаем страницу визуализатора +export const loadVisualizerPage = async () => { + try { + const response = await fetch("pages/visualizer.html"); + if (!response.ok) throw new Error(`Ошибка: ${response.status}`); + const html = await response.text(); + document.getElementById("content").innerHTML = html; + + // Инициализируем визуализатор, если canvas доступен + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const visualizerAudio = document.querySelector("audio"); + if (visualizerAudio) startVisualizer(visualizerAudio, audioContext); + console.log("Страница визуализатора загружена"); + } catch (error) { + console.error("Ошибка загрузки страницы визуализатора:", error); + } +}; + +// Загружаем страницу добавления треков +export const loadAddSongsPage = async () => { + try { + const response = await fetch("pages/add-songs.html"); + if (!response.ok) throw new Error(`Ошибка: ${response.status}`); + const html = await response.text(); + document.getElementById("content").innerHTML = html; + AddNewSongs(); // Настройка формы для загрузки треков + console.log("Страница добавления треков загружена"); + } catch (error) { + console.error("Ошибка загрузки страницы добавления треков:", error); + } +}; diff --git a/js/player.js b/js/player.js new file mode 100644 index 0000000..b9cdc10 --- /dev/null +++ b/js/player.js @@ -0,0 +1,125 @@ +import { formatTime, updateBackground } from './utils.js'; + +let myAudio = new Audio(); +let currentSongIndex = 0; +let songs = []; +let isPlaying = false; +let rangeInput = document.getElementById('progressSlider'); + +export const initializePlayer = (loadedSongs) => { + songs = loadedSongs; + const savedState = JSON.parse(localStorage.getItem("playerState")); + + // Восстанавливаем состояние плеера, если оно сохранено + if (savedState) { + currentSongIndex = savedState.currentSongIndex; + myAudio.currentTime = savedState.currentTime || 0; + updateBackground(rangeInput); + updatePlayerInfo(songs[currentSongIndex]); + isPlaying = savedState.isPlaying; + if (isPlaying) { + document.getElementById("play-pause").innerHTML = ``; + myAudio.play(); + } + } else { + updatePlayerInfo(songs[currentSongIndex]); + } + + myAudio.volume = localStorage.getItem('volume') || 0.5; + savePlayerState(); // Сохраняем начальное состояние + console.log('Вот он - localeStorage',savedState, localStorage.getItem('volume')); +}; + +export const updatePlayerInfo = (song) => { + document.querySelector(".player__cover_img").src = song.cover; + document.querySelector(".player__info__title").textContent = song.title; + document.querySelector(".player__info__artist").textContent = song.artist; + myAudio.src = song.audio; +}; + +const autoPlay = (currentTime,duration) => { + if (currentTime === duration) { + document.getElementById("play-pause").innerHTML = ``; + if (currentSongIndex != songs.length - 1) { + currentSongIndex = currentSongIndex + 1; + } else { + currentSongIndex = 0; + } + playSong(songs[currentSongIndex]); + } +} + +// Сохранение состояния плеера в localStorage +const savePlayerState = () => { + const state = { + currentSongIndex, + currentTime: myAudio.currentTime, + isPlaying: !myAudio.paused + }; + localStorage.setItem("playerState", JSON.stringify(state)); + console.log("Сохранение в localestorage сработало", state); +}; + +// Добавляем обработчики событий для сохранения состояния +myAudio.addEventListener("timeupdate", savePlayerState); +myAudio.addEventListener("pause", savePlayerState); +myAudio.addEventListener("play", savePlayerState); + +export const getIndex = (index) => { + currentSongIndex = index; + console.log('Получение индекса',index, currentSongIndex); +} + +export const playSong = (song) => { + console.log('Инфа о песне', song); + document.getElementById("play-pause").innerHTML = ``; + updatePlayerInfo(song); + myAudio.play(); +}; + +export const togglePlayPause = () => { + if (myAudio.paused) { + myAudio.play(); + document.getElementById("play-pause").innerHTML = ``; + } else { + myAudio.pause(); + document.getElementById("play-pause").innerHTML = ``; + } +}; + +export const playNextSong = () => { + currentSongIndex = (currentSongIndex + 1) % songs.length; + console.log('Текущий индекс песни в playsong', currentSongIndex); + playSong(songs[currentSongIndex]); +}; + +export const playPreviousSong = () => { + currentSongIndex = (currentSongIndex - 1 + songs.length) % songs.length; + playSong(songs[currentSongIndex]); +}; + +// Обработка изменения прогресса трека +myAudio.addEventListener("timeupdate", () => { + const currentTime = myAudio.currentTime; + const duration = myAudio.duration; + const progress = (currentTime / duration) * 100; + updateBackground(rangeInput); + autoPlay(currentTime, duration); + document.getElementById("progressSlider").value = progress; + document.getElementById("currentTime").textContent = formatTime(currentTime); + document.getElementById("totalTime").textContent = formatTime(duration); +}); + +// Обработка изменения ползунка прогресса +document.getElementById("progressSlider").addEventListener("input", (event) => { + const value = event.target.value; + myAudio.currentTime = (value / 100) * myAudio.duration; +}); + +// Управление громкостью +document.getElementById("volumeSlider").addEventListener("input", (event) => { + myAudio.volume = event.target.value; + localStorage.setItem('volume', myAudio.volume); + console.log('ГРОМКОСТЬ после двигания', localStorage.getItem('volume')); + +}); diff --git a/js/search.js b/js/search.js new file mode 100644 index 0000000..20a7118 --- /dev/null +++ b/js/search.js @@ -0,0 +1,13 @@ +// Функция для фильтрации песен по запросу +export const searchSongs = (songs ,query) => { + const filteredSongs = songs.filter(song => + song.name.toLowerCase().includes(query.toLowerCase()) || + song.artist.toLowerCase().includes(query.toLowerCase()) + ); + renderSongs(filteredSongs); // Перерисовываем список с отфильтрованными песнями +}; + +document.getElementById("searchInput").addEventListener("input", (event) => { + const query = event.target.value; + searchSongs(query); +}); \ No newline at end of file diff --git a/js/tracks.js b/js/tracks.js new file mode 100644 index 0000000..8fb141f --- /dev/null +++ b/js/tracks.js @@ -0,0 +1,51 @@ +import { playSong, initializePlayer, getIndex } from './player.js'; + +let songs = []; // Полный список треков +let filteredSongs = []; // Отфильтрованный список треков (на основе поиска) + +export const getSongs = async () => { + try { + if (songs.length === 0) { + const response = await fetch("../uploads/songs.json"); + songs = await response.json(); + filteredSongs = songs; + renderSongs(filteredSongs) + initializePlayer(filteredSongs) + } else { + renderSongs(filteredSongs); + } + } catch (error) { + console.error("Ошибка загрузки песен:", error); + } +}; + +export const renderSongs = (songs) => { + const wrapper = document.getElementById("songsWrapper"); + wrapper.innerHTML = songs.map((song, index) => ` +
    + cover +
    +

    ${song.title}

    +

    ${song.artist}

    +
    +
    + `).join(""); + + document.querySelectorAll(".song").forEach((songElement, index) => { + songElement.addEventListener("click", () => { + getIndex(index) + playSong(songs[index]); + }); + }); +}; + +// Функция для фильтрации треков на основе поискового запроса +export const searchSongs = (query) => { + query = query.toLowerCase(); + filteredSongs = songs.filter(song => + song.title.toLowerCase().includes(query) || + song.artist.toLowerCase().includes(query) + ); + initializePlayer(filteredSongs); + renderSongs(filteredSongs); // Перерисовываем список на основе результатов поиска +}; \ No newline at end of file diff --git a/js/upload.js b/js/upload.js new file mode 100644 index 0000000..2022782 --- /dev/null +++ b/js/upload.js @@ -0,0 +1,28 @@ +export const AddNewSongs = () => { + document.getElementById('uploadForm').addEventListener('submit', function (event) { + event.preventDefault(); + + const title = document.getElementById('title').value; + const artist = document.getElementById('artist').value; + const audioFile = document.getElementById('audio').files[0]; + const cover = document.getElementById('cover').value; + + const formData = new FormData(); + formData.append('title', title); + formData.append('artist', artist); + formData.append('audio', audioFile); + formData.append('cover', cover); + + fetch('http://127.0.0.1:5000/upload', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + document.getElementById('message').innerText = data.message; + }) + .catch(error => { + document.getElementById('message').innerText = 'Error: ' + error.message; + }); + }); +}; diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..ac551e1 --- /dev/null +++ b/js/utils.js @@ -0,0 +1,34 @@ +export const formatTime = (time) => { + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60).toString().padStart(2, "0"); + return `${minutes}:${seconds}`; +}; + +export const shuffle = (array) => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +}; + +// Изменение цвета вслед за точкой на временной шкале + + +// Функция для обновления значения кастомного свойства CSS +export function updateBackground(rangeInput) { + const value = rangeInput.value; + const min = rangeInput.min ? rangeInput.min : 0; + const max = rangeInput.max ? rangeInput.max : 100; + const percentage = (value - min) / (max - min) * 100; + + rangeInput.addEventListener('mousedown', function () { + rangeInput.style.setProperty('--thumb-width', '10px'); + }) + + rangeInput.addEventListener('mouseup', function () { + rangeInput.style.setProperty('--thumb-width', '0px'); + }) + + rangeInput.style.setProperty('--value', `${percentage}%`); +} \ No newline at end of file diff --git a/js/visualizer.js b/js/visualizer.js new file mode 100644 index 0000000..d1bee92 --- /dev/null +++ b/js/visualizer.js @@ -0,0 +1,31 @@ +export const startVisualizer = (VisualizerAudio, audioContext) => { + const analyser = audioContext.createAnalyser(); + analyser.fftSize = 256; + + const source = audioContext.createMediaElementSource(VisualizerAudio); + source.connect(analyser); + analyser.connect(audioContext.destination); + + const canvas = document.getElementById("visualizerCanvas"); + const ctx = canvas.getContext("2d"); + + function renderFrame() { + requestAnimationFrame(renderFrame); + const bufferLength = analyser.frequencyBinCount; + const dataArray = new Uint8Array(bufferLength); + analyser.getByteFrequencyData(dataArray); + + ctx.clearRect(0, 0, canvas.width, canvas.height); + const barWidth = (canvas.width / bufferLength) * 2.5; + let x = 0; + + dataArray.forEach((value) => { + const barHeight = value / 2; + ctx.fillStyle = `rgb(${value + 100}, 50, 150)`; + ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight); + x += barWidth + 1; + }); + } + + renderFrame(); +}; diff --git a/media/ANNA TSUCHIYA - Rose.mp3 b/media/ANNA TSUCHIYA - Rose.mp3 deleted file mode 100644 index 23aa9a0..0000000 Binary files a/media/ANNA TSUCHIYA - Rose.mp3 and /dev/null differ diff --git a/media/Ado - Odo.mp3 b/media/Ado - Odo.mp3 deleted file mode 100644 index 0355831..0000000 Binary files a/media/Ado - Odo.mp3 and /dev/null differ diff --git a/media/Ado - Show.mp3 b/media/Ado - Show.mp3 deleted file mode 100644 index 09ffac2..0000000 Binary files a/media/Ado - Show.mp3 and /dev/null differ diff --git a/media/Ado - Unravel.mp3 b/media/Ado - Unravel.mp3 deleted file mode 100644 index cd35a97..0000000 Binary files a/media/Ado - Unravel.mp3 and /dev/null differ diff --git a/media/Ado - Usseewa.mp3 b/media/Ado - Usseewa.mp3 deleted file mode 100644 index db52081..0000000 Binary files a/media/Ado - Usseewa.mp3 and /dev/null differ diff --git a/media/Animadrop - Stuck in a Timeloop.mp3 b/media/Animadrop - Stuck in a Timeloop.mp3 deleted file mode 100644 index b16ac65..0000000 Binary files a/media/Animadrop - Stuck in a Timeloop.mp3 and /dev/null differ diff --git a/media/Apashe - Work (ft. Vo Williams).mp3 b/media/Apashe - Work (ft. Vo Williams).mp3 deleted file mode 100644 index 3f8c13d..0000000 Binary files a/media/Apashe - Work (ft. Vo Williams).mp3 and /dev/null differ diff --git a/media/Asper X - Bad Trip.mp3 b/media/Asper X - Bad Trip.mp3 deleted file mode 100644 index 837126f..0000000 Binary files a/media/Asper X - Bad Trip.mp3 and /dev/null differ diff --git a/media/BABYMETAL - BxMxC.mp3 b/media/BABYMETAL - BxMxC.mp3 deleted file mode 100644 index cb305db..0000000 Binary files a/media/BABYMETAL - BxMxC.mp3 and /dev/null differ diff --git a/media/BABYMETAL - Divine Attack - Shingeki.mp3 b/media/BABYMETAL - Divine Attack - Shingeki.mp3 deleted file mode 100644 index 03668cb..0000000 Binary files a/media/BABYMETAL - Divine Attack - Shingeki.mp3 and /dev/null differ diff --git a/media/BABYMETAL - Karate.mp3 b/media/BABYMETAL - Karate.mp3 deleted file mode 100644 index 59efaa6..0000000 Binary files a/media/BABYMETAL - Karate.mp3 and /dev/null differ diff --git a/media/BABYMETAL feat. F.HERO - PA PA YA!!.mp3 b/media/BABYMETAL feat. F.HERO - PA PA YA!!.mp3 deleted file mode 100644 index bb3fc49..0000000 Binary files a/media/BABYMETAL feat. F.HERO - PA PA YA!!.mp3 and /dev/null differ diff --git "a/media/BABYMETAL feat. Joakim Brod\303\251n - Oh! MAJINAI.mp3" "b/media/BABYMETAL feat. Joakim Brod\303\251n - Oh! MAJINAI.mp3" deleted file mode 100644 index 47ee39e..0000000 Binary files "a/media/BABYMETAL feat. Joakim Brod\303\251n - Oh! MAJINAI.mp3" and /dev/null differ diff --git a/media/Bad Omens - THE DEATH OF PEACE OF MIND.mp3 b/media/Bad Omens - THE DEATH OF PEACE OF MIND.mp3 deleted file mode 100644 index e935891..0000000 Binary files a/media/Bad Omens - THE DEATH OF PEACE OF MIND.mp3 and /dev/null differ diff --git "a/media/Creepy Nuts - \343\202\252\343\203\210\343\203\216\343\202\261(Otonoke).mp3" "b/media/Creepy Nuts - \343\202\252\343\203\210\343\203\216\343\202\261(Otonoke).mp3" deleted file mode 100644 index f6281f5..0000000 Binary files "a/media/Creepy Nuts - \343\202\252\343\203\210\343\203\216\343\202\261(Otonoke).mp3" and /dev/null differ diff --git a/media/Eurielle - City of the dead.mp3 b/media/Eurielle - City of the dead.mp3 deleted file mode 100644 index 5a4ad80..0000000 Binary files a/media/Eurielle - City of the dead.mp3 and /dev/null differ diff --git "a/media/FIZICA - \320\223\320\276\321\202\321\215\320\274 2.mp3" "b/media/FIZICA - \320\223\320\276\321\202\321\215\320\274 2.mp3" deleted file mode 100644 index 859f7c7..0000000 Binary files "a/media/FIZICA - \320\223\320\276\321\202\321\215\320\274 2.mp3" and /dev/null differ diff --git "a/media/FIZICA - \320\223\320\276\321\202\321\215\320\274 3.mp3" "b/media/FIZICA - \320\223\320\276\321\202\321\215\320\274 3.mp3" deleted file mode 100644 index 249f671..0000000 Binary files "a/media/FIZICA - \320\223\320\276\321\202\321\215\320\274 3.mp3" and /dev/null differ diff --git "a/media/FIZICA - \320\223\320\276\321\202\321\215\320\274.mp3" "b/media/FIZICA - \320\223\320\276\321\202\321\215\320\274.mp3" deleted file mode 100644 index 4ffcf13..0000000 Binary files "a/media/FIZICA - \320\223\320\276\321\202\321\215\320\274.mp3" and /dev/null differ diff --git "a/media/FIZICA - \320\247\320\276\320\272\320\265\321\200.mp3" "b/media/FIZICA - \320\247\320\276\320\272\320\265\321\200.mp3" deleted file mode 100644 index 58d161d..0000000 Binary files "a/media/FIZICA - \320\247\320\276\320\272\320\265\321\200.mp3" and /dev/null differ diff --git a/media/Falling In Reverse - ZOMBIFIED.mp3 b/media/Falling In Reverse - ZOMBIFIED.mp3 deleted file mode 100644 index ac6ba95..0000000 Binary files a/media/Falling In Reverse - ZOMBIFIED.mp3 and /dev/null differ diff --git "a/media/GroTTesque - \320\241\321\202\320\260\320\273\321\214\320\275\320\276\320\265 \321\201\320\265\321\200\320\264\321\206\320\265.mp3" "b/media/GroTTesque - \320\241\321\202\320\260\320\273\321\214\320\275\320\276\320\265 \321\201\320\265\321\200\320\264\321\206\320\265.mp3" deleted file mode 100644 index 54cb492..0000000 Binary files "a/media/GroTTesque - \320\241\321\202\320\260\320\273\321\214\320\275\320\276\320\265 \321\201\320\265\321\200\320\264\321\206\320\265.mp3" and /dev/null differ diff --git a/media/Halestorm - I Miss The Misery.mp3 b/media/Halestorm - I Miss The Misery.mp3 deleted file mode 100644 index 6aecad6..0000000 Binary files a/media/Halestorm - I Miss The Misery.mp3 and /dev/null differ diff --git a/media/Halsey - Control.mp3 b/media/Halsey - Control.mp3 deleted file mode 100644 index 67804b9..0000000 Binary files a/media/Halsey - Control.mp3 and /dev/null differ diff --git a/media/Hidden Citizens feat. Keeley Bumford - Immortalized.mp3 b/media/Hidden Citizens feat. Keeley Bumford - Immortalized.mp3 deleted file mode 100644 index 0192e3f..0000000 Binary files a/media/Hidden Citizens feat. Keeley Bumford - Immortalized.mp3 and /dev/null differ diff --git "a/media/Hidden Citizens feat. Ra\314\212nya - Let Me Out.mp3" "b/media/Hidden Citizens feat. Ra\314\212nya - Let Me Out.mp3" deleted file mode 100644 index 96118cf..0000000 Binary files "a/media/Hidden Citizens feat. Ra\314\212nya - Let Me Out.mp3" and /dev/null differ diff --git "a/media/Hidden Citizens feat. Ra\314\212nya - Paint It Black.mp3" "b/media/Hidden Citizens feat. Ra\314\212nya - Paint It Black.mp3" deleted file mode 100644 index 8da9941..0000000 Binary files "a/media/Hidden Citizens feat. Ra\314\212nya - Paint It Black.mp3" and /dev/null differ diff --git "a/media/Louna - \320\237\320\276\320\273\321\216\321\201\320\260.mp3" "b/media/Louna - \320\237\320\276\320\273\321\216\321\201\320\260.mp3" deleted file mode 100644 index 435ea2b..0000000 Binary files "a/media/Louna - \320\237\320\276\320\273\321\216\321\201\320\260.mp3" and /dev/null differ diff --git a/media/MIYAVI - Other Side.mp3 b/media/MIYAVI - Other Side.mp3 deleted file mode 100644 index 3efddfc..0000000 Binary files a/media/MIYAVI - Other Side.mp3 and /dev/null differ diff --git "a/media/Norma Tale - \320\232\320\270\321\201\321\202\320\276\321\207\320\272\320\260 II.mp3" "b/media/Norma Tale - \320\232\320\270\321\201\321\202\320\276\321\207\320\272\320\260 II.mp3" deleted file mode 100644 index 579ce25..0000000 Binary files "a/media/Norma Tale - \320\232\320\270\321\201\321\202\320\276\321\207\320\272\320\260 II.mp3" and /dev/null differ diff --git "a/media/RAVANNA - \320\226\320\263\320\270.mp3" "b/media/RAVANNA - \320\226\320\263\320\270.mp3" deleted file mode 100644 index dcca1e0..0000000 Binary files "a/media/RAVANNA - \320\226\320\263\320\270.mp3" and /dev/null differ diff --git "a/media/RAVANNA feat. Ai Mori - \320\232\320\276\320\274\320\260.mp3" "b/media/RAVANNA feat. Ai Mori - \320\232\320\276\320\274\320\260.mp3" deleted file mode 100644 index 58f38b4..0000000 Binary files "a/media/RAVANNA feat. Ai Mori - \320\232\320\276\320\274\320\260.mp3" and /dev/null differ diff --git a/media/RIOT - Overkill.mp3 b/media/RIOT - Overkill.mp3 deleted file mode 100644 index cd044c2..0000000 Binary files a/media/RIOT - Overkill.mp3 and /dev/null differ diff --git a/media/Rammstein - Sonne.mp3 b/media/Rammstein - Sonne.mp3 deleted file mode 100644 index 3219612..0000000 Binary files a/media/Rammstein - Sonne.mp3 and /dev/null differ diff --git a/media/SKYND - Chris Watts.mp3 b/media/SKYND - Chris Watts.mp3 deleted file mode 100644 index 9d9738d..0000000 Binary files a/media/SKYND - Chris Watts.mp3 and /dev/null differ diff --git a/media/SKYND - Elisa Lam.mp3 b/media/SKYND - Elisa Lam.mp3 deleted file mode 100644 index 460fddb..0000000 Binary files a/media/SKYND - Elisa Lam.mp3 and /dev/null differ diff --git a/media/SKYND - Jim Jones(2019).mp3 b/media/SKYND - Jim Jones(2019).mp3 deleted file mode 100644 index 368904d..0000000 Binary files a/media/SKYND - Jim Jones(2019).mp3 and /dev/null differ diff --git a/media/SKYND - Katherine Knight.mp3 b/media/SKYND - Katherine Knight.mp3 deleted file mode 100644 index 9f35337..0000000 Binary files a/media/SKYND - Katherine Knight.mp3 and /dev/null differ diff --git a/media/SKYND - Lay down your life.mp3 b/media/SKYND - Lay down your life.mp3 deleted file mode 100644 index ebbc4f7..0000000 Binary files a/media/SKYND - Lay down your life.mp3 and /dev/null differ diff --git a/media/SKYND - Michelle Carter.mp3 b/media/SKYND - Michelle Carter.mp3 deleted file mode 100644 index d0e1aaa..0000000 Binary files a/media/SKYND - Michelle Carter.mp3 and /dev/null differ diff --git a/media/SKYND - Richard Ramirez.mp3 b/media/SKYND - Richard Ramirez.mp3 deleted file mode 100644 index 3dc7676..0000000 Binary files a/media/SKYND - Richard Ramirez.mp3 and /dev/null differ diff --git a/media/SKYND - Tyler Hadley.mp3 b/media/SKYND - Tyler Hadley.mp3 deleted file mode 100644 index 9338dc6..0000000 Binary files a/media/SKYND - Tyler Hadley.mp3 and /dev/null differ diff --git a/media/SKYND feat. Bill $Aber - Columbine (feat. Bill $Aber).mp3 b/media/SKYND feat. Bill $Aber - Columbine (feat. Bill $Aber).mp3 deleted file mode 100644 index 9a1e2e2..0000000 Binary files a/media/SKYND feat. Bill $Aber - Columbine (feat. Bill $Aber).mp3 and /dev/null differ diff --git a/media/SKYND feat. Jonathan Davis - Gary Heidnik (feat. Jonathan Davis).mp3 b/media/SKYND feat. Jonathan Davis - Gary Heidnik (feat. Jonathan Davis).mp3 deleted file mode 100644 index a99da77..0000000 Binary files a/media/SKYND feat. Jonathan Davis - Gary Heidnik (feat. Jonathan Davis).mp3 and /dev/null differ diff --git a/media/blueberry - EVA 3.mp3 b/media/blueberry - EVA 3.mp3 deleted file mode 100644 index f832717..0000000 Binary files a/media/blueberry - EVA 3.mp3 and /dev/null differ diff --git "a/media/pyrokinesis - \320\233\320\265\320\263\320\265\320\275\320\264\320\260 \320\276 \320\221\320\276\320\263\320\270\320\275\320\265 \320\223\321\200\320\276\320\267.mp3" "b/media/pyrokinesis - \320\233\320\265\320\263\320\265\320\275\320\264\320\260 \320\276 \320\221\320\276\320\263\320\270\320\275\320\265 \320\223\321\200\320\276\320\267.mp3" deleted file mode 100644 index 39bcd18..0000000 Binary files "a/media/pyrokinesis - \320\233\320\265\320\263\320\265\320\275\320\264\320\260 \320\276 \320\221\320\276\320\263\320\270\320\275\320\265 \320\223\321\200\320\276\320\267.mp3" and /dev/null differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c81ffde --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1492 @@ +{ + "name": "personal-music", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "personal-music", + "version": "0.1.0", + "dependencies": { + "@fastify/cors": "^10.0.2", + "@fastify/multipart": "^9.0.3", + "@fastify/static": "^8.1.1", + "dotenv": "^16.5.0", + "fastify": "^5.3.3", + "music-metadata": "^11.12.3", + "pino-pretty": "^13.1.3" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@fastify/busboy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", + "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", + "license": "MIT" + }, + "node_modules/@fastify/cors": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.1.0.tgz", + "integrity": "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "mnemonist": "0.40.0" + } + }, + "node_modules/@fastify/deepmerge": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.2.1.tgz", + "integrity": "sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/multipart": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-9.4.0.tgz", + "integrity": "sha512-Z404bzZeLSXTBmp/trCBuoVFX28pM7rhv849Q5TsbTFZHuk1lc4QjQITTPK92DKVpXmNtJXeHSSc7GYvqFpxAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@fastify/deepmerge": "^3.0.0", + "@fastify/error": "^4.0.0", + "fastify-plugin": "^5.0.0", + "secure-json-parse": "^4.0.0" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/proxy-addr/node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz", + "integrity": "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/send/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@fastify/static": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.3.0.tgz", + "integrity": "sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^0.5.4", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^11.0.0" + } + }, + "node_modules/@fastify/static/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@fastify/static/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@fastify/static/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@fastify/static/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/fast-copy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz", + "integrity": "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.8.4", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.4.tgz", + "integrity": "sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastify/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/find-my-way": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.5.0.tgz", + "integrity": "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mnemonist": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.0.tgz", + "integrity": "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/music-metadata": { + "version": "11.12.3", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.12.3.tgz", + "integrity": "sha512-n6hSTZkuD59qWgHh6IP5dtDlDZQXoxk/bcA85Jywg8Z1iFrlNgl2+GTFgjZyn52W5UgQpV42V4XqrQZZAMbZTQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.2", + "@tokenizer/token": "^0.3.0", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "file-type": "^21.3.1", + "media-typer": "^1.1.0", + "strtok3": "^10.3.4", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0", + "win-guid": "^0.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz", + "integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.0.tgz", + "integrity": "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-regex2/node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/win-guid": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz", + "integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9333cf1 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "personal-music", + "version": "0.1.0", + "type": "module", + "scripts": { + "start": "node server/index.js", + "dev": "node --watch server/index.js" + }, + "dependencies": { + "@fastify/cors": "^10.0.2", + "@fastify/multipart": "^9.0.3", + "@fastify/static": "^8.1.1", + "dotenv": "^16.5.0", + "fastify": "^5.3.3", + "music-metadata": "^11.12.3", + "pino-pretty": "^13.1.3" + } +} diff --git a/pages/add-songs.html b/pages/add-songs.html new file mode 100644 index 0000000..18d5b9f --- /dev/null +++ b/pages/add-songs.html @@ -0,0 +1,20 @@ + + + + +
    +

    Добавление новой песни

    +
    +
    + + + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/pages/song_page.html b/pages/song_page.html new file mode 100644 index 0000000..e69de29 diff --git a/pages/tracks.html b/pages/tracks.html new file mode 100644 index 0000000..0cf0039 --- /dev/null +++ b/pages/tracks.html @@ -0,0 +1,7 @@ + + +
    + +
    diff --git a/pages/visualizer.html b/pages/visualizer.html new file mode 100644 index 0000000..9276491 --- /dev/null +++ b/pages/visualizer.html @@ -0,0 +1,6 @@ + + + diff --git a/scripts/enrich-metadata.js b/scripts/enrich-metadata.js new file mode 100644 index 0000000..30aa2f3 --- /dev/null +++ b/scripts/enrich-metadata.js @@ -0,0 +1,38 @@ +// Читает ID3-теги из MP3 и обогащает songs.json полями duration + album +import { readFile, writeFile, access } from 'fs/promises'; +import { resolve } from 'path'; +import { parseFile } from 'music-metadata'; + +const root = new URL('..', import.meta.url).pathname.slice(1); // убираем leading slash на Windows +const songsPath = resolve(root, 'uploads/songs.json'); + +const tracks = JSON.parse(await readFile(songsPath, 'utf8')); +let ok = 0, fail = 0; + +for (const track of tracks) { + const absPath = resolve(root, track.audio); + + try { + await access(absPath); // файл существует? + const { common, format } = await parseFile(absPath, { duration: true }); + + if (format.duration) { + track.duration = Math.round(format.duration); + } + if (common.album && common.album !== track.title) { + track.album = common.album; + } + + const dur = track.duration + ? `${Math.floor(track.duration / 60)}:${String(track.duration % 60).padStart(2, '0')}` + : '?:??'; + console.log(`✓ [${dur}] ${track.artist} — ${track.title}${track.album ? ` (${track.album})` : ''}`); + ok++; + } catch (err) { + console.warn(`✗ ${track.title}: ${err.message}`); + fail++; + } +} + +await writeFile(songsPath, JSON.stringify(tracks, null, 2), 'utf8'); +console.log(`\nГотово: ${ok} обновлено, ${fail} ошибок`); diff --git a/server/config.js b/server/config.js new file mode 100644 index 0000000..7bae7d5 --- /dev/null +++ b/server/config.js @@ -0,0 +1,39 @@ +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +// Загружаем .env вручную (без dotenv для совместимости с ESM) +try { + const env = readFileSync(resolve(process.cwd(), '.env'), 'utf8'); + env.split('\n').forEach(line => { + const [key, ...rest] = line.split('='); + if (key && !key.startsWith('#') && rest.length) { + process.env[key.trim()] = rest.join('=').trim(); + } + }); +} catch { + // .env не найден — используем только process.env +} + +export const config = { + port: parseInt(process.env.PORT || '3000', 10), + host: process.env.HOST || '0.0.0.0', + + storage: { + adapter: process.env.STORAGE_ADAPTER || 'local', + mediaDir: process.env.MEDIA_DIR || 'uploads/media', + songsFile: process.env.SONGS_FILE || 'uploads/songs.json', + }, + + node: { + name: process.env.NODE_NAME || 'My Music Node', + isPublic: process.env.NODE_PUBLIC === 'true', + }, + + federation: { + enabled: process.env.FEDERATION_ENABLED !== 'false', + mode: process.env.FEDERATION_MODE || 'manual', + peersFile: process.env.PEERS_FILE || 'server/peers.json', + }, + + rateLimitRpm: parseInt(process.env.RATE_LIMIT_RPM || '60', 10), +}; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..27af9db --- /dev/null +++ b/server/index.js @@ -0,0 +1,57 @@ +import Fastify from 'fastify'; +import cors from '@fastify/cors'; +import staticFiles from '@fastify/static'; +import multipart from '@fastify/multipart'; +import { resolve } from 'path'; +import { config } from './config.js'; + +import tracksRoutes from './routes/tracks.js'; +import peersRoutes from './routes/peers.js'; +import libraryRoutes from './routes/library.js'; +import uploadRoutes from './routes/upload.js'; + +const app = Fastify({ + logger: { + transport: { + target: 'pino-pretty', + options: { colorize: true }, + }, + }, +}); + +// CORS — разрешаем запросы с фронта (в dev-режиме) +await app.register(cors, { + origin: true, +}); + +// Multipart для загрузки файлов +await app.register(multipart, { limits: { fileSize: 100 * 1024 * 1024 } }); // 100MB + +// Статические файлы фронтенда +await app.register(staticFiles, { + root: resolve(process.cwd(), 'client'), + prefix: '/', + // Папка uploads тоже должна быть доступна (обложки, если будут локальные) +}); + +// Регистрация роутов API +// Новый модуль = новый файл + одна строка здесь +const routes = [ + { plugin: tracksRoutes, prefix: '/api' }, + { plugin: peersRoutes, prefix: '/api' }, + { plugin: libraryRoutes, prefix: '/api' }, + { plugin: uploadRoutes, prefix: '/api' }, +]; + +for (const { plugin, prefix } of routes) { + await app.register(plugin, { prefix }); +} + +// Запуск сервера +try { + await app.listen({ port: config.port, host: config.host }); + console.log(`\n Personal Music — запущен на http://localhost:${config.port}\n`); +} catch (err) { + app.log.error(err); + process.exit(1); +} diff --git a/server/peers.json b/server/peers.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/server/peers.json @@ -0,0 +1 @@ +[] diff --git a/server/routes/library.js b/server/routes/library.js new file mode 100644 index 0000000..305fe58 --- /dev/null +++ b/server/routes/library.js @@ -0,0 +1,34 @@ +import { config } from '../config.js'; +import { storage } from '../storage/index.js'; + +export default async function libraryRoutes(app) { + // GET /api/library — публичные метаданные этого узла + // Этот эндпоинт используется при добавлении пира + app.get('/library', async () => { + const tracks = await storage.getTracks(config.storage.songsFile); + + // Если узел не публичный — возвращаем только метаданные без треков + if (!config.node.isPublic) { + return { + name: config.node.name, + isPublic: false, + trackCount: 0, + tracks: [], + }; + } + + return { + name: config.node.name, + isPublic: true, + trackCount: tracks.length, + tracks: tracks.map(t => ({ + id: t.id, + title: t.title, + artist: t.artist, + cover: t.cover, + // audio — не возвращаем путь, только stream URL + streamUrl: `/api/tracks/${t.id}/stream`, + })), + }; + }); +} diff --git a/server/routes/peers.js b/server/routes/peers.js new file mode 100644 index 0000000..49f649e --- /dev/null +++ b/server/routes/peers.js @@ -0,0 +1,91 @@ +import { readFile, writeFile } from 'fs/promises'; +import { resolve } from 'path'; +import { config } from '../config.js'; + +const peersPath = () => resolve(process.cwd(), config.federation.peersFile); + +const readPeers = async () => { + try { + const data = await readFile(peersPath(), 'utf8'); + return JSON.parse(data); + } catch { + return []; + } +}; + +const savePeers = async (peers) => { + await writeFile(peersPath(), JSON.stringify(peers, null, 2), 'utf8'); +}; + +export default async function peersRoutes(app) { + // GET /api/peers — список известных узлов + app.get('/peers', async () => { + return readPeers(); + }); + + // POST /api/peers — добавить узел вручную + // Body: { url: "http://friend-server:3000", name: "Friend's Node" } + app.post('/peers', { + schema: { + body: { + type: 'object', + required: ['url'], + properties: { + url: { type: 'string', format: 'uri' }, + name: { type: 'string', maxLength: 100 }, + }, + }, + }, + }, async (req, reply) => { + const peers = await readPeers(); + const { url, name } = req.body; + + if (peers.find(p => p.url === url)) { + return reply.code(409).send({ error: 'Peer already exists' }); + } + + // Проверяем доступность узла + try { + const res = await fetch(`${url}/api/library`); + if (!res.ok) throw new Error('Bad response'); + const library = await res.json(); + + const peer = { + url, + name: name || library.name || url, + addedAt: new Date().toISOString(), + isPublic: library.isPublic, + trackCount: library.trackCount, + }; + + peers.push(peer); + await savePeers(peers); + return reply.code(201).send(peer); + } catch { + return reply.code(400).send({ error: 'Cannot reach peer at the given URL' }); + } + }); + + // DELETE /api/peers — удалить узел по URL + app.delete('/peers', { + schema: { + body: { + type: 'object', + required: ['url'], + properties: { + url: { type: 'string' }, + }, + }, + }, + }, async (req, reply) => { + const peers = await readPeers(); + const filtered = peers.filter(p => p.url !== req.body.url); + + if (filtered.length === peers.length) { + return reply.code(404).send({ error: 'Peer not found' }); + } + + await savePeers(filtered); + return reply.code(204).send(); + }); +} diff --git a/server/routes/tracks.js b/server/routes/tracks.js new file mode 100644 index 0000000..5246c6b --- /dev/null +++ b/server/routes/tracks.js @@ -0,0 +1,49 @@ +import { config } from '../config.js'; +import { storage } from '../storage/index.js'; + +export default async function tracksRoutes(app) { + // GET /api/tracks — список всех треков + app.get('/tracks', async (req, reply) => { + const tracks = await storage.getTracks(config.storage.songsFile); + return tracks; + }); + + // GET /api/tracks/:id — один трек по id + app.get('/tracks/:id', async (req, reply) => { + const tracks = await storage.getTracks(config.storage.songsFile); + const track = tracks.find(t => String(t.id) === req.params.id); + if (!track) return reply.code(404).send({ error: 'Track not found' }); + return track; + }); + + // GET /api/tracks/:id/stream — аудио-стриминг с поддержкой Range + app.get('/tracks/:id/stream', async (req, reply) => { + const tracks = await storage.getTracks(config.storage.songsFile); + const track = tracks.find(t => String(t.id) === req.params.id); + + if (!track) return reply.code(404).send({ error: 'Track not found' }); + + const exists = await storage.fileExists(track.audio); + if (!exists) return reply.code(404).send({ error: 'Audio file not found' }); + + const rangeHeader = req.headers['range']; + const { stream, size, start, end, mime } = await storage.getAudioStream(track.audio, rangeHeader); + + if (rangeHeader) { + reply.code(206).headers({ + 'Content-Type': mime, + 'Content-Range': `bytes ${start}-${end}/${size}`, + 'Accept-Ranges': 'bytes', + 'Content-Length': end - start + 1, + }); + } else { + reply.code(200).headers({ + 'Content-Type': mime, + 'Accept-Ranges': 'bytes', + 'Content-Length': size, + }); + } + + return reply.send(stream); + }); +} diff --git a/server/routes/upload.js b/server/routes/upload.js new file mode 100644 index 0000000..c3755c3 --- /dev/null +++ b/server/routes/upload.js @@ -0,0 +1,46 @@ +import { createWriteStream } from 'fs'; +import { pipeline } from 'stream/promises'; +import { resolve } from 'path'; +import { config } from '../config.js'; +import { storage } from '../storage/index.js'; + +export default async function uploadRoutes(app) { + app.post('/upload', async (req, reply) => { + const parts = req.parts(); + const fields = {}; + let audioFile = null; + + for await (const part of parts) { + if (part.file) { + const safeName = part.filename.replace(/[^a-zA-Z0-9.\-_ ]/g, '_'); + const fileName = `${Date.now()}-${safeName}`; + const filePath = `${config.storage.mediaDir}/${fileName}`; + const absPath = resolve(process.cwd(), filePath); + await pipeline(part.file, createWriteStream(absPath)); + audioFile = { fileName, filePath }; + } else { + fields[part.fieldname] = part.value; + } + } + + if (!audioFile) { + return reply.code(400).send({ error: 'No audio file provided' }); + } + + const tracks = await storage.getTracks(config.storage.songsFile); + const newId = tracks.length ? Math.max(...tracks.map(t => t.id)) + 1 : 1; + + const newTrack = { + id: newId, + title: fields.title || 'Unknown', + artist: fields.artist || 'Unknown', + cover: fields.cover || '', + audio: audioFile.filePath, + }; + + tracks.push(newTrack); + await storage.saveTracks(config.storage.songsFile, tracks); + + return reply.code(201).send(newTrack); + }); +} diff --git a/server/storage/index.js b/server/storage/index.js new file mode 100644 index 0000000..328584b --- /dev/null +++ b/server/storage/index.js @@ -0,0 +1,16 @@ +import { config } from '../config.js'; +import { localAdapter } from './local.js'; + +const adapters = { + local: localAdapter, + // s3: s3Adapter, ← будущие адаптеры подключаются здесь + // backblaze: bbAdapter, +}; + +const adapter = adapters[config.storage.adapter]; + +if (!adapter) { + throw new Error(`Unknown storage adapter: "${config.storage.adapter}"`); +} + +export const storage = adapter; diff --git a/server/storage/local.js b/server/storage/local.js new file mode 100644 index 0000000..fa787e4 --- /dev/null +++ b/server/storage/local.js @@ -0,0 +1,60 @@ +import { readFile, writeFile, stat } from 'fs/promises'; +import { createReadStream } from 'fs'; +import { resolve } from 'path'; + +const root = process.cwd(); + +export const localAdapter = { + /** + * Вернуть список всех треков (из songs.json). + */ + async getTracks(songsFile) { + const data = await readFile(resolve(root, songsFile), 'utf8'); + return JSON.parse(data); + }, + + /** + * Сохранить обновлённый список треков. + */ + async saveTracks(songsFile, tracks) { + await writeFile(resolve(root, songsFile), JSON.stringify(tracks, null, 2), 'utf8'); + }, + + /** + * Вернуть стрим аудиофайла с поддержкой Range. + * @returns {{ stream, size, mime }} + */ + async getAudioStream(filePath, rangeHeader) { + const absPath = resolve(root, filePath); + const { size } = await stat(absPath); + const mime = 'audio/mpeg'; + + if (!rangeHeader) { + return { stream: createReadStream(absPath), size, start: 0, end: size - 1, mime }; + } + + const [startStr, endStr] = rangeHeader.replace('bytes=', '').split('-'); + const start = parseInt(startStr, 10); + const end = endStr ? parseInt(endStr, 10) : Math.min(start + 1024 * 1024, size - 1); + + return { + stream: createReadStream(absPath, { start, end }), + size, + start, + end, + mime, + }; + }, + + /** + * Проверить, существует ли файл. + */ + async fileExists(filePath) { + try { + await stat(resolve(root, filePath)); + return true; + } catch { + return false; + } + }, +}; diff --git a/songs.json b/songs.json deleted file mode 100644 index 5283312..0000000 --- a/songs.json +++ /dev/null @@ -1,322 +0,0 @@ -[ - { - "id" : 1, - "cover" : "https://repository-images.githubusercontent.com/415407744/50faf33c-1ed7-4b37-be52-bde9decae2c9", - "artist" : "RIOT", - "name" : "Overkill", - "link" : "/media/RIOT - Overkill.mp3" - }, - { - "id" : 2, - "cover" : "https://avatars.mds.yandex.net/i?id=1860210a460d55628ad54c38cb0115b8_l-9783932-images-thumbs&n=13", - "artist" : "Ado", - "name" : "Show", - "link" : "/media/Ado - Show.mp3" - }, - { - "id" : 3, - "cover" : "https://images.genius.com/8daaaa0c73899597c9ff7e0e66b84f13.1000x1000x1.png", - "artist" : "Ado", - "name" : "Usseewa", - "link" : "/media/Ado - Usseewa.mp3" - }, - { - "id" : 4, - "cover" : "https://images.genius.com/57d901243d411351ca34035064a3f608.1000x1000x1.jpg", - "artist" : "BABYMETAL", - "name" : "BxMxC", - "link" : "/media/BABYMETAL - BxMxC.mp3" - }, - { - "id" : 5, - "cover" : "https://upload.wikimedia.org/wikipedia/ru/1/12/Babymetal_Divine_Attack_single_cover.jpg", - "artist" : "BABYMETAL", - "name" : "Divine Attack - Shingeki", - "link" : "/media/BABYMETAL - Divine Attack - Shingeki.mp3" - }, - { - "id" : 6, - "cover" : "https://i.ytimg.com/vi/9XGXzmz53uY/maxresdefault.jpg", - "artist" : "BABYMETAL", - "name" : "Karate", - "link" : "/media/BABYMETAL - Karate.mp3" - }, - { - "id" : 7, - "cover" : "https://avatars.yandex.net/get-music-content/163479/6bd01cd7.a.7797117-1/m1000x1000?webp=false", - "artist" : "BABYMETAL feat. F.HERO", - "name" : "PA PA YA!!", - "link" : "/media/BABYMETAL feat. F.HERO - PA PA YA!!.mp3" - }, - { - "id" : 8, - "cover" : "https://tyumen.pult.ru/upload/iblock/19a/19a55d576dbf62be1b8f1b4af7086048.jpg", - "artist" : "BABYMETAL feat. Joakim Brodén", - "name" : "Oh! MAJINAI", - "link" : "/media/BABYMETAL feat. Joakim Brodén - Oh! MAJINAI.mp3" - }, - { - "id" : 9, - "cover" : "https://upload.wikimedia.org/wikipedia/ru/5/5a/BadOmens_TDOPOM.jpg", - "artist" : "Bad Omens", - "name" : "THE DEATH OF PEACE OF MIND", - "link" : "/media/Bad Omens - THE DEATH OF PEACE OF MIND.mp3" - }, - { - "id" : 10, - "cover" : "https://avatars.yandex.net/get-music-content/118603/8b6699e3.a.7787365-1/m1000x1000?webp=false", - "artist" : "Hidden Citizens feat. Rånya", - "name" : "Paint It Black", - "link" : "/media/Hidden Citizens feat. Rånya - Paint It Black.mp3" - }, - { - "id" : 11, - "cover" : "https://avatars.mds.yandex.net/get-marketpic/5962532/pic6432852efa7e9f3c6d103d9cafc22f0b/orig", - "artist" : "Louna", - "name" : "Полюса", - "link" : "/media/Louna - Полюса.mp3" - }, - { - "id" : 12, - "cover" : "https://avatars.yandex.net/get-music-content/108289/e482d95e.a.8112870-1/m1000x1000?webp=false", - "artist" : "MIYAVI", - "name" : "Other Side", - "link" : "/media/MIYAVI - Other Side.mp3" - }, - { - "id" : 13, - "cover" : "https://avatars.yandex.net/get-music-content/4010467/36fe1a48.a.17779951-1/m1000x1000?webp=false", - "artist" : "pyrokinesis", - "name" : "Легенда о Богине Гроз", - "link" : "/media/pyrokinesis - Легенда о Богине Гроз.mp3" - }, - { - "id" : 14, - "cover" : "https://cdn1.ozone.ru/s3/multimedia-m/6600598942.jpg", - "artist" : "Rammstein", - "name" : "Sonne", - "link" : "/media/Rammstein - Sonne.mp3" - }, - { - "id" : 15, - "cover" : "https://avatars.yandex.net/get-music-content/4384958/1ed003ad.a.16201558-1/m1000x1000?webp=false", - "artist" : "RAVANNA", - "name" : "Жги", - "link" : "/media/RAVANNA - Жги.mp3" - }, - { - "id" : 16, - "cover" : "https://i.ytimg.com/vi/BnK_V_yjy2E/maxresdefault.jpg", - "artist" : "RAVANNA feat. Ai Mori", - "name" : "Кома", - "link" : "/media/RAVANNA feat. Ai Mori - Кома.mp3" - }, - { - "id" : 17, - "cover" : "https://avatars.yandex.net/get-music-content/28589/eba28764.a.3104937-1/m1000x1000?webp=false", - "artist" : "Eurielle", - "name" : "City of the dead", - "link" : "/media/Eurielle - City of the dead.mp3" - }, - { - "id" : 18, - "cover" : "https://res.cloudinary.com/epitaph/image/upload/h_925,w_925/v1/epitaph/releases/88888-X_FallingInReverse_Zombified", - "artist" : "Falling In Reverse", - "name" : "ZOMBIFIED", - "link" : "/media/Falling In Reverse - ZOMBIFIED.mp3" - }, - { - "id" : 19, - "cover" : "https://images.genius.com/e61a18e39814f6dd49f65fcb0e198f40.1000x1000x1.jpg", - "artist" : "FIZICA", - "name" : "Готэм 2", - "link" : "/media/FIZICA - Готэм 2.mp3" - }, - { - "id" : 20, - "cover" : "https://images.genius.com/e61a18e39814f6dd49f65fcb0e198f40.1000x1000x1.jpg", - "artist" : "FIZICA", - "name" : "Готэм 3", - "link" : "/media/FIZICA - Готэм 3.mp3" - }, - { - "id" : 21, - "cover" : "https://images.genius.com/e61a18e39814f6dd49f65fcb0e198f40.1000x1000x1.jpg", - "artist" : "FIZICA", - "name" : "Готэм", - "link" : "/media/FIZICA - Готэм.mp3" - }, - { - "id" : 22, - "cover" : "https://images.genius.com/e61a18e39814f6dd49f65fcb0e198f40.1000x1000x1.jpg", - "artist" : "FIZICA", - "name" : "Чокер", - "link" : "/media/FIZICA - Чокер.mp3" - }, - { - "id" : 23, - "cover" : "https://is1-ssl.mzstatic.com/image/thumb/Music123/v4/ab/8c/13/ab8c1397-5cf9-b078-b797-70229a4303e3/cover.jpg/600x600bf-60.jpg", - "artist" : "GroTTesque", - "name" : "Стальное сердце", - "link" : "/media/GroTTesque - Стальное сердце.mp3" - }, - { - "id" : 24, - "cover" : "https://i.scdn.co/image/ab67616d0000b273b03ce145dfc370474ce2757a", - "artist" : "Halestorm", - "name" : "I Miss The Misery", - "link" : "/media/Halestorm - I Miss The Misery.mp3" - }, - { - "id" : 25, - "cover" : "https://static.wikia.nocookie.net/halsey/images/6/67/Badlands_Album_Cover.png", - "artist" : "Halsey", - "name" : "Control", - "link" : "/media/Halsey - Control.mp3" - }, - { - "id" : 26, - "cover" : "https://cdns-images.dzcdn.net/images/cover/02c28dce05f4590e812c43f3f17d8e49/0x1900-000000-80-0-0.jpg", - "artist" : "Hidden Citizens feat. Keeley Bumford", - "name" : "Immortalized", - "link" : "/media/Hidden Citizens feat. Keeley Bumford - Immortalized.mp3" - }, - { - "id" : 27, - "cover" : "https://t2.genius.com/unsafe/1249x0/https%3A%2F%2Fimages.genius.com%2Fa636627b58d07ad23513b2047cc3cea2.1000x1000x1.jpg", - "artist" : "Hidden Citizens feat. Rånya", - "name" : "Let Me Out", - "link" : "/media/Hidden Citizens feat. Rånya - Let Me Out.mp3" - }, - { - "id" : 28, - "cover" : "https://avatars.yandex.net/get-music-content/10930741/8f124fe5.a.29590705-1/m1000x1000?webp=false", - "artist" : "Ado", - "name" : "Unravel", - "link" : "/media/Ado - Unravel.mp3" - }, - { - "id" : 29, - "cover" : "https://avatars.mds.yandex.net/i?id=39a8810483883253ccfbe3488aa79845_l-4553678-images-thumbs&n=13", - "artist" : "Ado", - "name" : "Odo", - "link" : "/media/Ado - Odo.mp3" - }, - { - "id" : 30, - "cover" : "https://static.wikia.nocookie.net/nana/images/4/4e/Rose.jpg", - "artist" : "ANNA TSUCHIYA", - "name" : "Rose", - "link" : "/media/ANNA TSUCHIYA - Rose.mp3" - }, - { - "id" : 31, - "cover" : "https://cdns-images.dzcdn.net/images/cover/5489ee19bd016cfb2067082274fd3367/0x1900-000000-80-0-0.jpg", - "artist" : "Asper X", - "name" : "Bad Trip", - "link" : "/media/Asper X - Bad Trip.mp3" - }, - { - "id" : 32, - "cover" : "https://avatars.yandex.net/get-music-content/5678677/0c73458f.a.21314673-1/m1000x1000?webp=false", - "artist" : "SKYND", - "name" : "Chris Watts", - "link" : "/media/SKYND - Chris Watts.mp3" - } - , - { - "id" : 33, - "cover" : "https://lastfm.freetls.fastly.net/i/u/ar0/695fbe26f80c429dc8ece78a185d9161.jpg", - "artist" : "SKYND", - "name" : "Elisa Lam", - "link" : "/media/SKYND - Elisa Lam.mp3" - } - , - { - "id" : 34, - "cover" : "https://avatars.mds.yandex.net/i?id=eeecd5da9af4722fca7c7e9f04b020b6_l-8174327-images-thumbs&n=13", - "artist" : "SKYND", - "name" : "Jim Jones", - "link" : "/media/SKYND - Jim Jones(2019).mp3" - } - , - { - "id" : 35, - "cover" : "https://avatars.yandex.net/get-music-content/139444/8f8cb75a.a.7911606-2/m1000x1000?webp=false", - "artist" : "SKYND", - "name" : "Katherine Knight", - "link" : "/media/SKYND - Katherine Knight.mp3" - } - , - { - "id" : 36, - "cover" : "https://avatars.yandex.net/get-music-content/139444/8f8cb75a.a.7911606-2/m1000x1000?webp=false", - "artist" : "SKYND", - "name" : "Lay down your life", - "link" : "/media/SKYND - Lay down your life.mp3" - } - , - { - "id" : 37, - "cover" : "https://avatars.yandex.net/get-music-content/4581417/d46e0552.a.15349549-1/m1000x1000?webp=false", - "artist" : "SKYND", - "name" : "Michelle Carter", - "link" : "/media/SKYND - Michelle Carter.mp3" - } - , - { - "id" : 38, - "cover" : "https://avatars.yandex.net/get-music-content/149669/26c23a6e.a.5511746-1/m1000x1000?webp=false", - "artist" : "SKYND", - "name" : "Richard Ramirez", - "link" : "/media/SKYND - Richard Ramirez.mp3" - } - , - { - "id" : 39, - "cover" : "https://avatars.yandex.net/get-music-content/139444/8f8cb75a.a.7911606-2/m1000x1000?webp=false", - "artist" : "SKYND", - "name" : "Tyler Hadley", - "link" : "/media/SKYND - Tyler Hadley.mp3" - } - , - { - "id" : 40, - "cover" : "https://i.ytimg.com/vi/dWW2zXUOIC0/maxresdefault.jpg", - "artist" : "Norma Tale", - "name" : "Кисточка II", - "link" : "/media/Norma Tale - Кисточка II.mp3" - } - , - { - "id" : 41, - "cover" : "https://i.scdn.co/image/ab67616d0000b2731724b0cef6a23963d21c3b8a", - "artist" : "Creepy Nuts", - "name" : "オトノケ(Otonoke)", - "link" : "/media/Creepy Nuts - オトノケ(Otonoke).mp3" - } - , - { - "id" : 42, - "cover" : "https://i.ytimg.com/vi/hNjeAnwGhzc/hqdefault.jpg", - "artist" : "Apashe ft. Vo Williams", - "name" : "Work", - "link" : "/media/Apashe - Work (ft. Vo Williams).mp3" - } - , - { - "id" : 43, - "cover" : "https://i.ytimg.com/vi/mvzdWgfG1vM/maxresdefault.jpg", - "artist" : "blueberry", - "name" : "EVA 3", - "link" : "/media/blueberry - EVA 3.mp3" - } - , - { - "id" : 44, - "cover" : "https://i.ytimg.com/vi/FWBjzQnDb8o/maxresdefault.jpg", - "artist" : "Animadrop", - "name" : "Stuck in a Timeloop", - "link" : "/media/Animadrop - Stuck in a Timeloop.mp3" - } -] \ No newline at end of file diff --git a/style.css b/style.css deleted file mode 100644 index 901a141..0000000 --- a/style.css +++ /dev/null @@ -1,171 +0,0 @@ -html, body{ - margin: 0; - padding: auto; - justify-content: center; -} - -.color { - color: #F2E8C6; -} - -.header { - display: flex; - padding: 0 12px 12px 12px; - background-color: #800000; - margin: auto; - height: 100px; - width: 50%; -} - -.site_name { - color: #F2E8C6; -} - -.menu a { - color: #F2E8C6; - text-decoration: none; -} - - -.sort { - display: flex; - flex-direction: row; - margin-left: auto; -} - - -.songsWrapper { - display: flex; - flex-wrap: wrap; - margin-top: 24px; - margin-left: 25%; - width: 55%; -} - -.song { - width: 190px; - display: flex; - flex-direction: column; - text-align: center; - border-radius: 5px; - margin-right: 24px; - overflow-wrap: break-word; /* Переносит длинные слова */ - word-wrap: break-word; /* Поддержка для более старых браузеров */ - word-break: break-all; /* Позволяет разбивать длинные слова */ - white-space: normal; /* Позволяет переносу текста */ - cursor: pointer; - transition: transform 0.5s ease, background-color 0.5s ease, border-radius 0.5s ease; - -} - -.song:hover { - transform: scale(1.1); - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4); /* Более глубокая тень при наведении */ - border-radius: 5px; - background-color: #800000; - color: #F2E8C6; -} - -.song_image { - height: 190px; - width: 190px; -} - -.song_image img { - object-fit: cover; - height: 100%; - width: 100%; - border-radius: 10px; -} - -.player { - border-radius: 4px; - position: fixed; - bottom: -40px; - display: flex; - align-items: center; - justify-content: space-between; /* Размещение элементов по краям */ - box-sizing: border-box; - flex-direction: row; - padding-right: 10px; - background: rgba(128,0,0,0.7); - box-shadow: 20px 20px 40px -6px rgba(0,0,0,0.2); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - - height: 100px; - width: 52%; - left: 50%; - transform: translate(-50%, -50%); -} - - -.player__info { - display: flex; - justify-content: center; /* Центровка по горизонтали */ - align-items: center; /* Центровка по вертикали */ - text-align: center; - margin-right: auto; - box-sizing: border-box; - padding-right: 10px; - text-align: left; - color: #F2E8C6; -} - - -.player__cover_img { - object-fit: cover; - padding-right: 10px; - border-radius: 4px; - height: 100px; - width: 100px; -} - -.player__info_text { - display: flex; - flex-direction: column; - box-sizing:content-box; -} - - -.player__control { - display: flex; - flex-direction: column; - justify-content: center; - margin: auto; -} - -.player__control button, input{ - cursor: pointer; -} - -.css-button-fully-rounded--green { - min-width: 130px; - height: 40px; - color: #fff; - padding: 5px 10px; - font-weight: bold; - cursor: pointer; - transition: all 0.3s ease; - position: relative; - display: inline-block; - outline: none; - border-radius: 20px; - border: 2px solid #57cc99; - background: #57cc99; - } - .css-button-fully-rounded--green:hover { - background: #fff; - color: #57cc99 - } - - -.player__control__duration { - display: flex; - flex-direction: row; -} - -.player__control__volume { - display: flex; - height: 1005px; -} diff --git a/uploads/app.py b/uploads/app.py new file mode 100644 index 0000000..242318e --- /dev/null +++ b/uploads/app.py @@ -0,0 +1,61 @@ +from flask import Flask, request, jsonify +from werkzeug.utils import secure_filename +import os +import json +import random + +app = Flask(__name__) +app.config['UPLOAD_FOLDER'] = 'media/' +app.config['JSON_FILE'] = 'songs.json' + +# Создаем папку для загрузок, если она не существует +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + +def generate_unique_id(songs): + """Функция для генерации уникального числового ID.""" + while True: + new_id = random.randint(100000, 999999) # Генерируем ID из четырех цифр + if all(song.get('id') != new_id for song in songs): + return new_id + +@app.route('/upload', methods=['POST']) +def upload(): + title = request.form['title'] + artist = request.form['artist'] + audio_file = request.files['audio'] + cover_path = request.form['cover'] + + if audio_file: + # Сохраняем файл аудио + audio_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(audio_file.filename)) + audio_file.save(audio_path) + + # Читаем существующий JSON-файл или создаем новый + try: + with open(app.config['JSON_FILE'], 'r', encoding='utf-8') as f: + songs = json.load(f) + except FileNotFoundError: + songs = [] + + # Генерируем уникальный числовой ID + song_id = generate_unique_id(songs) + + # Добавляем новую песню + songs.append({ + 'id': song_id, + 'title': title, + 'artist': artist, + 'audio': 'uploads/' + audio_path, + 'cover': cover_path + }) + + # Сохраняем обновленный JSON-файл + with open(app.config['JSON_FILE'], 'w', encoding='utf-8') as f: + json.dump(songs, f, ensure_ascii=False, indent=2) + + return jsonify({'message': 'Song uploaded successfully', 'id': song_id}), 200 + else: + return jsonify({'message': 'Failed to upload song'}), 400 + +if __name__ == '__main__': + app.run(debug=True) diff --git a/uploads/songs.json b/uploads/songs.json new file mode 100644 index 0000000..1bd38a8 --- /dev/null +++ b/uploads/songs.json @@ -0,0 +1,612 @@ +[ + { + "id": 1, + "cover": "https://repository-images.githubusercontent.com/415407744/50faf33c-1ed7-4b37-be52-bde9decae2c9", + "artist": "RIOT", + "title": "Overkill", + "audio": "uploads/media/RIOT - Overkill.mp3", + "duration": 343 + }, + { + "id": 2, + "cover": "https://avatars.mds.yandex.net/i?id=1860210a460d55628ad54c38cb0115b8_l-9783932-images-thumbs&n=13", + "artist": "Ado", + "title": "Show", + "audio": "uploads/media/Ado - Show.mp3", + "duration": 190 + }, + { + "id": 3, + "cover": "https://images.genius.com/8daaaa0c73899597c9ff7e0e66b84f13.1000x1000x1.png", + "artist": "Ado", + "title": "Usseewa", + "audio": "uploads/media/Ado - Usseewa.mp3", + "duration": 205 + }, + { + "id": 4, + "cover": "https://images.genius.com/57d901243d411351ca34035064a3f608.1000x1000x1.jpg", + "artist": "BABYMETAL", + "title": "BxMxC", + "audio": "uploads/media/BABYMETAL - BxMxC.mp3", + "duration": 183 + }, + { + "id": 5, + "cover": "https://upload.wikimedia.org/wikipedia/ru/1/12/Babymetal_Divine_Attack_single_cover.jpg", + "artist": "BABYMETAL", + "title": "Divine Attack - Shingeki", + "audio": "uploads/media/BABYMETAL - Divine Attack - Shingeki.mp3", + "duration": 219 + }, + { + "id": 6, + "cover": "https://i.ytimg.com/vi/9XGXzmz53uY/maxresdefault.jpg", + "artist": "BABYMETAL", + "title": "Karate", + "audio": "uploads/media/BABYMETAL - Karate.mp3", + "duration": 264 + }, + { + "id": 7, + "cover": "https://avatars.yandex.net/get-music-content/163479/6bd01cd7.a.7797117-1/m1000x1000?webp=false", + "artist": "BABYMETAL feat. F.HERO", + "title": "PA PA YA!!", + "audio": "uploads/media/BABYMETAL feat. F.HERO - PA PA YA!!.mp3", + "duration": 238 + }, + { + "id": 8, + "cover": "https://tyumen.pult.ru/upload/iblock/19a/19a55d576dbf62be1b8f1b4af7086048.jpg", + "artist": "BABYMETAL feat. Joakim Brodén", + "title": "Oh! MAJINAI", + "audio": "uploads/media/BABYMETAL feat. Joakim Brodén - Oh! MAJINAI.mp3", + "duration": 193 + }, + { + "id": 9, + "cover": "https://upload.wikimedia.org/wikipedia/ru/5/5a/BadOmens_TDOPOM.jpg", + "artist": "Bad Omens", + "title": "THE DEATH OF PEACE OF MIND", + "audio": "uploads/media/Bad Omens - THE DEATH OF PEACE OF MIND.mp3", + "duration": 242 + }, + { + "id": 10, + "cover": "https://avatars.yandex.net/get-music-content/118603/8b6699e3.a.7787365-1/m1000x1000?webp=false", + "artist": "Hidden Citizens feat. Rånya", + "title": "Paint It Black", + "audio": "uploads/media/Hidden Citizens feat. Rånya - Paint It Black.mp3", + "duration": 231 + }, + { + "id": 11, + "cover": "https://avatars.mds.yandex.net/get-marketpic/5962532/pic6432852efa7e9f3c6d103d9cafc22f0b/orig", + "artist": "Louna", + "title": "Полюса", + "audio": "uploads/media/Louna - Полюса.mp3", + "duration": 317 + }, + { + "id": 12, + "cover": "https://avatars.yandex.net/get-music-content/108289/e482d95e.a.8112870-1/m1000x1000?webp=false", + "artist": "MIYAVI", + "title": "Other Side", + "audio": "uploads/media/MIYAVI - Other Side.mp3", + "duration": 230 + }, + { + "id": 13, + "cover": "https://avatars.yandex.net/get-music-content/4010467/36fe1a48.a.17779951-1/m1000x1000?webp=false", + "artist": "pyrokinesis", + "title": "Легенда о Богине Гроз", + "audio": "uploads/media/pyrokinesis - Легенда о Богине Гроз.mp3", + "duration": 205 + }, + { + "id": 14, + "cover": "https://cdn1.ozone.ru/s3/multimedia-m/6600598942.jpg", + "artist": "Rammstein", + "title": "Sonne", + "audio": "uploads/media/Rammstein - Sonne.mp3", + "duration": 246 + }, + { + "id": 15, + "cover": "https://avatars.yandex.net/get-music-content/4384958/1ed003ad.a.16201558-1/m1000x1000?webp=false", + "artist": "RAVANNA", + "title": "Жги", + "audio": "uploads/media/RAVANNA - Жги.mp3", + "duration": 217 + }, + { + "id": 16, + "cover": "https://i.ytimg.com/vi/BnK_V_yjy2E/maxresdefault.jpg", + "artist": "RAVANNA feat. Ai Mori", + "title": "Кома", + "audio": "uploads/media/RAVANNA feat. Ai Mori - Кома.mp3", + "duration": 255 + }, + { + "id": 17, + "cover": "https://avatars.yandex.net/get-music-content/28589/eba28764.a.3104937-1/m1000x1000?webp=false", + "artist": "Eurielle", + "title": "City of the dead", + "audio": "uploads/media/Eurielle - City of the dead.mp3", + "duration": 339 + }, + { + "id": 18, + "cover": "https://res.cloudinary.com/epitaph/image/upload/h_925,w_925/v1/epitaph/releases/88888-X_FallingInReverse_Zombified", + "artist": "Falling In Reverse", + "title": "ZOMBIFIED", + "audio": "uploads/media/Falling In Reverse - ZOMBIFIED.mp3", + "duration": 219 + }, + { + "id": 19, + "cover": "https://images.genius.com/e61a18e39814f6dd49f65fcb0e198f40.1000x1000x1.jpg", + "artist": "FIZICA", + "title": "Готэм 2", + "audio": "uploads/media/FIZICA - Готэм 2.mp3", + "duration": 194 + }, + { + "id": 20, + "cover": "https://images.genius.com/e61a18e39814f6dd49f65fcb0e198f40.1000x1000x1.jpg", + "artist": "FIZICA", + "title": "Готэм 3", + "audio": "uploads/media/FIZICA - Готэм 3.mp3", + "duration": 202 + }, + { + "id": 21, + "cover": "https://images.genius.com/e61a18e39814f6dd49f65fcb0e198f40.1000x1000x1.jpg", + "artist": "FIZICA", + "title": "Готэм", + "audio": "uploads/media/FIZICA - Готэм.mp3", + "duration": 218 + }, + { + "id": 22, + "cover": "https://images.genius.com/e61a18e39814f6dd49f65fcb0e198f40.1000x1000x1.jpg", + "artist": "FIZICA", + "title": "Чокер", + "audio": "uploads/media/FIZICA - Чокер.mp3", + "duration": 196 + }, + { + "id": 23, + "cover": "https://is1-ssl.mzstatic.com/image/thumb/Music123/v4/ab/8c/13/ab8c1397-5cf9-b078-b797-70229a4303e3/cover.jpg/600x600bf-60.jpg", + "artist": "GroTTesque", + "title": "Стальное сердце", + "audio": "uploads/media/GroTTesque - Стальное сердце.mp3", + "duration": 265 + }, + { + "id": 24, + "cover": "https://i.scdn.co/image/ab67616d0000b273b03ce145dfc370474ce2757a", + "artist": "Halestorm", + "title": "I Miss The Misery", + "audio": "uploads/media/Halestorm - I Miss The Misery.mp3", + "duration": 184 + }, + { + "id": 25, + "cover": "https://static.wikia.nocookie.net/halsey/images/6/67/Badlands_Album_Cover.png", + "artist": "Halsey", + "title": "Control", + "audio": "uploads/media/Halsey - Control.mp3", + "duration": 215 + }, + { + "id": 26, + "cover": "https://cdns-images.dzcdn.net/images/cover/02c28dce05f4590e812c43f3f17d8e49/0x1900-000000-80-0-0.jpg", + "artist": "Hidden Citizens feat. Keeley Bumford", + "title": "Immortalized", + "audio": "uploads/media/Hidden Citizens feat. Keeley Bumford - Immortalized.mp3", + "duration": 229 + }, + { + "id": 27, + "cover": "https://t2.genius.com/unsafe/1249x0/https%3A%2F%2Fimages.genius.com%2Fa636627b58d07ad23513b2047cc3cea2.1000x1000x1.jpg", + "artist": "Hidden Citizens feat. Rånya", + "title": "Let Me Out", + "audio": "uploads/media/Hidden Citizens feat. Rånya - Let Me Out.mp3", + "duration": 197 + }, + { + "id": 28, + "cover": "https://avatars.yandex.net/get-music-content/10930741/8f124fe5.a.29590705-1/m1000x1000?webp=false", + "artist": "Ado", + "title": "Unravel", + "audio": "uploads/media/Ado - Unravel.mp3", + "duration": 240 + }, + { + "id": 29, + "cover": "https://avatars.mds.yandex.net/i?id=39a8810483883253ccfbe3488aa79845_l-4553678-images-thumbs&n=13", + "artist": "Ado", + "title": "Odo", + "audio": "uploads/media/Ado - Odo.mp3", + "duration": 209 + }, + { + "id": 30, + "cover": "https://static.wikia.nocookie.net/nana/images/4/4e/Rose.jpg", + "artist": "ANNA TSUCHIYA", + "title": "Rose", + "audio": "uploads/media/ANNA TSUCHIYA - Rose.mp3", + "duration": 228 + }, + { + "id": 31, + "cover": "https://cdns-images.dzcdn.net/images/cover/5489ee19bd016cfb2067082274fd3367/0x1900-000000-80-0-0.jpg", + "artist": "Asper X", + "title": "Bad Trip", + "audio": "uploads/media/Asper X - Bad Trip.mp3", + "duration": 215 + }, + { + "id": 32, + "cover": "https://avatars.yandex.net/get-music-content/5678677/0c73458f.a.21314673-1/m1000x1000?webp=false", + "artist": "SKYND", + "title": "Chris Watts", + "audio": "uploads/media/SKYND - Chris Watts.mp3", + "duration": 278 + }, + { + "id": 33, + "cover": "https://lastfm.freetls.fastly.net/i/u/ar0/695fbe26f80c429dc8ece78a185d9161.jpg", + "artist": "SKYND", + "title": "Elisa Lam", + "audio": "uploads/media/SKYND - Elisa Lam.mp3", + "duration": 244 + }, + { + "id": 34, + "cover": "https://avatars.mds.yandex.net/i?id=eeecd5da9af4722fca7c7e9f04b020b6_l-8174327-images-thumbs&n=13", + "artist": "SKYND", + "title": "Jim Jones", + "audio": "uploads/media/SKYND - Jim Jones(2019).mp3", + "duration": 394 + }, + { + "id": 35, + "cover": "https://avatars.yandex.net/get-music-content/139444/8f8cb75a.a.7911606-2/m1000x1000?webp=false", + "artist": "SKYND", + "title": "Katherine Knight", + "audio": "uploads/media/SKYND - Katherine Knight.mp3", + "duration": 308 + }, + { + "id": 36, + "cover": "https://avatars.yandex.net/get-music-content/139444/8f8cb75a.a.7911606-2/m1000x1000?webp=false", + "artist": "SKYND", + "title": "Lay down your life", + "audio": "uploads/media/SKYND - Lay down your life.mp3", + "duration": 394 + }, + { + "id": 37, + "cover": "https://avatars.yandex.net/get-music-content/4581417/d46e0552.a.15349549-1/m1000x1000?webp=false", + "artist": "SKYND", + "title": "Michelle Carter", + "audio": "uploads/media/SKYND - Michelle Carter.mp3", + "duration": 298 + }, + { + "id": 38, + "cover": "https://avatars.yandex.net/get-music-content/149669/26c23a6e.a.5511746-1/m1000x1000?webp=false", + "artist": "SKYND", + "title": "Richard Ramirez", + "audio": "uploads/media/SKYND - Richard Ramirez.mp3", + "duration": 343 + }, + { + "id": 39, + "cover": "https://avatars.yandex.net/get-music-content/139444/8f8cb75a.a.7911606-2/m1000x1000?webp=false", + "artist": "SKYND", + "title": "Tyler Hadley", + "audio": "uploads/media/SKYND - Tyler Hadley.mp3", + "duration": 292 + }, + { + "id": 40, + "cover": "https://i.ytimg.com/vi/dWW2zXUOIC0/maxresdefault.jpg", + "artist": "Norma Tale", + "title": "Кисточка II", + "audio": "uploads/media/Norma Tale - Кисточка II.mp3", + "duration": 181 + }, + { + "id": 41, + "cover": "https://i.scdn.co/image/ab67616d0000b2731724b0cef6a23963d21c3b8a", + "artist": "Creepy Nuts", + "title": "オトノケ(Otonoke)", + "audio": "uploads/media/Creepy Nuts - オトノケ(Otonoke).mp3", + "duration": 196 + }, + { + "id": 42, + "cover": "https://i.ytimg.com/vi/hNjeAnwGhzc/hqdefault.jpg", + "artist": "Apashe ft. Vo Williams", + "title": "Work", + "audio": "uploads/media/Apashe - Work (ft. Vo Williams).mp3", + "duration": 235 + }, + { + "id": 43, + "cover": "https://i.ytimg.com/vi/mvzdWgfG1vM/maxresdefault.jpg", + "artist": "blueberry", + "title": "EVA 3", + "audio": "uploads/media/blueberry - EVA 3.mp3", + "duration": 166 + }, + { + "id": 44, + "cover": "https://i.ytimg.com/vi/FWBjzQnDb8o/maxresdefault.jpg", + "artist": "Animadrop", + "title": "Stuck in a Timeloop", + "audio": "uploads/media/Animadrop - Stuck in a Timeloop.mp3", + "duration": 416 + }, + { + "id": 45, + "cover": "https://i.ytimg.com/vi/ylIB_Sqm8YM/maxresdefault.jpg", + "artist": "pyrokinesis", + "title": "Легенда о богине Мечей", + "audio": "uploads/media/pyrokinesis - Легенда о Богине Мечей.mp3", + "duration": 235 + }, + { + "id": 482708, + "title": "Snow Drive", + "artist": "Araki", + "audio": "uploads/media/Araki_-_Snow_Drive_.mp3", + "cover": "https://i.ytimg.com/vi/9w1rsWrzBwY/maxresdefault.jpg", + "duration": 267 + }, + { + "id": 942880, + "title": "Teo", + "artist": "Omoi", + "audio": "uploads/media/Omoi_-_Teo.mp3", + "cover": "https://i.ytimg.com/vi/bmkY2yc1K7Q/maxresdefault.jpg", + "duration": 210 + }, + { + "id": 667995, + "title": "Teo", + "artist": "Araki", + "audio": "uploads/media/Araki_-_Teo.mp3", + "cover": "https://i.ytimg.com/vi/WysJSFW0LU8/maxresdefault.jpg", + "duration": 210 + }, + { + "id": 190369, + "title": "I pray", + "artist": "Denkare", + "audio": "uploads/media/Denkare_-_I_pray.mp3", + "cover": "https://i.ytimg.com/vi/mUARzh2zHDo/maxresdefault.jpg", + "duration": 322, + "album": "電気式音楽集II" + }, + { + "id": 816639, + "title": "Amore", + "artist": "BABYMETAL", + "audio": "uploads/media/BABYMETAL_-_05._Amore.mp3", + "cover": "https://i.ytimg.com/vi/uD4QJyWqUiE/maxresdefault.jpg", + "duration": 282, + "album": "Metal Resistance" + }, + { + "id": 962785, + "title": "Storytellers", + "artist": "Foreground Eclipse", + "audio": "uploads/media/Foreground_Eclipse_-_Storytellers.mp3", + "cover": "https://i.ytimg.com/vi/FMvD0-K-hoI/maxresdefault.jpg", + "duration": 233 + }, + { + "id": 719505, + "title": "Реванш", + "artist": "LASCALA", + "audio": "uploads/media/LASCALA_-_.mp3", + "cover": "https://i.ytimg.com/vi/epSHdRWHxG4/maxresdefault.jpg", + "duration": 235 + }, + { + "id": 759704, + "title": "How you like that", + "artist": "BLACKPINK", + "audio": "uploads/media/BLACKPINK_-_How_You_Like_That.mp3", + "cover": "https://cokodive.com/cdn/shop/products/apple-music-blackpink-special-edition-how-you-like-that-15622046023760_1600x.jpg?v=1628411100", + "duration": 181 + }, + { + "id": 194647, + "title": "Revive", + "artist": "UNIONE", + "audio": "uploads/media/UNIONE_-_Revive.mp3", + "cover": "https://cdns-images.dzcdn.net/images/cover/2bab47ee78b6cad51c5634e89518e99a/0x1900-000000-80-0-0.jpg", + "duration": 280 + }, + { + "id": 398316, + "title": "Savege", + "artist": "Bahari", + "audio": "uploads/media/Bahari_-_Savage.mp3", + "cover": "https://lastfm.freetls.fastly.net/i/u/ar0/f7319a8dfcb2932f065ed34263ce7062.jpg", + "duration": 164 + }, + { + "id": 490228, + "title": "Yofukashino Uta", + "artist": "Creepy Nuts", + "audio": "uploads/media/Creepy_Nuts_-_Yofukashino_Uta.mp3", + "cover": "https://cdn-image.zvuk.com/pic?type=release&id=11151184&size=300x300&hash=b3064b6f-5ea0-45dd-bd7d-d520df72da03", + "duration": 240 + }, + { + "id": 489774, + "title": "Hayloft II", + "artist": "Mother Mother", + "audio": "uploads/media/Mother_Mother_-_Hayloft_II.mp3", + "cover": "https://cdn-image.zvuk.com/pic?type=release&id=22125952&size=400x400&hash=09507c10-537c-41f1-b786-4c6a56f2a884", + "duration": 215 + }, + { + "id": 251677, + "title": "IDOL", + "artist": "YOASOBI", + "audio": "uploads/media/YOASOBI_-_IDOL.mp3", + "cover": "https://upload.wikimedia.org/wikipedia/ru/thumb/3/35/Idol-yoasobi.jpg/1200px-Idol-yoasobi.jpg", + "duration": 213 + }, + { + "id": 960084, + "title": "Yuusha", + "artist": "YOASOBI", + "audio": "uploads/media/YOASOBI_Official_Music_Video_TV.mp3", + "cover": "https://sun9-12.userapi.com/s/v1/ig2/1pWeH9tXwkjpaynJFDR4opnI3OSDf3QL6cTXoC0R6AxGE9jKD75WAbstrS-DyPmqDXlTKXL0B0qaCQF6sqwAe_pK.jpg?quality=96&as=32x48,48x72,72x109,108x163,160x242,240x362,360x544,480x725,540x816,640x967,720x1087,1080x1631,1280x1933,1356x2048&from=bu&u=zuEFWghv_k0ENDbNAty1SjwXbnUI1TeZUVw60HubRQI&cs=534x807", + "duration": 204 + }, + { + "id": 292108, + "title": "Hayloft", + "artist": "Mother Mother", + "audio": "uploads/media/Mother_Mother_-_Hayloft_-_LastGangRadio_youtube.mp3", + "cover": "https://lastfm.freetls.fastly.net/i/u/ar0/826cd46695ae445ca9db6ca7d694cdc0.jpg", + "duration": 182 + }, + { + "id": 627635, + "title": "Skygall", + "artist": "Adele", + "audio": "uploads/media/Adele_Skyfall-original.mp3", + "cover": "https://avatars.yandex.net/get-music-content/2114230/0b7e42bb.a.8953684-1/m1000x1000?webp=false", + "duration": 290 + }, + { + "id": 520568, + "title": "Ghost Rule", + "artist": "DECO_27", + "audio": "uploads/media/DECO_27_-_Ghost_Rule.mp3", + "cover": "https://i.ytimg.com/vi/PRFoBWsVXws/maxresdefault.jpg", + "duration": 210 + }, + { + "id": 915302, + "title": "Zombie", + "artist": "Bad Wolves", + "audio": "uploads/media/Bad_Wolves_-_Zombie.mp3", + "cover": "https://lastfm.freetls.fastly.net/i/u/ar0/3a9968f93c6c64d0b888cbfec41c4cc9.jpg", + "duration": 255 + }, + { + "id": 166503, + "title": "Slowmotion", + "artist": "JOHAN x Goddamn", + "audio": "uploads/media/JOHAN_x_Goddamn_-_Slowmotion.mp3", + "cover": "https://avatars.yandex.net/get-music-content/5502420/c092875b.a.24380375-1/m1000x1000?webp=false", + "duration": 238 + }, + { + "id": 348291, + "title": "River", + "artist": "Anonymouz", + "audio": "uploads/media/Anonymouz_-_River__VINLAND_SAGA_SEASON_2_OP.mp3", + "cover": "https://images.genius.com/512bb23b64d6cf045e3721688ae1943f.1000x1000x1.jpg", + "duration": 202 + }, + { + "id": 548170, + "title": "Prove", + "artist": "One Ok Rock", + "audio": "uploads/media/One_Ok_Rock_-_Prove_Official_Audio.mp3", + "cover": "https://avatars.yandex.net/get-music-content/6201394/c980ddcf.a.23430934-1/m1000x1000?webp=false", + "duration": 227 + }, + { + "id": 612840, + "title": "Outta Sight", + "artist": "One Ok Rock", + "audio": "uploads/media/One_Ok_Rock_-_Outta_Sight_Official_Audio.mp3", + "cover": "https://lastfm.freetls.fastly.net/i/u/ar0/d720deebec038c7dae1d9f72d72a6eb2.jpg", + "duration": 202 + }, + { + "id": 333702, + "title": "Mukanjyo", + "artist": "Survive Said The Prophet", + "audio": "uploads/media/Survive_Said_The_Prophet_-_Mukanjyo.mp3", + "cover": "https://i.ytimg.com/vi/ZQE6aj5DhYk/maxresdefault.jpg", + "duration": 192 + }, + { + "id": 942311, + "title": "Kiminosei", + "artist": "the perggies", + "audio": "uploads/media/the_pergies_-_Kiminosei.mp3", + "cover": "https://i.ytimg.com/vi/KbT3JsJmd14/maxresdefault.jpg", + "duration": 263 + }, + { + "id": 910766, + "title": "My Hero", + "artist": "MAN WITH A MISSION", + "audio": "uploads/media/Inuyashiki_OpeningFULL_My_Hero_-_MAN_WITH_A_MISSION.mp3", + "cover": "https://lastfm.freetls.fastly.net/i/u/ar0/eafb171e2d852ef06313555705fd594e.jpg", + "duration": 262 + }, + { + "id": 197571, + "title": "Abyss", + "artist": "YUNGBLUD", + "audio": "uploads/media/YUNGBLUD_-_Abyss.mp3", + "cover": "https://avatars.yandex.net/get-music-content/6386858/17e75da3.a.30447850-2/m1000x1000?webp=false", + "duration": 123 + }, + { + "id": 123760, + "title": "Brave Shine", + "artist": "Aimer", + "audio": "uploads/media/Aimer_-_Brave_Shine.mp3", + "cover": "https://avatars.yandex.net/get-music-content/2359742/7c05cda6.a.10987377-1/m1000x1000?webp=false", + "duration": 232 + }, + { + "id": 204269, + "title": "Gira Gira (ギラギラ)", + "artist": "Ado", + "audio": "uploads/media/Ado_Gira_Gira_.mp3", + "cover": "https://i.ytimg.com/vi/nSZ36z3_10g/maxresdefault.jpg", + "duration": 277 + }, + { + "id": 908633, + "title": "Backlight (逆光)", + "artist": "Ado", + "audio": "uploads/media/Ado_-_Backlight.mp3", + "cover": "https://images.genius.com/fd8b5c7f2a03083abe5734ff76509067.1000x1000x1.png", + "duration": 236 + }, + { + "id": 952950, + "title": "Tot Musica", + "artist": "Ado", + "audio": "uploads/media/Uta_Tot_Musica_AMVMADOne_Piece_Red.mp3", + "cover": "https://images.genius.com/8daaaa0c73899597c9ff7e0e66b84f13.1000x1000x1.png", + "duration": 191 + }, + { + "id": 145404, + "title": "Rabbit Hole (ラビットホール)", + "artist": "DECO*27 feat. 初音ミク", + "audio": "uploads/media/DECO27_-_feat._.mp3", + "cover": "https://i.ytimg.com/vi/eSW2LVbPThw/maxresdefault.jpg", + "duration": 162 + } +] \ No newline at end of file