yt-audio-player

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 16e3b7bd30a12f2a44bdd52db871fd0d6db63eec
parent 3e01d5479a0390b65981c4d37bdc26d9aa72cfcd
Author: minerva-jupiter <ryouturn@gmail.com>
Date:   Sat, 22 Mar 2025 20:32:53 +0900

とりあえずcommit

Diffstat:
Msrc/components/AlbumList.vue | 4++--
Msrc/components/PlayUI.vue | 17+++++++++++++----
Msrc/components/SongList.vue | 20+++++++++++++++++---
Asrc/player.ts | 37+++++++++++++++++++++++++++++++++++++
Asrc/youtube.js | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 149 insertions(+), 9 deletions(-)

diff --git a/src/components/AlbumList.vue b/src/components/AlbumList.vue @@ -3,9 +3,9 @@ import albums from '../assets/albums.json'; </script> <template> - <h2>AlbumList: {{ albums[$route.params.artist_id].AlbumArtist }}</h2> + <h2>AlbumList: {{ albums[Number($route.params.artist_id)].AlbumArtist }}</h2> <div class="row mt-3"> - <div class="card m-3" style="width: 18rem;" v-for="(Albums, index) in albums[$route.params.artist_id].Albums" @click="() => $router.push(`/artist/${$route.params.artist_id}/album/${index}`)"> + <div class="card m-3" style="width: 18rem;" v-for="(Albums, index) in albums[Number($route.params.artist_id)].Albums" @click="() => $router.push(`/artist/${$route.params.artist_id}/album/${index}`)"> <img class="card-img-top" :src=Albums.items[0].snippet.thumbnails.medium.url alt="Album Cover" /> <div class="card-body"> <h5 class="card-title">{{ Albums.AlbumTitle }}</h5> diff --git a/src/components/PlayUI.vue b/src/components/PlayUI.vue @@ -1,7 +1,16 @@ <template> <div class="fixed-bottom"> - <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16"> - <path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393"/> + <div class="d-flex justify-content-center"> + <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" fill="currentColor" class="bi bi-stop-circle" viewBox="0 0 16 16"> + <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16"/> + <path d="M5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5z"/> </svg> + <svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" fill="currentColor" class="bi bi-chevron-bar-right" viewBox="0 0 16 16"> + <path fill-rule="evenodd" d="M4.146 3.646a.5.5 0 0 0 0 .708L7.793 8l-3.647 3.646a.5.5 0 0 0 .708.708l4-4a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708 0M11.5 1a.5.5 0 0 1 .5.5v13a.5.5 0 0 1-1 0v-13a.5.5 0 0 1 .5-.5"/> + </svg> + </div> </div> -</template>- \ No newline at end of file +</template> + +<script setup lang="ts"> +</script>+ \ No newline at end of file diff --git a/src/components/SongList.vue b/src/components/SongList.vue @@ -1,13 +1,27 @@ <script setup lang="ts"> import albums from '../assets/albums.json'; -albums[0].Albums[0].items[0].snippet.title +import player from '../player'; + import type { Queue } from '../player'; + import { useRoute } from 'vue-router'; + const route = useRoute(); + + const play_song = (index: number) => { + let songs: Queue[] = []; + for (let i = 0; i < albums[Number(route.params.artist_id)].Albums[Number(route.params.album_id)].items.length; i++) { + if (i >= index) { + const song: Queue = {title: albums[Number(route.params.artist_id)].Albums[Number(route.params.album_id)].items[i].snippet.title, video_id: albums[Number(route.params.artist_id)].Albums[Number(route.params.album_id)].items[i].snippet.resourceId.videoId}; + songs.push(song); + } + } + player.add_to_queue(songs); + } </script> <template> - <h2>{{ albums[$route.params.artist_id].Albums[$route.params.album_id].AlbumTitle }}</h2> + <h2>{{ albums[Number($route.params.artist_id)].Albums[Number($route.params.album_id)].AlbumTitle }}</h2> <div> <ul class="list-group"> - <li v-for="(song, index) in albums[$route.params.artist_id].Albums[$route.params.album_id].items" class="list-group-item"> + <li v-for="(song, index) in albums[Number($route.params.artist_id)].Albums[Number($route.params.album_id)].items" class="list-group-item" @click="() => play_song(index)"> {{ song.snippet.title }} </li> </ul> diff --git a/src/player.ts b/src/player.ts @@ -0,0 +1,36 @@ +import { ref } from 'vue'; + + export interface Queue { + video_id: string; + title: string; + }; + + let queue: Queue[] = []; + const isPlaying = ref(false); + + function add_to_queue(list: Queue[]): void { + console.log("add_to_queue was called"); + for (let i = 0; i < list.length; i++) { + queue.push(list[i]); + }; + console.log(queue); + } + + function remove_queue(): void { + console.log("remove_queue was called"); + queue = []; + } + + function next_song(): void { + console.log("next_song was called"); + queue.shift(); + console.log(queue); + } + + function play(): void { + console.log("play_pause was called"); + isPlaying.value = !isPlaying.value; + } + + + export default {add_to_queue, remove_queue, next_song, play, isPlaying};+ \ No newline at end of file diff --git a/src/youtube.js b/src/youtube.js @@ -0,0 +1,79 @@ +import axios from 'axios'; + import vm from 'vm'; + + let videoId = 'aqz-KE-bpKQ'; + + /** + * From the Youtube API, retrieve metadata about the video (title, video format and audio format) + */ + async function retrieveMetadata(videoId) { + const response = await axios.post('https://www.youtube.com/youtubei/v1/player', { + "videoId": videoId, + "context": { + "client": { "clientName": "WEB", "clientVersion": "2.20230810.05.00" } + } + }); + + const formats = response.data.streamingData.adaptiveFormats; + + return [ + response.data.videoDetails.title, + formats.filter(w => w.mimeType.startsWith("video/webm"))[0], + formats.filter(w => w.mimeType.startsWith("audio/webm"))[0], + ]; + } + + /** + * From the Youtube Web Page, retrieve the challenge algorithm for the n query parameter + */ + async function retrieveChallenge(video_id){ + + /** + * Find the URL of the javascript file for the current player version + */ + async function retrieve_player_url(video_id) { + let response = await axios.get('https://www.youtube.com/embed/' + video_id); + let player_hash = /\/s\/player\/(\w+)\/player_ias.vflset\/\w+\/base.js/.exec(response.data)[1] + return `https://www.youtube.com/s/player/${player_hash}/player_ias.vflset/en_US/base.js` + } + + const player_url = await retrieve_player_url(video_id); + + const response = await axios.get(player_url); + let challenge_name = /\.get\("n"\)\)&&\(b=([a-zA-Z0-9$]+)(?:\[(\d+)\])?\([a-zA-Z0-9]\)/.exec(response.data)[1]; + challenge_name = new RegExp(`var ${challenge_name}\\s*=\\s*\\[(.+?)\\]\\s*[,;]`).exec(response.data)[1]; + + const challenge = new RegExp(`${challenge_name}\\s*=\\s*function\\s*\\(([\\w$]+)\\)\\s*{(.+?}\\s*return\\ [\\w$]+.join\\(""\\))};`, "s").exec(response.data)[2]; + + return challenge; + } + + /** + * Solve the challenge and replace the n query parameter from the url + */ + function solveChallenge(challenge, formatUrl) { + const url = new URL(formatUrl); + + const n = url.searchParams.get("n"); + const n_transformed = vm.runInNewContext(`((a) => {${challenge}})('${n}')`); + + url.searchParams.set("n", n_transformed); + return url.toString(); + } + + + console.log() + console.log("Retrieving metadata") + const [title, video, audio] = await retrieveMetadata(videoId); + const challenge = await retrieveChallenge(videoId); + + console.log() + console.log("Solving challenge") + video.url = solveChallenge(challenge, video.url); + audio.url = solveChallenge(challenge, audio.url); + + export async function get_url(video_id){ + const [title, video, audio] = await retrieveMetadata(video_id); + audio.url = solveChallenge(challenge, audio.url); + return(audio.url); + }+ \ No newline at end of file