diff --git a/README.md b/README.md index decd0a7..d18d32a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Features: - Puts you in charge of your playlist by saving all video information necessarry to play (name, time, video_id) with the playlist. To save your playlist, simply bookmark the open tab of Streamly or click "Save Playlist" to copy a link to your clipboard. - Runs independently from any server and requires no association with YouTube (API keys, etc...). + + Note: You may even use Streamly just stored on your computer, but likely YouTube will give you an error stating that the video creator did not allow the video to play on this site. The same will occur if you try to open a YouTube embed on your computer, as they don't recognize the `file://` site location as a valid website. You may resolve this easily by placing Streamly on a file hosting service that allows you to access the file without downloading (ie. forking on GitHub). - Works on all modern browsers (Chrome, Firefox, more...) as well as browsers on Android phones/tablets. diff --git a/index.html b/index.html index b7ea90f..291c942 100644 --- a/index.html +++ b/index.html @@ -26,9 +26,21 @@

Streamly - + +
+
+
+
Close
+

Settings


+

Streamly Station


+

Connect to a Streamly Station

+
+ + +
+
@@ -118,8 +130,21 @@

Streamly Streamly " + name + "" + - "" + time + ""; + var trElement = "" + name + "" + + "" + time + ""; if ($("#videosTable > tr").length > 0) { if (spot > 1) { $("#videosTable > tr").eq(spot-2).after(trElement); @@ -239,11 +244,13 @@ function loopVideo() { var VideoFunctions = function() { this.play = function() { + sendStation("videofunctionsplay"); videoPaused = false; document.title = "Streamly - " + decodeURIComponent(videos[videoIteration][0]); $("#favicon").attr("href", faviconPlay); } this.pause = function() { + sendStation("videofunctionspause"); videoPaused = true; if (videos[0] !== undefined && videos[0] !== null) { document.title = "Streamly - " + decodeURIComponent(videos[0]); @@ -255,12 +262,14 @@ var VideoFunctions = function() { var videoFunctions = new VideoFunctions(); function forwardVideo() { + sendStation("forwardvideo"); if (changeIteration(1) <= videoCounter) { loopVideo(); } } function backVideo() { + sendStation("backvideo"); if (!backRestart) { if (changeIteration(-2) > -1) { videoIteration = changeIteration(-2); @@ -316,6 +325,35 @@ function getPlaylist() { } } +function appendPlaylist(playlist) { + try { + playlist = window.atob(playlist); + playlist = JSON.parse(playlist); + + if (playlist[0] !== undefined && playlist[0] !== null) { + if (videos[0] === undefined || videos[0] === null) { + $("#playlistNameBox").val(decodeURIComponent(playlist[0])); + } + } + + for (var i = 1; i < playlist.length; i++) { + videoCounter++; + var printTime = msConversion(playlist[i][1] * 1000); + addVideoToList(playlist[i][0], printTime, videoCounter); + } + + playlist.splice(0, 1); + videos = videos.concat(playlist); + + setPlaylist(); + } + catch(err) { + alert("Uh oh... It looks like this playlist URL is broken, however, you may still be able to retrieve your data.\n\n" + + "Make sure that you save the URL that you have now, and contact me (the administrator) by submitting an issue on Streamly's Github page.\n\n" + + "I'm really sorry about this inconvenience.\n\nerr: " + err); + } +} + var dataPlayerRunning = false; function getVideoData(id) { console.log("getVideoData dataPlayerRunning=" + dataPlayerRunning + " videoId=" + videoId + " id=" + id); @@ -583,6 +621,8 @@ function addVideo(name, time, id) { videos[iteration] = video; } + sendStation("addvideo," + video); + if (playlistShuffle) { addedVideosWhileShuffled.push(video); } @@ -600,17 +640,17 @@ function addVideo(name, time, id) { } } -function actionPlayVideo(element) { - var index = $(".playButton").index(element); - videoIteration = index; +function actionPlayVideo(iteration) { + sendStation("actionplayvideo," + iteration); + videoIteration = iteration; videoPaused = false; loopVideo(); $("#favicon").attr("href", faviconPlay); } -function actionRemoveVideo(element) { - var index = $(".removeButton").index(element) + 1; - if (index === videoIteration) { +function actionRemoveVideo(iteration) { + sendStation("actionremovevideo," + iteration); + if (iteration === videoIteration) { if (videoIteration + 1 <= videoCounter) { forwardVideo(); videoIteration = changeIteration(-1); @@ -623,12 +663,12 @@ function actionRemoveVideo(element) { videoIteration = changeIteration(-1); } } - else if (index < videoIteration) { + else if (iteration < videoIteration) { videoIteration = changeIteration(-1); } videoCounter--; - videos.splice(index, 1); - removeVideoFromList(index, true); + videos.splice(iteration, 1); + removeVideoFromList(iteration, true); setPlaylist(); makeSortable(); @@ -636,7 +676,17 @@ function actionRemoveVideo(element) { addAutoplayVideo(); } +function buttonPlayVideo(element) { + var index = $(".playButton").index(element); + actionPlayVideo(index); +} +function buttonRemoveVideo(element) { + var index = $(".removeButton").index(element) + 1; + actionRemoveVideo(index); +} + function actionMoveVideo(oldIndex, newIndex) { + sendStation("actionmovevideo," + oldIndex + "," + newIndex); videos.move(oldIndex, newIndex); if (oldIndex == videoIteration) { videoIteration = newIndex; @@ -699,15 +749,18 @@ function videoPreviews() { var PlaylistFeatures = function() { this.playNext = function() { + sendStation("playlistfeaturesplaynext"); playlistPlayNext = (playlistPlayNext ? false : true); $(".fa-arrow-circle-right").css("color", (playlistPlayNext ? "#F77F00" : "grey")); } this.repeat = function() { + sendStation("playlistfeaturesrepeat"); playlistRepeat = (playlistRepeat ? false : true); videoPreviews(); $(".fa-repeat").css("color", (playlistRepeat ? "#F77F00" : "grey")); } this.shuffle = function() { + sendStation("playlistfeaturesshuffle"); playlistShuffle = (playlistShuffle ? false : true); shufflePlaylist(); $(".fa-random").css("color", (playlistShuffle ? "#F77F00" : "grey")); @@ -730,10 +783,26 @@ var PlaylistFeatures = function() { var playlistFeatures = new PlaylistFeatures; function urlValidate(url) { - var regex = /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube.com\/embed\/)([^?&]+)/i; - url = url.match(regex); - if (url !== null && url[1] !== null) { - return url[1]; + var youtubeRegex = /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube.com\/embed\/)([^?&]+)/i; + var streamlyRegex = /.*#(.+)/i; + + function checkMatch(url, regex) { + var doesMatch = url.match(regex); + if (doesMatch !== null && doesMatch[1] !== null) { + return doesMatch[1]; + } + else { + return false; + } + } + + var checkoutYoutube = checkMatch(url, youtubeRegex); + var checkoutStreamly = checkMatch(url, streamlyRegex); + if (checkoutYoutube) { + return ["youtube", checkoutYoutube]; + } + else if (checkoutStreamly) { + return ["streamly", checkoutStreamly]; } else { return false; @@ -778,13 +847,19 @@ function input(type) { } } else if (isUrl) { - inputBox = isUrl; - getVideoData(inputBox); - $("#inputBox").val("").attr("placeholder", loadingPlaceholder); - if (typeof popup !== "undefined") { - popup.close(); + if (isUrl[0] === "youtube") { + inputBox = isUrl[1]; + getVideoData(inputBox); + $("#inputBox").val("").attr("placeholder", loadingPlaceholder); + if (typeof popup !== "undefined") { + popup.close(); + } + $("#youtube").css("display", "block"); + } + else if (isUrl[0] === "streamly") { + appendPlaylist(isUrl[1]); + $("#inputBox").val("").attr("placeholder", placeholder); } - $("#youtube").css("display", "block"); } else if ($(window).width() > 600 && inputBox.indexOf("\\") === -1) { if (inputBox.slice(-2) === " l") { @@ -825,3 +900,114 @@ document.addEventListener("drop", function(event) { document.addEventListener("dragover", function(event) { event.preventDefault(); }); + +// Start Streamly Station + +function sendStation(what) { + if (stationServer !== undefined && stationServer !== null) { + if (!stationRxQuiet) { + stationTxQuiet = true; + console.log("Station Tx: " + what); + stationSocket.emit("msg", what); + } + else { + stationRxQuiet = false; + } + } +} + +function loadStation() { + stationSocket = io("http://" + stationServer); + alert("Streamly Station \"" + stationServer + "\" connected!"); + + $("#stationIcon").css("display", "initial"); + + stationSocket.on("msg", function(msg) { + console.log("Station Rx: " + msg); + + var msgData = msg.split(","); + if (!stationTxQuiet) { + stationRxQuiet = true; + + $("#stationIcon").css("color", "red"); + setTimeout(function() { + $("#stationIcon").css("color", "#00ff00"); + }, 300); + + switch (msgData[0]) { + case "addvideo": + addVideo(msgData[1], msgData[2], msgData[3]); + break; + case "playerending": + loopVideo(); + break; + case "actionplayvideo": + actionPlayVideo(+msgData[1]); + break; + case "actionremovevideo": + actionRemoveVideo(+msgData[1]); + break; + case "forwardvideo": + forwardVideo(); + break; + case "backvideo": + backVideo(); + break; + case "videofunctionsplay": + player.playVideo(); + break; + case "videofunctionspause": + player.pauseVideo(); + break; + case "playlistfeaturesplaynext": + playlistFeatures.playNext(); + break; + case "playlistfeaturesrepeat": + playlistFeatures.repeat(); + break; + case "playlistfeaturesshuffle": + playlistFeatures.shuffle(); + break; + case "actionmovevideo": + actionMoveVideo(+msgData[1], +msgData[2]); + var from = "#videosTable tr:nth-child(" + (+msgData[1] + 1) + ")"; + var to = "#videosTable tr:nth-child(" + msgData[2] + ")"; + console.log("FROM: " + from); + console.log("TO: " + to); + $(to).after($(from)); + break; + } + } + else { + stationTxQuiet = false; + } + }); +} + +function connectStation(server) { + stationServer = server; + $.ajax({ + url: "http://" + stationServer + "/socket.io/socket.io.js", + dataType: "script", + success: loadStation + }); +} + +function disconnectStation() { + stationSocket.disconnect(); + $("#stationIcon").css("display", "none"); +} + +var securityWarning = false; +function actionConnectStation() { + var station = $("#connectStationBox").val(); + if (window.location.protocol === "https:" && securityWarning === false) { + securityWarning = true; + alert("Note: Due to security protections, scripts on secured pages with 'https://' cannot make unsecured connections. " + + "Streamly Station runs without any onboard security, so this request will probably be blocked and you'll get a notification that the site requested unsecured scripts.\n\n" + + "In order to use Streamly Station, either make an exception to 'Load unsafe scripts' or replace the 'https://' with 'http://' in the URL."); + } + connectStation(station); +} + +// End Streamly Station diff --git a/styles.css b/styles.css index 3a85324..d9741a1 100644 --- a/styles.css +++ b/styles.css @@ -74,10 +74,12 @@ footer { border-radius: 999px; left: 15px; padding: 5px; - width: 50%; + width: calc(100% - 450px); + max-width: 600px; } #saveButton, -#settingsButton { +#settingsButton, +#stationIcon { cursor: pointer; } #saveButton { @@ -85,11 +87,60 @@ footer { padding: 5px 7px; background-color: #fff; } +#settingsButton, +#stationIcon { + position: relative; +} #settingsButton { top: 1px; left: 35px; color: #fff; } +#stationIcon { + color: #00ff00; + left: 45px; + display: none; +} +#settingsCloseButton { + position: absolute; + top: 0; + right: 0; + cursor: pointer; + padding: 10px; +} +#settingsCloseButton:hover { + color: white; +} +#settingsWindow { + width: 600px; + position: fixed; + background: grey; + height: 400px; + z-index: 3; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #2b2b2b; + text-align: left; + padding: 10px; + color: #ccc; + display: none; +} +#settingsShadow { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 2; + background: black; + background-color: rgba(0, 0, 0, 0.75); + display: none; +} +#connectStationBox { + width: 75%; + font-family: 'Roboto',sans-serif; +} td, th, #main, @@ -337,10 +388,19 @@ tr.placeholder:before { header { height: auto !important; } - #inputBox, #saveButton, #settingsButton { + #inputBox, #saveButton { position: initial; margin-bottom: 5px; } + #settingsButton, + #stationIcon { + float: right; + padding: 10px; + left: auto; + } + #settingsWindow { + width: auto; + } #inputBox { width: calc(100% - 120px) !important; }