diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index f98332e..235023f 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -64,6 +64,10 @@ const channelSchema = new mongoose.Schema({ required: true, default: true }, + streamURL: { + type: mongoose.SchemaTypes.String, + default: '' + } }, permissions: { type: channelPermissionSchema, diff --git a/src/utils/media/yanker.js b/src/utils/media/yanker.js index 09cb486..02f9193 100644 --- a/src/utils/media/yanker.js +++ b/src/utils/media/yanker.js @@ -32,8 +32,15 @@ module.exports.yankMedia = async function(url, title){ //return media object list from IA module return await iaUtil.fetchMetadata(pullType.id, title); case "yt": - //return mediao object list from the YT-DLP module's youtube function + //return media object list from the YT-DLP module's youtube function return await ytdlpUtil.fetchYoutubeMetadata(pullType.id, title); + case "ytp": + //return media object list from YT-DLP module's youtube playlist function + //return await ytdlpUtil.fetchYoutubePlaylistMetadata(pullType.id, title); + //Holding off on this since YT-DLP takes 10 years to do a playlist as it needs to pull each and every video one-by-one + //Maybe in the future a piped alternative might be in order, however this would most likely require us to host our own local instance. + //Though it could give us added resistance against youtube/google's rolling IP bans + return null; case "dm": //return mediao object list from the YT-DLP module's dailymotion function return await ytdlpUtil.fetchDailymotionMetadata(pullType.id, title); @@ -85,7 +92,7 @@ module.exports.getMediaType = async function(url){ //If we have link to a resource from archive.org if(match = url.match(/archive\.org\/(?:details|download)\/([a-zA-Z0-9\/._-\s\%]+)/)){ - //return internet archive code + //return internet archive upload id and filepath return { type: "ia", id: match[1] @@ -101,6 +108,15 @@ module.exports.getMediaType = async function(url){ } } + //If we have a match to a youtube playlist + if((match = url.match(/youtube\.com\/playlist\?list=([a-zA-Z0-9_-]{34})/)) || (match = url.match(/youtu\.be\/playlist\?list=([a-zA-Z0-9_-]{34})/))){ + //return youtube playlist id + return { + type: "ytp", + id: match[1] + } + } + //If we have a match to a dailymotion video if(match = url.match(/dailymotion\.com\/video\/([a-z0-9]{7})/)){ return { diff --git a/src/utils/media/ytdlpUtils.js b/src/utils/media/ytdlpUtils.js index af5805d..c487b86 100644 --- a/src/utils/media/ytdlpUtils.js +++ b/src/utils/media/ytdlpUtils.js @@ -32,7 +32,28 @@ const loggerUtils = require('../loggerUtils.js') module.exports.fetchYoutubeMetadata = async function(id, title){ try{ //Try to pull media from youtube id - const media = await fetchMetadata(`https://youtu.be/${id}`, title, 'yt'); + const media = await fetchVideoMetadata(`https://youtu.be/${id}`, title, 'yt'); + + //Return found media + return media; + //If something went wrong + }catch(err){ + //If our IP was banned by youtube + if(err.message.match("Sign in to confirm you’re not a bot.")){ + //Make our own error with blackjack and hookers + throw loggerUtils.exceptionSmith("The server's IP address has been banned by youtube. Please contact your server's administrator.", "queue"); + //Otherwise if we don't have a good way to handle it + }else{ + //toss it back up + throw err; + } + } +} + +module.exports.fetchYoutubePlaylistMetadata = async function(id, title){ + try{ + //Try to pull media from youtube id + const media = await fetchPlaylistMetadata(`https://youtu.be/playlist?list=${id}`, title, 'yt'); //Return found media return media; @@ -52,22 +73,19 @@ module.exports.fetchYoutubeMetadata = async function(id, title){ module.exports.fetchDailymotionMetadata = async function(id, title){ //Pull media from dailymotion link - const media = await fetchMetadata(`https://dailymotion.com/video/${id}`, title, 'dm'); + const media = await fetchVideoMetadata(`https://dailymotion.com/video/${id}`, title, 'dm'); //Return found media; return media; } -//Generic YTDLP function meant to be used by service-sepecific fetchers which will then be used to fetch video metadata -async function fetchMetadata(link, title, type, format = 'b'){ +//Generic single video YTDLP function meant to be used by service-sepecific fetchers which will then be used to fetch video metadata +async function fetchVideoMetadata(link, title, type, format = 'b'){ //Create media list const mediaList = []; - //Pull raw metadata - const rawMetadata = await ytdlp(link, { - dumpSingleJson: true, - format - }); + //Pull raw metadata from YT-DLP + const rawMetadata = await ytdlpFetch(link, format); //Pull data from rawMetadata, sanatizing title to prevent XSS const name = validator.escape(validator.trim(rawMetadata.title)); @@ -85,4 +103,17 @@ async function fetchMetadata(link, title, type, format = 'b'){ //Return list of media return mediaList; +} + +//YT-DLP takes forever to handle playlists, we'll handle this via piped in the future perhaps +/*async function fetchPlaylistMetadata(link, title, type, format = 'b'){ +}*/ + +//Wrapper function for YT-DLP NPM package with pre-set cli-flags +async function ytdlpFetch(link, format = 'b'){ + //return promise from ytdlp + return ytdlp(link, { + dumpSingleJson: true, + format + }); } \ No newline at end of file diff --git a/src/validators/channelValidator.js b/src/validators/channelValidator.js index b0390a3..60c9170 100644 --- a/src/validators/channelValidator.js +++ b/src/validators/channelValidator.js @@ -80,6 +80,11 @@ module.exports.settingsMap = function(){ optional: true, isBoolean: true, errorMessage: "Bad channel settings map." + }, + 'settingsMap.streamURL': { + optional: true, + isURL: true, + errorMessage: "Invalid Stream URL" } }) ); diff --git a/src/views/partial/channelSettings/settings.ejs b/src/views/partial/channelSettings/settings.ejs index b54b138..b2bd804 100644 --- a/src/views/partial/channelSettings/settings.ejs +++ b/src/views/partial/channelSettings/settings.ejs @@ -20,7 +20,14 @@ along with this program. If not, see . %> <% Object.keys(channel.settings).forEach((key) => { %> - class="channel-preference-list-item" type="checkbox" <% if(channel.settings[key]){ %> checked <% } %>> + <% switch(typeof channel.settings[key]){ + case "string": %> + class="channel-preference-list-item" value="<%- channel.settings[key] %>"> + <% break; + default: %> + class="channel-preference-list-item" type="checkbox" <% if(channel.settings[key]){ %> checked <% } %>> + <% break; + } %> <% }); %> diff --git a/www/js/channelSettings.js b/www/js/channelSettings.js index a98bd08..980b1cb 100644 --- a/www/js/channelSettings.js +++ b/www/js/channelSettings.js @@ -235,13 +235,28 @@ class prefrenceList{ } async submitUpdate(event){ + //Get key from event target const key = event.target.id.replace("channel-preference-",""); - const value = event.target.checked; + + //Pull value from event target + let value = event.target.value; + + //If this is a checkmark + if(event.target.type == "checkbox"){ + //Use the .checked property instead of .value + value = event.target.checked; + } + + //Create settings map const settingsMap = new Map([ [key, value] ]); - this.handleUpdate(await utils.ajax.setChannelSetting(this.channel, settingsMap), event.target, key); + //Send update and collect results + const update = await utils.ajax.setChannelSetting(this.channel, settingsMap); + + //Handle update from server + this.handleUpdate(update, event.target, key); } handleUpdate(data, target, key){