diff --git a/app/public/background.js b/app/public/background.js index 69fd28c..be9a26c 100644 --- a/app/public/background.js +++ b/app/public/background.js @@ -19,6 +19,9 @@ HBO Now and HBO Max logos are trademark and intellectual property of WarnerMedia /* global chrome */ /* global firebase */ +const APP_VERSION = '1.6.0'; +const APP_VERSION_STRING = APP_VERSION.replace(/\./g, ''); + const firebaseConfig = { apiKey: 'AIzaSyCJLuYpzXLDRmu2ttzFuSGmYcpFxqToWFU', authDomain: 'watch-party-crx.firebaseapp.com', @@ -68,11 +71,40 @@ const sendAuth = async (sendResponse) => { sendResponse(firebase.auth().currentUser); }; -chrome.runtime.onInstalled.addListener(() => { - chrome.tabs.create({ - url: 'https://www.chromewatchparty.com/welcome', - active: true, - }); +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason) { + if (details.reason === 'install') { + if (window.localStorage.getItem('watchPartyVersion')) { + if (window.localStorage.getItem('watchPartyVersion') !== APP_VERSION) { + chrome.tabs.create({ + url: `http://www.chromewatchparty.com/changes/v${APP_VERSION}`, + active: true, + }); + } + } else { + chrome.tabs.create({ + url: 'https://www.chromewatchparty.com/welcome', + active: true, + }); + window.localStorage.setItem('watchPartyVersion', APP_VERSION); + } + } else if (details.reason === 'update') { + if (window.localStorage.getItem('watchPartyVersion')) { + if (window.localStorage.getItem('watchPartyVersion') !== APP_VERSION) { + chrome.tabs.create({ + url: `http://www.chromewatchparty.com/changes/v${APP_VERSION}`, + active: true, + }); + } + } else { + chrome.tabs.create({ + url: `http://www.chromewatchparty.com/changes/v${APP_VERSION}`, + active: true, + }); + window.localStorage.setItem('watchPartyVersion', APP_VERSION); + } + } + } }); chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { diff --git a/app/public/manifest.json b/app/public/manifest.json index 90a4f9c..5df15e1 100644 --- a/app/public/manifest.json +++ b/app/public/manifest.json @@ -17,7 +17,9 @@ "https://*.youtube.com/*", "https://play.hbonow.com/*", "https://play.hbomax.com/*", - "https://*.peacocktv.com/*" + "https://*.peacocktv.com/*", + "https://play.google.com/movies/*", + "https://*.crunchyroll.com/*" ], "content_security_policy": "script-src 'self' https://www.gstatic.com/ https://*.firebaseio.com https://www.googleapis.com; object-src 'self'", "content_scripts": [ @@ -30,7 +32,9 @@ "https://*.youtube.com/*", "https://play.hbonow.com/*", "https://play.hbomax.com/*", - "https://*.peacocktv.com/*" + "https://*.peacocktv.com/*", + "https://play.google.com/*", + "https://*.crunchyroll.com/*" ], "run_at": "document_idle", "js": ["/static/js/content.js"], diff --git a/app/src/Api.js b/app/src/Api.js index 3a5dcf1..4721bde 100644 --- a/app/src/Api.js +++ b/app/src/Api.js @@ -35,6 +35,8 @@ export const PLAYER_SOURCE = { HBONOW: 'hbonow.', HBOMAX: 'hbomax.', PEACOCK: 'peacocktv.', + GOOGLEPLAY: 'play.google.', + CRUNCHYROLL: 'crunchyroll.', LOCAL: 'localhost', }; @@ -48,6 +50,8 @@ export const getPlayerSource = () => { if (HREF.includes(PLAYER_SOURCE.HBONOW)) return PLAYER_SOURCE.HBONOW; if (HREF.includes(PLAYER_SOURCE.HBOMAX)) return PLAYER_SOURCE.HBOMAX; if (HREF.includes(PLAYER_SOURCE.PEACOCK)) return PLAYER_SOURCE.PEACOCK; + if (HREF.includes(PLAYER_SOURCE.GOOGLEPLAY)) return PLAYER_SOURCE.GOOGLEPLAY; + if (HREF.includes(PLAYER_SOURCE.CRUNCHYROLL)) return PLAYER_SOURCE.CRUNCHYROLL; if (HREF.includes(PLAYER_SOURCE.LOCAL)) return PLAYER_SOURCE.LOCAL; }; diff --git a/app/src/VideoListener.js b/app/src/VideoListener.js index b6ad2ba..2c489b3 100644 --- a/app/src/VideoListener.js +++ b/app/src/VideoListener.js @@ -39,6 +39,7 @@ export default class VideoListener { this.onpause = this.onpause.bind(this); this.onplay = this.onplay.bind(this); this.shouldPushSync = this.shouldPushSync.bind(this); + this.crunchyRollListener = this.crunchyRollListener.bind(this); } shouldPushSync() { return ( @@ -73,8 +74,13 @@ export default class VideoListener { this.isUpdating = true; if (this.video.element) { // REMOVE LISTENER BEFORE SYNCING - this.video.element.removeEventListener('playing', this.onplay); - this.video.element.removeEventListener('pause', this.onpause); + switch(this.video.source) { + case PLAYER_SOURCE.CRUNCHYROLL: + window.removeEventListener('watchParty', this.crunchyRollListener); + default: + this.video.element.removeEventListener('playing', this.onplay); + this.video.element.removeEventListener('pause', this.onpause); + } await this.timeout(250); // SEEK if (Math.abs(serverPlayer.time - this.video.element.currentTime) > 1.5) { @@ -114,6 +120,17 @@ export default class VideoListener { simulate(sliderHead, 'mouseup'); await this.timeout(500); break; + case PLAYER_SOURCE.GOOGLEPLAY: + this.video.element.currentTime = serverPlayer.time; + break; + case PLAYER_SOURCE.CRUNCHYROLL: + window.postMessage(JSON.stringify({ + watchPartyDirection: 'TO_SCRIPT', + type: 'WATCH_PARTY_SEEK', + status: serverPlayer.status, + time: serverPlayer.time + })); + break; default: break; } @@ -126,9 +143,18 @@ export default class VideoListener { PLAYER_SOURCE.NETFLIX === this.video.source || PLAYER_SOURCE.HBOMAX === this.video.source || PLAYER_SOURCE.HBONOW === this.video.source - ) + ) { window.postMessage({ type: 'PAUSE', time: serverPlayer.time }, '*'); - else await this.video.element.pause(); + } else if (PLAYER_SOURCE.CRUNCHYROLL === this.video.source) { + window.postMessage(JSON.stringify({ + watchPartyDirection: 'TO_SCRIPT', + type: 'WATCH_PARTY_STATUS', + status: serverPlayer.status, + time: serverPlayer.time + })); + } else { + await this.video.element.pause(); + } break; case PLAYER_STATUS.PLAYING: if ( @@ -136,20 +162,33 @@ export default class VideoListener { PLAYER_SOURCE.NETFLIX === this.video.source || PLAYER_SOURCE.HBOMAX === this.video.source || PLAYER_SOURCE.HBONOW === this.video.source - ) + ) { window.postMessage({ type: 'PLAY', time: serverPlayer.time }, '*'); - else + } else if (PLAYER_SOURCE.CRUNCHYROLL === this.video.source) { + window.postMessage(JSON.stringify({ + watchPartyDirection: 'TO_SCRIPT', + type: 'WATCH_PARTY_STATUS', + status: serverPlayer.status, + time: serverPlayer.time + })); + } else { await this.video.element .play() .catch((err) => console.error('WatchParty erred when syncing play.')); + } break; default: break; } await this.timeout(500); // RE-ADD LISTENERS FOR FUTURE SYNC - this.video.element.addEventListener('playing', this.onplay); - this.video.element.addEventListener('pause', this.onpause); + switch(this.video.source) { + case PLAYER_SOURCE.CRUNCHYROLL: + window.addEventListener('watchParty', this.crunchyRollListener); + default: + this.video.element.addEventListener('playing', this.onplay); + this.video.element.addEventListener('pause', this.onpause); + } } this.isUpdating = false; } @@ -218,6 +257,18 @@ export default class VideoListener { return peacockshow === '' && peacockepisode === '' ? null : peacockshow + '\n' + peacockepisode; + case PLAYER_SOURCE.GOOGLEPLAY: + const playerIframe = document.querySelector('#yDmH0d > div.llhEMd.bYEzqc.LAhoGb.iWO5td > div > div.LhXUod.XL54pf.J9Nfi.iWO5td > span > div > div > iframe'); + var googleplayshow = ''; + if (playerIframe) { + const titleElement = playerIframe.contentWindow.document.querySelector('.playing-title'); + googleplayshow = titleElement ? titleElement.innerText : ''; + } + return googleplayshow === '' ? null : googleplayshow; + case PLAYER_SOURCE.CRUNCHYROLL: + const crunchyrollTitle = document.querySelector('#template_body > div:nth-child(4) > div.showmedia-trail.cf > div > h1'); + const crunchyrollshow = crunchyrollTitle ? crunchyrollTitle.innerText : ''; + return crunchyrollshow === '' ? null : crunchyrollshow; default: break; } @@ -236,6 +287,15 @@ export default class VideoListener { showName: await this.getShowName(), }); } + async crunchyRollListener(ev) { + if (ev.detail.watchPartyDirection === 'TO_APP' && ev.detail.type === 'WATCH_PARTY_PLAYER_UPDATE') { + this.updateStatus({ + status: ev.detail.status, + time: ev.detail.time, + showName: await this.getShowName() + }); + } + } async firstTimeSync(videoElement) { this.updateStatus({ element: videoElement, @@ -249,8 +309,18 @@ export default class VideoListener { this.updateStatus({ showName: await this.getShowName(), }); - videoElement.addEventListener('playing', this.onplay); - videoElement.addEventListener('pause', this.onpause); + switch (this.video.source) { + case PLAYER_SOURCE.CRUNCHYROLL: + window.postMessage(JSON.stringify({ + watchPartyDirection: 'TO_SCRIPT', + type: 'WATCH_PARTY_ADD_LISTENERS' + })); + window.addEventListener('watchParty', this.crunchyRollListener); + break; + default: + videoElement.addEventListener('playing', this.onplay); + videoElement.addEventListener('pause', this.onpause); + } } async start() { this.updateStatus({ source: getPlayerSource(), showName: await this.getShowName() }); @@ -454,6 +524,51 @@ export default class VideoListener { ); } break; + case PLAYER_SOURCE.GOOGLEPLAY: + const playerIframe = document.querySelector('#yDmH0d > div.llhEMd.bYEzqc.LAhoGb.iWO5td > div > div.LhXUod.XL54pf.J9Nfi.iWO5td > span > div > div > iframe'); + if (playerIframe) { + const googlePlayVideoElement = playerIframe.contentWindow.document.querySelector('video'); + if (googlePlayVideoElement) { + this.firstTimeSync(googlePlayVideoElement); + } + } else { + setInterval( + this.video.element + ? null + : () => { + const pif = document.querySelector('#yDmH0d > div.llhEMd.bYEzqc.LAhoGb.iWO5td > div > div.LhXUod.XL54pf.J9Nfi.iWO5td > span > div > div > iframe'); + if (pif) { + const gpve = pif.contentWindow.document.querySelector('video'); + if (gpve) { + this.firstTimeSync(gpve); + } + } + }, + 1000 + ); + } + case PLAYER_SOURCE.CRUNCHYROLL: + const crunchyRollInject = `var sendToWatchPartyApp = function(pS, pT) { var customWatchPartyEvent = new CustomEvent('watchParty', { detail: { watchPartyDirection: 'TO_APP', type: 'WATCH_PARTY_PLAYER_UPDATE', status: pS, time: pT } }); window.dispatchEvent(customWatchPartyEvent); }, watchPartyGetTimeAndSend = function(pS) { if (window.VILOS_PLAYERJS) { window.VILOS_PLAYERJS.getCurrentTime(function(pT) { sendToWatchPartyApp(pS, pT); }); } }, watchPartySendPlaying = function() { watchPartyGetTimeAndSend('PLAYING'); }, watchPartySendPaused = function() { watchPartyGetTimeAndSend('PAUSED'); }, watchPartyInteraction = function(e) { if (e.source === window) { const data = JSON.parse(e.data); if (data.watchPartyDirection === 'TO_SCRIPT') { if (data.type === 'WATCH_PARTY_ADD_LISTENERS') { if (window.VILOS_PLAYERJS) { window.VILOS_PLAYERJS.on('play', watchPartySendPlaying, 'player.js'); window.VILOS_PLAYERJS.on('pause', watchPartySendPaused, 'player.js'); } } else if (data.type === 'WATCH_PARTY_SEEK') { if (window.VILOS_PLAYERJS) { window.VILOS_PLAYERJS.setCurrentTime(data.time); } } else if (data.type === 'WATCH_PARTY_STATUS') { if (window.VILOS_PLAYERJS) { if (data.status === 'PAUSED') window.VILOS_PLAYERJS.pause(); if (data.status === 'PLAYING') window.VILOS_PLAYERJS.play(); } } } } }; window.addEventListener("message", watchPartyInteraction);`; + const crunchyRollInjectScript = document.createElement('script'); + crunchyRollInjectScript.text = crunchyRollInject; + document.head.appendChild(crunchyRollInjectScript); + const crunchyrollIframe = document.getElementById('vilos-player'); + if (crunchyrollIframe) { + this.firstTimeSync(crunchyrollIframe); + } else { + setInterval( + this.video.element + ? null + : () => { + const cif = document.getElementById('vilos-player'); + if (cif) { + this.firstTimeSync(cif); + } + }, + 1000 + ); + } + break; case PLAYER_SOURCE.LOCAL: const localVideoElement = document.getElementsByTagName('video')[0]; if (localVideoElement) { diff --git a/app/src/index.css b/app/src/index.css index 870de2a..5373a24 100644 --- a/app/src/index.css +++ b/app/src/index.css @@ -73,7 +73,8 @@ body.hulu.visible .Player__container,/* HULU */ body.amazon-prime.visible #dv-web-player.dv-player-fullscreen, /* AMAZON PRIME */ body.disney.visible #hudson-wrapper, /* DISNEY */ body.youtube.visible #content.ytd-app, /* YOUTUBE */ -body.youtube.visible #masthead-container /* YOUTUBE HEADER */ { +body.youtube.visible #masthead-container, /* YOUTUBE HEADER */ +body.googleplay.visible div.llhEMd.bYEzqc.LAhoGb.iWO5td /* GOOGLE PLAY MOVIES */ { max-width: var(--body-width) !important; } /* HULU MINIMIZED VIDEO */ @@ -151,6 +152,48 @@ body.peacock.visible .playback-header__container { position: relative; max-width: 100%; } +/* GOOGLE PLAY STYLING */ +body.googleplay.visible { + overflow: hidden; +} +body.googleplay.visible div.llhEMd.bYEzqc.LAhoGb.iWO5td > div.mjANdc.eEPege { + padding: 0 !important; +} +body.googleplay.visible div.llhEMd.bYEzqc.LAhoGb.iWO5td > div.mjANdc.eEPege > div.LhXUod.XL54pf.J9Nfi.iWO5td, +body.googleplay.visible div.llhEMd.bYEzqc.LAhoGb.iWO5td > div > div.LhXUod.XL54pf.J9Nfi.iWO5td > span > div > div > iframe { + min-height: 100%; + width: 100% !important; + height: 100% !important; +} +body.googleplay.visible div.llhEMd.bYEzqc.LAhoGb.iWO5td > div > div.LhXUod.XL54pf.J9Nfi.iWO5td > span > div, +body.googleplay.visible div.llhEMd.bYEzqc.LAhoGb.iWO5td > div > div.LhXUod.XL54pf.J9Nfi.iWO5td > span > div > div { + height: 100%; +} +/* CRUNCHYROLL STYLING */ +body.crunchyroll.visible #showmedia_video { + position: fixed; + z-index: 99999; + width: calc(100% - var(--party-width)); + background: black; + top: 0; + left: 0; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} +body.crunchyroll.visible #showmedia_video_box { + width: 100% !important; + height: 100% !important; +} +body.crunchyroll.visible #showmedia_video_player { + display: flex !important; + align-items: center !important; +} +body.crunchyroll.visible #vilos-player { + max-height: 42vw !important; +} + /* FULL SCREEN BUTTON REMOVER diff --git a/app/src/index.js b/app/src/index.js index c68d050..59c1905 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -95,6 +95,12 @@ switch (getPlayerSource()) { case PLAYER_SOURCE.PEACOCK: document.body.classList.add('peacock'); break; + case PLAYER_SOURCE.GOOGLEPLAY: + document.body.classList.add('googleplay'); + break; + case PLAYER_SOURCE.CRUNCHYROLL: + document.body.classList.add('crunchyroll'); + break; case PLAYER_SOURCE.LOCAL: document.body.classList.add('local'); break; diff --git a/functions/app.js b/functions/app.js index c4a460f..efabb82 100644 --- a/functions/app.js +++ b/functions/app.js @@ -7,7 +7,8 @@ const cors = require('cors'); // WEBHOOKS const webhooks = express(); webhooks.use(cors({ origin: true })); -webhooks.all('/conferenceEnded',Webhooks.conferenceEnd); +webhooks.all('/conferenceEnded', Webhooks.conferenceEnd); +webhooks.all('/scheduledCleanup', Webhooks.scheduledCleanUp); // VERSION 1 const app = express(); diff --git a/functions/index.js b/functions/index.js index 605dd62..0fae717 100644 --- a/functions/index.js +++ b/functions/index.js @@ -1,4 +1,3 @@ -const FunctionModules = require('./modules/index'); const functions = require('firebase-functions'); const secret = require('./FirebaseSecret.json'); const admin = require('firebase-admin'); @@ -15,9 +14,5 @@ admin.initializeApp(FIREBASE_OPTIONS); exports.widgets = functions.https.onRequest(REST.v1); // VERSION 2 exports.widgets2 = functions.https.onRequest(REST.v2); - -exports.scheduledCleanUp = functions.pubsub - .schedule('every 60 minutes') - .onRun(FunctionModules.scheduledCleanUp); - +// WEBHOOKS exports.webhooks = functions.https.onRequest(REST.webhooks); diff --git a/functions/modules/index.js b/functions/modules/index.js index b3888b1..5433a8c 100644 --- a/functions/modules/index.js +++ b/functions/modules/index.js @@ -1,6 +1,4 @@ const authenticator = require('./authenticator'); -const cleanUpRoom = require('./cleanUpRoom'); -const scheduledCleanUp = require('./scheduledCleanUp'); const createRoom = require('./createRoom'); const joinRoom = require('./joinRoom'); const sendMessage = require('./sendMessage'); @@ -21,8 +19,6 @@ module.exports = { updatePlayer, leaveRoom, deleteRoom, - scheduledCleanUp, - cleanUpRoom, timeDiff, tokenValidator, tokenRefresher, diff --git a/functions/modules/updatePlayer.js b/functions/modules/updatePlayer.js index 7ae9253..2ccd39c 100644 --- a/functions/modules/updatePlayer.js +++ b/functions/modules/updatePlayer.js @@ -2,18 +2,6 @@ const admin = require('firebase-admin'); module.exports = async (req, res) => { const { player, roomId, uid, showChanged, userName } = JSON.parse(req.body); - // const statusString = - // player.status === 'PLAYING' - // ? 'started playing from' - // : player.status === 'PAUSED' - // ? 'paused at' - // : player.status === 'NOT_PLAYING' - // ? 'stopped at' - // : 'is at'; - // const timeString = - // player.time > 3600 - // ? new Date(player.time * 1000).toISOString().substr(11, 8) - // : new Date(player.time * 1000).toISOString().substr(14, 5); if (!roomId) return res.status(204).send({ message: 'Party name not sent.' }); const [roomExists, roomData] = await admin .firestore() @@ -32,17 +20,19 @@ module.exports = async (req, res) => { .then((userSnap) => userSnap.exists); if (userExists) { if (showChanged === true && player.showName) { - await admin - .firestore() - .collection('rooms') - .doc(roomId) - .collection('messages') - .add({ - uid: '', - name: '', - text: `${userName} changed \n${player.showName ? 'to ' + player.showName : ''}`, - time: new Date(), - }); + if (roomData.player.showName !== player.showName) { + await admin + .firestore() + .collection('rooms') + .doc(roomId) + .collection('messages') + .add({ + uid: '', + name: '', + text: `${userName} changed \n${player.showName ? 'to ' + player.showName : ''}`, + time: new Date(), + }); + } } if (roomData.status !== player.status) { await admin @@ -56,23 +46,6 @@ module.exports = async (req, res) => { console.error(`Failed to update player : \n\t Room: ${roomId} \n\t ${uid}`, err); res.status(500).send({ message: "Uh oh party could't be synced", err }); }); - // await admin - // .firestore() - // .collection('rooms') - // .doc(roomId) - // .collection('messages') - // .add({ - // uid: '', - // name: '', - // text: `${userName} ${statusString} ${timeString}`, - // time: new Date(), - // }) - // .catch((err) => { - // console.error( - // `Failed to add system update player message : \n\t Room: ${roomId} \n\t ${uid}`, - // err - // ); - // }); return res.status(200).send({ message: 'Synced party player' }); } else { return res.status(208).send({ message: 'Player already up to date' }); diff --git a/functions/modules2/updatePlayer.js b/functions/modules2/updatePlayer.js index 7ae9253..9783f51 100644 --- a/functions/modules2/updatePlayer.js +++ b/functions/modules2/updatePlayer.js @@ -32,7 +32,8 @@ module.exports = async (req, res) => { .then((userSnap) => userSnap.exists); if (userExists) { if (showChanged === true && player.showName) { - await admin + if (player.showChanged !== roomData.player.showName) { + await admin .firestore() .collection('rooms') .doc(roomId) @@ -43,6 +44,7 @@ module.exports = async (req, res) => { text: `${userName} changed \n${player.showName ? 'to ' + player.showName : ''}`, time: new Date(), }); + } } if (roomData.status !== player.status) { await admin diff --git a/functions/modules/cleanUpRoom.js b/functions/webhooks/cleanUpRoom.js similarity index 100% rename from functions/modules/cleanUpRoom.js rename to functions/webhooks/cleanUpRoom.js diff --git a/functions/webhooks/index.js b/functions/webhooks/index.js index 5cf0ebb..b6655a4 100644 --- a/functions/webhooks/index.js +++ b/functions/webhooks/index.js @@ -1,5 +1,7 @@ const conferenceEnd = require('./conferenceEnd'); +const scheduledCleanUp = require('./scheduledCleanUp'); module.exports = { conferenceEnd, + scheduledCleanUp }; diff --git a/functions/modules/scheduledCleanUp.js b/functions/webhooks/scheduledCleanUp.js similarity index 92% rename from functions/modules/scheduledCleanUp.js rename to functions/webhooks/scheduledCleanUp.js index 2fc91a5..1c3caa0 100644 --- a/functions/modules/scheduledCleanUp.js +++ b/functions/webhooks/scheduledCleanUp.js @@ -9,8 +9,9 @@ const timeDiff = (ds1, ds2) => { return Math.abs(Math.round(diff)); }; -module.exports = async () => { +module.exports = async (req, res) => { const startedTime = new Date(); + res.status(200).send({message:'Started processing'}); const roomList = await admin .firestore() .collection('rooms') @@ -29,7 +30,7 @@ module.exports = async () => { if ( // MORE THAN 24 HOURS AND NOT IN USE (timeDiff(roomSnap.createTime.toDate(), startedTime) >= 24 && - timeDiff(roomSnap.updateTime.toDate(), startedTime) >= 12) || + timeDiff(roomSnap.updateTime.toDate(), startedTime) >= 4) || // LESS THAN 24 HOURS BUT NOT IN USE timeDiff(roomSnap.updateTime.toDate(), startedTime) >= 12 ) { @@ -45,4 +46,5 @@ module.exports = async () => { ); }); }); + return; };