@yt-kit/core 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # YT-KIT (core)
2
+
3
+ Kit de herramientas para manejar multimedia orientada a videos.
4
+
5
+ ## Instalación
6
+
7
+ ```sh
8
+ # Con npm
9
+ npm install @yt-kit/core
10
+
11
+ # Con pnpm
12
+ pnpm add -E @yt-kit/core
13
+
14
+ # Con bun
15
+ bun add -E @yt-kit/core
16
+ ```
17
+
18
+ ## Sobre la estructura de carpetas
19
+
20
+ El archivo `pruebas.ts` es un archivo para probar el funcionamiento del proyecto antes de lanzar una nueva versión.
21
+
22
+ `src/index.ts` es el archivo principal para las builds, no para desarrollo. En todo caso, el archivo "principal" sería `pruebas.ts`.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from './tasks/download/download';
2
2
  export * from './types/videoTypes';
3
- export * from './types/processTypes';
3
+ export * from './types/childProcessTypes';
4
4
  export { STANDARD_RESOLUTIONS } from './lib/constants';
package/dist/index.js CHANGED
@@ -2,6 +2,6 @@
2
2
  export * from './tasks/download/download';
3
3
  // Tipos
4
4
  export * from './types/videoTypes';
5
- export * from './types/processTypes';
5
+ export * from './types/childProcessTypes';
6
6
  // Constantes públicas (seguras de exportar)
7
7
  export { STANDARD_RESOLUTIONS } from './lib/constants';
@@ -1,15 +1,15 @@
1
1
  export interface Downloader {
2
- download(url: string, options: DownloadTasksOptions): Promise<DownloadResult>;
2
+ download(url: string, ytId: string, options: DownloadTasksOptions): Promise<DownloadResult>;
3
3
  }
4
4
  export interface DownloadOptions {
5
5
  id: string;
6
- outputFolder?: string;
6
+ outputPath?: string;
7
7
  filename?: string;
8
8
  }
9
9
  export interface DownloadTasksOptions {
10
10
  id: string;
11
11
  type: 'video' | 'audio';
12
- outputFolder: string;
12
+ outputPath: string;
13
13
  filename: string;
14
14
  }
