Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6d6d3a4
Create localExtraction.ts
n-ce Jul 14, 2025
b94ef9e
Update localExtraction.ts
n-ce Jul 14, 2025
bc08411
Update getStreamData.ts
n-ce Jul 14, 2025
aa98df3
fix bugs
n-ce Jul 15, 2025
f497d7b
use http for local extraction
n-ce Jul 15, 2025
694d83b
remove piped enforcement control flow from audio error handler
n-ce Jul 26, 2025
4c7d50d
Inject '- Topic' when available for songs playlist returned by Hyperpipe
n-ce Jul 29, 2025
9304e9b
Support music streams directly from search instead of inferring
n-ce Jul 29, 2025
cb70395
remove music stream inferenec
n-ce Jul 29, 2025
19299ae
fix search music stream checker injection
n-ce Jul 29, 2025
8a3c8ee
change updater source to v7x8 branch
n-ce Jul 29, 2025
cd1dfb3
music context injection for upfront list items
n-ce Jul 29, 2025
a0d7209
add initial lastUpdated field to library
n-ce Jul 29, 2025
e5acbca
improve lastUpdated integration
n-ce Jul 29, 2025
815236e
jioSaavn integration out of beta
n-ce Jul 29, 2025
e52b2e0
fix jioSaavn control flow
n-ce Jul 29, 2025
5c366a2
improve jioSaavn track detection
n-ce Jul 29, 2025
dcd1d8a
improve music track detection
n-ce Jul 29, 2025
30f6c20
improve jiosaavn track detection
n-ce Jul 29, 2025
ec23098
several fixes
n-ce Jul 29, 2025
7e0326c
fix channel url missing of enqueued lists
n-ce Jul 29, 2025
bcda05d
convert to lowercase in jiosaavn detection
n-ce Jul 29, 2025
07e7e8a
update to new uma system
n-ce Jul 31, 2025
afcad47
fix WatchVideo
n-ce Jul 31, 2025
e48604c
fix localExtractor
n-ce Jul 31, 2025
dbab3b2
fix updater
n-ce Jul 31, 2025
27ee5bd
fix WatchVideo
n-ce Jul 31, 2025
3d22f5a
fix WatchVideo
n-ce Jul 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,9 @@

ytify is a resource efficient audio streaming client for YouTube & YTMusic. <a
style="text-decoration:underline;" target="_blank" href="https://github.com/n-ce/ytify/wiki/usage">Learn
more about how to use it effectively.</a>
more about how to use it effectively.</a> <a
href="https://raw.githubusercontent.com/n-ce/Uma/main/dynamic_instances.json">Health: </a>
<samp>⬜⬜⬜⬜</samp>
<br>
<br>
<p style="font-size:smaller;margin-top:1rem">
Expand Down
2 changes: 1 addition & 1 deletion netlify/edge-functions/fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
2 changes: 1 addition & 1 deletion src/components/ItemsLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 0 additions & 10 deletions src/components/Settings/playback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 3 additions & 1 deletion src/components/StreamItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down Expand Up @@ -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()}
>
<span>

Expand Down
29 changes: 13 additions & 16 deletions src/components/WatchVideo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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() {
Expand Down Expand Up @@ -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`
<track
src=${store.api.invidious[0] + v.url}
srclang=${v.label}
src=${(store.api.status === 'P' ? '' : store.api.invidious[0]) + v.url}
srclang=${v.name}
>
</track>
`)}
Expand Down
10 changes: 5 additions & 5 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -102,7 +103,6 @@ declare global {
title: string,
uploader: string,
duration: number,
uploader: string,
uploaderUrl: string,
livestream: boolean,
hls: string
Expand All @@ -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>[],
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/lib/libraryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -97,6 +97,7 @@ export function renderCollection(
author: v.author,
duration: v.duration || '',
channelUrl: v.channelUrl,
lastUpdated: v.lastUpdated || new Date().toISOString(),
draggable: draggable
})
)
Expand Down
56 changes: 8 additions & 48 deletions src/lib/player.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -22,21 +22,22 @@ 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);

if (data && 'audioStreams' in data)
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;
}

Expand Down Expand Up @@ -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);
});
}
15 changes: 10 additions & 5 deletions src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export const params = (new URL(location.href)).searchParams;

export let state = {
enforceProxy: false,
enforcePiped: false,
jiosaavn: false,
defaultSuperCollection: 'featured',
customInstance: '',
Expand Down Expand Up @@ -74,7 +73,8 @@ export const store: {
supportsOpus: Promise<boolean>,
data: Piped | undefined,
legacy: boolean,
fallback: string
fallback: string,
useSaavn: boolean
},
lrcSync: (arg0: number) => {} | void,
queue: {
Expand All @@ -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
},
Expand All @@ -113,7 +115,8 @@ export const store: {
}).then(res => res.supported),
data: undefined,
legacy: !('OffscreenCanvas' in window),
fallback: ''
fallback: '',
useSaavn: state.jiosaavn,
},
lrcSync: () => { },
queue: {
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ 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');
const link = new URL(url);
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;
}
Expand Down Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/modules/audioErrorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading