From 9e0b148c99ae62c92c9650cd34abf25b5bee3235 Mon Sep 17 00:00:00 2001 From: legoskid <94498086+legoskid@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:41:47 -0800 Subject: [PATCH 1/5] First update to home page --- app/2015ytm.js | 3 + app/eracast.js | 116 +++++++++++++++++++++++++++++++++++ app/home.js | 159 +++++++++++++++++++++++++++++++++++++++++++++++- app/index.html | 2 + app/settings.js | 11 +++- 5 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 app/eracast.js diff --git a/app/2015ytm.js b/app/2015ytm.js index 07b53119..2ab20402 100644 --- a/app/2015ytm.js +++ b/app/2015ytm.js @@ -168,6 +168,7 @@ if (APP_STOP_TEXT_SELECTION_expflag == undefined) { localStorage.setItem("APP_STOP_TEXT_SELECTION", "true"); APP_STOP_TEXT_SELECTION_expflag = localStorage.getItem("APP_STOP_TEXT_SELECTION"); } +ERACAST_MODE_option = localStorage.getItem("ERACAST_MODE"); newErrorHtml = ``; + if (APP_NEW_ERROR_SCREEN_expflag == "true"){error.innerHTML=newErrorHtml}; + pageCont.before(error); + error.querySelector("button").onclick = function(){ + renderData(); + error.remove(); + }; + return; + }); + } catch (e) { + console.error('EraCast mode failed:', e); + } + return;//EraCast mode + } + const getHomeData3 = new XMLHttpRequest(); getHomeData3.open('GET', APIbaseURL + 'api/v1/popular', true); @@ -720,7 +877,7 @@ function renderDataTrending(homeShelfTrendingType, shelfTitle) { shelf.appendChild(verticalList); data.forEach(function(item) { - renderCompactMediaItem(verticalList, "shelf", item.videoId, item.videoThumbnails[3].url, item.lengthSeconds, item.title, item.author, item.authorId, item.publishedText, item.viewCount, item.type); + renderCompactMediaItem(verticalList, "shelf", item.videoId, ERACAST_MODE_option ? item.videoThumbnails[0].url : item.videoThumbnails[3].url, item.lengthSeconds, item.title, item.author, item.authorId, item.publishedText, item.viewCount, item.type); }); if (data.length > 3) { diff --git a/app/index.html b/app/index.html index cb80a8d0..13813c6d 100644 --- a/app/index.html +++ b/app/index.html @@ -73,6 +73,8 @@ + + diff --git a/app/settings.js b/app/settings.js index 362540ac..136f79fc 100644 --- a/app/settings.js +++ b/app/settings.js @@ -218,7 +218,16 @@ function settingsPage() { }; settingBlocks = [ - settingBooleanDark + settingBooleanDark, + { + "type": "boolean", + "title": EraCast_text_string, + "subtitle": EraCastDesc_text_string, + "pressed": ERACAST_MODE_option == "true", + "pressed-default": false, + "disabled": false, + "lsitem": "ERACAST_MODE" + } ]; settingBlocks.forEach(function(item){ if (item.type == "boolean") { From da908e7270ea2fb9929580151692b57eb4a30f57 Mon Sep 17 00:00:00 2001 From: legoskid <94498086+legoskid@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:53:52 -0800 Subject: [PATCH 2/5] Video playback part 1 --- app/eracast.js | 26 ++++++++++++++++++++++++++ app/player.js | 11 +++++++++++ 2 files changed, 37 insertions(+) diff --git a/app/eracast.js b/app/eracast.js index 3a74750a..cf16aab4 100644 --- a/app/eracast.js +++ b/app/eracast.js @@ -113,4 +113,30 @@ return []; } }; + + // @param {string} videoId + // @returns {Promise} + window.fetchEraCast1080WebmUrl = async function fetchEraCast1080WebmUrl(videoId) { + try { + if (!videoId || typeof videoId !== 'string') return ''; + + const watchUrl = `https://www.eracast.cc/watch?v=${encodeURIComponent(videoId)}`; + const res = await fetch(watchUrl, { method: 'GET', mode: 'cors' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + + const html = await res.text(); + + // This regex gets 1080p video + const re = /targetDiv\.setAttribute\(\s*['"]src['"]\s*,\s*['"]([^'"]*?_1080\.)['"]\s*\+\s*ext\s*\)\s*;/; + const m = html.match(re); + if (!m || !m[1]) return ''; + + const prefix = m[1]; + const abs = new URL(prefix + 'mp4', watchUrl).href; // this is not the math function + return abs; + } catch (err) { + console.error('fetchEraCast1080WebmUrl error:', err); + return ''; + } + }; })(); diff --git a/app/player.js b/app/player.js index 47b5855a..a9bc0422 100644 --- a/app/player.js +++ b/app/player.js @@ -827,11 +827,22 @@ const playerxhttpr = new XMLHttpRequest(); /* playerxhttpr.open('GET', 'https://inv.tux.pizza/api/v1/videos/' + YTmVideoId, true); */ /* playerxhttpr.open('GET', 'https://invidious.nerdvpn.de/api/v1/videos/' + YTmVideoId, true); playerxhttpr.setRequestHeader('Authorization','Basic eXRtMTU6SlFKNTNLckxBRVk2RTVxaGdjbTM4UGtTenczYlpYbWs='); */ +if (ERACAST_MODE_option == "true") { + window.fetchEraCast1080WebmUrl().then(function(data) { + playerxhttpr.open('GET', data, true); + + + playerxhttpr.send(); + }); +} else { playerxhttpr.open('GET', APIbaseURLNew + 'dl?cgeo=US&id=' + YTmVideoId, true); playerxhttpr.setRequestHeader('x-rapidapi-key', '4b0791fe33mshce00ad033774274p196706jsn957349df7a8f'); playerxhttpr.setRequestHeader('x-rapidapi-host', 'yt-api.p.rapidapi.com'); + playerxhttpr.send(); +} + playerxhttpr.onerror = function(){ videoPlayer.classList.add("player-has-error"); From 46e62fb2c129347412f6d3f207b46abdc59f6143 Mon Sep 17 00:00:00 2001 From: legoskid <94498086+legoskid@users.noreply.github.com> Date: Thu, 8 Jan 2026 21:08:08 -0800 Subject: [PATCH 3/5] Video playback part 2 --- app/eracast.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/app/eracast.js b/app/eracast.js index cf16aab4..7d0c7589 100644 --- a/app/eracast.js +++ b/app/eracast.js @@ -115,10 +115,10 @@ }; // @param {string} videoId - // @returns {Promise} + // @returns {Promise<{thumbnail:string,title:string,formats:{url:string}[]}|{}>} window.fetchEraCast1080WebmUrl = async function fetchEraCast1080WebmUrl(videoId) { try { - if (!videoId || typeof videoId !== 'string') return ''; + if (!videoId || typeof videoId !== 'string') return {}; const watchUrl = `https://www.eracast.cc/watch?v=${encodeURIComponent(videoId)}`; const res = await fetch(watchUrl, { method: 'GET', mode: 'cors' }); @@ -126,17 +126,31 @@ const html = await res.text(); + const dom = new DOMParser().parseFromString(html, 'text/html'); + const ogImage = dom.querySelector('meta[property="og:image"]')?.getAttribute('content') || ''; + const ogTitle = dom.querySelector('meta[property="og:title"]')?.getAttribute('content') || ''; + // This regex gets 1080p video const re = /targetDiv\.setAttribute\(\s*['"]src['"]\s*,\s*['"]([^'"]*?_1080\.)['"]\s*\+\s*ext\s*\)\s*;/; const m = html.match(re); - if (!m || !m[1]) return ''; + if (!m || !m[1]) return {}; const prefix = m[1]; - const abs = new URL(prefix + 'mp4', watchUrl).href; // this is not the math function - return abs; + const videoUrl = new URL(prefix + 'mp4', watchUrl).href; // this is not the math function + + console.log({ + thumbnail: ogImage, + title: ogTitle, + formats: [{ url: videoUrl }] + }); + return { + thumbnail: ogImage, + title: ogTitle, + formats: [{ url: videoUrl }] + }; } catch (err) { console.error('fetchEraCast1080WebmUrl error:', err); - return ''; + return {}; } }; })(); From ec0e28b43b0c6afdfb8a512be77a14f2a7063067 Mon Sep 17 00:00:00 2001 From: legoskid <94498086+legoskid@users.noreply.github.com> Date: Fri, 9 Jan 2026 18:23:02 -0800 Subject: [PATCH 4/5] WORKING PLAYER! --- app/player.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/app/player.js b/app/player.js index a9bc0422..011102e0 100644 --- a/app/player.js +++ b/app/player.js @@ -828,11 +828,56 @@ const playerxhttpr = new XMLHttpRequest(); /* playerxhttpr.open('GET', 'https://invidious.nerdvpn.de/api/v1/videos/' + YTmVideoId, true); playerxhttpr.setRequestHeader('Authorization','Basic eXRtMTU6SlFKNTNLckxBRVk2RTVxaGdjbTM4UGtTenczYlpYbWs='); */ if (ERACAST_MODE_option == "true") { - window.fetchEraCast1080WebmUrl().then(function(data) { - playerxhttpr.open('GET', data, true); - + window.fetchEraCast1080WebmUrl(YTmVideoId).then(function(data) { + video.poster = data.thumbnail[3].url; + video.innerHTML = ``; + video.dataset.title = data.title; + /* storyboardURL = "https://inv.tux.pizza" + data.storyboards[2].url; */ + SBVideo.src = data.formats[0].url; + +/* const sbxhttpr = new XMLHttpRequest(); + +sbxhttpr.open('GET', storyboardURL, true); - playerxhttpr.send(); +sbxhttpr.send(); + +sbxhttpr.onerror = function(){ + console.error('Unable to retrieve the storyboard for this video (' + sbxhttpr.status + ')'); +}; + +sbxhttpr.onload = function() { + if (sbxhttpr.status === 200) { + SBData = sbxhttpr.response; + } else { + sbxhttpr.onerror(); + }; +}; */ + + data.formats.forEach(function(item) { + const lastFS = data.formats.slice(-1)[0] + const vidSource = document.createElement("source"); + vidSource.src = item.url; + vidSource.type = item.mimeType; + vidSource.setAttribute("label", item.qualityLabel); + vidSource.setAttribute("selected", false); + if (item == lastFS) { + vidSource.setAttribute("selected", true); + }; + video.appendChild(vidSource); + if (vidSource.getAttribute("selected", "true")) { + video.src = item.url; + }; + }); + if (data.captions) { + data.captions.captionTracks.forEach(function(item) { + const vidTrack = document.createElement("track"); + vidTrack.kind = "captions"; + vidTrack.src = item.baseUrl; + vidTrack.srclang = item.languageCode; + vidTrack.label = item.name; + video.appendChild(vidTrack); + }); + }; }); } else { playerxhttpr.open('GET', APIbaseURLNew + 'dl?cgeo=US&id=' + YTmVideoId, true); From 8c7e770ff9c1a186ab560881382805ba2b7ce5a7 Mon Sep 17 00:00:00 2001 From: legoskid <94498086+legoskid@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:23:43 -0800 Subject: [PATCH 5/5] Video details working (kinda) --- app/eracast.js | 116 +++++++++++++++++++++++++++++++++++++++++++++++++ app/watch.js | 115 +++++++++++++++++++++++++++++------------------- 2 files changed, 186 insertions(+), 45 deletions(-) diff --git a/app/eracast.js b/app/eracast.js index 7d0c7589..619b829b 100644 --- a/app/eracast.js +++ b/app/eracast.js @@ -153,4 +153,120 @@ return {}; } }; + + // @param {string} videoId + // @returns {Promise} + window.fetchEraCastVideoInfo = async function fetchEraCastVideoInfo(videoId) { + try { + if (!videoId || typeof videoId !== 'string') return {}; + + const watchUrl = `https://www.eracast.cc/watch?v=${encodeURIComponent(videoId)}`; + const res = await fetch(watchUrl, { method: 'GET', mode: 'cors' }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + + const html = await res.text(); + const dom = new DOMParser().parseFromString(html, 'text/html'); + + const title = dom.querySelector('meta[property="og:title"]')?.getAttribute('content') || ''; + + const parseLengthSeconds = (t) => { + if (!t) return 0; + const parts = t.split(':').map(p => parseInt(p, 10)); + if (parts.some(isNaN)) return 0; + if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; + if (parts.length === 2) return parts[0] * 60 + parts[1]; + return parts[0]; + }; + + const lengthText = (dom.querySelector('.video-time')?.textContent || '').trim(); + const lengthSeconds = parseLengthSeconds(lengthText); + + const channelTitle = (dom.querySelector('.g-hovercard')?.textContent || '').trim(); + + const subscribeBtn = dom.querySelector( + '[class~="yt-uix-button"][class~="yt-uix-button-size-default"][class~="yt-uix-button-subscribe-branded"][class~="yt-uix-button-has-icon"][class~="no-icon-markup"][class~="yt-uix-subscription-button"][class~="yt-can-buffer"]' + ); + const channelId = subscribeBtn?.getAttribute('data-channel-external-id') || ''; + + const description = (dom.querySelector('#eow-description')?.textContent || '').trim(); + + const viewCountRaw = (dom.querySelector('.view-count')?.textContent || '').trim(); + const viewCountNumber = (() => { + // Commas + const m = viewCountRaw.replace(/\u00a0/g, ' ').match(/([0-9][0-9,\.]*)/); + if (!m) return 0; + const n = parseInt(m[1].replace(/[^0-9]/g, ''), 10); + return Number.isFinite(n) ? n : 0; + })(); + + const watchTimeText = (dom.querySelector('.watch-time-text')?.textContent || '').replace(/\s+/g, ' ').trim(); + const toIsoPublished = (t) => { + // Published date -> ISO 8601 (To be compatible) + const cleaned = (t || '').replace(/^Published on\s+/i, '').trim(); + const d = new Date(cleaned); + if (Number.isNaN(d.getTime())) return ''; + d.setHours(0, 0, 0, 0); + const pad = (x) => String(x).padStart(2, '0'); + const offMin = -d.getTimezoneOffset(); + const sign = offMin >= 0 ? '+' : '-'; + const abs = Math.abs(offMin); + const offH = pad(Math.floor(abs / 60)); + const offM = pad(abs % 60); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T00:00:00${sign}${offH}:${offM}`; + }; + const publishDateIso = toIsoPublished(watchTimeText); + + const likeCountText = (dom.querySelector('#watch-like span')?.textContent || '').trim(); + const dislikeCountText = (dom.querySelector('#watch-dislike span')?.textContent || '').trim(); + + const subscriberCountCore = (dom.querySelector('.yt-subscriber-count')?.textContent || '').trim(); + const subscriberCountText = subscriberCountCore ? `${subscriberCountCore} subscribers` : ''; + + const channelThumbnail = dom.querySelector('.yt-thumb-clip img')?.getAttribute('src') || ''; + + const thumbnailList = (() => { + const ogImage = dom.querySelector('meta[property="og:image"]')?.getAttribute('content') || ''; + if (!ogImage) return []; + return [{ url: ogImage, width: 0, height: 0 }]; + })(); + + return { + id: videoId, + title, + lengthSeconds: String(lengthSeconds), + keywords: [], + channelTitle, + channelId, + description, + thumbnail: thumbnailList, + allowRatings: true, + viewCount: String(viewCountNumber), + isPrivate: false, + isUnpluggedCorpus: false, + isLiveContent: false, + isLive: false, + isCrawlable: true, + isFamilySafe: true, + availableCountries: [], + isUnlisted: false, + category: '', + publishDate: publishDateIso, + publishedAt: publishDateIso, + uploadDate: publishDateIso, + isShortsEligible: false, + likeCount: likeCountText.replace(/[^0-9]/g, '') || likeCountText, + dislikeCount: dislikeCountText.replace(/[^0-9]/g, '') || dislikeCountText, + hasCaption: false, + storyboards: [], + playableInEmbed: true, + channelThumbnail: [{url:channelThumbnail}], + subscriberCountText, + extraMeta: [], + relatedVideos: { continuation: '', data: [{videoId: videoId,title:"placeholder video btw",channelTitle:"YouTube Mobile 2015/legoskid",thumbnail:[{url:""},{url:""}],lengthText:"0:00"}] } + }; + } catch (err) { + console.error('fetchEraCastVideoInfo error:', err); + return {}; + } + }; })(); diff --git a/app/watch.js b/app/watch.js index a782e031..bfff90c4 100644 --- a/app/watch.js +++ b/app/watch.js @@ -22,50 +22,8 @@ function renderWatchPage(parent) { insertYTmPlayer(playerCont2); - const getWatchData = new XMLHttpRequest(); - /* getWatchData.open('GET', APIbaseURL + 'api/v1/videos/' + playerVideoId, true); */ - /* getWatchData.open('GET', APIbaseURLWatch + 'api/v1/videos/' + playerVideoId, true); */ - /* getWatchData.setRequestHeader('Authorization','Basic eXRtMTU6SlFKNTNLckxBRVk2RTVxaGdjbTM4UGtTenczYlpYbWs='); */ - getWatchData.open('GET', APIbaseURLNew + 'video/info?extend=1&geo=US&id=' + playerVideoId, true); - getWatchData.setRequestHeader('x-rapidapi-key', '4b0791fe33mshce00ad033774274p196706jsn957349df7a8f'); - getWatchData.setRequestHeader('x-rapidapi-host', 'yt-api.p.rapidapi.com'); - - getWatchData.onerror = function(event) { - console.error("An error occurred with this operation (" + getWatchData.status + ")"); - - contItem.remove(); - - const error = document.createElement("div"); - error.classList.add('error-container'); - error.innerHTML = `
- -There was an error connecting to the server -
-
`; - if (APP_NEW_ERROR_SCREEN_expflag == "true"){error.innerHTML=newErrorHtml}; - /* if (JSON.parse(getWatchData.response)) { - const data = JSON.parse(getWatchData.response); - if (data.error) { - error.querySelector(".error-text").textContent = data.error; - } - } */ - const errorBtn = error.querySelector("button"); - errorBtn.onclick = function(){renderWatchPage(parent)}; - parent.appendChild(error); - if (JSON.parse(getWatchData.response)) { - const data = JSON.parse(getWatchData.response); - if (data.error) { - error.querySelector(".error-text").textContent = data.error; - } - } - return; - }; - - getWatchData.send(); - - getWatchData.onload = function() { - if (getWatchData.status === 200) { - const data = JSON.parse(getWatchData.response); + const fetchAndRenderWatchData = function(data) { + console.log(data); playerNextVideoId = data.relatedVideos.data[0].videoId; @@ -486,7 +444,7 @@ function renderWatchPage(parent) { videoOwner.innerHTML = `
- +