diff --git a/.gitignore b/.gitignore index bc53b03..1b0fc7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ -# any excluded dir with a .keep file will stay in github, but all other files will not :) +# any excluded dir with a .keep file. Will stay in github, but all other files will not :) !.keep # ignore the following folders /node_modules /public/mp3cache/** /public/imagecache/** +docker-dompose.yaml diff --git a/classes/arr/radarr.js b/classes/arr/radarr.js index e28ea2f..ed611c1 100644 --- a/classes/arr/radarr.js +++ b/classes/arr/radarr.js @@ -4,12 +4,23 @@ const util = require("./../core/utility"); const core = require("./../core/cache"); const axios = require("axios"); +/** + * @desc Used to communicate with Radarr to obtain a list of future releases + * @param radarrUrl + * @param radarrToken + */ class Radarr { constructor(radarrUrl, radarrToken) { this.radarrUrl = radarrUrl; this.radarrToken = radarrToken; } + /** + * @desc Gets the movie titles that fall within the range specified + * @param {string} startDate - in yyyy-mm-dd format - Generally todays date + * @param {string} endDate - in yyyy-mm-dd format - future date + * @returns {Promise} json results - results of search + */ async GetComingSoonRawData(startDate, endDate) { let response; @@ -24,7 +35,7 @@ class Radarr { "&end=" + endDate ) - .catch(err => { + .catch((err) => { throw err; }); } catch (err) { @@ -36,7 +47,15 @@ class Radarr { } // ******* TODO - not yet done below this point!!!!! - async GetComingSoon(startDate, endDate, premieres) { + + + /** + * @desc Get Movie coming soon data and formats into mediaCard array + * @param {string} startDate - in yyyy-mm-dd format - Generally todays date + * @param {string} endDate - in yyyy-mm-dd format - future date + * @returns {Promise} mediaCards array - results of search + */ + async GetComingSoon(startDate, endDate) { let csCards = []; // get raw data first let raw = await this.GetComingSoonRawData(startDate, endDate); @@ -152,10 +171,12 @@ class Radarr { if (csCards.length == 0) { console.log(now.toLocaleString() + " No Coming soon titles found"); } else { - console.log(now.toLocaleString() + " Coming soon 'Movie' titles refreshed"); + console.log( + now.toLocaleString() + " Coming soon 'Movie' titles refreshed" + ); } return csCards; } } -module.exports = Radarr; \ No newline at end of file +module.exports = Radarr; diff --git a/classes/arr/sonarr.js b/classes/arr/sonarr.js index 78be389..1098644 100644 --- a/classes/arr/sonarr.js +++ b/classes/arr/sonarr.js @@ -4,15 +4,27 @@ const util = require("./../core/utility"); const core = require("./../core/cache"); const axios = require("axios"); +/** + * @desc Used to communicate with Sonarr to obtain a list of future releases + * @param sonarrUrl + * @param sonarrToken + */ class Sonarr { constructor(sonarrUrl, sonarrToken) { this.sonarrUrl = sonarrUrl; this.sonarrToken = sonarrToken; } + /** + * @desc Gets the tv titles that fall within the range specified + * @param {string} startDate - in yyyy-mm-dd format - Generally todays date + * @param {string} endDate - in yyyy-mm-dd format - future date + * @returns {Promise} json results - results of search + */ async GetComingSoonRawData(startDate, endDate) { let response; + // call sonarr API and return results try { response = await axios .get( @@ -24,10 +36,11 @@ class Sonarr { "&end=" + endDate ) - .catch(err => { + .catch((err) => { throw err; }); } catch (err) { + // displpay error if call failed let d = new Date(); console.log(d.toLocaleString() + " Sonarr error: ", err.message); } @@ -35,6 +48,13 @@ class Sonarr { return response; } + /** + * @desc Get TV coming soon data and formats into mediaCard array + * @param {string} startDate - in yyyy-mm-dd format - Generally todays date + * @param {string} endDate - in yyyy-mm-dd format - future date + * @param {string} premieres - boolean (string format) to show only season premieres + * @returns {Promise} mediaCards array - results of search + */ async GetComingSoon(startDate, endDate, premieres) { let csCards = []; // get raw data first diff --git a/classes/cards/CardType.js b/classes/cards/CardType.js index e94b410..ac29866 100644 --- a/classes/cards/CardType.js +++ b/classes/cards/CardType.js @@ -1,3 +1,7 @@ + /** + * @desc A card type enum + * @returns nothing + */ class CardType { static CardTypeEnum = {NowScreening: "Now Screening", OnDemand: "On-Demand", ComingSoon: "Coming Soon", Playing: "Playing", IFrame: "Iframe", Picture: "Custom Picture"}; diff --git a/classes/cards/MediaCard.js b/classes/cards/MediaCard.js index 1a63773..fae5244 100644 --- a/classes/cards/MediaCard.js +++ b/classes/cards/MediaCard.js @@ -1,5 +1,9 @@ const util = require("./../core/utility"); +/** + * @desc mediaCards base class for defining every card that is showed in the poster app + * @returns nothing + */ class MediaCard { constructor() { this.ID = null; @@ -30,6 +34,10 @@ class MediaCard { this.rendered = ""; } + /** + * @desc renders the properties of the card into html, then sets this to the 'rendered' property + * @returns nothing + */ async Render() { let hidden = ""; if (this.cardType != "Now Screening") hidden = "hidden"; diff --git a/classes/core/cache.js b/classes/core/cache.js index fefaa57..763cc48 100644 --- a/classes/core/cache.js +++ b/classes/core/cache.js @@ -3,17 +3,32 @@ const request = require("request"); const fsExtra = require("fs-extra"); const util = require("./utility"); -class Core { +/** + * @desc Cache class manages the downloaad, cleanup and random selection of mp3 and poster image assets. Methods are static. + * @returns nothing + */ +class Cache { constructor() { return; } + /** + * @desc Downloads the poster image + * @param {string} url - the full url to the picture file + * @param {string} fileName - the filename to save the image file as + * @returns nothing + */ static async CacheImage(url, fileName) { const savePath = "./public/imagecache/" + fileName; await this.download(url, savePath, fileName); return; } + /** + * @desc Downloads the tv mp3 file from tvthemes.plexapp.com + * @param {string} fileName - the filename to download and save. this in the format of tvdbid.mp3 + * @returns nothing + */ static async CacheMP3(fileName) { const savePath = "./public/mp3cache/" + fileName; const url = "http://tvthemes.plexapp.com/" + fileName; @@ -22,16 +37,23 @@ class Core { return; } + /** + * @desc Download any asset, providing it does not already exist in the save location + * @param {string} url - the full url to the asset + * @param {string} savePath - the path to save the asset to + * @param {string} fileName - the filename to save the asset as + * @returns nothing + */ static async download(url, savePath, fileName) { // download file function const download = (url, savePath, callback) => { try { request.head(url, (err, res, body) => { request(url) - .pipe(fs.createWriteStream(savePath,{autoClose:true})) + .pipe(fs.createWriteStream(savePath, { autoClose: true })) .on("close", callback) .on("error", (err) => { - console.log('download failed: ' + err.message); + console.log("download failed: " + err.message); }) .on("finish", () => { //console.log("Download Completed"); @@ -58,23 +80,36 @@ class Core { } } + /** + * @desc Deletes all files from the MP3Cache folder + * @returns nothing + */ static async DeleteMP3Cache() { const directory = "./public/mp3cache/"; fsExtra.emptyDirSync(directory); console.log("✅ MP3 cache cleared"); } + /** + * @desc Deletes all files from the imageCache folder + * @returns nothing + */ static async DeleteImageCache() { const directory = "./public/imagecache/"; fsExtra.emptyDirSync(directory); console.log("✅ Image cache cleared"); } + /** + * @desc Returns a single random mp3 filename from the randomthemese folder + * @returns {string} fileName - a random filename + */ static async GetRandomMP3() { let directory = "./public/randomthemes"; + // calls radom_items function to return a random item from an array let randomFile = await util.random_item(fs.readdirSync(directory)); return randomFile; } } -module.exports = Core; +module.exports = Cache; diff --git a/classes/core/globalPage.js b/classes/core/globalPage.js index 67e927f..8ac4bec 100644 --- a/classes/core/globalPage.js +++ b/classes/core/globalPage.js @@ -1,5 +1,14 @@ const cache = require("./cache"); +/** + * @desc globalPage object is passed to poster.ejs and contains all browser settings and card data + * @param {number} slideDuration - how long each slide will be visible for (seconds) + * @param {number} refreshPeriod - how long before the brpwser does a refresh and data is refreshed (seconds) + * @param {string} playThemesIfAvailable - boolean - will enable theme music wherever it is set + * @param {string} playGenericThemes - boolean - will play random themes from the /randomthemes directory for movies + * @param {string} fadeTransition - boolean - if true, will fade transition. false will slide. + * @returns {} globalPage + */ class globalPage { constructor( slideDuration, @@ -18,6 +27,10 @@ class globalPage { return; } + /** + * @desc Takes merged mediaCard set and applies card order number and active card slide, then generates the rendered HTML for each media card. + * @returns nothing + */ async OrderAndRenderCards() { if (this.cards.length != 0) { let webID = 0; @@ -38,7 +51,6 @@ class globalPage { } return; } - } module.exports = globalPage; diff --git a/classes/core/utility.js b/classes/core/utility.js index bafae31..a7875d1 100644 --- a/classes/core/utility.js +++ b/classes/core/utility.js @@ -1,5 +1,13 @@ +/** + * @desc utility class for string and object handling + * @returns {} utility + */ class utility { - // return true if null + /** + * @desc Returns true is null, empty or undefined + * @param {string} val + * @returns {Promise} boolean - true empty, undefined or null + */ static async isEmpty(val) { if (val == undefined || val == "" || val == null) { return true; @@ -8,7 +16,11 @@ class utility { } } - // return empty string if null + /** + * @desc Returns an empty string if undefined, null or empty, else the submitted value + * @param {string} val + * @returns {Promise} string - either an empty string or the submitted string value + */ static async emptyIfNull(val) { if (val == undefined || val == null || val == "") { return ""; @@ -17,14 +29,22 @@ class utility { } } - // gets a random item from an array + /** + * @desc Gets a random item from an array + * @param {Array} items - a given array of anything + * @returns {Promise} object - returns one random item + */ static async random_item(items) { return items[Math.floor(Math.random() * items.length)]; } - // builds random set of on-demand cards - static async build_random_od_set(numberOnDemand,mediaCards) { - + /** + * @desc builds random set of on-demand cards + * @param {number} numberOnDemand - the number of on-demand cards to return + * @param {object} mediaCards - an array of on-demand mediaCards + * @returns {Promise} mediaCard[] - an array of mediaCards + */ + static async build_random_od_set(numberOnDemand, mediaCards) { let onDemandCards = []; for await (let i of Array(numberOnDemand).keys()) { let odc; @@ -35,4 +55,4 @@ class utility { } } -module.exports = utility; \ No newline at end of file +module.exports = utility; diff --git a/classes/mediaservers/plex.js b/classes/mediaservers/plex.js index 968a937..5ebad9d 100644 --- a/classes/mediaservers/plex.js +++ b/classes/mediaservers/plex.js @@ -4,6 +4,14 @@ const cType = require("./../cards/CardType"); const util = require("./../core/utility"); const core = require("./../core/cache"); +/** + * @desc Used to communicate with Plex + * @param {string} HTTPS - set this to true if Plex only allows secure connections + * @param {string} plexIP - the IP or fqdn of the plex server + * @param {number} plexPort - the port number used by Plex + * @param {string} plexToken - the Plex token + * @returns {object} Plex API client object + */ class Plex { constructor({ HTTPS, plexIP, plexPort, plexToken }) { this.https = HTTPS; @@ -21,6 +29,10 @@ class Plex { }); } +/** + * @desc Get raw results for now screening + * @returns {object} JSON - Plex now screening results + */ async GetNowScreeningRawData() { this.nowScreening = await this.client .query("/status/sessions") @@ -35,6 +47,11 @@ class Plex { return this.nowScreening; } +/** + * @desc Gets now screening cards + * @param {string} playGenericThemes - will set movies to play a random generic theme fro the /randomthemes folder + * @returns {object} mediaCard[] - Returns an array of mediaCards + */ async GetNowScreening(playGenenericThemes) { // get raw data first let nsCards = []; @@ -236,6 +253,13 @@ class Plex { return nsCards; } +/** + * @desc Gets random on-demand cards + * @param {string} onDemandLibraries - a comma seperated lists of the libraries to pull on-demand titles from + * @param {number} The number of titles to pull from each library + * @param {string} playGenericThemes - will set movies to play a random generic theme fro the /randomthemes folder + * @returns {object} mediaCard[] - Returns an array of mediaCards + */ async GetOnDemand(onDemandLibraries, numberOnDemand, playGenenericThemes) { // get library keys let odCards = []; @@ -407,12 +431,17 @@ class Plex { return odCards; } - // Get library keys for selected on-demand libraries + /** + * @desc Get Plex library keys for selected on-demand libraries + * @param {string} onDemandLibraries - a comma seperated lists of the libraries to pull on-demand titles from + * @returns {object} number[] - Returns an array of library key numbers + */ async GetLibraryKeys(onDemandLibraries) { if (!onDemandLibraries || onDemandLibraries.length == 0) { onDemandLibraries = " "; } + // Get the key for each library and push into an array let keys = []; return onDemandLibraries.split(",").reduce(async (acc, value) => { return await this.client.query("/library/sections/").then( @@ -432,6 +461,11 @@ class Plex { }, Promise.resolve(0)); } + /** + * @desc Get a mediaCard array for all titles in a given library (all is needed so random selections can be chosen later) + * @param {number} libKey - The plex library key number + * @returns {object} mediaCard[] - Returns an array of mediaCards + */ async GetAllMediaForLibrary(libKey) { let mediaCards = []; return await this.client.query("/library/sections/" + libKey + "/all").then( @@ -452,6 +486,12 @@ class Plex { ); } + /** + * @desc Gets the specified, random, number of titles from a specified set of libraries + * @param {string} onDemandLibraries - a comma seperated lists of the libraries to pull on-demand titles from + * @param {number} numberOnDemand - the number of results to return from each library + * @returns {object} mediaCard[] - Returns an array of on-demand mediaCards + */ async GetOnDemandRawData(onDemandLibraries, numberOnDemand) { // Get a list of random titles from selected libraries let odSet = []; diff --git a/docker-compose.yml b/docker-compose.yml index 8bf9e94..a6c4d41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: context: . dockerfile: ./Dockerfile environment: - TZ: Australia/Brisbane + TZ: "Australia/Brisbane" NODE_ENV: production PLEXTOKEN: "" PLEXIP: 192.168.1.135 @@ -29,7 +29,7 @@ services: RADARR_TOKEN: "" RADARR_CAL_DAYS: 15 volumes: - - c:/code/poster/public/randomthemes:/usr/src/app/public/randomthemes + - ./poster/public/randomthemes:/usr/src/app/public/randomthemes ports: - 9876:3000 diff --git a/index.js b/index.js index 1172000..af7aaf7 100644 --- a/index.js +++ b/index.js @@ -14,14 +14,15 @@ const sonr = require("./classes/arr/sonarr"); // Plex Settings const plexToken = process.env.PLEXTOKEN || ""; const plexIP = process.env.PLEXIP || "192.168.1.135"; -const plexHTTPS = process.env.PLEX_HTTPS || false; +const plexHTTPS = process.env.PLEX_HTTPS || "false"; const plexPort = process.env.PLEX_PORT || 32400; // sonarr settings const sonarrURL = process.env.SONARR_URL || "http://192.168.1.135:8989"; -const sonarrToken = process.env.SONARR_TOKEN || ""; +const sonarrToken = + process.env.SONARR_TOKEN || ""; const sonarrCalDays = process.env.SONARR_CAL_DAYS || 175; // how far to look ahead in days (set to a low number if premieres is false) -const sonarrPremieres = process.env.SONARR_PREMIERES || true; // only show season premieres +const sonarrPremieres = process.env.SONARR_PREMIERES || "true"; // only show season premieres // radarr settings - not yet implemented const radarrURL = process.env.RADARR_URL || "http://192.168.1.135:7878"; @@ -29,16 +30,17 @@ const radarrToken = process.env.RADARR_TOKEN || ""; const radarrCalDays = process.env.RADARR_CAL_DAYS || 15; // how far to look ahead in days // general settings -const fade = process.env.FADE || true; // transitions will slide, unless fade is set to true -const slideDuration = process.env.SLIDE_DURATION || 7; // seconds for slide transition +const fade = process.env.FADE || "true"; // transitions will slide, unless fade is set to true +const slideDuration = process.env.SLIDE_DURATION || 7; // seconds for slide transition const refreshPeriod = process.env.REFRESH_PERIOD || 120; // browser refresh period - should be longer than combined (cards x slide_duration) -const playThemes = process.env.PLAY_THEMES || true; // enables theme music where appropriate -const playGenenericThemes = process.env.PLAY_GENERIC_THEMES || true; // will play a random generic themes from 'randomtheme' folder for movies +const playThemes = process.env.PLAY_THEMES || "true"; // enables theme music where appropriate +const playGenenericThemes = process.env.PLAY_GENERIC_THEMES || "true"; // will play a random generic themes from 'randomtheme' folder for movies // on-demand settings const numberOnDemand = process.env.NUMBER_ON_DEMAND || 2; // how many random on-demand titles to show const OnDemandRefresh = process.env._ON_DEMAND_REFRESH || 30; // how often, in minutes, to refresh the on-demand titles -const onDemandLibraries = process.env.ON_DEMAND_LIBRARIES || "anime,movies,tv shows"; // libraries to pull on-demand titles from ** only last library is actually working!!!! +const onDemandLibraries = + process.env.ON_DEMAND_LIBRARIES || "anime,movies,tv shows"; // libraries to pull on-demand titles from ** only last library is actually working!!!! console.log("--------------------------------------------------------"); console.log("| POSTER - Your media display |"); @@ -52,6 +54,7 @@ console.log("--------------------------------------------------------"); core.DeleteMP3Cache(); core.DeleteImageCache(); +// global variables let odCards = []; let nsCards = []; let csCards = []; @@ -61,33 +64,50 @@ let onDemandClock; let sonarrClock; let houseKeepingClock; +/** + * @desc Wrapper function to call Sonarr coming soon. + * @returns {Promise} mediaCards array - coming soon + */ async function loadSonarrComingSoon() { - // stop the clock - clearInterval(sonarrClock); + // stop the clock + clearInterval(sonarrClock); + // instatntiate sonarr class let sonarr = new sonr(sonarrURL, sonarrToken, sonarrPremieres); // set up date range and date formats let today = new Date(); let later = new Date(); + console.log(today.toISOString().split("T")[0]); later.setDate(later.getDate() + sonarrCalDays); + console.log(later.toISOString().split("T")[0]); let now = today.toISOString().split("T")[0]; let ltr = later.toISOString().split("T")[0]; + // call sonarr coming soon csCards = await sonarr.GetComingSoon(now, ltr, sonarrPremieres); + + // restart the 24 hour timer sonarrClock = setInterval(loadSonarrComingSoon, 86400000); // daily + return csCards; } +/** + * @desc Wrapper function to call now screening method. + * @returns {Promise} mediaCards array - results of now screening search + */ async function loadNowScreening() { - // stop the clock - clearInterval(nowScreeningClock); + // stop the clock + clearInterval(nowScreeningClock); // load MediaServer(s) (switch statement for different server settings server option - TODO) let ms = new pms({ plexHTTPS, plexIP, plexPort, plexToken }); + // call now screening method nsCards = await ms.GetNowScreening(playGenenericThemes); - // load now showing and on-demand cards, else just on-demand (if present) + // Concatenate cards for all objects load now showing and on-demand cards, else just on-demand (if present) + // TODO - move this into its own function! let mCards = []; if (nsCards.length > 0) { mCards = nsCards.concat(odCards); @@ -107,11 +127,14 @@ async function loadNowScreening() { } } + // setup transition - fade or default slide let fadeTransition = ""; if (fade) { fadeTransition = "carousel-fade"; } + // put everything into global class, ready to be passed to poster.ejs + // render html for all cards await globalPage.OrderAndRenderCards(playGenenericThemes); globalPage.refreshPeriod = refreshPeriod * 1000; globalPage.slideDuration = slideDuration * 1000; @@ -119,11 +142,15 @@ async function loadNowScreening() { globalPage.playGenericThemes = playGenenericThemes; globalPage.fadeTransition = fadeTransition; - //console.log('now showing called'); + // restart the clock nowScreeningClock = setInterval(loadNowScreening, 60000); // every minute - return; + return nsCards; } + /** + * @desc Wrapper function to call on-demand method + * @returns {Promise} mediaCards array - results of on-demand search + */ async function loadOnDemand() { // stop the clock clearInterval(onDemandClock); @@ -131,17 +158,22 @@ async function loadOnDemand() { // load MediaServer(s) (switch statement for different server settings server option - TODO) let ms = new pms({ plexHTTPS, plexIP, plexPort, plexToken }); - odCards = await ms.GetOnDemand( - onDemandLibraries, - numberOnDemand, - playGenenericThemes - ); + odCards = await ms.GetOnDemand( + onDemandLibraries, + numberOnDemand, + playGenenericThemes + ); // restart interval timer - onDemandClock = setInterval(loadOnDemand, (OnDemandRefresh*60000)); - return; + onDemandClock = setInterval(loadOnDemand, OnDemandRefresh * 60000); + + return odCards; } +/** + * @desc Cleans up image and MP3 cache directories + * @returns nothing + */ async function houseKeeping() { // stop the clock clearInterval(houseKeepingClock); @@ -149,47 +181,61 @@ async function houseKeeping() { await core.DeleteMP3Cache(); await core.DeleteImageCache(); // restart timer - setInterval(houseKeeping,86400000); // daily + setInterval(houseKeeping, 86400000); // daily } +/** + * @desc Starts everything - calls coming soon 'tv', on-demand and now screening functions. Then initialises timers + * @returns nothing + */ async function startup() { // initial load of card providers await loadSonarrComingSoon(); await loadOnDemand(); await loadNowScreening(); - + let now = new Date(); - console.log(now.toLocaleString() + " Now screening titles refreshed (First run only)"); + console.log( + now.toLocaleString() + " Now screening titles refreshed (First run only)" + ); console.log(" "); console.log("✅ Application ready on http://hostIP:3000"); console.log(" "); - + // set intervals for timers nowScreeningClock = setInterval(loadNowScreening, 60000); // every minute - onDemandClock = setInterval(loadOnDemand, (OnDemandRefresh*60000)); + onDemandClock = setInterval(loadOnDemand, OnDemandRefresh * 60000); sonarrClock = setInterval(loadSonarrComingSoon, 86400000); // daily houseKeepingClock = setInterval(houseKeeping, 86400000); // daily + + return; } -// call all card providers - initial load and set scheduled runs +// call all card providers - initial card loads and sets scheduled runs startup(); //use ejs templating engine app.set("view engine", "ejs"); + +// sets public folder for assets app.use(express.static(path.join(__dirname, "public"))); +// set routes app.get("/", (req, res) => { res.render("index", { globals: globalPage }); // index refers to index.ejs }); +// health check - TODO app.get("/health", (req, res) => { res.json(app.locals.globals); }); +// settings page TODO app.get("/settings", (req, res) => { res.json(app.locals.globals); }); +// start listening on port 3000 app.listen(3000, () => { console.log( `✅ Web server started on internal port 3000 diff --git a/public/javascript/script.js b/public/javascript/script.js index e69de29..1d6c594 100644 --- a/public/javascript/script.js +++ b/public/javascript/script.js @@ -0,0 +1 @@ +// TODO move script from poster.ejs file into this file \ No newline at end of file