merge: 11.1 api update
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
Run service tests / test service functionality (push) Has been cancelled
Run tests / check lockfile correctness (push) Has been cancelled
Run tests / web sanity check (push) Has been cancelled
Run tests / api sanity check (push) Has been cancelled
Run service tests / test service: ${{ matrix.service }} (push) Has been cancelled

This commit is contained in:
wukko 2025-06-08 20:53:33 +06:00
commit 0bcb28c44c
No known key found for this signature in database
GPG Key ID: 3E30B3F26C7B4AA2
13 changed files with 111 additions and 28 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "@imput/cobalt-api", "name": "@imput/cobalt-api",
"description": "save what you love", "description": "save what you love",
"version": "11.0.3", "version": "11.1",
"author": "imput", "author": "imput",
"exports": "./src/cobalt.js", "exports": "./src/cobalt.js",
"type": "module", "type": "module",

View File

@ -9,6 +9,7 @@ import { fileURLToPath } from "url";
import { env, isCluster } from "./config.js" import { env, isCluster } from "./config.js"
import { Red } from "./misc/console-text.js"; import { Red } from "./misc/console-text.js";
import { initCluster } from "./misc/cluster.js"; import { initCluster } from "./misc/cluster.js";
import { setupEnvWatcher } from "./core/env.js";
const app = express(); const app = express();
@ -24,6 +25,10 @@ if (env.apiURL) {
await initCluster(); await initCluster();
} }
if (env.envFile) {
setupEnvWatcher();
}
runAPI(express, app, __dirname, cluster.isPrimary); runAPI(express, app, __dirname, cluster.isPrimary);
} else { } else {
console.log( console.log(

View File

@ -1,5 +1,5 @@
import { getVersion } from "@imput/version-info"; import { getVersion } from "@imput/version-info";
import { loadEnvs, validateEnvs, setupEnvWatcher } from "./core/env.js"; import { loadEnvs, validateEnvs } from "./core/env.js";
const version = await getVersion(); const version = await getVersion();
@ -12,20 +12,23 @@ export const canonicalEnv = Object.freeze(structuredClone(process.env));
export const setTunnelPort = (port) => env.tunnelPort = port; export const setTunnelPort = (port) => env.tunnelPort = port;
export const isCluster = env.instanceCount > 1; export const isCluster = env.instanceCount > 1;
export const updateEnv = (newEnv) => { export const updateEnv = (newEnv) => {
const changes = [];
// tunnelPort is special and needs to get carried over here // tunnelPort is special and needs to get carried over here
newEnv.tunnelPort = env.tunnelPort; newEnv.tunnelPort = env.tunnelPort;
for (const key in env) { for (const key in env) {
if (String(env[key]) !== String(newEnv[key])) {
changes.push(key);
}
env[key] = newEnv[key]; env[key] = newEnv[key];
} }
return changes;
} }
await validateEnvs(env); await validateEnvs(env);
if (env.envFile) {
setupEnvWatcher();
}
export { export {
env, env,
genericUserAgent, genericUserAgent,

View File

@ -5,7 +5,7 @@ import { updateEnv, canonicalEnv, env as currentEnv } from "../config.js";
import { FileWatcher } from "../misc/file-watcher.js"; import { FileWatcher } from "../misc/file-watcher.js";
import { isURL } from "../misc/utils.js"; import { isURL } from "../misc/utils.js";
import * as cluster from "../misc/cluster.js"; import * as cluster from "../misc/cluster.js";
import { Yellow } from "../misc/console-text.js"; import { Green, Yellow } from "../misc/console-text.js";
const forceLocalProcessingOptions = ["never", "session", "always"]; const forceLocalProcessingOptions = ["never", "session", "always"];
@ -109,6 +109,8 @@ export const validateEnvs = async (env) => {
if (env.externalProxy && env.freebindCIDR) { if (env.externalProxy && env.freebindCIDR) {
throw new Error('freebind is not available when external proxy is enabled') throw new Error('freebind is not available when external proxy is enabled')
} }
return env;
} }
const reloadEnvs = async (contents) => { const reloadEnvs = async (contents) => {
@ -136,14 +138,34 @@ const reloadEnvs = async (contents) => {
...newEnvs, ...newEnvs,
}; };
const parsed = loadEnvs(candidate); const parsed = await validateEnvs(
await validateEnvs(parsed); loadEnvs(candidate)
updateEnv(parsed); );
cluster.broadcast({ env_update: resolvedContents }); cluster.broadcast({ env_update: resolvedContents });
return updateEnv(parsed);
} }
const wrapReload = (contents) => { const wrapReload = (contents) => {
reloadEnvs(contents) reloadEnvs(contents)
.then(changes => {
if (changes.length === 0) {
return;
}
console.log(`${Green('[✓]')} envs reloaded successfully!`);
for (const key of changes) {
const value = currentEnv[key];
const isSecret = key.toLowerCase().includes('apikey')
|| key.toLowerCase().includes('secret');
if (!value) {
console.log(` removed: ${key}`);
} else {
console.log(` changed: ${key} -> ${isSecret ? '***' : value}`);
}
}
})
.catch((e) => { .catch((e) => {
console.error(`${Yellow('[!]')} Failed reloading environment variables at ${new Date().toISOString()}.`); console.error(`${Yellow('[!]')} Failed reloading environment variables at ${new Date().toISOString()}.`);
console.error('Error:', e); console.error('Error:', e);

View File

@ -13,7 +13,8 @@ export const supportsReusePort = async () => {
server.on('error', (err) => (server.close(), reject(err))); server.on('error', (err) => (server.close(), reject(err)));
}); });
return true; const [major, minor] = process.versions.node.split('.').map(Number);
return major > 23 || (major === 23 && minor >= 1);
} catch { } catch {
return false; return false;
} }

View File

@ -3,6 +3,7 @@ import { resolveRedirectingURL } from "../url.js";
const videoRegex = /"url":"(https:\/\/v1\.pinimg\.com\/videos\/.*?)"/g; const videoRegex = /"url":"(https:\/\/v1\.pinimg\.com\/videos\/.*?)"/g;
const imageRegex = /src="(https:\/\/i\.pinimg\.com\/.*\.(jpg|gif))"/g; const imageRegex = /src="(https:\/\/i\.pinimg\.com\/.*\.(jpg|gif))"/g;
const notFoundRegex = /"__typename"\s*:\s*"PinNotFound"/;
export default async function(o) { export default async function(o) {
let id = o.id; let id = o.id;
@ -19,6 +20,10 @@ export default async function(o) {
headers: { "user-agent": genericUserAgent } headers: { "user-agent": genericUserAgent }
}).then(r => r.text()).catch(() => {}); }).then(r => r.text()).catch(() => {});
const invalidPin = html.match(notFoundRegex);
if (invalidPin) return { error: "fetch.empty" };
if (!html) return { error: "fetch.fail" }; if (!html) return { error: "fetch.fail" };
const videoLink = [...html.matchAll(videoRegex)] const videoLink = [...html.matchAll(videoRegex)]

View File

@ -40,6 +40,27 @@ async function findClientID() {
} catch {} } catch {}
} }
const findBestForPreset = (transcodings, preset) => {
let inferior;
for (const entry of transcodings) {
const protocol = entry?.format?.protocol;
if (entry.snipped || protocol?.includes('encrypted')) {
continue;
}
if (entry?.preset?.startsWith(`${preset}_`)) {
if (protocol === 'progressive') {
return entry;
}
inferior = entry;
}
}
return inferior;
}
export default async function(obj) { export default async function(obj) {
const clientId = await findClientID(); const clientId = await findClientID();
if (!clientId) return { error: "fetch.fail" }; if (!clientId) return { error: "fetch.fail" };
@ -89,9 +110,9 @@ export default async function(obj) {
} }
let bestAudio = "opus", let bestAudio = "opus",
selectedStream = json.media.transcodings.find(v => v.preset === "opus_0_0"); selectedStream = findBestForPreset(json.media.transcodings, "opus");
const mp3Media = json.media.transcodings.find(v => v.preset === "mp3_0_0"); const mp3Media = findBestForPreset(json.media.transcodings, "mp3");
// use mp3 if present if user prefers it or if opus isn't available // use mp3 if present if user prefers it or if opus isn't available
if (mp3Media && (obj.format === "mp3" || !selectedStream)) { if (mp3Media && (obj.format === "mp3" || !selectedStream)) {
@ -113,9 +134,16 @@ export default async function(obj) {
if (!file) return { error: "fetch.empty" }; if (!file) return { error: "fetch.empty" };
const artist = json.user?.username?.trim();
const fileMetadata = { const fileMetadata = {
title: json.title.trim(), title: json.title?.trim(),
artist: json.user.username.trim(), album: json.publisher_metadata?.album_title?.trim(),
artist,
album_artist: artist,
composer: json.publisher_metadata?.writer_composer?.trim(),
genre: json.genre?.trim(),
date: json.display_date?.trim().slice(0, 10),
copyright: json.license?.trim(),
} }
return { return {

View File

@ -280,7 +280,7 @@ export default async function (o) {
// some videos (mainly those with AI dubs) don't have any tracks marked as default // some videos (mainly those with AI dubs) don't have any tracks marked as default
// why? god knows, but we assume that a default track is marked as such in the title // why? god knows, but we assume that a default track is marked as such in the title
if (!audio) { if (!audio) {
audio = selected.audio.find(i => i.name.endsWith("- original")); audio = selected.audio.find(i => i.name.endsWith("original"));
} }
if (o.dubLang) { if (o.dubLang) {
@ -369,9 +369,9 @@ export default async function (o) {
audio = sorted_formats[codec].bestAudio; audio = sorted_formats[codec].bestAudio;
if (audio?.audio_track && !audio?.audio_track?.audio_is_default) { if (audio?.audio_track && !audio?.is_original) {
audio = sorted_formats[codec].audio.find(i => audio = sorted_formats[codec].audio.find(i =>
i?.audio_track?.audio_is_default i?.is_original
); );
} }
@ -380,7 +380,7 @@ export default async function (o) {
i.language?.startsWith(o.dubLang) && i.audio_track i.language?.startsWith(o.dubLang) && i.audio_track
); );
if (dubbedAudio && !dubbedAudio?.audio_track?.audio_is_default) { if (dubbedAudio && !dubbedAudio?.is_original) {
audio = dubbedAudio; audio = dubbedAudio;
dubbedLanguage = dubbedAudio.language; dubbedLanguage = dubbedAudio.language;
} }

View File

@ -17,9 +17,12 @@ const ffmpegArgs = {
const metadataTags = [ const metadataTags = [
"album", "album",
"composer",
"genre",
"copyright", "copyright",
"title", "title",
"artist", "artist",
"album_artist",
"track", "track",
"date", "date",
]; ];

View File

@ -8,6 +8,16 @@
"status": "redirect" "status": "redirect"
} }
}, },
{
"name": "invalid link",
"url": "https://www.pinterest.com/pin/eeeeeee/",
"params": {},
"expected": {
"code": 400,
"status": "error",
"errorCode": "error.api.fetch.empty"
}
},
{ {
"name": "regular video (isAudioOnly)", "name": "regular video (isAudioOnly)",
"url": "https://www.pinterest.com/pin/70437485604616/", "url": "https://www.pinterest.com/pin/70437485604616/",

View File

@ -86,7 +86,7 @@
}, },
{ {
"name": "go+ song, should fail", "name": "go+ song, should fail",
"url": "https://soundcloud.com/dualipa/illusion-1", "url": "https://soundcloud.com/dualipa/physical-feat-troye-sivan",
"params": {}, "params": {},
"expected": { "expected": {
"code": 400, "code": 400,

View File

@ -129,14 +129,17 @@ the response will always be a JSON object containing the `status` key, which is
#### output.metadata object #### output.metadata object
all keys in this table are optional. all keys in this table are optional.
| key | type | description | | key | type | description |
|:------------|:---------|:-------------------------------------------| |:---------------|:---------|:-------------------------------------------|
| `album` | `string` | album name or collection title | | `album` | `string` | album name or collection title |
| `copyright` | `string` | copyright information or ownership details | | `composer` | `string` | composer of the track |
| `title` | `string` | title of the track or media file | | `genre` | `string` | track's genre(s) |
| `artist` | `string` | artist or creator name | | `copyright` | `string` | copyright information or ownership details |
| `track` | `string` | track number or position in album | | `title` | `string` | title of the track or media file |
| `date` | `string` | release date or creation date | | `artist` | `string` | artist or creator name |
| `album_artist` | `string` | album's artist or creator name |
| `track` | `string` | track number or position in album |
| `date` | `string` | release date or creation date |
#### audio object #### audio object
| key | type | value | | key | type | value |

View File

@ -45,9 +45,12 @@ type CobaltTunnelResponse = {
export const CobaltFileMetadataKeys = [ export const CobaltFileMetadataKeys = [
'album', 'album',
'composer',
'genre',
'copyright', 'copyright',
'title', 'title',
'artist', 'artist',
'album_artist',
'track', 'track',
'date' 'date'
]; ];