From 6d6d3a4e35b5a791443d4bc898e06bd89c4426e1 Mon Sep 17 00:00:00 2001 From: Animesh <69345507+n-ce@users.noreply.github.com> Date: Mon, 14 Jul 2025 22:26:50 +0530 Subject: [PATCH 01/28] Create localExtraction.ts --- src/modules/localExtraction.ts | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/modules/localExtraction.ts diff --git a/src/modules/localExtraction.ts b/src/modules/localExtraction.ts new file mode 100644 index 000000000..c93d5ae93 --- /dev/null +++ b/src/modules/localExtraction.ts @@ -0,0 +1,65 @@ +export default async function fetchDatafromLocal(id: string): Promise { + const res = await fetch('https://localhost:9999/streams/'+ id); + + if (!res.ok) { + const errorData = await res.json().catch(() => ({ message: `HTTP error! status: ${res.status}` })); + throw new Error(`Local/production fetch failed: ${errorData.message || 'Unknown error'}`); + } + + const data = await res.json(); + + const { videoDetails, streamingData, captions } = data; + + if (!videoDetails || !streamingData) { + throw new Error('Invalid local data structure: missing videoDetails or streamingData.'); + } + + const audioStreams = streamingData.adaptiveFormats + ? streamingData.adaptiveFormats + .filter((f: any) => f.mimeType?.startsWith('audio')) + .map((f: any) => ({ + url: f.url, + bitrate: parseInt(f.bitrate), + codec: f.mimeType.includes('opus') ? 'opus' : (f.mimeType.includes('aac') ? 'aac' : 'unknown'), + contentLength: parseInt(f.contentLength || '0'), + quality: f.audioQuality || `${Math.floor(parseInt(f.bitrate) / 1024)} kbps`, + mimeType: f.mimeType, + })) + : []; + + const videoStreams = streamingData.adaptiveFormats + ? streamingData.adaptiveFormats + .filter((f: any) => f.mimeType?.startsWith('video')) + .map((f: any) => ({ + url: f.url, + quality: f.qualityLabel || f.quality, + resolution: `${f.width || ''}x${f.height || ''}`.replace(/^x|x$/, ''), + type: f.mimeType, + })) + : []; + + const captionTracks = captions?.playerCaptionsTracklistRenderer?.captionTracks + ? captions.playerCaptionsTracklistRenderer.captionTracks.map((track: any) => ({ + baseUrl: track.baseUrl, + name: track.name?.runs?.[0]?.text || 'Unknown', + vssId: track.vssId, + languageCode: track.languageCode, + kind: track.kind, + isTranslatable: track.isTranslatable, + })) + : []; + + return ({ + title: videoDetails.title, + uploader: videoDetails.author, + duration: parseInt(videoDetails.lengthSeconds), + uploaderUrl: `/channel/${videoDetails.channelId}`, + liveStream: videoDetails.isLiveContent, + captions: captionTracks.length ? captionTracks : undefined, + relatedStreams: [], + videoStreams: videoStreams, + audioStreams: audioStreams, + hls: streamingData.hlsManifestUrl || undefined, + }); + +} From b94ef9ed0077977289db9e0e77d21c9e367f35e7 Mon Sep 17 00:00:00 2001 From: Animesh <69345507+n-ce@users.noreply.github.com> Date: Mon, 14 Jul 2025 22:32:35 +0530 Subject: [PATCH 02/28] Update localExtraction.ts --- src/modules/localExtraction.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/modules/localExtraction.ts b/src/modules/localExtraction.ts index c93d5ae93..3b13849f3 100644 --- a/src/modules/localExtraction.ts +++ b/src/modules/localExtraction.ts @@ -1,4 +1,4 @@ -export default async function fetchDatafromLocal(id: string): Promise { +export async function(id: string): Promise { const res = await fetch('https://localhost:9999/streams/'+ id); if (!res.ok) { @@ -6,13 +6,10 @@ export default async function fetchDatafromLocal(id: string): Promise { throw new Error(`Local/production fetch failed: ${errorData.message || 'Unknown error'}`); } - const data = await res.json(); + const { videoDetails, streamingData, captions } = await res.json(); - const { videoDetails, streamingData, captions } = data; - - if (!videoDetails || !streamingData) { + if (!videoDetails || !streamingData) throw new Error('Invalid local data structure: missing videoDetails or streamingData.'); - } const audioStreams = streamingData.adaptiveFormats ? streamingData.adaptiveFormats From bc084110d1d6c3f8619095ca39786fef7fcc0caa Mon Sep 17 00:00:00 2001 From: Animesh <69345507+n-ce@users.noreply.github.com> Date: Mon, 14 Jul 2025 22:37:11 +0530 Subject: [PATCH 03/28] Update getStreamData.ts --- src/modules/getStreamData.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/getStreamData.ts b/src/modules/getStreamData.ts index 3dccacb00..5989ae692 100644 --- a/src/modules/getStreamData.ts +++ b/src/modules/getStreamData.ts @@ -92,8 +92,10 @@ export default async function( return ff[0].value || { message: 'No HLS sources are available.' }; }); + const useLocal = async () => await import('localExtraction.ts').then(mod => mod.default(id)); - return state.HLS ? useHls() : state.enforcePiped ? usePiped() : useInvidious(); + + return (location.port === '9999') ? useLocal() : state.HLS ? useHls() : state.enforcePiped ? usePiped() : useInvidious(); } From aa98df3531ddee33d0c359e3cb313fa381b12129 Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 15 Jul 2025 12:40:32 +0530 Subject: [PATCH 04/28] fix bugs --- package.json | 10 +++---- src/modules/getStreamData.ts | 2 +- src/modules/localExtraction.ts | 55 +++++++++++++++++----------------- src/stylesheets/header.css | 6 +++- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 4468fd75f..a709d4b1a 100644 --- a/package.json +++ b/package.json @@ -7,19 +7,19 @@ "preview": "vite preview" }, "dependencies": { - "hls.js": "^1.6.6", + "hls.js": "^1.6.7", "sortablejs": "^1.15.6", "uhtml": "^4.7.1" }, "devDependencies": { - "@netlify/blobs": "^10.0.3", - "@netlify/edge-functions": "^2.15.5", - "@types/node": "^24.0.10", + "@netlify/blobs": "^10.0.4", + "@netlify/edge-functions": "^2.15.6", + "@types/node": "^24.0.13", "@types/sortablejs": "^1.15.8", "autoprefixer": "^10.4.21", "eruda": "^3.4.3", "typescript": "^5.8.3", - "vite": "^7.0.2", + "vite": "^7.0.4", "vite-plugin-pwa": "^1.0.1" }, "browserslist": [ diff --git a/src/modules/getStreamData.ts b/src/modules/getStreamData.ts index 5989ae692..f3bdbd5df 100644 --- a/src/modules/getStreamData.ts +++ b/src/modules/getStreamData.ts @@ -92,7 +92,7 @@ export default async function( return ff[0].value || { message: 'No HLS sources are available.' }; }); - const useLocal = async () => await import('localExtraction.ts').then(mod => mod.default(id)); + const useLocal = async () => await import('./localExtraction.ts').then(mod => mod.fetchDataFromLocal(id)); return (location.port === '9999') ? useLocal() : state.HLS ? useHls() : state.enforcePiped ? usePiped() : useInvidious(); diff --git a/src/modules/localExtraction.ts b/src/modules/localExtraction.ts index 3b13849f3..69bd39ee2 100644 --- a/src/modules/localExtraction.ts +++ b/src/modules/localExtraction.ts @@ -1,5 +1,5 @@ -export async function(id: string): Promise { - const res = await fetch('https://localhost:9999/streams/'+ id); +export async function fetchDataFromLocal(id: string): Promise { + const res = await fetch('https://localhost:9999/streams/' + id); if (!res.ok) { const errorData = await res.json().catch(() => ({ message: `HTTP error! status: ${res.status}` })); @@ -13,46 +13,47 @@ export async function(id: string): Promise { const audioStreams = streamingData.adaptiveFormats ? streamingData.adaptiveFormats - .filter((f: any) => f.mimeType?.startsWith('audio')) - .map((f: any) => ({ - url: f.url, - bitrate: parseInt(f.bitrate), - codec: f.mimeType.includes('opus') ? 'opus' : (f.mimeType.includes('aac') ? 'aac' : 'unknown'), - contentLength: parseInt(f.contentLength || '0'), - quality: f.audioQuality || `${Math.floor(parseInt(f.bitrate) / 1024)} kbps`, - mimeType: f.mimeType, - })) + .filter((f: any) => f.mimeType?.startsWith('audio')) + .map((f: any) => ({ + url: f.url, + bitrate: parseInt(f.bitrate), + codec: f.mimeType.includes('opus') ? 'opus' : (f.mimeType.includes('aac') ? 'aac' : 'unknown'), + contentLength: parseInt(f.contentLength || '0'), + quality: f.audioQuality || `${Math.floor(parseInt(f.bitrate) / 1024)} kbps`, + mimeType: f.mimeType, + })) : []; const videoStreams = streamingData.adaptiveFormats ? streamingData.adaptiveFormats - .filter((f: any) => f.mimeType?.startsWith('video')) - .map((f: any) => ({ - url: f.url, - quality: f.qualityLabel || f.quality, - resolution: `${f.width || ''}x${f.height || ''}`.replace(/^x|x$/, ''), - type: f.mimeType, - })) + .filter((f: any) => f.mimeType?.startsWith('video')) + .map((f: any) => ({ + url: f.url, + quality: f.qualityLabel || f.quality, + resolution: `${f.width || ''}x${f.height || ''}`.replace(/^x|x$/, ''), + type: f.mimeType, + })) : []; const captionTracks = captions?.playerCaptionsTracklistRenderer?.captionTracks ? captions.playerCaptionsTracklistRenderer.captionTracks.map((track: any) => ({ - baseUrl: track.baseUrl, - name: track.name?.runs?.[0]?.text || 'Unknown', - vssId: track.vssId, - languageCode: track.languageCode, - kind: track.kind, - isTranslatable: track.isTranslatable, - })) + baseUrl: track.baseUrl, + name: track.name?.runs?.[0]?.text || 'Unknown', + vssId: track.vssId, + languageCode: track.languageCode, + kind: track.kind, + isTranslatable: track.isTranslatable, + })) : []; return ({ + instance: 'Local Extractor', title: videoDetails.title, uploader: videoDetails.author, duration: parseInt(videoDetails.lengthSeconds), uploaderUrl: `/channel/${videoDetails.channelId}`, - liveStream: videoDetails.isLiveContent, - captions: captionTracks.length ? captionTracks : undefined, + livestream: videoDetails.isLiveContent, + captions: captionTracks, relatedStreams: [], videoStreams: videoStreams, audioStreams: audioStreams, diff --git a/src/stylesheets/header.css b/src/stylesheets/header.css index d6c21c9f3..618a93ea5 100644 --- a/src/stylesheets/header.css +++ b/src/stylesheets/header.css @@ -62,7 +62,11 @@ section>header { cursor: pointer; &:has(a) { - cursor: auto; + cursor: none; + + a { + cursor: pointer; + } } i { From f497d7b15847e04b94ae69031b119fdfd60280e8 Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 15 Jul 2025 14:11:24 +0530 Subject: [PATCH 05/28] use http for local extraction --- src/modules/localExtraction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/localExtraction.ts b/src/modules/localExtraction.ts index 69bd39ee2..2ddb8b78b 100644 --- a/src/modules/localExtraction.ts +++ b/src/modules/localExtraction.ts @@ -1,5 +1,5 @@ export async function fetchDataFromLocal(id: string): Promise { - const res = await fetch('https://localhost:9999/streams/' + id); + const res = await fetch('http://localhost:9999/streams/' + id); if (!res.ok) { const errorData = await res.json().catch(() => ({ message: `HTTP error! status: ${res.status}` })); From 694d83b2bc6b158fd4cc2218c1becbdcb8b33b6d Mon Sep 17 00:00:00 2001 From: Animesh <69345507+n-ce@users.noreply.github.com> Date: Sat, 26 Jul 2025 12:44:25 +0530 Subject: [PATCH 06/28] remove piped enforcement control flow from audio error handler --- src/modules/audioErrorHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/audioErrorHandler.ts b/src/modules/audioErrorHandler.ts index 25f25fc6d..88f2c8674 100644 --- a/src/modules/audioErrorHandler.ts +++ b/src/modules/audioErrorHandler.ts @@ -8,9 +8,9 @@ export default function(audio: HTMLAudioElement) { const id = store.stream.id; const { fallback } = store.player; const { index, invidious } = store.api; - const { enforcePiped, HLS, customInstance } = state; + const { HLS, customInstance } = state; - if (enforcePiped || HLS || customInstance) + if (HLS || customInstance) return notify(message); const origin = new URL(audio.src).origin; From 4c7d50d7e1036cd26f3c32c57fa654732c6b2cb8 Mon Sep 17 00:00:00 2001 From: Animesh <69345507+n-ce@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:29:49 +0530 Subject: [PATCH 07/28] Inject '- Topic' when available for songs playlist returned by Hyperpipe --- src/modules/fetchList.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/modules/fetchList.ts b/src/modules/fetchList.ts index ff3496813..784f024f9 100644 --- a/src/modules/fetchList.ts +++ b/src/modules/fetchList.ts @@ -107,6 +107,15 @@ export default async function fetchList( (item: StreamItem) => !existingItems.includes( item.url.slice(-11)) ); + + if (useHyperpipe) + data.relatedStreams = data.relatedStreams.map( + (item: StreamItem) => { + if (!item.uploaderName.endsWith(' - Topic')) + item.uploaderName += ' - Topic'; + return item; + } + ); listData = listData.concat(filterOutMembersOnly(data.relatedStreams)); render(listContainer, ItemsLoader(listData)); From 9304e9bd1c298aa207fc4cae05d2358db793843e Mon Sep 17 00:00:00 2001 From: Animesh <69345507+n-ce@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:54:52 +0530 Subject: [PATCH 08/28] Support music streams directly from search instead of inferring --- src/modules/fetchSearchResults.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/modules/fetchSearchResults.ts b/src/modules/fetchSearchResults.ts index 7fe29ae40..8e7260fdb 100644 --- a/src/modules/fetchSearchResults.ts +++ b/src/modules/fetchSearchResults.ts @@ -91,6 +91,15 @@ const fetchWithPiped = ( results = items?.filter((item: StreamItem) => !item.isShort); + if (searchFilters.value === 'music_songs') + results = items.map( + (item: StreamItem) => { + if (!item.uploaderName.endsWith(' - Topic')) + item.uploaderName += ' - Topic'; + return item; + } + ); + if (currentObserver) currentObserver.disconnect(); @@ -102,6 +111,14 @@ const fetchWithPiped = ( (data.items as StreamItem[]) .filter((item) => !item.isShort && item.duration !== -1) .forEach((i) => { + if (searchFilters.value === 'music_songs') + i = i.map( + (item: StreamItem) => { + if (!item.uploaderName.endsWith(' - Topic')) + item.uploaderName += ' - Topic'; + return item; + } + ); if (results.find((v) => v.url === i.url) === undefined) results.push(i); }); From cb7039500a362fe7f7e60b8cd02dd22031883b66 Mon Sep 17 00:00:00 2001 From: Animesh <69345507+n-ce@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:56:04 +0530 Subject: [PATCH 09/28] remove music stream inferenec --- src/components/ItemsLoader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ItemsLoader.ts b/src/components/ItemsLoader.ts index ea213e284..d786800e8 100644 --- a/src/components/ItemsLoader.ts +++ b/src/components/ItemsLoader.ts @@ -30,7 +30,7 @@ export default function(itemsArray: string | StreamItem[]) { id: item.videoId || item.url.substring(9), href: hostResolver(item.url || ('/watch?v=' + item.videoId)), title: item.title, - author: (item.uploaderName || item.author) + (location.search.endsWith('music_songs') ? ' - Topic' : ''), + author: (item.uploaderName || item.author), duration: (item.duration || item.lengthSeconds) > 0 ? convertSStoHHMMSS(item.duration || item.lengthSeconds) : 'LIVE', uploaded: item.uploadedDate || item.publishedText, channelUrl: item.uploaderUrl || item.authorUrl, From 19299aeccab4a9a85ff5e55c258566b88d7c842b Mon Sep 17 00:00:00 2001 From: Animesh <69345507+n-ce@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:08:32 +0530 Subject: [PATCH 10/28] fix search music stream checker injection --- src/modules/fetchSearchResults.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/modules/fetchSearchResults.ts b/src/modules/fetchSearchResults.ts index 8e7260fdb..51c154eca 100644 --- a/src/modules/fetchSearchResults.ts +++ b/src/modules/fetchSearchResults.ts @@ -112,13 +112,9 @@ const fetchWithPiped = ( .filter((item) => !item.isShort && item.duration !== -1) .forEach((i) => { if (searchFilters.value === 'music_songs') - i = i.map( - (item: StreamItem) => { - if (!item.uploaderName.endsWith(' - Topic')) - item.uploaderName += ' - Topic'; - return item; - } - ); + if(!i.uploaderName.endsWith(' - Topic')) + i.uploaderName += ' - Topic'; + if (results.find((v) => v.url === i.url) === undefined) results.push(i); }); From 8a3c8ee30922e165a928e17d5386c9cff49ccb2d Mon Sep 17 00:00:00 2001 From: Animesh <69345507+n-ce@users.noreply.github.com> Date: Tue, 29 Jul 2025 14:23:53 +0530 Subject: [PATCH 11/28] change updater source to v7x8 branch --- src/components/UpdatePrompt.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/UpdatePrompt.ts b/src/components/UpdatePrompt.ts index 6e6c2ff81..a4b80afdf 100644 --- a/src/components/UpdatePrompt.ts +++ b/src/components/UpdatePrompt.ts @@ -4,8 +4,8 @@ import { html, render } from 'uhtml'; export default async function(dialog: HTMLDialogElement) { - const commitsSrc = 'https://api.github.com/repos/n-ce/ytify/commits/main'; - const commitsLink = 'https://github.com/n-ce/ytify/commits'; + const commitsSrc = 'https://api.github.com/repos/n-ce/ytify/commits/v7x8'; + const commitsLink = 'https://github.com/n-ce/ytify/commits/v7x8'; const list = await fetch(commitsSrc) .then(res => res.json()) .then(data => data.commit.message.split('-')) From cd1dfb320d50609ae0f88c1f2506649b85c2c23f Mon Sep 17 00:00:00 2001 From: Animesh <69345507+n-ce@users.noreply.github.com> Date: Tue, 29 Jul 2025 15:35:17 +0530 Subject: [PATCH 12/28] music context injection for upfront list items --- src/modules/fetchList.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/modules/fetchList.ts b/src/modules/fetchList.ts index 784f024f9..655bc470a 100644 --- a/src/modules/fetchList.ts +++ b/src/modules/fetchList.ts @@ -60,12 +60,22 @@ export default async function fetchList( if (listContainer.classList.contains('reverse')) listContainer.classList.remove('reverse'); + if (useHyperpipe) + group.relatedStreams = group.relatedStreams.map( + (item: StreamItem) => { + if (!item.uploaderName.endsWith(' - Topic')) + item.uploaderName += ' - Topic'; + return item; + } + ); + const filterOutMembersOnly = (streams: StreamItem[]) => (type === 'channel' && streams.length) ? // hide members-only streams streams.filter((s: StreamItem) => s.views !== -1) : streams; listData = filterOutMembersOnly(group.relatedStreams); + render(listContainer, ItemsLoader(listData)); if (location.pathname !== '/list') From a0d72095bef0a10b60b1a404abad80ff71f66b2c Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 29 Jul 2025 16:22:23 +0530 Subject: [PATCH 13/28] add initial lastUpdated field to library --- src/index.d.ts | 4 ++-- src/lib/libraryUtils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 2071a3647..503d58818 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -40,7 +40,8 @@ declare global { title: string, author: string, duration: string - channelUrl: string + channelUrl: string, + lastUpdated?: string } type List = Record<'id' | 'name' | 'thumbnail', string> @@ -102,7 +103,6 @@ declare global { title: string, uploader: string, duration: number, - uploader: string, uploaderUrl: string, livestream: boolean, hls: string diff --git a/src/lib/libraryUtils.ts b/src/lib/libraryUtils.ts index 17e96f6f4..03ecf76de 100644 --- a/src/lib/libraryUtils.ts +++ b/src/lib/libraryUtils.ts @@ -41,7 +41,7 @@ export function toCollection( } // create if collection does not exists else db[collection] = {}; - + data.lastUpdated = new Date().toISOString(); db[collection][id] = data; } From e5acbcab4ccf9b647b2ba1ad3c4ec6c2e29573fb Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 29 Jul 2025 16:39:13 +0530 Subject: [PATCH 14/28] improve lastUpdated integration --- src/components/StreamItem.ts | 4 +++- src/lib/libraryUtils.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/StreamItem.ts b/src/components/StreamItem.ts index 84d0921aa..ddebb545d 100644 --- a/src/components/StreamItem.ts +++ b/src/components/StreamItem.ts @@ -13,7 +13,8 @@ export default function(data: { channelUrl?: string, views?: string, img?: string, - draggable?: boolean + draggable?: boolean, + lastUpdated?: string }) { let anchor!: HTMLAnchorElement; let imgsrc = ''; @@ -60,6 +61,7 @@ export default function(data: { data-channel_url=${data.channelUrl} data-duration=${data.duration} data-thumbnail=${imgsrc} + data-last_updated=${data.lastUpdated || new Date().toISOString()} > diff --git a/src/lib/libraryUtils.ts b/src/lib/libraryUtils.ts index 03ecf76de..18445ba40 100644 --- a/src/lib/libraryUtils.ts +++ b/src/lib/libraryUtils.ts @@ -97,6 +97,7 @@ export function renderCollection( author: v.author, duration: v.duration || '', channelUrl: v.channelUrl, + lastUpdated: v.lastUpdated || new Date().toISOString(), draggable: draggable }) ) From 815236eda87fc1f1cda29de0a898c30eaadc8cba Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 29 Jul 2025 17:33:44 +0530 Subject: [PATCH 15/28] jioSaavn integration out of beta --- src/lib/player.ts | 53 +++++----------------------------------- src/lib/store.ts | 6 +++-- src/lib/utils.ts | 1 + src/locales/en.json | 2 +- src/modules/jioSaavn.ts | 46 ++++++++++++++++++++++++++++++++++ src/modules/listUtils.ts | 3 ++- 6 files changed, 60 insertions(+), 51 deletions(-) create mode 100644 src/modules/jioSaavn.ts diff --git a/src/lib/player.ts b/src/lib/player.ts index c7972f620..23cfafad4 100644 --- a/src/lib/player.ts +++ b/src/lib/player.ts @@ -1,4 +1,4 @@ -import { audio, favButton, favIcon, playButton, qualityView, title as ptitle } from "./dom"; +import { audio, favButton, favIcon, playButton, title } from "./dom"; import { convertSStoHHMMSS } from "./utils"; import { params, state, store } from "./store"; import { setMetaData } from "../modules/setMetadata"; @@ -22,13 +22,13 @@ export default async function player(id: string | null = '') { playButton.classList.replace(playButton.className, 'ri-loader-3-line'); - if (useSaavn) { + if (store.player.useSaavn) { if (state.jiosaavn && store.stream.author.endsWith('Topic')) - return saavnPlayer(); + return import('../modules/jioSaavn').then(mod => mod.default()); } - else useSaavn = true; + else store.player.useSaavn = state.jiosaavn; - ptitle.textContent = 'Fetching Data...'; + title.textContent = 'Fetching Data...'; const data = await getStreamData(id); @@ -36,7 +36,7 @@ export default async function player(id: string | null = '') { store.player.data = data; else { playButton.classList.replace(playButton.className, 'ri-stop-circle-fill'); - ptitle.textContent = data.message || data.error || 'Fetching Data Failed'; + title.textContent = data.message || data.error || 'Fetching Data Failed'; return; } @@ -107,44 +107,3 @@ export default async function player(id: string | null = '') { } -let useSaavn = true; -function saavnPlayer() { - ptitle.textContent = 'Fetching Data via JioSaavn...'; - const { title, author, id } = store.stream; - const query = encodeURIComponent(`${title} ${author.slice(0, -8)}`); - - fetch(`${store.api.jiosaavn}/api/search/songs?query=${query}`) - .then(res => res.json()) - .then(_ => _.data.results[0]) - .then(data => { - const { name, downloadUrl, artists } = data; - - if ( - title.startsWith(name) && - author.startsWith(artists.primary[0].name) - ) - store.player.data = data; - - else throw new Error('Music stream not found'); - - setMetaData(store.stream); - - const { url, quality } = downloadUrl[{ - low: 1, - medium: downloadUrl.length - 2, - high: downloadUrl.length - 1 - }[state.quality]]; - - audio.src = url.replace('http:', 'https:'); - qualityView.textContent = quality + ' AAC'; - params.set('s', id); - - if (location.pathname === '/') - history.replaceState({}, '', location.origin + '?s=' + params.get('s')); - }) - .catch(e => { - ptitle.textContent = e.message || e.error || 'JioSaavn Playback Failure'; - useSaavn = false; - player(store.stream.id); - }); -} diff --git a/src/lib/store.ts b/src/lib/store.ts index c95e70620..36923f3ea 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -74,7 +74,8 @@ export const store: { supportsOpus: Promise, data: Piped | undefined, legacy: boolean, - fallback: string + fallback: string, + useSaavn: boolean }, lrcSync: (arg0: number) => {} | void, queue: { @@ -113,7 +114,8 @@ export const store: { }).then(res => res.supported), data: undefined, legacy: !('OffscreenCanvas' in window), - fallback: '' + fallback: '', + useSaavn: state.jiosaavn, }, lrcSync: () => { }, queue: { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c568bc5ea..1867bde69 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -210,6 +210,7 @@ export async function superClick(e: Event) { sta.author = elp.author as string; sta.channelUrl = elp.channel_url as string; sta.duration = elp.duration as string; + sta.lastUpdated = elp.last_updated as string || new Date().toISOString(); const dialog = document.createElement('dialog'); document.body.appendChild(dialog); import('../components/ActionsMenu.ts') diff --git a/src/locales/en.json b/src/locales/en.json index dbe95ee8e..a54b499a1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -114,7 +114,7 @@ "settings_always_proxy_streams": "Always Proxy Streams", "settings_stable_volume": "Prefer Stable Volume", "settings_hls": "HTTP Live Streaming", - "settings_jiosaavn": "Prefer JioSaavn for Music (Beta)", + "settings_jiosaavn": "Prefer JioSaavn for Music", "settings_watchmode": "Watch Mode", "settings_library": "Library", "settings_set_as_default_tab": "Set as Default Tab", diff --git a/src/modules/jioSaavn.ts b/src/modules/jioSaavn.ts new file mode 100644 index 000000000..21a404da0 --- /dev/null +++ b/src/modules/jioSaavn.ts @@ -0,0 +1,46 @@ +import { store, params, state } from '../lib/store'; +import { audio, qualityView, title } from '../lib/dom'; +import { setMetaData } from './setMetadata'; +import player from '../lib/player'; + +export default function() { + title.textContent = 'Fetching Data via JioSaavn...'; + const { author, id } = store.stream; + const query = encodeURIComponent(`${store.stream.title.replace(/\(.*?\)/g, '')} ${author.slice(0, -8)}`); + + + fetch(`${store.api.jiosaavn}/api/search/songs?query=${query}`) + .then(res => res.json()) + .then(_ => _.data.results[0]) + .then(data => { + const { name, downloadUrl, artists } = data; + + if ( + store.stream.title.startsWith(name) && + author.startsWith(artists.primary[0].name) + ) + store.player.data = data; + + else throw new Error('Music stream not found'); + + setMetaData(store.stream); + + const { url, quality } = downloadUrl[{ + low: 1, + medium: downloadUrl.length - 2, + high: downloadUrl.length - 1 + }[state.quality]]; + + audio.src = url.replace('http:', 'https:'); + qualityView.textContent = quality + ' AAC'; + params.set('s', id); + + if (location.pathname === '/') + history.replaceState({}, '', location.origin + '?s=' + params.get('s')); + }) + .catch(e => { + title.textContent = e.message || e.error || 'JioSaavn Playback Failure'; + store.player.useSaavn = false; + player(store.stream.id); + }); +} diff --git a/src/modules/listUtils.ts b/src/modules/listUtils.ts index 9f9852ecd..7883fa04e 100644 --- a/src/modules/listUtils.ts +++ b/src/modules/listUtils.ts @@ -50,7 +50,8 @@ export function importList() { 'title': sender.title, 'author': sender.author, 'duration': sender.duration, - 'channelUrl': sender.channel_url + 'channelUrl': sender.channel_url, + 'lastUpdated': sender.last_updated || new Date().toISOString() }; }); From e52b2e0bec4a63ea7681aa552df133dbb943b58f Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 29 Jul 2025 17:43:15 +0530 Subject: [PATCH 16/28] fix jioSaavn control flow --- package.json | 10 +++++----- src/lib/player.ts | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index a709d4b1a..7210154f6 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,15 @@ "uhtml": "^4.7.1" }, "devDependencies": { - "@netlify/blobs": "^10.0.4", - "@netlify/edge-functions": "^2.15.6", - "@types/node": "^24.0.13", + "@netlify/blobs": "^10.0.8", + "@netlify/edge-functions": "^2.16.2", + "@types/node": "^24.1.0", "@types/sortablejs": "^1.15.8", "autoprefixer": "^10.4.21", "eruda": "^3.4.3", "typescript": "^5.8.3", - "vite": "^7.0.4", - "vite-plugin-pwa": "^1.0.1" + "vite": "^7.0.6", + "vite-plugin-pwa": "^1.0.2" }, "browserslist": [ "defaults" diff --git a/src/lib/player.ts b/src/lib/player.ts index 23cfafad4..13ca9f1cc 100644 --- a/src/lib/player.ts +++ b/src/lib/player.ts @@ -22,11 +22,12 @@ export default async function player(id: string | null = '') { playButton.classList.replace(playButton.className, 'ri-loader-3-line'); - if (store.player.useSaavn) { - if (state.jiosaavn && store.stream.author.endsWith('Topic')) + if (state.jiosaavn) { + if (!store.player.useSaavn) + store.player.useSaavn = true; + else if (store.stream.author.endsWith('Topic')) return import('../modules/jioSaavn').then(mod => mod.default()); } - else store.player.useSaavn = state.jiosaavn; title.textContent = 'Fetching Data...'; From 5c366a264353e0756b916a0b32381d59fefbe1c9 Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 29 Jul 2025 17:55:52 +0530 Subject: [PATCH 17/28] improve jioSaavn track detection --- src/modules/jioSaavn.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/modules/jioSaavn.ts b/src/modules/jioSaavn.ts index 21a404da0..a1bc7e901 100644 --- a/src/modules/jioSaavn.ts +++ b/src/modules/jioSaavn.ts @@ -11,17 +11,21 @@ export default function() { fetch(`${store.api.jiosaavn}/api/search/songs?query=${query}`) .then(res => res.json()) - .then(_ => _.data.results[0]) - .then(data => { - const { name, downloadUrl, artists } = data; + .then(res => { - if ( - store.stream.title.startsWith(name) && - author.startsWith(artists.primary[0].name) - ) - store.player.data = data; + const matchingTrack = res.data.results.find((track: { + name: string, + artists: { primary: { name: string }[] } + }) => + store.stream.title.startsWith(track.name) && author.startsWith(track.artists.primary[0].name) - else throw new Error('Music stream not found'); + ); + if (!matchingTrack) throw new Error('Music stream not found in JioSaavn results'); + store.player.data = matchingTrack; + + return matchingTrack.downloadUrl; + }) + .then(downloadUrl => { setMetaData(store.stream); From dcd1d8aa5035c937ccb46a4db8f452490b56e381 Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 29 Jul 2025 18:09:45 +0530 Subject: [PATCH 18/28] improve music track detection --- src/modules/fetchList.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/modules/fetchList.ts b/src/modules/fetchList.ts index 655bc470a..88a9b12f5 100644 --- a/src/modules/fetchList.ts +++ b/src/modules/fetchList.ts @@ -19,6 +19,7 @@ export default async function fetchList( let listData: StreamItem[] = []; const useHyperpipe = !mix && (store.actionsMenu.author.endsWith(' - Topic') || store.list.name.startsWith('Artist')); + const musicEnforcer = url.includes('OLAK5uy'); if (useHyperpipe) { url = await getPlaylistIdFromArtist(url) || ''; @@ -60,7 +61,7 @@ export default async function fetchList( if (listContainer.classList.contains('reverse')) listContainer.classList.remove('reverse'); - if (useHyperpipe) + if (musicEnforcer) group.relatedStreams = group.relatedStreams.map( (item: StreamItem) => { if (!item.uploaderName.endsWith(' - Topic')) @@ -68,14 +69,14 @@ export default async function fetchList( return item; } ); - + const filterOutMembersOnly = (streams: StreamItem[]) => (type === 'channel' && streams.length) ? // hide members-only streams streams.filter((s: StreamItem) => s.views !== -1) : streams; listData = filterOutMembersOnly(group.relatedStreams); - + render(listContainer, ItemsLoader(listData)); if (location.pathname !== '/list') @@ -117,8 +118,8 @@ export default async function fetchList( (item: StreamItem) => !existingItems.includes( item.url.slice(-11)) ); - - if (useHyperpipe) + + if (musicEnforcer) data.relatedStreams = data.relatedStreams.map( (item: StreamItem) => { if (!item.uploaderName.endsWith(' - Topic')) From 30f6c2013eb0f89d96624d948d33310d2d919d5e Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 29 Jul 2025 18:19:01 +0530 Subject: [PATCH 19/28] improve jiosaavn track detection --- src/modules/jioSaavn.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/jioSaavn.ts b/src/modules/jioSaavn.ts index a1bc7e901..0442aae7d 100644 --- a/src/modules/jioSaavn.ts +++ b/src/modules/jioSaavn.ts @@ -17,8 +17,9 @@ export default function() { name: string, artists: { primary: { name: string }[] } }) => - store.stream.title.startsWith(track.name) && author.startsWith(track.artists.primary[0].name) + store.stream.title.startsWith(track.name) && + track.artists.primary.some(artist => author.startsWith(artist.name)) ); if (!matchingTrack) throw new Error('Music stream not found in JioSaavn results'); store.player.data = matchingTrack; From ec23098e53fdbcae2f1db6b49d7f65c08aee3462 Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 29 Jul 2025 19:30:59 +0530 Subject: [PATCH 20/28] several fixes --- src/modules/fetchSearchResults.ts | 20 ++++++++++---------- src/modules/jioSaavn.ts | 2 +- src/modules/localExtraction.ts | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/modules/fetchSearchResults.ts b/src/modules/fetchSearchResults.ts index 51c154eca..9c1ac2c25 100644 --- a/src/modules/fetchSearchResults.ts +++ b/src/modules/fetchSearchResults.ts @@ -92,14 +92,14 @@ const fetchWithPiped = ( items?.filter((item: StreamItem) => !item.isShort); if (searchFilters.value === 'music_songs') - results = items.map( - (item: StreamItem) => { - if (!item.uploaderName.endsWith(' - Topic')) - item.uploaderName += ' - Topic'; - return item; - } - ); - + results = results.map( + (item: StreamItem) => { + if (!item.uploaderName.endsWith(' - Topic')) + item.uploaderName += ' - Topic'; + return item; + } + ); + if (currentObserver) currentObserver.disconnect(); @@ -112,9 +112,9 @@ const fetchWithPiped = ( .filter((item) => !item.isShort && item.duration !== -1) .forEach((i) => { if (searchFilters.value === 'music_songs') - if(!i.uploaderName.endsWith(' - Topic')) + if (!i.uploaderName.endsWith(' - Topic')) i.uploaderName += ' - Topic'; - + if (results.find((v) => v.url === i.url) === undefined) results.push(i); }); diff --git a/src/modules/jioSaavn.ts b/src/modules/jioSaavn.ts index 0442aae7d..eecbe01e1 100644 --- a/src/modules/jioSaavn.ts +++ b/src/modules/jioSaavn.ts @@ -6,7 +6,7 @@ import player from '../lib/player'; export default function() { title.textContent = 'Fetching Data via JioSaavn...'; const { author, id } = store.stream; - const query = encodeURIComponent(`${store.stream.title.replace(/\(.*?\)/g, '')} ${author.slice(0, -8)}`); + const query = encodeURIComponent(`${store.stream.title.replace(/\(.*?\)/g, '')} ${author.replace(' - Topic', '')}`); fetch(`${store.api.jiosaavn}/api/search/songs?query=${query}`) diff --git a/src/modules/localExtraction.ts b/src/modules/localExtraction.ts index 2ddb8b78b..3cc88ffc8 100644 --- a/src/modules/localExtraction.ts +++ b/src/modules/localExtraction.ts @@ -1,4 +1,4 @@ -export async function fetchDataFromLocal(id: string): Promise { +export async function fetchDataFromLocal(id: string): Promise { const res = await fetch('http://localhost:9999/streams/' + id); if (!res.ok) { From 7e0326c170a165dac83da59afef950afb1854f6d Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 29 Jul 2025 22:11:13 +0530 Subject: [PATCH 21/28] fix channel url missing of enqueued lists --- src/scripts/list.ts | 2 ++ src/scripts/queue.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/scripts/list.ts b/src/scripts/list.ts index bdccbd637..bc2bcb291 100644 --- a/src/scripts/list.ts +++ b/src/scripts/list.ts @@ -30,6 +30,8 @@ new Sortable(listContainer, { function listToQ(container: HTMLDivElement) { const items = container.querySelectorAll('.streamItem') as NodeListOf; items.forEach(item => { + item.dataset.channelUrl = item.dataset.channel_url; + item.dataset.lastUpdated = item.dataset.last_updated; store.queue.append(item.dataset); }); goTo('/upcoming'); diff --git a/src/scripts/queue.ts b/src/scripts/queue.ts index 801eb8e36..f1efb615b 100644 --- a/src/scripts/queue.ts +++ b/src/scripts/queue.ts @@ -142,7 +142,7 @@ render(queuetools, template); store.queue.firstChild = () => queuelist.firstElementChild as HTMLElement; -store.queue.append = function(data: DOMStringMap | CollectionItem, prepend: boolean = false) { +store.queue.append = function(data: DOMStringMap | CollectionItem & { draggable: boolean }, prepend: boolean = false) { if (!data.id) return; const { list, firstChild } = store.queue; @@ -174,6 +174,7 @@ store.queue.append = function(data: DOMStringMap | CollectionItem, prepend: bool author: data.author || '', duration: data.duration || '', channelUrl: data.channelUrl, + lastUpdated: data.lastUpdated, draggable: true })); From bcda05dbb58a1416a7b9cb98f1d2430c36b1b9cf Mon Sep 17 00:00:00 2001 From: n-ce Date: Tue, 29 Jul 2025 22:30:25 +0530 Subject: [PATCH 22/28] convert to lowercase in jiosaavn detection --- src/modules/jioSaavn.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/jioSaavn.ts b/src/modules/jioSaavn.ts index eecbe01e1..86d8894c3 100644 --- a/src/modules/jioSaavn.ts +++ b/src/modules/jioSaavn.ts @@ -18,8 +18,8 @@ export default function() { artists: { primary: { name: string }[] } }) => - store.stream.title.startsWith(track.name) && - track.artists.primary.some(artist => author.startsWith(artist.name)) + store.stream.title.toLowerCase().startsWith(track.name.toLowerCase()) && + track.artists.primary.some(artist => author.toLowerCase().startsWith(artist.name.toLowerCase())) ); if (!matchingTrack) throw new Error('Music stream not found in JioSaavn results'); store.player.data = matchingTrack; From 07e7e8a452a3667f38cc1997d159d2b908ee8f81 Mon Sep 17 00:00:00 2001 From: n-ce Date: Thu, 31 Jul 2025 19:39:44 +0530 Subject: [PATCH 23/28] update to new uma system --- index.html | 4 +++- package.json | 4 ++-- src/components/Settings/playback.ts | 10 ---------- src/components/UpdatePrompt.ts | 4 ++-- src/lib/store.ts | 9 ++++++--- src/lib/utils.ts | 2 +- src/modules/fetchList.ts | 8 +++++--- src/modules/getStreamData.ts | 10 +++++----- src/modules/start.ts | 14 ++++++++++---- src/scripts/queue.ts | 2 +- vite.config.ts | 2 +- 11 files changed, 36 insertions(+), 33 deletions(-) diff --git a/index.html b/index.html index 8a1a81426..7cf572bb6 100644 --- a/index.html +++ b/index.html @@ -203,7 +203,9 @@ ytify is a resource efficient audio streaming client for YouTube & YTMusic. Learn - more about how to use it effectively. + more about how to use it effectively. Health: + ⬜⬜⬜⬜

diff --git a/package.json b/package.json index 7210154f6..edf71a662 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@netlify/blobs": "^10.0.8", - "@netlify/edge-functions": "^2.16.2", + "@netlify/edge-functions": "^2.16.3", "@types/node": "^24.1.0", "@types/sortablejs": "^1.15.8", "autoprefixer": "^10.4.21", @@ -25,4 +25,4 @@ "browserslist": [ "defaults" ] -} +} \ No newline at end of file diff --git a/src/components/Settings/playback.ts b/src/components/Settings/playback.ts index 99c9169ce..e93c6d12a 100644 --- a/src/components/Settings/playback.ts +++ b/src/components/Settings/playback.ts @@ -67,16 +67,6 @@ export default function() { } })} - ${ToggleSwitch({ - id: "enforcePipedSwitch", - name: 'settings_enforce_piped', - checked: state.enforcePiped, - handler: () => { - setState('enforcePiped', !state.enforcePiped); - quickSwitch(); - } - })} - ${ToggleSwitch({ id: "enforceProxySwitch", name: 'settings_always_proxy_streams', diff --git a/src/components/UpdatePrompt.ts b/src/components/UpdatePrompt.ts index a4b80afdf..a48919c2e 100644 --- a/src/components/UpdatePrompt.ts +++ b/src/components/UpdatePrompt.ts @@ -4,8 +4,8 @@ import { html, render } from 'uhtml'; export default async function(dialog: HTMLDialogElement) { - const commitsSrc = 'https://api.github.com/repos/n-ce/ytify/commits/v7x8'; - const commitsLink = 'https://github.com/n-ce/ytify/commits/v7x8'; + const commitsSrc = 'https://api.github.com/repos/n-ce/ytify/commits'; + const commitsLink = 'https://github.com/n-ce/ytify/commits'; const list = await fetch(commitsSrc) .then(res => res.json()) .then(data => data.commit.message.split('-')) diff --git a/src/lib/store.ts b/src/lib/store.ts index 36923f3ea..abda9a844 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -3,7 +3,6 @@ export const params = (new URL(location.href)).searchParams; export let state = { enforceProxy: false, - enforcePiped: false, jiosaavn: false, defaultSuperCollection: 'featured', customInstance: '', @@ -87,8 +86,10 @@ export const store: { streamHistory: string[] api: { piped: string[], + proxy: string[], + status: 'U' | 'P' | 'I' | 'N', invidious: string[], - hyperpipe: string, + hyperpipe: string[], jiosaavn: string, index: number }, @@ -133,9 +134,11 @@ export const store: { streamHistory: [], api: { piped: ['https://pipedapi.kavin.rocks'], + proxy: [], invidious: ['https://iv.ggtyler.dev'], - hyperpipe: 'https://hyperpipeapi.onrender.com', + hyperpipe: ['https://hyperpipeapi.onrender.com'], jiosaavn: 'https://saavn.dev', + status: 'P', index: 0 }, linkHost: state.linkHost || location.origin, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 1867bde69..0daf80d8a 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -38,7 +38,7 @@ export function proxyHandler(url: string, prefetch: boolean = false) { const origin = link.origin.slice(8); const host = link.searchParams.get('host'); - return state.enforceProxy ? + return (state.enforceProxy || store.api.status === 'P') ? (url + (host ? '' : `&host=${origin}`)) : (host && !state.customInstance) ? url.replace(origin, host) : url; } diff --git a/src/modules/fetchList.ts b/src/modules/fetchList.ts index 88a9b12f5..541c9712f 100644 --- a/src/modules/fetchList.ts +++ b/src/modules/fetchList.ts @@ -176,8 +176,8 @@ export default async function fetchList( listContainer.addEventListener('click', superClick); -const getPlaylistIdFromArtist = (id: string): Promise => - fetch(store.api.hyperpipe + id) +const getPlaylistIdFromArtist = (id: string, index = 0): Promise => + fetch(store.api.hyperpipe[index] + id) .then(res => res.json()) .then(data => { if (!('playlistId' in data)) @@ -187,7 +187,9 @@ const getPlaylistIdFromArtist = (id: string): Promise => store.list.thumbnail = store.list.thumbnail || '/a-' + data.thumbnails[0]?.url?.split('/a-')[1]?.split('=')[0]; return '/playlists/' + data.playlistId; }) - .catch(err => { + .catch(async err => { + if (index + 1 === store.api.hyperpipe.length) + return await getPlaylistIdFromArtist(id, index); notify(err.message); return ''; }) diff --git a/src/modules/getStreamData.ts b/src/modules/getStreamData.ts index f3bdbd5df..a43ef3b94 100644 --- a/src/modules/getStreamData.ts +++ b/src/modules/getStreamData.ts @@ -5,7 +5,7 @@ export default async function( prefetch: boolean = false ): Promise> { - const { invidious, piped } = store.api; + const { invidious, piped, proxy, status } = store.api; const { fallback, hls } = store.player; const fetchDataFromPiped = ( @@ -70,11 +70,11 @@ export default async function( else return useInvidious(index + 1); }); - const usePiped = (index = 0): Promise => fetchDataFromPiped(piped[index]) + const usePiped = (src = piped, index = 0): Promise => fetchDataFromPiped(src[index]) .catch(() => { - if (index + 1 === piped.length) + if (index + 1 === src.length) return useInvidious(); - else return usePiped(index + 1); + else return usePiped(src, index + 1); }); const useHls = () => Promise @@ -95,7 +95,7 @@ export default async function( const useLocal = async () => await import('./localExtraction.ts').then(mod => mod.fetchDataFromLocal(id)); - return (location.port === '9999') ? useLocal() : state.HLS ? useHls() : state.enforcePiped ? usePiped() : useInvidious(); + return (location.port === '9999') ? useLocal() : state.HLS ? useHls() : status === 'I' ? useInvidious() : status === 'N' ? fetchDataFromPiped(fallback) : usePiped(status === 'U' ? piped : proxy); } diff --git a/src/modules/start.ts b/src/modules/start.ts index 509cc1f76..e70f5aaab 100644 --- a/src/modules/start.ts +++ b/src/modules/start.ts @@ -15,19 +15,25 @@ export default async function() { const [pi, iv, useInvidious] = customInstance.split(','); store.player.hls.api[0] = - store.api.piped[0] = pi; - store.api.invidious[0] = iv; - state.enforcePiped = !useInvidious; + store.api.proxy[0] = store.api.piped[0] = pi; + if (useInvidious) { + store.api.invidious[0] = iv; + store.api.status = 'I'; + } } else await fetch('https://raw.githubusercontent.com/n-ce/Uma/main/dynamic_instances.json') .then(res => res.json()) .then(data => { + document.querySelector('samp')!.textContent = { + U: "⬛⬛⬛⬛", P: "⬛⬛⬛⬜", I: "🟧⬛⬛⬜⬜", N: "⬛⬜⬜⬜" + }[data.health as 'U']; + store.api.piped = data.piped; + store.api.proxy = data.proxy; store.api.invidious = data.invidious; store.api.hyperpipe = data.hyperpipe; store.api.jiosaavn = data.jiosaavn; store.player.hls.api = data.hls; - state.enforcePiped = state.enforcePiped || data.status === 1; store.player.fallback = location.origin; }); diff --git a/src/scripts/queue.ts b/src/scripts/queue.ts index f1efb615b..7c7b1bd9d 100644 --- a/src/scripts/queue.ts +++ b/src/scripts/queue.ts @@ -142,7 +142,7 @@ render(queuetools, template); store.queue.firstChild = () => queuelist.firstElementChild as HTMLElement; -store.queue.append = function(data: DOMStringMap | CollectionItem & { draggable: boolean }, prepend: boolean = false) { +store.queue.append = function(data: DOMStringMap | CollectionItem, prepend: boolean = false) { if (!data.id) return; const { list, firstChild } = store.queue; diff --git a/vite.config.ts b/vite.config.ts index 54a5c9f9c..73bdaebca 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,7 @@ import { readdirSync } from 'fs'; export default defineConfig(({ command }) => ({ define: { Locales: readdirSync(resolve(__dirname, './src/locales')).map(file => file.slice(0, 2)), - Build: JSON.stringify("v7x8 Jul 25") + Build: JSON.stringify(((d = new Date()) => `v7x8 ${d.getFullYear().toString().slice(-2)}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getDate().toString().padStart(2, '0')}`)()) }, plugins: [ injectEruda(command === 'serve'), From afcad47dbd841b606a75780275261b2dad44b998 Mon Sep 17 00:00:00 2001 From: n-ce Date: Thu, 31 Jul 2025 20:02:46 +0530 Subject: [PATCH 24/28] fix WatchVideo --- netlify/edge-functions/fallback.ts | 2 +- src/components/WatchVideo.ts | 24 ++++++++++-------------- src/index.d.ts | 6 +++--- src/modules/getStreamData.ts | 7 +++++-- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/netlify/edge-functions/fallback.ts b/netlify/edge-functions/fallback.ts index d726b0860..9efbd2859 100644 --- a/netlify/edge-functions/fallback.ts +++ b/netlify/edge-functions/fallback.ts @@ -38,7 +38,7 @@ export default async (_: Request, context: Context) => { type: _.mimeType, })), relatedStreams: [], // empty array for compatibility - captions: [], // empty array for compatibility + subtitles: [], // empty array for compatibility livestream: streamData.isLiveContent }; diff --git a/src/components/WatchVideo.ts b/src/components/WatchVideo.ts index 58f059653..99b278dcf 100644 --- a/src/components/WatchVideo.ts +++ b/src/components/WatchVideo.ts @@ -13,7 +13,6 @@ export default async function(dialog: HTMLDialogElement) { const media = { video: [] as string[][], - captions: [] as Captions[] }; let video!: HTMLVideoElement; const audio = new Audio(); @@ -34,11 +33,10 @@ export default async function(dialog: HTMLDialogElement) { const data = await getStreamData(store.actionsMenu.id) as unknown as Piped & { - captions: Captions[], - videoStreams: Record<'url' | 'type' | 'resolution', string>[] + videoStreams: Record<'url' | 'codec' | 'resolution', string>[] }; - const hasAv1 = data.videoStreams.find(v => v.type.includes('av01'))?.url; - const hasVp9 = data.videoStreams.find(v => v.type.includes('vp9'))?.url; + const hasAv1 = data.videoStreams.find(v => v.codec.includes('av01'))?.url; + const hasVp9 = data.videoStreams.find(v => v.codec.includes('vp9'))?.url; const hasOpus = data.audioStreams.find(a => a.mimeType.includes('opus'))?.url; const useOpus = hasOpus && await store.player.supportsOpus; const audioArray = handleXtags(data.audioStreams) @@ -48,17 +46,15 @@ export default async function(dialog: HTMLDialogElement) { media.video = data.videoStreams .filter(f => { - const av1 = hasAv1 && supportsAv1 && f.type.includes('av01'); + const av1 = hasAv1 && supportsAv1 && f.codec.includes('av01'); if (av1) return true; - const vp9 = !hasAv1 && f.type.includes('vp9'); + const vp9 = !hasAv1 && f.codec.includes('vp9'); if (vp9) return true; - const avc = !hasVp9 && f.type.includes('avc1'); + const avc = !hasVp9 && f.codec.includes('avc1'); if (avc) return true; }) .map(f => ([f.resolution, f.url])); - media.captions = data.captions - function close() { audio.pause(); @@ -129,12 +125,12 @@ export default async function(dialog: HTMLDialogElement) { }} > - ${media.captions.length ? + ${data.subtitles.length ? html` - ${media.captions.map(v => html` + ${data.subtitles.map(v => html` `)} diff --git a/src/index.d.ts b/src/index.d.ts index 503d58818..5379b80c6 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -114,10 +114,10 @@ declare global { uploaderUrl: string, type: string }[], - audioStreams: AudioStream[] + audioStreams: AudioStream[], + subtitles: Record<'url' | 'name', string>[] } - type Captions = Record<'label' | 'url', string>; type Invidious = { adaptiveFormats: Record<'type' | 'bitrate' | 'encoding' | 'clen' | 'url' | 'resolution' | 'quality', string>[], @@ -128,8 +128,8 @@ declare global { authorUrl: string, videoId: string }[], - captions: Captions[], title: string, + captions: Record<'url' | 'label', string>[], author: string, lengthSeconds: number, authorUrl: string, diff --git a/src/modules/getStreamData.ts b/src/modules/getStreamData.ts index a43ef3b94..5c5ffa922 100644 --- a/src/modules/getStreamData.ts +++ b/src/modules/getStreamData.ts @@ -33,7 +33,10 @@ export default async function( duration: data.lengthSeconds, uploaderUrl: data.authorUrl, liveStream: data.liveNow, - captions: data.captions, + subtitles: data.captions.map(c => ({ + name: c.label, + url: c.url + })), relatedStreams: data.recommendedVideos.map(v => ({ url: '/watch?v=' + v.videoId, title: v.title, @@ -46,7 +49,7 @@ export default async function( url: v.url, quality: v.quality, resolution: v.resolution, - type: v.type + codec: v.type })), audioStreams: data.adaptiveFormats.filter((f) => f.type.startsWith('audio')).map((v) => ({ bitrate: parseInt(v.bitrate), From e48604cd49e07d7d43da4d40a3b83d3f813326e1 Mon Sep 17 00:00:00 2001 From: n-ce Date: Thu, 31 Jul 2025 20:04:28 +0530 Subject: [PATCH 25/28] fix localExtractor --- src/modules/localExtraction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/localExtraction.ts b/src/modules/localExtraction.ts index 3cc88ffc8..ead821b04 100644 --- a/src/modules/localExtraction.ts +++ b/src/modules/localExtraction.ts @@ -1,4 +1,4 @@ -export async function fetchDataFromLocal(id: string): Promise { +export async function fetchDataFromLocal(id: string): Promise { const res = await fetch('http://localhost:9999/streams/' + id); if (!res.ok) { @@ -53,7 +53,7 @@ export async function fetchDataFromLocal(id: string): Promise Date: Thu, 31 Jul 2025 20:06:13 +0530 Subject: [PATCH 26/28] fix updater --- src/components/UpdatePrompt.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/UpdatePrompt.ts b/src/components/UpdatePrompt.ts index a48919c2e..6e6c2ff81 100644 --- a/src/components/UpdatePrompt.ts +++ b/src/components/UpdatePrompt.ts @@ -4,7 +4,7 @@ import { html, render } from 'uhtml'; export default async function(dialog: HTMLDialogElement) { - const commitsSrc = 'https://api.github.com/repos/n-ce/ytify/commits'; + const commitsSrc = 'https://api.github.com/repos/n-ce/ytify/commits/main'; const commitsLink = 'https://github.com/n-ce/ytify/commits'; const list = await fetch(commitsSrc) .then(res => res.json()) From 27ee5bdd2c9e131294b32b0840d386a6c87c9a07 Mon Sep 17 00:00:00 2001 From: n-ce Date: Thu, 31 Jul 2025 20:18:51 +0530 Subject: [PATCH 27/28] fix WatchVideo --- src/components/WatchVideo.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/WatchVideo.ts b/src/components/WatchVideo.ts index 99b278dcf..2a3e7bf9c 100644 --- a/src/components/WatchVideo.ts +++ b/src/components/WatchVideo.ts @@ -35,9 +35,9 @@ export default async function(dialog: HTMLDialogElement) { const data = await getStreamData(store.actionsMenu.id) as unknown as Piped & { videoStreams: Record<'url' | 'codec' | 'resolution', string>[] }; - const hasAv1 = data.videoStreams.find(v => v.codec.includes('av01'))?.url; - const hasVp9 = data.videoStreams.find(v => v.codec.includes('vp9'))?.url; - const hasOpus = data.audioStreams.find(a => a.mimeType.includes('opus'))?.url; + const hasAv1 = data.videoStreams.find(v => v.codec?.includes('av01'))?.url; + const hasVp9 = data.videoStreams.find(v => v.codec?.includes('vp9'))?.url; + const hasOpus = data.audioStreams.find(a => a.mimeType.includes('webm'))?.url; const useOpus = hasOpus && await store.player.supportsOpus; const audioArray = handleXtags(data.audioStreams) .filter(a => a.mimeType.includes(useOpus ? 'opus' : 'mp4a')) @@ -46,11 +46,11 @@ export default async function(dialog: HTMLDialogElement) { media.video = data.videoStreams .filter(f => { - const av1 = hasAv1 && supportsAv1 && f.codec.includes('av01'); + const av1 = hasAv1 && supportsAv1 && f.codec?.includes('av01'); if (av1) return true; - const vp9 = !hasAv1 && f.codec.includes('vp9'); + const vp9 = !hasAv1 && f.codec?.includes('vp9'); if (vp9) return true; - const avc = !hasVp9 && f.codec.includes('avc1'); + const avc = !hasVp9 && f.codec?.includes('avc1'); if (avc) return true; }) .map(f => ([f.resolution, f.url])); From 3d22f5a7a93d482db3934616724f9143836cc842 Mon Sep 17 00:00:00 2001 From: n-ce Date: Thu, 31 Jul 2025 20:38:07 +0530 Subject: [PATCH 28/28] fix WatchVideo --- src/components/WatchVideo.ts | 9 +++++---- src/lib/utils.ts | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/WatchVideo.ts b/src/components/WatchVideo.ts index 2a3e7bf9c..175e96696 100644 --- a/src/components/WatchVideo.ts +++ b/src/components/WatchVideo.ts @@ -33,14 +33,14 @@ export default async function(dialog: HTMLDialogElement) { const data = await getStreamData(store.actionsMenu.id) as unknown as Piped & { - videoStreams: Record<'url' | 'codec' | 'resolution', string>[] + videoStreams: Record<'url' | 'codec' | 'resolution' | 'quality', string>[] }; - const hasAv1 = data.videoStreams.find(v => v.codec?.includes('av01'))?.url; + const hasAv1 = data.videoStreams.filter(v => v.codec?.includes('av01')).length === 4 ? data.videoStreams.find(v => v.codec?.includes('av01'))?.url : false; const hasVp9 = data.videoStreams.find(v => v.codec?.includes('vp9'))?.url; const hasOpus = data.audioStreams.find(a => a.mimeType.includes('webm'))?.url; const useOpus = hasOpus && await store.player.supportsOpus; const audioArray = handleXtags(data.audioStreams) - .filter(a => a.mimeType.includes(useOpus ? 'opus' : 'mp4a')) + .filter(a => a.mimeType.includes(useOpus ? 'webm' : 'mp4a')) .sort((a, b) => parseInt(a.bitrate) - parseInt(b.bitrate)); @@ -53,7 +53,8 @@ export default async function(dialog: HTMLDialogElement) { const avc = !hasVp9 && f.codec?.includes('avc1'); if (avc) return true; }) - .map(f => ([f.resolution, f.url])); + .map(f => ([f.resolution || f.quality, f.url])); + function close() { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 0daf80d8a..bbe5ff5be 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -31,6 +31,7 @@ export const hostResolver = (url: string) => export function proxyHandler(url: string, prefetch: boolean = false) { + const isVideo = Boolean(document.querySelector('video')); store.api.index = 0; if (!prefetch) title.textContent = i18n('player_audiostreams_insert'); @@ -38,7 +39,7 @@ export function proxyHandler(url: string, prefetch: boolean = false) { const origin = link.origin.slice(8); const host = link.searchParams.get('host'); - return (state.enforceProxy || store.api.status === 'P') ? + return (state.enforceProxy || (!isVideo && store.api.status === 'P')) ? (url + (host ? '' : `&host=${origin}`)) : (host && !state.customInstance) ? url.replace(origin, host) : url; }