commit 16e3b7bd30a12f2a44bdd52db871fd0d6db63eec
parent 3e01d5479a0390b65981c4d37bdc26d9aa72cfcd
Author: minerva-jupiter <ryouturn@gmail.com>
Date: Sat, 22 Mar 2025 20:32:53 +0900
とりあえずcommit
Diffstat:
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