mirror of
https://github.com/imputnet/cobalt.git
synced 2025-07-18 19:28:29 +00:00
Merge branch 'current' into support/odysee
This commit is contained in:
commit
5773892426
@ -88,7 +88,7 @@
|
|||||||
"ChangelogPressToHide": "collapse",
|
"ChangelogPressToHide": "collapse",
|
||||||
"Donate": "donate",
|
"Donate": "donate",
|
||||||
"DonateSub": "help it stay online",
|
"DonateSub": "help it stay online",
|
||||||
"DonateExplanation": "cobalt doesn't shove ads in your face and doesn't sell your personal data, meaning that it's <span class=\"text-backdrop\">completely free to use</span> for everyone. but development and maintenance of a media-heavy service used by over 750k people is quite costly. both in terms of time and money.\n\nif cobalt helped you in the past and you want to keep it growing and evolving, you can return the favor by making a donation!\n\nyour donation will help all cobalt users: educators, students, content creators, artists, musicians, and many, many more!\n\nin past, donations have let cobalt:\n*; increase stability and uptime to nearly 100%.\n*; speed up ALL downloads, especially heavier ones.\n*; open the api for free public use.\n*; withstand several huge user influxes with 0 downtime.\n*; add resource-intensive features (such as gif conversion).\n*; continue improving our infrastructure.\n*; keep developers happy.\n\n<span class=\"text-backdrop\">every cent matters and is extremely appreciated</span>, you can truly make a difference!\n\nif you can't donate, share cobalt with a friend! we don't get ads anywhere, so cobalt is spread by word of mouth.\nsharing is the easiest way to help achieve the goal of better internet for everyone.",
|
"DonateExplanation": "cobalt doesn't shove ads in your face and doesn't sell your personal data, meaning that it's <span class=\"text-backdrop\">completely free to use</span> for everyone. but development and maintenance of a media-heavy service used by over 1 million people is quite costly. both in terms of time and money.\n\nif cobalt helped you in the past and you want to keep it growing and evolving, you can return the favor by making a donation!\n\nyour donation will help all cobalt users: educators, students, content creators, artists, musicians, and many, many more!\n\nin past, donations have let cobalt:\n*; increase stability and uptime to nearly 100%.\n*; speed up ALL downloads, especially heavier ones.\n*; open the api for free public use.\n*; withstand several huge user influxes with 0 downtime.\n*; add resource-intensive features (such as gif conversion).\n*; continue improving our infrastructure.\n*; keep developers happy.\n\n<span class=\"text-backdrop\">every cent matters and is extremely appreciated</span>, you can truly make a difference!\n\nif you can't donate, share cobalt with a friend! we don't get ads anywhere, so cobalt is spread by word of mouth.\nsharing is the easiest way to help achieve the goal of better internet for everyone.",
|
||||||
"DonateVia": "donate via",
|
"DonateVia": "donate via",
|
||||||
"SettingsVideoMute": "mute audio",
|
"SettingsVideoMute": "mute audio",
|
||||||
"SettingsVideoMuteExplanation": "removes audio from video downloads when possible.",
|
"SettingsVideoMuteExplanation": "removes audio from video downloads when possible.",
|
||||||
@ -100,7 +100,7 @@
|
|||||||
"FollowSupport": "keep in touch with cobalt for news, support, and more:",
|
"FollowSupport": "keep in touch with cobalt for news, support, and more:",
|
||||||
"SourceCode": "explore source code, report issues, star or fork the repo:",
|
"SourceCode": "explore source code, report issues, star or fork the repo:",
|
||||||
"PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires rendering, then data about requested content is encrypted and temporarily stored in server's RAM. it's necessary for this feature to function.\n\nencrypted data is stored for <span class=\"text-backdrop\">90 seconds</span> and then permanently removed.\n\nstored data is only possible to decrypt with unique encryption keys from your download link. furthermore, the official cobalt codebase doesn't provide a way to read temporarily stored data outside of processing functions.\n\nyou can check cobalt's <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">source code</a> yourself and see that everything is as stated.",
|
"PrivacyPolicy": "cobalt's privacy policy is simple: no data about you is ever collected or stored. zero, zilch, nada, nothing.\nwhat you download is solely your business, not mine or anyone else's.\n\nif your download requires rendering, then data about requested content is encrypted and temporarily stored in server's RAM. it's necessary for this feature to function.\n\nencrypted data is stored for <span class=\"text-backdrop\">90 seconds</span> and then permanently removed.\n\nstored data is only possible to decrypt with unique encryption keys from your download link. furthermore, the official cobalt codebase doesn't provide a way to read temporarily stored data outside of processing functions.\n\nyou can check cobalt's <a class=\"text-backdrop link\" href=\"{repo}\" target=\"_blank\">source code</a> yourself and see that everything is as stated.",
|
||||||
"ErrorYTUnavailable": "this youtube video is unavailable. it could be age or region restricted. try another one!",
|
"ErrorYTUnavailable": "this youtube video is unavailable. it could be visibility or region restricted. try another one!",
|
||||||
"ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality in settings!",
|
"ErrorYTTryOtherCodec": "i couldn't find anything to download with your settings. try another codec or quality in settings!",
|
||||||
"SettingsCodecSubtitle": "youtube codec",
|
"SettingsCodecSubtitle": "youtube codec",
|
||||||
"SettingsCodecDescription": "h264: best support across apps/platforms, average detail level. max quality is 1080p.\nav1: best quality, small file size, most detail. supports 8k & HDR.\nvp9: same quality as av1, but file is x2 bigger. supports 4k & HDR.\n\npick h264 if you want best compatibility.\npick av1 if you want best quality and efficiency.",
|
"SettingsCodecDescription": "h264: best support across apps/platforms, average detail level. max quality is 1080p.\nav1: best quality, small file size, most detail. supports 8k & HDR.\nvp9: same quality as av1, but file is x2 bigger. supports 4k & HDR.\n\npick h264 if you want best compatibility.\npick av1 if you want best quality and efficiency.",
|
||||||
@ -156,6 +156,8 @@
|
|||||||
"SettingsYoutubeDub": "use browser language",
|
"SettingsYoutubeDub": "use browser language",
|
||||||
"SettingsYoutubeDubDescription": "uses your browser's default language for youtube dubbed audio tracks. works even if cobalt ui isn't translated to your language.",
|
"SettingsYoutubeDubDescription": "uses your browser's default language for youtube dubbed audio tracks. works even if cobalt ui isn't translated to your language.",
|
||||||
"ErrorInvalidContentType": "invalid content type header",
|
"ErrorInvalidContentType": "invalid content type header",
|
||||||
"UpdateOneMillion": "1 million users and blazing speed"
|
"UpdateOneMillion": "1 million users and blazing speed",
|
||||||
|
"ErrorYTAgeRestrict": "this youtube video is age-restricted, so i can't see it. try another one!",
|
||||||
|
"ErrorYTLogin": "couldn't get this youtube video because it requires sign in.\n\nthis limitation is an a/b test done by google to seemingly stop scraping, coincidentally affecting all 3rd party tools and even their own clients.\n\nyou can track the <a class=\"text-backdrop link\" href=\"{repo}/issues/551\" target=\"_blank\">issue on github</a>."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,7 @@
|
|||||||
"ChangelogPressToHide": "скрыть",
|
"ChangelogPressToHide": "скрыть",
|
||||||
"Donate": "донаты",
|
"Donate": "донаты",
|
||||||
"DonateSub": "ты можешь помочь!",
|
"DonateSub": "ты можешь помочь!",
|
||||||
"DonateExplanation": "кобальт не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span> для всех. но разработка и поддержка медиа сервиса, которым пользуются более 750 тысяч людей, обходится довольно затратно.\n\nесли кобальт тебе помог и ты хочешь, чтобы он продолжал расти и развиваться, то это можно сделать через донаты!\n\nтвой донат поможет всем, кто пользуется кобальтом: преподавателям, студентам, музыкантам, художникам, контент-мейкерам и многим-многим другим!\n\nв прошлом донаты помогли кобальту:\n*; повысить стабильность и аптайм почти до 100%.\n*; ускорить ВСЕ загрузки, особенно наиболее тяжёлые.\n*; открыть api для бесплатного использования.\n*; выдержать несколько огромных наплывов пользователей без перебоев.\n*; добавить ресурсоемкие фичи (например конвертацию в gif).\n*; продолжать улучшать нашу инфраструктуру.\n*; радовать разработчиков.\n\n<span class=\"text-backdrop\">каждый донат невероятно ценится</span> и помогает кобальту развиваться!\n\nесли ты не можешь отправить донат, то поделись кобальтом с другом! мы нигде не размещаем рекламу, поэтому кобальт распространяется из уст в уста.\nподелиться - самый простой способ помочь достичь цели лучшего интернета для всех.",
|
"DonateExplanation": "кобальт не пихает рекламу тебе в лицо и не продаёт твои личные данные, а значит работает <span class=\"text-backdrop\">совершенно бесплатно</span> для всех. но разработка и поддержка медиа сервиса, которым пользуются более миллиона людей, обходится довольно затратно.\n\nесли кобальт тебе помог и ты хочешь, чтобы он продолжал расти и развиваться, то это можно сделать через донаты!\n\nтвой донат поможет всем, кто пользуется кобальтом: преподавателям, студентам, музыкантам, художникам, контент-мейкерам и многим-многим другим!\n\nв прошлом донаты помогли кобальту:\n*; повысить стабильность и аптайм почти до 100%.\n*; ускорить ВСЕ загрузки, особенно наиболее тяжёлые.\n*; открыть api для бесплатного использования.\n*; выдержать несколько огромных наплывов пользователей без перебоев.\n*; добавить ресурсоемкие фичи (например конвертацию в gif).\n*; продолжать улучшать нашу инфраструктуру.\n*; радовать разработчиков.\n\n<span class=\"text-backdrop\">каждый донат невероятно ценится</span> и помогает кобальту развиваться!\n\nесли ты не можешь отправить донат, то поделись кобальтом с другом! мы нигде не размещаем рекламу, поэтому кобальт распространяется из уст в уста.\nподелиться - самый простой способ помочь достичь цели лучшего интернета для всех.",
|
||||||
"DonateVia": "открыть",
|
"DonateVia": "открыть",
|
||||||
"SettingsVideoMute": "убрать аудио",
|
"SettingsVideoMute": "убрать аудио",
|
||||||
"SettingsVideoMuteExplanation": "убирает звук при загрузке видео, но только когда это возможно.",
|
"SettingsVideoMuteExplanation": "убирает звук при загрузке видео, но только когда это возможно.",
|
||||||
@ -157,6 +157,7 @@
|
|||||||
"SettingsTikTokH265Description": "скачивает видео с tiktok в 1080p и h265/hevc, когда это возможно.",
|
"SettingsTikTokH265Description": "скачивает видео с tiktok в 1080p и h265/hevc, когда это возможно.",
|
||||||
"SettingsYoutubeDub": "использовать язык браузера",
|
"SettingsYoutubeDub": "использовать язык браузера",
|
||||||
"SettingsYoutubeDubDescription": "использует главный язык браузера для аудиодорожек на youtube. работает даже если кобальт не переведён в твой язык.",
|
"SettingsYoutubeDubDescription": "использует главный язык браузера для аудиодорожек на youtube. работает даже если кобальт не переведён в твой язык.",
|
||||||
"UpdateOneMillion": "миллион и невероятная скорость"
|
"UpdateOneMillion": "миллион и невероятная скорость",
|
||||||
|
"ErrorYTAgeRestrict": "это видео ограничено по возрасту, поэтому я не могу его скачать. попробуй другое!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import { Innertube, Session } from 'youtubei.js';
|
|||||||
import { env } from '../../config.js';
|
import { env } from '../../config.js';
|
||||||
import { cleanString } from '../../sub/utils.js';
|
import { cleanString } from '../../sub/utils.js';
|
||||||
import { fetch } from 'undici'
|
import { fetch } from 'undici'
|
||||||
|
import { getCookie } from '../cookie/manager.js'
|
||||||
|
|
||||||
const ytBase = Innertube.create().catch(e => e);
|
const ytBase = Innertube.create().catch(e => e);
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ const cloneInnertube = async (customFetch) => {
|
|||||||
innertube.session.api_version,
|
innertube.session.api_version,
|
||||||
innertube.session.account_index,
|
innertube.session.account_index,
|
||||||
innertube.session.player,
|
innertube.session.player,
|
||||||
undefined,
|
getCookie('youtube')?.toString(),
|
||||||
customFetch ?? innertube.session.http.fetch,
|
customFetch ?? innertube.session.http.fetch,
|
||||||
innertube.session.cache
|
innertube.session.cache
|
||||||
);
|
);
|
||||||
@ -61,7 +62,7 @@ export default async function(o) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
info = await yt.getBasicInfo(o.id, 'WEB');
|
info = await yt.getBasicInfo(o.id, 'IOS');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (e?.message === 'This video is unavailable') {
|
if (e?.message === 'This video is unavailable') {
|
||||||
return { error: 'ErrorCouldntFetch' };
|
return { error: 'ErrorCouldntFetch' };
|
||||||
@ -72,7 +73,18 @@ export default async function(o) {
|
|||||||
|
|
||||||
if (!info) return { error: 'ErrorCantConnectToServiceAPI' };
|
if (!info) return { error: 'ErrorCantConnectToServiceAPI' };
|
||||||
|
|
||||||
if (info.playability_status.status !== 'OK') return { error: 'ErrorYTUnavailable' };
|
const playability = info.playability_status;
|
||||||
|
|
||||||
|
if (playability.status === 'LOGIN_REQUIRED') {
|
||||||
|
if (playability.reason.endsWith('bot')) {
|
||||||
|
return { error: 'ErrorYTLogin' }
|
||||||
|
}
|
||||||
|
if (playability.reason.endsWith('age')) {
|
||||||
|
return { error: 'ErrorYTAgeRestrict' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playability.status !== 'OK') return { error: 'ErrorYTUnavailable' };
|
||||||
if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' };
|
if (info.basic_info.is_live) return { error: 'ErrorLiveVideo' };
|
||||||
|
|
||||||
// return a critical error if returned video is "Video Not Available"
|
// return a critical error if returned video is "Video Not Available"
|
||||||
|
56
src/modules/stream/internal-hls.js
Normal file
56
src/modules/stream/internal-hls.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { createInternalStream } from './manage.js';
|
||||||
|
import HLS from 'hls-parser';
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
function transformObject(streamInfo, hlsObject) {
|
||||||
|
if (hlsObject === undefined) {
|
||||||
|
return (object) => transformObject(streamInfo, object);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullUrl = hlsObject.uri.startsWith("/")
|
||||||
|
? new URL(hlsObject.uri, streamInfo.url).toString()
|
||||||
|
: new URL(path.join(streamInfo.url, "/../", hlsObject.uri)).toString();
|
||||||
|
hlsObject.uri = createInternalStream(fullUrl, streamInfo);
|
||||||
|
|
||||||
|
return hlsObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformMasterPlaylist(streamInfo, hlsPlaylist) {
|
||||||
|
const makeInternalStream = transformObject(streamInfo);
|
||||||
|
|
||||||
|
const makeInternalVariants = (variant) => {
|
||||||
|
variant = transformObject(streamInfo, variant);
|
||||||
|
variant.video = variant.video.map(makeInternalStream);
|
||||||
|
variant.audio = variant.audio.map(makeInternalStream);
|
||||||
|
return variant;
|
||||||
|
};
|
||||||
|
hlsPlaylist.variants = hlsPlaylist.variants.map(makeInternalVariants);
|
||||||
|
|
||||||
|
return hlsPlaylist;
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformMediaPlaylist(streamInfo, hlsPlaylist) {
|
||||||
|
const makeInternalSegments = transformObject(streamInfo);
|
||||||
|
hlsPlaylist.segments = hlsPlaylist.segments.map(makeInternalSegments);
|
||||||
|
hlsPlaylist.prefetchSegments = hlsPlaylist.prefetchSegments.map(makeInternalSegments);
|
||||||
|
return hlsPlaylist;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HLS_MIME_TYPES = ["application/vnd.apple.mpegurl", "audio/mpegurl", "application/x-mpegURL"];
|
||||||
|
|
||||||
|
export function isHlsRequest (req) {
|
||||||
|
return HLS_MIME_TYPES.includes(req.headers['content-type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleHlsPlaylist(streamInfo, req, res) {
|
||||||
|
let hlsPlaylist = await req.body.text();
|
||||||
|
hlsPlaylist = HLS.parse(hlsPlaylist);
|
||||||
|
|
||||||
|
hlsPlaylist = hlsPlaylist.isMasterPlaylist
|
||||||
|
? transformMasterPlaylist(streamInfo, hlsPlaylist)
|
||||||
|
: transformMediaPlaylist(streamInfo, hlsPlaylist);
|
||||||
|
|
||||||
|
hlsPlaylist = HLS.stringify(hlsPlaylist);
|
||||||
|
|
||||||
|
res.send(hlsPlaylist);
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
import { request } from 'undici';
|
import { request } from 'undici';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import { assert } from 'console';
|
import { assert } from 'console';
|
||||||
import { getHeaders } from './shared.js';
|
import { getHeaders, pipe } from './shared.js';
|
||||||
|
import { handleHlsPlaylist, isHlsRequest } from './internal-hls.js';
|
||||||
|
|
||||||
const CHUNK_SIZE = BigInt(8e6); // 8 MB
|
const CHUNK_SIZE = BigInt(8e6); // 8 MB
|
||||||
const min = (a, b) => a < b ? a : b;
|
const min = (a, b) => a < b ? a : b;
|
||||||
@ -66,8 +67,7 @@ async function handleYoutubeStream(streamInfo, res) {
|
|||||||
if (headerValue) res.setHeader(headerName, headerValue);
|
if (headerValue) res.setHeader(headerName, headerValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.pipe(res);
|
pipe(stream, res, () => res.end());
|
||||||
stream.on('error', () => res.end());
|
|
||||||
} catch {
|
} catch {
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
@ -97,8 +97,11 @@ export async function internalStream(streamInfo, res) {
|
|||||||
if (req.statusCode < 200 || req.statusCode > 299)
|
if (req.statusCode < 200 || req.statusCode > 299)
|
||||||
return res.end();
|
return res.end();
|
||||||
|
|
||||||
req.body.pipe(res);
|
if (isHlsRequest(req)) {
|
||||||
req.body.on('error', () => res.end());
|
await handleHlsPlaylist(streamInfo, req, res);
|
||||||
|
} else {
|
||||||
|
pipe(req.body, res, () => res.end());
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
streamInfo.controller.abort();
|
streamInfo.controller.abort();
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { randomBytes } from "crypto";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js";
|
import { decryptStream, encryptStream, generateHmac } from "../sub/crypto.js";
|
||||||
import { env, hlsExceptions } from "../config.js";
|
import { env } from "../config.js";
|
||||||
import { strict as assert } from "assert";
|
import { strict as assert } from "assert";
|
||||||
|
|
||||||
// optional dependency
|
// optional dependency
|
||||||
@ -106,12 +106,6 @@ export function destroyInternalStream(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function wrapStream(streamInfo) {
|
function wrapStream(streamInfo) {
|
||||||
/* m3u8 links are currently not supported
|
|
||||||
* for internal streams, skip them */
|
|
||||||
if (hlsExceptions.includes(streamInfo.service)) {
|
|
||||||
return streamInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = streamInfo.urls;
|
const url = streamInfo.urls;
|
||||||
|
|
||||||
if (typeof url === 'string') {
|
if (typeof url === 'string') {
|
||||||
|
@ -29,3 +29,13 @@ export function getHeaders(service) {
|
|||||||
return Object.entries({ ...defaultHeaders, ...serviceHeaders[service] })
|
return Object.entries({ ...defaultHeaders, ...serviceHeaders[service] })
|
||||||
.reduce((p, [key, val]) => ({ ...p, [key]: String(val) }), {})
|
.reduce((p, [key, val]) => ({ ...p, [key]: String(val) }), {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function pipe(from, to, done) {
|
||||||
|
from.on('error', done)
|
||||||
|
.on('close', done);
|
||||||
|
|
||||||
|
to.on('error', done)
|
||||||
|
.on('close', done);
|
||||||
|
|
||||||
|
from.pipe(to);
|
||||||
|
}
|
@ -6,7 +6,7 @@ import { create as contentDisposition } from "content-disposition-header";
|
|||||||
import { metadataManager } from "../sub/utils.js";
|
import { metadataManager } from "../sub/utils.js";
|
||||||
import { destroyInternalStream } from "./manage.js";
|
import { destroyInternalStream } from "./manage.js";
|
||||||
import { env, ffmpegArgs, hlsExceptions } from "../config.js";
|
import { env, ffmpegArgs, hlsExceptions } from "../config.js";
|
||||||
import { getHeaders, closeResponse } from "./shared.js";
|
import { getHeaders, closeResponse, pipe } from "./shared.js";
|
||||||
|
|
||||||
function toRawHeaders(headers) {
|
function toRawHeaders(headers) {
|
||||||
return Object.entries(headers)
|
return Object.entries(headers)
|
||||||
@ -28,16 +28,6 @@ function killProcess(p) {
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pipe(from, to, done) {
|
|
||||||
from.on('error', done)
|
|
||||||
.on('close', done);
|
|
||||||
|
|
||||||
to.on('error', done)
|
|
||||||
.on('close', done);
|
|
||||||
|
|
||||||
from.pipe(to);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCommand(args) {
|
function getCommand(args) {
|
||||||
if (!isNaN(env.processingPriority)) {
|
if (!isNaN(env.processingPriority)) {
|
||||||
return ['nice', ['-n', env.processingPriority.toString(), ffmpeg, ...args]]
|
return ['nice', ['-n', env.processingPriority.toString(), ffmpeg, ...args]]
|
||||||
|
Loading…
Reference in New Issue
Block a user