diff --git a/WebApi.cs b/WebApi.cs index 24d4fac0..11d88a94 100644 --- a/WebApi.cs +++ b/WebApi.cs @@ -147,6 +147,10 @@ namespace VRCX { request.UserAgent = value; } + else if (string.Compare(key, "Referer", StringComparison.OrdinalIgnoreCase) == 0) + { + request.Referer = value; + } else { request.Headers.Add(key, value); diff --git a/html/src/app.js b/html/src/app.js index 87e2d7ec..671994b9 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -4329,14 +4329,19 @@ speechSynthesis.getVoices(); if ( noty.type === 'Notification' || noty.type === 'LocationDestination' + // skip unused entries ) { return; } - // remove current user + if (noty.type === 'VideoPlay' && !noty.videoName) { + // skip videos without names + return; + } if ( noty.type !== 'VideoPlay' && noty.displayName === API.currentUser.displayName ) { + // remove current user return; } noty.isFriend = false; @@ -4864,11 +4869,7 @@ speechSynthesis.getVoices(); this.speak(noty.data); break; case 'VideoPlay': - var videoName = ''; - if (noty.videoName) { - videoName = `: ${noty.videoName}`; - } - this.speak(`Now playing video${videoName}`); + this.speak(`Now playing: ${noty.videoName}`); break; case 'BlockedOnPlayerJoined': this.speak(`Blocked user ${noty.displayName} has joined`); @@ -5051,13 +5052,9 @@ speechSynthesis.getVoices(); AppApi.XSNotification('VRCX', noty.data, timeout, image); break; case 'VideoPlay': - var videoName = noty.videoUrl; - if (noty.videoName) { - videoName = noty.videoName; - } AppApi.XSNotification( 'VRCX', - `Now playing: ${videoName}`, + `Now playing: ${noty.videoName}`, timeout, image ); @@ -5238,11 +5235,11 @@ speechSynthesis.getVoices(); AppApi.DesktopNotification('Event', noty.data, image); break; case 'VideoPlay': - var videoName = noty.videoUrl; - if (noty.videoName) { - videoName = noty.videoName; - } - AppApi.DesktopNotification('Now playing', videoName, image); + AppApi.DesktopNotification( + 'Now playing', + noty.videoName, + image + ); break; case 'BlockedOnPlayerJoined': AppApi.DesktopNotification( @@ -7382,14 +7379,8 @@ speechSynthesis.getVoices(); database.addGamelogPortalSpawnToDatabase(entry); break; case 'video-play': - var entry = { - created_at: gameLog.dt, - type: 'VideoPlay', - data: gameLog.videoUrl, - displayName: gameLog.displayName - }; - database.addGamelogVideoPlayToDatabase(entry); - break; + this.addGameLogVideo(gameLog, location, userId, pushToTable); + return; case 'notification': var entry = { created_at: gameLog.dt, @@ -7412,6 +7403,90 @@ speechSynthesis.getVoices(); } }; + $app.methods.addGameLogVideo = async function ( + gameLog, + location, + userId, + pushToTable + ) { + var videoUrl = gameLog.videoUrl; + var youtubeVideoId = ''; + var videoId = ''; + var videoName = ''; + var videoLength = ''; + var displayName = ''; + if (typeof gameLog.displayName !== 'undefined') { + displayName = gameLog.displayName; + } + try { + var url = new URL(videoUrl); + var id1 = url.pathname; + var id2 = url.searchParams.get('v'); + if (id1 && id1.length === 12) { + youtubeVideoId = id2.substring(1, 12); + } + if (id2 && id2.length === 11) { + youtubeVideoId = id2; + } + if (this.youTubeApi && youtubeVideoId) { + var data = await this.lookupYouTubeVideo(youtubeVideoId); + if ( + data || + (data.status === 200 && data.pageInfo.totalResults !== 0) + ) { + videoId = 'YouTube'; + videoName = data.items[0].snippet.title; + videoLength = this.convertYoutubeTime( + data.items[0].contentDetails.duration + ); + } else { + console.error(`YouTube video lookup failed status: ${status}`); + } + } + } catch { + console.error(`Invalid URL: ${url}`); + } + var entry = { + created_at: gameLog.dt, + type: 'VideoPlay', + videoUrl, + videoId, + videoName, + videoLength, + location, + displayName, + userId + }; + if (pushToTable) { + this.queueGameLogNoty(entry); + this.gameLogTable.data.push(entry); + } + database.addGamelogVideoPlayToDatabase(entry); + }; + + $app.methods.lookupYouTubeVideo = async function (videoId) { + var data = {}; + var apiKey = 'AIzaSyA-iUQCpWf5afEL3NanEOSxbzziPMU3bxY'; + if (this.youTubeApiKey) { + apiKey = this.youTubeApiKey; + } + try { + var response = await webApiService.execute({ + url: `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&part=snippet,contentDetails&key=${apiKey}`, + method: 'GET', + headers: { + 'User-Agent': appVersion, + Referer: 'https://vrcx.pypy.moe' + } + }); + data = JSON.parse(response.data); + data.status = response.status; + } catch { + console.error(`YouTube video lookup failed for ${videoId}`); + } + return data; + }; + $app.methods.sweepGameLog = function () { var {data} = this.gameLogTable; // 로그는 7일까지만 남김 @@ -9092,6 +9167,9 @@ speechSynthesis.getVoices(); this.updateVRConfigVars(); }; + $app.data.youTubeApi = configRepository.getBool('VRCX_youtubeAPI'); + $app.data.youTubeApiKey = configRepository.getString('VRCX_youtubeAPIKey'); + var downloadProgressStateChange = function () { this.updateVRConfigVars(); }; @@ -13872,6 +13950,57 @@ speechSynthesis.getVoices(); this.VRChatConfigFile.screenshot_res_width = res.width; }; + // YouTube API + + $app.data.youTubeApiKey = ''; + + $app.data.youTubeApiDialog = { + visible: false + }; + + API.$on('LOGOUT', function () { + $app.youTubeApiDialog.visible = false; + }); + + $app.methods.testYouTubeApiKey = async function () { + if (!this.youTubeApiKey) { + this.$message({ + message: 'YouTube API key removed', + type: 'success' + }); + this.youTubeApiDialog.visible = false; + return; + } + var data = await this.lookupYouTubeVideo('dQw4w9WgXcQ'); + if (!data || data.status !== 200) { + this.youTubeApiKey = ''; + this.$message({ + message: `Invalid YouTube API key, error code: ${data.status}`, + type: 'error' + }); + } else { + configRepository.setString( + 'VRCX_youtubeAPIKey', + this.youTubeApiKey + ); + this.$message({ + message: 'YouTube API key valid!', + type: 'success' + }); + } + this.youTubeApiDialog.visible = false; + }; + + $app.methods.changeYouTubeApi = function () { + configRepository.setBool('VRCX_youtubeAPI', this.youTubeApi); + }; + + $app.methods.showYouTubeApiDialog = function () { + this.$nextTick(() => adjustDialogZ(this.$refs.youTubeApiDialog.$el)); + var D = this.youTubeApiDialog; + D.visible = true; + }; + // Asset Bundle Cacher $app.methods.updateVRChatWorldCache = function () { diff --git a/html/src/index.pug b/html/src/index.pug index a9be5fd1..faae7ee7 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -904,8 +904,7 @@ html div.options-container-item el-switch(v-model="worldAutoCacheGPSFilter" @change="saveOpenVROption" inactive-text="VIP" active-text="Everyone" :disabled="worldAutoCacheGPS === 'Never'") div.options-container-item - el-button-group - el-button(size="small" icon="el-icon-download" @click="showDownloadDialog") Download History + el-button(size="small" icon="el-icon-download" @click="showDownloadDialog") Download History br span.sub-header Automatically Manage Cache When Closing VRChat div.options-container-item @@ -929,6 +928,13 @@ html el-button-group el-button(size="small" icon="el-icon-s-operation" @click="showLaunchOptions()") Launch Options el-button(size="small" icon="el-icon-s-operation" @click="showVRChatConfig()") VRChat config.json + div.options-container + span.header YouTube API + div.options-container-item + span.name Enabled + el-switch(v-model="youTubeApi" @change="changeYouTubeApi") + div.options-container-item + el-button(size="small" icon="el-icon-caret-right" @click="showYouTubeApiDialog") YouTube API Key div.options-container(style="margin-top:45px;border-top:1px solid #eee;padding-top:30px") span.header Legal Notice div.options-container-item @@ -1732,6 +1738,16 @@ html el-button(size="small" @click="VRChatConfigDialog.visible = false") Cancel el-button(type="primary" size="small" :disabled="VRChatConfigDialog.loading" @click="saveVRChatConfigFile") Save + //- dialog: YouTube Api Dialog + el-dialog.x-dialog(ref="youTubeApiDialog" :visible.sync="youTubeApiDialog.visible" title="YouTube API" width="400px") + div(style='font-size:12px;') + | Enter your YouTube API Key #[br] + el-input(type="textarea" v-model="youTubeApiKey" placeholder="YouTube API Key" maxlength="39" show-word-limit style="dispaly:block;margin-top:10px") + template(#footer) + div(style="display:flex") + el-button(size="small" @click="openExternalLink('https://rapidapi.com/blog/how-to-get-youtube-api-key/')") Guide + el-button(type="primary" size="small" @click="testYouTubeApiKey" style="margin-left:auto") Save + //- dialog: Cache Download el-dialog.x-dialog(ref="downloadDialog" :visible.sync="downloadDialog.visible" title="Download History" width="770px") div(v-if="downloadInProgress && downloadCurrent.ref") diff --git a/html/src/vr.js b/html/src/vr.js index c33ad7dd..7fd90882 100644 --- a/html/src/vr.js +++ b/html/src/vr.js @@ -400,11 +400,7 @@ speechSynthesis.getVoices(); text = noty.data; break; case 'VideoPlay': - var videoName = noty.videoUrl; - if (noty.videoName) { - videoName = noty.videoName; - } - text = `Now playing: ${videoName}`; + text = `Now playing: ${noty.videoName}`; break; case 'BlockedOnPlayerJoined': text = `Blocked user ${noty.displayName} has joined`;