const jmpInfo = JSON.parse(window.atob("@@data@@")); window.jmpInfo = jmpInfo; const features = [ "filedownload", "displaylanguage", "htmlaudioautoplay", "htmlvideoautoplay", "externallinks", "clientsettings", "multiserver", "exitmenu", "remotecontrol", "fullscreenchange", "filedownload", "remotevideo", "displaymode", "screensaver", "fileinput" ]; const plugins = [ 'mpvVideoPlayer', 'mpvAudioPlayer', 'jmpInputPlugin', 'jmpUpdatePlugin', 'jellyscrubPlugin', 'skipIntroPlugin' ]; function loadScript(src) { return new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = src; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); } // Add plugin loaders for (const plugin of plugins) { window[plugin] = async () => { await loadScript(`${jmpInfo.scriptPath}${plugin}.js`); return window["_" + plugin]; }; } window.NativeShell = { openUrl(url, target) { window.api.system.openExternalUrl(url); }, downloadFile(downloadInfo) { window.api.system.openExternalUrl(downloadInfo.url); }, openClientSettings() { showSettingsModal(); }, getPlugins() { return plugins; } }; function getDeviceProfile() { const CodecProfiles = []; if (jmpInfo.settings.video.force_transcode_dovi) { CodecProfiles.push({ 'Type': 'Video', 'Conditions': [ { 'Condition': 'NotEquals', 'Property': 'VideoRangeType', 'Value': 'DOVI' } ] }); } if (jmpInfo.settings.video.force_transcode_hdr) { CodecProfiles.push({ 'Type': 'Video', 'Conditions': [ { 'Condition': 'Equals', 'Property': 'VideoRangeType', 'Value': 'SDR' } ] }); } if (jmpInfo.settings.video.force_transcode_hi10p) { CodecProfiles.push({ 'Type': 'Video', 'Conditions': [ { 'Condition': 'LessThanEqual', 'Property': 'VideoBitDepth', 'Value': '8', } ] }); } if (jmpInfo.settings.video.force_transcode_hevc) { CodecProfiles.push({ 'Type': 'Video', 'Codec': 'hevc', 'Conditions': [ { 'Condition': 'Equals', 'Property': 'Width', 'Value': '0', } ], }); CodecProfiles.push({ 'Type': 'Video', 'Codec': 'h265', 'Conditions': [ { 'Condition': 'Equals', 'Property': 'Width', 'Value': '0', } ], }); } if (jmpInfo.settings.video.force_transcode_av1) { CodecProfiles.push({ 'Type': 'Video', 'Codec': 'av1', 'Conditions': [ { 'Condition': 'Equals', 'Property': 'Width', 'Value': '0', } ], }); } if (jmpInfo.settings.video.force_transcode_4k) { CodecProfiles.push({ 'Type': 'Video', 'Conditions': [ { 'Condition': 'LessThanEqual', 'Property': 'Width', 'Value': '1920', }, { 'Condition': 'LessThanEqual', 'Property': 'Height', 'Value': '1080', } ] }); } const DirectPlayProfiles = [{'Type': 'Audio'}, {'Type': 'Photo'}]; if (!jmpInfo.settings.video.always_force_transcode) { DirectPlayProfiles.push({'Type': 'Video'}); } return { 'Name': 'Jellyfin Media Player', 'MaxStaticBitrate': 1000000000, 'MusicStreamingTranscodingBitrate': 1280000, 'TimelineOffsetSeconds': 5, 'TranscodingProfiles': [ {'Type': 'Audio'}, { 'Container': 'ts', 'Type': 'Video', 'Protocol': 'hls', 'AudioCodec': 'aac,mp3,ac3,opus,vorbis', 'VideoCodec': jmpInfo.settings.video.allow_transcode_to_hevc ? ( jmpInfo.settings.video.prefer_transcode_to_h265 ? 'h265,hevc,h264,mpeg4,mpeg2video' : 'h264,h265,hevc,mpeg4,mpeg2video' ) : 'h264,mpeg4,mpeg2video', 'MaxAudioChannels': jmpInfo.settings.audio.channels === "2.0" ? '2' : '6' }, {'Container': 'jpeg', 'Type': 'Photo'} ], DirectPlayProfiles, 'ResponseProfiles': [], 'ContainerProfiles': [], CodecProfiles, 'SubtitleProfiles': [ {'Format': 'srt', 'Method': 'External'}, {'Format': 'srt', 'Method': 'Embed'}, {'Format': 'ass', 'Method': 'External'}, {'Format': 'ass', 'Method': 'Embed'}, {'Format': 'sub', 'Method': 'Embed'}, {'Format': 'sub', 'Method': 'External'}, {'Format': 'ssa', 'Method': 'Embed'}, {'Format': 'ssa', 'Method': 'External'}, {'Format': 'smi', 'Method': 'Embed'}, {'Format': 'smi', 'Method': 'External'}, {'Format': 'pgssub', 'Method': 'Embed'}, {'Format': 'dvdsub', 'Method': 'Embed'}, {'Format': 'dvbsub', 'Method': 'Embed'}, {'Format': 'pgs', 'Method': 'Embed'} ] }; } async function createApi() { try { // Can't append script until document exists await new Promise(resolve => { document.addEventListener('DOMContentLoaded', resolve); }); await loadScript('qrc:///qtwebchannel/qwebchannel.js'); } catch (e) { // try clearing out any cached CSPs let foundCache = false; for (const cache of await caches.keys()) { const dataDeleted = await caches.delete(cache); if (dataDeleted) { foundCache = true; } } if (foundCache) { window.location.reload(); } throw e; } const channel = await new Promise((resolve) => { /*global QWebChannel */ new QWebChannel(window.qt.webChannelTransport, resolve); }); return channel.objects; } let rawSettings = {}; Object.assign(rawSettings, jmpInfo.settings); const settingsFromStorage = window.sessionStorage.getItem('settings'); if (settingsFromStorage) { rawSettings = JSON.parse(settingsFromStorage); Object.assign(jmpInfo.settings, rawSettings); } const settingsDescriptionsFromStorage = window.sessionStorage.getItem('settingsDescriptions'); if (settingsDescriptionsFromStorage) { jmpInfo.settingsDescriptions = JSON.parse(settingsDescriptionsFromStorage); } jmpInfo.settingsDescriptionsUpdate = []; jmpInfo.settingsUpdate = []; window.apiPromise = createApi(); window.initCompleted = new Promise(async (resolve) => { window.api = await window.apiPromise; const settingUpdate = (section, key) => ( (data) => new Promise(resolve => { rawSettings[section][key] = data; window.sessionStorage.setItem("settings", JSON.stringify(rawSettings)); window.api.settings.setValue(section, key, data, resolve); }) ); const setSetting = (section, key) => { Object.defineProperty(jmpInfo.settings[section], key, { set: settingUpdate(section, key), get: () => rawSettings[section][key] }); }; for (const settingGroup of Object.keys(rawSettings)) { jmpInfo.settings[settingGroup] = {}; for (const setting of Object.keys(rawSettings[settingGroup])) { setSetting(settingGroup, setting, jmpInfo.settings[settingGroup][setting]); } } window.api.settings.sectionValueUpdate.connect( (section, data) => { Object.assign(rawSettings[section], data); for (const callback of jmpInfo.settingsUpdate) { try { callback(section, data); } catch (e) { console.error("Update handler failed:", e); } } // Settings will be outdated if page reloads, so save them to session storage window.sessionStorage.setItem("settings", JSON.stringify(rawSettings)); } ); window.api.settings.groupUpdate.connect( (section, data) => { jmpInfo.settingsDescriptions[section] = data.settings; for (const callback of jmpInfo.settingsDescriptionsUpdate) { try { callback(section, data); } catch (e) { console.error("Description update handler failed:", e); } } // Settings will be outdated if page reloads, so save them to session storage window.sessionStorage.setItem("settingsDescriptions", JSON.stringify(jmpInfo.settingsDescriptions)); } ); resolve(); }); window.NativeShell.AppHost = { init() {}, getDefaultLayout() { return jmpInfo.mode; }, supports(command) { return features.includes(command.toLowerCase()); }, getDeviceProfile, getSyncProfile: getDeviceProfile, appName() { return "Jellyfin Media Player"; }, appVersion() { return navigator.userAgent.split(" ")[1]; }, deviceName() { return jmpInfo.deviceName; }, exit() { window.api.system.exit(); } }; async function showSettingsModal() { await initCompleted; const modalContainer = document.createElement("div"); modalContainer.className = "dialogContainer"; modalContainer.style.backgroundColor = "rgba(0,0,0,0.5)"; modalContainer.addEventListener("click", e => { if (e.target == modalContainer) { modalContainer.remove(); } }); document.body.appendChild(modalContainer); const modalContainer2 = document.createElement("div"); modalContainer2.className = "focuscontainer dialog dialog-fixedSize dialog-small formDialog opened"; modalContainer.appendChild(modalContainer2); const modalHeader = document.createElement("div"); modalHeader.className = "formDialogHeader"; modalContainer2.appendChild(modalHeader); const title = document.createElement("h3"); title.className = "formDialogHeaderTitle"; title.textContent = "Jellyfin Media Player Settings"; modalHeader.appendChild(title); const modalContents = document.createElement("div"); modalContents.className = "formDialogContent smoothScrollY"; modalContents.style.paddingTop = "2em"; modalContents.style.marginBottom = "6.2em"; modalContainer2.appendChild(modalContents); const settingUpdateHandlers = {}; for (const section of Object.keys(jmpInfo.settingsDescriptions)) { const group = document.createElement("fieldset"); group.className = "editItemMetadataForm editMetadataForm dialog-content-centered"; group.style.border = 0; group.style.outline = 0; modalContents.appendChild(group); const createSection = async (clear) => { if (clear) { group.innerHTML = ""; } const values = jmpInfo.settings[section]; const settings = jmpInfo.settingsDescriptions[section]; const legend = document.createElement("legend"); const legendHeader = document.createElement("h2"); legendHeader.textContent = section; legendHeader.style.textTransform = "capitalize"; legend.appendChild(legendHeader); if (section == "plugins") { const legendSubHeader = document.createElement("h4"); legendSubHeader.textContent = "Plugins are UNOFFICIAL and require a restart to take effect."; legend.appendChild(legendSubHeader); } group.appendChild(legend); for (const setting of settings) { const label = document.createElement("label"); label.className = "inputContainer"; label.style.marginBottom = "1.8em"; label.style.display = "block"; label.style.textTransform = "capitalize"; if (setting.options) { const safeValues = {}; const control = document.createElement("select"); control.className = "emby-select-withcolor emby-select"; for (const option of setting.options) { safeValues[String(option.value)] = option.value; const opt = document.createElement("option"); opt.value = option.value; opt.selected = option.value == values[setting.key]; let optionName = option.title; const swTest = `${section}.${setting.key}.`; const swTest2 = `${section}.`; if (optionName.startsWith(swTest)) { optionName = optionName.substring(swTest.length); } else if (optionName.startsWith(swTest2)) { optionName = optionName.substring(swTest2.length); } opt.appendChild(document.createTextNode(optionName)); control.appendChild(opt); } control.addEventListener("change", async (e) => { jmpInfo.settings[section][setting.key] = safeValues[e.target.value]; }); const labelText = document.createElement('label'); labelText.className = "inputLabel"; labelText.textContent = setting.key + ": "; label.appendChild(labelText); label.appendChild(control); } else { const control = document.createElement("input"); control.type = "checkbox"; control.checked = values[setting.key]; control.addEventListener("change", e => { jmpInfo.settings[section][setting.key] = e.target.checked; }); label.appendChild(control); label.appendChild(document.createTextNode(" " + setting.key)); } group.appendChild(label); } }; settingUpdateHandlers[section] = () => createSection(true); createSection(); } const onSectionUpdate = (section) => { if (section in settingUpdateHandlers) { settingUpdateHandlers[section](); } }; jmpInfo.settingsDescriptionsUpdate.push(onSectionUpdate); jmpInfo.settingsUpdate.push(onSectionUpdate); if (jmpInfo.settings.main.userWebClient) { const group = document.createElement("fieldset"); group.className = "editItemMetadataForm editMetadataForm dialog-content-centered"; group.style.border = 0; group.style.outline = 0; modalContents.appendChild(group); const legend = document.createElement("legend"); const legendHeader = document.createElement("h2"); legendHeader.textContent = "Saved Server"; legend.appendChild(legendHeader); const legendSubHeader = document.createElement("h4"); legendSubHeader.textContent = ( "The server you first connected to is your saved server. " + "It provides the web client for Jellyfin Media Player in the absence of a bundled one. " + "You can use this option to change it to another one. This does NOT log you off." ); legend.appendChild(legendSubHeader); group.appendChild(legend); const resetSavedServer = document.createElement("button"); resetSavedServer.className = "raised button-cancel block btnCancel emby-button"; resetSavedServer.textContent = "Reset Saved Server" resetSavedServer.style.marginLeft = "auto"; resetSavedServer.style.marginRight = "auto"; resetSavedServer.style.maxWidth = "50%"; resetSavedServer.addEventListener("click", async () => { window.jmpInfo.settings.main.userWebClient = ''; window.location.href = jmpInfo.scriptPath + "/find-webclient.html"; }); group.appendChild(resetSavedServer); } const closeContainer = document.createElement("div"); closeContainer.className = "formDialogFooter"; modalContents.appendChild(closeContainer); const close = document.createElement("button"); close.className = "raised button-cancel block btnCancel formDialogFooterItem emby-button"; close.textContent = "Close" close.addEventListener("click", () => { modalContainer.remove(); }); closeContainer.appendChild(close); }