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'),