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/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/package.json b/package.json index 4468fd75f..edf71a662 100644 --- a/package.json +++ b/package.json @@ -7,22 +7,22 @@ "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.8", + "@netlify/edge-functions": "^2.16.3", + "@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.2", - "vite-plugin-pwa": "^1.0.1" + "vite": "^7.0.6", + "vite-plugin-pwa": "^1.0.2" }, "browserslist": [ "defaults" ] -} +} \ No newline at end of file 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, 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/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/components/WatchVideo.ts b/src/components/WatchVideo.ts index 58f059653..175e96696 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,30 +33,28 @@ 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' | 'quality', string>[] }; - const hasAv1 = data.videoStreams.find(v => v.type.includes('av01'))?.url; - const hasVp9 = data.videoStreams.find(v => v.type.includes('vp9'))?.url; - const hasOpus = data.audioStreams.find(a => a.mimeType.includes('opus'))?.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)); 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])); + .map(f => ([f.resolution || f.quality, f.url])); - media.captions = data.captions function close() { @@ -129,12 +126,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 2071a3647..5379b80c6 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 @@ -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/lib/libraryUtils.ts b/src/lib/libraryUtils.ts index 17e96f6f4..18445ba40 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; } @@ -97,6 +97,7 @@ export function renderCollection( author: v.author, duration: v.duration || '', channelUrl: v.channelUrl, + lastUpdated: v.lastUpdated || new Date().toISOString(), draggable: draggable }) ) diff --git a/src/lib/player.ts b/src/lib/player.ts index c7972f620..13ca9f1cc 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,14 @@ export default async function player(id: string | null = '') { playButton.classList.replace(playButton.className, 'ri-loader-3-line'); - if (useSaavn) { - if (state.jiosaavn && store.stream.author.endsWith('Topic')) - return saavnPlayer(); + 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 useSaavn = true; - ptitle.textContent = 'Fetching Data...'; + title.textContent = 'Fetching Data...'; const data = await getStreamData(id); @@ -36,7 +37,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 +108,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..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: '', @@ -74,7 +73,8 @@ export const store: { supportsOpus: Promise, data: Piped | undefined, legacy: boolean, - fallback: string + fallback: string, + useSaavn: boolean }, lrcSync: (arg0: number) => {} | void, queue: { @@ -86,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 }, @@ -113,7 +115,8 @@ export const store: { }).then(res => res.supported), data: undefined, legacy: !('OffscreenCanvas' in window), - fallback: '' + fallback: '', + useSaavn: state.jiosaavn, }, lrcSync: () => { }, queue: { @@ -131,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 c568bc5ea..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 ? + return (state.enforceProxy || (!isVideo && store.api.status === 'P')) ? (url + (host ? '' : `&host=${origin}`)) : (host && !state.customInstance) ? url.replace(origin, host) : url; } @@ -210,6 +211,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/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; diff --git a/src/modules/fetchList.ts b/src/modules/fetchList.ts index ff3496813..541c9712f 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,12 +61,22 @@ export default async function fetchList( if (listContainer.classList.contains('reverse')) listContainer.classList.remove('reverse'); + if (musicEnforcer) + 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') @@ -108,6 +119,15 @@ export default async function fetchList( item.url.slice(-11)) ); + if (musicEnforcer) + 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)); return data.nextpage || ''; @@ -156,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)) @@ -167,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/fetchSearchResults.ts b/src/modules/fetchSearchResults.ts index 7fe29ae40..9c1ac2c25 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 = results.map( + (item: StreamItem) => { + if (!item.uploaderName.endsWith(' - Topic')) + item.uploaderName += ' - Topic'; + return item; + } + ); + if (currentObserver) currentObserver.disconnect(); @@ -102,6 +111,10 @@ const fetchWithPiped = ( (data.items as StreamItem[]) .filter((item) => !item.isShort && item.duration !== -1) .forEach((i) => { + if (searchFilters.value === 'music_songs') + 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/getStreamData.ts b/src/modules/getStreamData.ts index 3dccacb00..5c5ffa922 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 = ( @@ -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), @@ -70,11 +73,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 @@ -92,8 +95,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.fetchDataFromLocal(id)); + - return 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/jioSaavn.ts b/src/modules/jioSaavn.ts new file mode 100644 index 000000000..86d8894c3 --- /dev/null +++ b/src/modules/jioSaavn.ts @@ -0,0 +1,51 @@ +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.replace(' - Topic', '')}`); + + + fetch(`${store.api.jiosaavn}/api/search/songs?query=${query}`) + .then(res => res.json()) + .then(res => { + + const matchingTrack = res.data.results.find((track: { + name: string, + artists: { primary: { name: string }[] } + }) => + + 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; + + return matchingTrack.downloadUrl; + }) + .then(downloadUrl => { + + 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() }; }); diff --git a/src/modules/localExtraction.ts b/src/modules/localExtraction.ts new file mode 100644 index 000000000..ead821b04 --- /dev/null +++ b/src/modules/localExtraction.ts @@ -0,0 +1,63 @@ +export async function fetchDataFromLocal(id: string): Promise { + const res = await fetch('http://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 { videoDetails, streamingData, captions } = await res.json(); + + 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 ({ + instance: 'Local Extractor', + title: videoDetails.title, + uploader: videoDetails.author, + duration: parseInt(videoDetails.lengthSeconds), + uploaderUrl: `/channel/${videoDetails.channelId}`, + livestream: videoDetails.isLiveContent, + subtitles: captionTracks, + relatedStreams: [], + videoStreams: videoStreams, + audioStreams: audioStreams, + hls: streamingData.hlsManifestUrl || undefined, + }); + +} 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/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..7c7b1bd9d 100644 --- a/src/scripts/queue.ts +++ b/src/scripts/queue.ts @@ -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 })); 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 { 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'),