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`;