15
15
  export interface DownloadResult {
@@ -0,0 +1,10 @@
1
+ import type { MediaType } from '../types/videoTypes';
2
+ import type { YtDlpFormat } from '../types/ytDlpFormatTypes';
3
+ export declare function getBetterFormat(a: YtDlpFormat | undefined, b: YtDlpFormat | undefined, { type, compareResolution }: {
4
+ type: MediaType;
5
+ compareResolution: boolean;
6
+ }): YtDlpFormat | undefined;
7
+ export declare function getWorstFormat(a: YtDlpFormat | undefined, b: YtDlpFormat | undefined, { type, compareResolution }: {
8
+ type: MediaType;
9
+ compareResolution: boolean;
10
+ }): YtDlpFormat | undefined;
@@ -0,0 +1,52 @@
1
+ function compareFormats({ a, b, compareResolution, type }) {
2
+ const canCompareQuality = (type === 'video') && compareResolution && (a.quality !== null) && (b.quality !== null);
3
+ const canCompareResolution = (type === 'video') && compareResolution && (a.height !== null) && (b.height !== null);
4
+ const canCompareFps = (type === 'video') && (a.fps !== null) && (b.fps !== null);
5
+ const canCompareAsr = (type === 'audio') && (a.asr !== null) && (b.asr !== null);
6
+ const canCompareTbr = (a.tbr !== null) && (b.tbr !== null);
7
+ let aScore = 0;
8
+ let bScore = 0;
9
+ if (canCompareQuality) {
10
+ if ((a.quality ?? 0) > (b.quality ?? 0))
11
+ aScore += 3;
12
+ else if ((a.quality ?? 0) < (b.quality ?? 0))
13
+ bScore += 3;
14
+ }
15
+ if (canCompareResolution) {
16
+ if ((a.height ?? 0) > (b.height ?? 0))
17
+ aScore += 3;
18
+ else if ((a.height ?? 0) < (b.height ?? 0))
19
+ bScore += 3;
20
+ }
21
+ if (canCompareFps) {
22
+ if ((a.fps ?? 0) > (b.fps ?? 0))
23
+ aScore += 2;
24
+ else if ((a.fps ?? 0) < (b.fps ?? 0))
25
+ bScore += 2;
26
+ }
27
+ if (canCompareAsr) {
28
+ if ((a.asr ?? 0) > (b.asr ?? 0))
29
+ aScore += 2;
30
+ else if ((a.asr ?? 0) < (b.asr ?? 0))
31
+ bScore += 2;
32
+ }
33
+ if (canCompareTbr) {
34
+ if ((a.tbr ?? 0) > (b.tbr ?? 0))
35
+ aScore += 1;
36
+ else if ((a.tbr ?? 0) < (b.tbr ?? 0))
37
+ bScore += 1;
38
+ }
39
+ return { aScore, bScore };
40
+ }
41
+ export function getBetterFormat(a, b, { type, compareResolution }) {
42
+ if (!a || !b)
43
+ return;
44
+ const { aScore, bScore } = compareFormats({ a, b, type, compareResolution });
45
+ return aScore > bScore ? a : b;
46
+ }
47
+ export function getWorstFormat(a, b, { type, compareResolution }) {
48
+ if (!a || !b)
49
+ return;
50
+ const { aScore, bScore } = compareFormats({ a, b, type, compareResolution });
51
+ return aScore > bScore ? b : a;
52
+ }
@@ -2,4 +2,8 @@ export declare const STANDARD_RESOLUTIONS: readonly [18, 144, 240, 360, 480, 720
2
2
  export declare const COMMANDS: {
3
3
  readonly 'yt-dlp': "yt-dlp-linux";
4
4
  };
5
- export declare const DEFAULT_FILENAME = "%(id)s.%(ext)s";
5
+ export declare const DEFAULT_FILENAME = "%(ytId)s.%(ext)s";
6
+ export declare const PATTERNS: {
7
+ readonly ID: "%(id)s";
8
+ readonly YT_ID: "%(ytId)s";
9
+ };
@@ -2,4 +2,8 @@ export const STANDARD_RESOLUTIONS = [18, 144, 240, 360, 480, 720, 1080, 1440, 21
2
2
  export const COMMANDS = {
3
3
  'yt-dlp': 'yt-dlp-linux'
4
4
  };
5
- export const DEFAULT_FILENAME = '%(id)s.%(ext)s';
5
+ export const DEFAULT_FILENAME = '%(ytId)s.%(ext)s';
6
+ export const PATTERNS = {
7
+ ID: '%(id)s',
8
+ YT_ID: '%(ytId)s'
9
+ };
@@ -0,0 +1,4 @@
1
+ import type { PATTERNS } from './constants';
2
+ export type Pattern = typeof PATTERNS[keyof typeof PATTERNS];
3
+ export type PatternData = string;
4
+ export declare function expandPattern(pattern: string, patternMap: Map<Pattern, PatternData>): string;
@@ -0,0 +1,7 @@
1
+ export function expandPattern(pattern, patternMap) {
2
+ let resolvedPattern = pattern;
3
+ for (const [key, value] of patternMap) {
4
+ resolvedPattern = resolvedPattern.replaceAll(key, value);
5
+ }
6
+ return resolvedPattern;
7
+ }
@@ -0,0 +1,7 @@
1
+ interface ResolverProps {
2
+ filename: string;
3
+ id: string;
4
+ ytId: string;
5
+ }
6
+ export declare function resolveFilename({ filename, id, ytId }: ResolverProps): string;
7
+ export {};
@@ -0,0 +1,11 @@
1
+ import { PATTERNS } from './constants';
2
+ import { expandPattern } from './expandPattern';
3
+ import { sanitizeFilename } from './sanitizeFilename';
4
+ export function resolveFilename({ filename, id, ytId }) {
5
+ const map = new Map([
6
+ [PATTERNS.ID, id],
7
+ [PATTERNS.YT_ID, ytId]
8
+ ]);
9
+ const raw = expandPattern(filename, map);
10
+ return sanitizeFilename(raw);
11
+ }
@@ -0,0 +1 @@
1
+ export declare function sanitizeFilename(raw: string): string;
@@ -0,0 +1,6 @@
1
+ // Por ahora esto está enfocado solo para Linux,
2
+ // pero en un futuro sería ideal que soporte más sistemas operativos
3
+ export function sanitizeFilename(raw) {
4
+ return raw
5
+ .replaceAll('/', '⁄');
6
+ }
@@ -1,2 +1,2 @@
1
- import type { CommandKey } from '../types/processTypes';
1
+ import type { CommandKey } from '../types/childProcessTypes';
2
2
  export declare function spawnAsync(command: CommandKey, args: string[], showOutput?: boolean): Promise<unknown>;
@@ -1,6 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { COMMANDS } from '../lib/constants';
3
- import { streamLog } from './logs';
3
+ import { streamLog } from './logger';
4
4
  export function spawnAsync(command, args, showOutput) {
5
5
  const _command = COMMANDS[command];
6
6
  return new Promise((resolve, reject) => {
@@ -1,6 +1,6 @@
1
1
  // import { spawnAsync } from 'src/core/lib/spawnAsync'
2
2
  // import type { DownloadOptions } from './interfaces/Downloader'
3
- import { DEFAULT_FILENAME } from 'src/lib/constants';
3
+ import { DEFAULT_FILENAME } from '../../lib/constants';
4
4
  import { formYoutubeUrl } from '../../lib/ytUtils';
5
5
  import { YtDlpDownloader } from '../../yt-dlp-downloader/YtDlpDownloader';
6
6
  export async function downloadVideo(ytId, options) {
@@ -8,20 +8,20 @@ export async function downloadVideo(ytId, options) {
8
8
  return;
9
9
  const url = formYoutubeUrl(ytId);
10
10
  const taskOptions = formDownloadTaskOptions('video', options);
11
- return new YtDlpDownloader().download(url, taskOptions);
11
+ return new YtDlpDownloader().download(url, ytId, taskOptions);
12
12
  }
13
13
  export async function downloadAudio(ytId, options) {
14
14
  if (!ytId || !options.id)
15
15
  return;
16
16
  const url = formYoutubeUrl(ytId);
17
17
  const taskOptions = formDownloadTaskOptions('audio', options);
18
- return new YtDlpDownloader().download(url, taskOptions);
18
+ return new YtDlpDownloader().download(url, ytId, taskOptions);
19
19
  }
20
20
  function formDownloadTaskOptions(type, options) {
21
21
  const taskOptions = {
22
22
  id: options.id,
23
23
  type,
24
- outputFolder: options.outputFolder ?? '.',
24
+ outputPath: options.outputPath ?? '.',
25
25
  filename: options.filename ?? DEFAULT_FILENAME
26
26
  };
27
27
  return taskOptions;
@@ -1,3 +1,5 @@
1
1
  import type { STANDARD_RESOLUTIONS } from '../lib/constants';
2
2
  export type Height = typeof STANDARD_RESOLUTIONS[number];
3
3
  export type Resolution = `${Height}p`;
4
+ export type FormatsToFind = Resolution | 'best-video' | 'worst-video' | 'best-audio' | 'worst-audio';
5
+ export type MediaType = 'audio' | 'video';
@@ -0,0 +1,61 @@
1
+ export type YtDlpFormat = {
2
+ format_id: string;
3
+ format_note: string;
4
+ ext: EXT;
5
+ protocol: Protocol;
6
+ acodec: Acodec;
7
+ vcodec: string;
8
+ url: string;
9
+ width: number | null;
10
+ height: number | null;
11
+ fps: number | null;
12
+ rows?: number;
13
+ columns?: number;
14
+ fragments?: Fragment[];
15
+ audio_ext: AudioEXT;
16
+ video_ext: VideoEXT;
17
+ vbr: number | null;
18
+ abr: number | null;
19
+ tbr: number | null;
20
+ resolution: string;
21
+ aspect_ratio: number | null;
22
+ filesize_approx: number | null;
23
+ http_headers: HTTPHeaders;
24
+ format: string;
25
+ asr?: number | null;
26
+ filesize?: number | null;
27
+ source_preference?: number;
28
+ audio_channels?: number | null;
29
+ quality?: number;
30
+ has_drm?: boolean;
31
+ language?: null | string;
32
+ language_preference?: number;
33
+ preference?: null;
34
+ dynamic_range?: DynamicRange | null;
35
+ container?: Container;
36
+ available_at?: number;
37
+ downloader_options?: DownloaderOptions;
38
+ };
39
+ export type Acodec = 'none' | 'mp4a.40.5' | 'opus' | 'mp4a.40.2';
40
+ export type AudioEXT = 'none' | 'm4a' | 'webm';
41
+ export type Container = 'm4a_dash' | 'webm_dash' | 'mp4_dash';
42
+ export type DownloaderOptions = {
43
+ http_chunk_size: number;
44
+ };
45
+ export type DynamicRange = 'SDR';
46
+ export type EXT = 'mhtml' | 'm4a' | 'webm' | 'mp4';
47
+ export type Fragment = {
48
+ url: string;
49
+ duration: number;
50
+ };
51
+ export type HTTPHeaders = {
52
+ 'User-Agent': string;
53
+ Accept: Accept;
54
+ 'Accept-Language': AcceptLanguage;
55
+ 'Sec-Fetch-Mode': SECFetchMode;
56
+ };
57
+ export type Accept = 'text/html,application/xhtml+xml,application/xmlq=0.9,*/*q=0.8';
58
+ export type AcceptLanguage = 'en-us,enq=0.5';
59
+ export type SECFetchMode = 'navigate';
60
+ export type Protocol = 'mhtml' | 'https';
61
+ export type VideoEXT = 'none' | 'mp4' | 'webm';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,12 @@
1
1
  import type { Downloader, DownloadTasksOptions, DownloadResult } from '../interfaces/Downloader';
2
+ import type { FormatsToFind } from '../types/videoTypes';
3
+ import type { YtDlpFormat } from '../types/ytDlpFormatTypes';
2
4
  export declare class YtDlpDownloader implements Downloader {
3
- download(url: string, options: DownloadTasksOptions): Promise<DownloadResult>;
5
+ download(url: string, ytId: string, options: DownloadTasksOptions): Promise<DownloadResult>;
6
+ findFormatId(url: string, formatToFind: FormatsToFind): Promise<{
7
+ foundSpecific: string | boolean;
8
+ formatId: string | undefined;
9
+ desiredFormat: YtDlpFormat | undefined;
10
+ }>;
4
11
  private buildYtDlpArgs;
5
12
  }
@@ -1,7 +1,9 @@
1
1
  import { spawnAsync } from '../lib/spawnAsync';
2
+ import { resolveFilename } from '../lib/resolveFilename';
3
+ import { getBetterFormat, getWorstFormat } from '../lib/compareFormats';
2
4
  export class YtDlpDownloader {
3
- async download(url, options) {
4
- const args = this.buildYtDlpArgs(url, options);
5
+ async download(url, ytId, options) {
6
+ const args = this.buildYtDlpArgs(url, ytId, options);
5
7
  // const result = await spawnAsync('yt-dlp', args, true)
6
8
  await spawnAsync('yt-dlp', args, true);
7
9
  return {
@@ -9,11 +11,70 @@ export class YtDlpDownloader {
9
11
  path: 'unknown'
10
12
  };
11
13
  }
12
- buildYtDlpArgs(url, options) {
14
+ async findFormatId(url, formatToFind) {
15
+ const isSpecificResolution = Boolean(formatToFind.match(/\d/));
16
+ let foundSpecific = isSpecificResolution ? false : 'N/A';
17
+ const args = ['--print', '%(formats)j', url];
18
+ let output = '';
19
+ try {
20
+ output = await spawnAsync('yt-dlp', args);
21
+ }
22
+ catch (err) {
23
+ console.error('Error consiguiendo el ID del formato');
24
+ throw err;
25
+ }
26
+ let formats = [];
27
+ try {
28
+ formats = JSON.parse(output);
29
+ }
30
+ catch (err) {
31
+ console.error('Error convirtiendo la salida de yt-dlp a JSON');
32
+ throw err;
33
+ }
34
+ if (!Array.isArray(formats)) {
35
+ throw new Error('Se esperaba un array');
36
+ }
37
+ let bestVideo = undefined;
38
+ let worstVideo = undefined;
39
+ let bestAudio = undefined;
40
+ let worstAudio = undefined;
41
+ let desiredFormat = undefined;
42
+ for (const format of formats) {
43
+ const audioOnly = format.resolution === 'audio only';
44
+ if (isSpecificResolution && format.format_note === formatToFind) {
45
+ foundSpecific = true;
46
+ desiredFormat = getBetterFormat(desiredFormat, format, { compareResolution: false, type: 'video' }) ?? format;
47
+ continue;
48
+ }
49
+ if (audioOnly) {
50
+ bestAudio = getBetterFormat(bestAudio, format, { compareResolution: false, type: 'audio' }) ?? format;
51
+ worstAudio = getWorstFormat(worstAudio, format, { compareResolution: false, type: 'audio' }) ?? format;
52
+ }
53
+ else {
54
+ bestVideo = getBetterFormat(bestVideo, format, { compareResolution: true, type: 'video' }) ?? format;
55
+ worstVideo = getWorstFormat(worstVideo, format, { compareResolution: true, type: 'video' }) ?? format;
56
+ }
57
+ }
58
+ if (!isSpecificResolution) {
59
+ const formats = {
60
+ 'best-video': bestVideo,
61
+ 'worst-video': worstVideo,
62
+ 'best-audio': bestAudio,
63
+ 'worst-audio': worstAudio
64
+ };
65
+ desiredFormat = formats[formatToFind];
66
+ }
67
+ return {
68
+ foundSpecific,
69
+ formatId: desiredFormat?.format_id,
70
+ desiredFormat
71
+ };
72
+ }
73
+ buildYtDlpArgs(url, ytId, options) {
13
74
  const { id, type } = options;
14
75
  const isVideo = type === 'video';
15
- const exportRoute = options.outputFolder;
16
- const exportName = options.filename;
76
+ const exportRoute = options.outputPath;
77
+ const exportName = resolveFilename({ filename: options.filename, id: options.id, ytId });
17
78
  const audioFormat = 'aac';
18
79
  const audioFormatPreferences = isVideo
19
80
  ? []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yt-kit/core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
File without changes
File without changes