From 92150fe361fa022863d6bef4eeb5dc771e5b72e0 Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Mon, 18 Sep 2023 23:25:05 +0200 Subject: [PATCH] fix: stdout can be 'null' (#167) --- .github/workflows/cron.yml | 25 ++ .github/workflows/main.yml | 11 +- .github/workflows/pull_request.yml | 9 +- README.md | 15 +- src/index.d.ts | 496 +++++++++++++++-------------- src/index.js | 2 +- test/args.js | 2 - test/error.js | 6 +- test/index.js | 18 +- 9 files changed, 313 insertions(+), 271 deletions(-) create mode 100644 .github/workflows/cron.yml diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml new file mode 100644 index 0000000..38bec15 --- /dev/null +++ b/.github/workflows/cron.yml @@ -0,0 +1,25 @@ +name: cron + +on: + schedule: + # Cron job every day at 12:00 + # https://crontab.guru/#0_12_*_*_* + - cron: '0 0 * * *' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: lts/* + - name: Setup PNPM + uses: pnpm/action-setup@v2 + with: + version: latest + run_install: true + - name: Test + run: pnpm test diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index edffcc7..e45ecb4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,10 +45,13 @@ jobs: uses: actions/setup-node@v3 with: node-version: lts/* - - name: Install - run: npm install --no-package-lock + - name: Setup PNPM + uses: pnpm/action-setup@v2 + with: + version: latest + run_install: true - name: Test - run: npm test + run: pnpm test - name: Report run: mkdir -p coverage && npx c8 report --reporter=text-lcov > coverage/lcov.info - name: Coverage @@ -63,4 +66,4 @@ jobs: git config --global user.email ${{ secrets.GIT_EMAIL }} git config --global user.name ${{ secrets.GIT_USERNAME }} git pull origin master - npm run release + pnpm run release diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index a89e55e..8910ab7 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -21,10 +21,13 @@ jobs: uses: actions/setup-node@v3 with: node-version: lts/* - - name: Install - run: npm install --no-package-lock + - name: Setup PNPM + uses: pnpm/action-setup@v2 + with: + version: latest + run_install: true - name: Test - run: npm test + run: pnpm test - name: Report run: mkdir -p coverage && npx c8 report --reporter=text-lcov > coverage/lcov.info - name: Coverage diff --git a/README.md b/README.md index 172cb85..b1184a6 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,7 @@ youtubedl('https://www.youtube.com/watch?v=6xKWiCMKKJg', { noCheckCertificates: true, noWarnings: true, preferFreeFormats: true, - addHeader: [ - 'referer:youtube.com', - 'user-agent:googlebot' - ] - + addHeader: ['referer:youtube.com', 'user-agent:googlebot'] }).then(output => console.log(output)) ``` @@ -121,9 +117,12 @@ Similar to main method but instead of a parsed output, it will return the intern const youtubedl = require('youtube-dl-exec') const fs = require('fs') -const subprocess = youtubedl.exec('https://www.youtube.com/watch?v=6xKWiCMKKJg', { - dumpSingleJson: true -}) +const subprocess = youtubedl.exec( + 'https://www.youtube.com/watch?v=6xKWiCMKKJg', + { + dumpSingleJson: true + } +) console.log(`Running subprocess as ${subprocess.pid}`) diff --git a/src/index.d.ts b/src/index.d.ts index bfe3443..1b73aae 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,265 +1,279 @@ /* eslint-disable */ declare module 'youtube-dl-exec' { - import { ExecaChildProcess, Options } from 'execa'; + import { ExecaChildProcess, Options } from 'execa' export type YtFormat = { - asr: number, - filesize: number, - format_id: string, - format_note: string, - fps: number, - height: number, - quality: number, - tbr: number, - vbr?: number, - url: string, - manifest_url: string, - width: number, - ext: string, - vcodec: string, - acodec: string, - abr: number, - downloader_options: unknown, - container: string, - format: string, - protocol: string, + asr: number + filesize: number + format_id: string + format_note: string + fps: number + height: number + quality: number + tbr: number + vbr?: number + url: string + manifest_url: string + width: number + ext: string + vcodec: string + acodec: string + abr: number + downloader_options: unknown + container: string + format: string + protocol: string http_headers: unknown } export type YtThumbnail = { - height: number, - url: string, - width: number, - resolution: string, - id: string, + height: number + url: string + width: number + resolution: string + id: string } export type YtResponse = { - id: string, - title: string, - formats: YtFormat[], - thumbnails: YtThumbnail[], - description: string, - upload_date: string, - uploader: string, - uploader_id: string, - uploader_url: string, - channel_id: string, - channel_url: string, - duration: number, - view_count: number, - average_rating: number, - age_limit: number, - webpage_url: string, - categories: string[], - tags: string[], - is_live: boolean, - like_count: number, - dislike_count: number, - channel: string, - track: string, - artist: string, - creator: string, - alt_title: string, - extractor: string, - webpage_url_basename: string, - extractor_key: string, - playlist: string, - playlist_index: number, - thumbnail: string, - display_id: string, - requested_subtitles: unknown, - asr: number, - filesize: number, - format_id: string, - format_note: string, - fps: number, - height: number, - quality: number, - tbr: number, - url: string, - width: number, - ext: string, - vcodec: string, - acodec: string, - abr: number, - downloader_options: { http_chunk_size: number }, - container: string, - format: string, - protocol: string, - http_headers: unknown, - fulltitle: string, + id: string + title: string + formats: YtFormat[] + thumbnails: YtThumbnail[] + description: string + upload_date: string + uploader: string + uploader_id: string + uploader_url: string + channel_id: string + channel_url: string + duration: number + view_count: number + average_rating: number + age_limit: number + webpage_url: string + categories: string[] + tags: string[] + is_live: boolean + like_count: number + dislike_count: number + channel: string + track: string + artist: string + creator: string + alt_title: string + extractor: string + webpage_url_basename: string + extractor_key: string + playlist: string + playlist_index: number + thumbnail: string + display_id: string + requested_subtitles: unknown + asr: number + filesize: number + format_id: string + format_note: string + fps: number + height: number + quality: number + tbr: number + url: string + width: number + ext: string + vcodec: string + acodec: string + abr: number + downloader_options: { http_chunk_size: number } + container: string + format: string + protocol: string + http_headers: unknown + fulltitle: string _filename: string } export type YtFlags = { - help?: boolean, - version?: boolean, - update?: boolean, - ignoreErrors?: boolean, - abortOnError?: boolean, - dumpUserAgent?: boolean, - listExtractors?: boolean, - extractorDescriptions?: boolean, - forceGenericExtractor?: boolean, - defaultSearch?: string, - ignoreConfig?: boolean, - configLocation?: string, - flatPlaylist?: boolean, - markWatched?: boolean, - noColor?: boolean, - proxy?: string, - socketTimeout?: number, - sourceAddress?: string, - forceIpv4?: boolean, - forceIpv6?: boolean, - geoVerificationProxy?: string, - geoBypass?: boolean, - geoBypassCountry?: string, - geoBypassIpBlock?: string, - playlistStart?: number, - playlistEnd?: number | 'last', - playlistItems?: string, - matchTitle?: string, - rejectTitle?: string, - maxDownloads?: number, - minFilesize?: string, - maxFilesize?: string, - date?: string, - datebefore?: string, - dateafter?: string, - minViews?: number, - maxViews?: number, - matchFilter?: string, - noPlaylist?: boolean, - yesPlaylist?: boolean, - ageLimit?: number, - downloadArchive?: string, - includeAds?: boolean, - limitRate?: string, - retries?: number | 'infinite', - skipUnavailableFragments?: boolean, - abortOnUnavailableFragment?: boolean, - keepFragments?: boolean, - bufferSize?: string, - noResizeBuffer?: boolean, - httpChunkSize?: string, - playlistReverse?: boolean, - playlistRandom?: boolean, - xattrSetFilesize?: boolean, - hlsPreferNative?: boolean, - hlsPreferFfmpeg?: boolean, - hlsUseMpegts?: boolean, - externalDownloader?: string, - externalDownloaderArgs?: string, - batchFile?: string, - id?: boolean, - output?: string, - outputNaPlaceholder?: string, - autonumberStart?: number, - restrictFilenames?: boolean, - noOverwrites?: boolean, - continue?: boolean, - noPart?: boolean, - noMtime?: boolean, - writeDescription?: boolean, - writeInfoJson?: boolean, - writeAnnotations?: boolean, - loadInfoJson?: string, - cookies?: string, - cacheDir?: string, - noCacheDir?: boolean, - rmCacheDir?: boolean, - writeThumbnail?: boolean, - writeAllThumbnails?: boolean, - listThumbnails?: boolean, - quiet?: boolean, - noWarnings?: boolean, - simulate?: boolean, - skipDownload?: boolean, - getUrl?: boolean, - getTitle?: boolean, - getId?: boolean, - getThumbnail?: boolean, - getDuration?: boolean, - getFilename?: boolean, - getFormat?: boolean, - dumpJson?: boolean, - dumpSingleJson?: boolean, - printJson?: boolean, - newline?: boolean, - noProgress?: boolean, - consoleTitle?: boolean, - verbose?: boolean, - dumpPages?: boolean, - writePages?: boolean, - printTraffic?: boolean, - callHome?: boolean, - encoding?: string, - noCheckCertificates?: boolean, - preferInsecure?: boolean, - userAgent?: string, - referer?: string, - addHeader?: string[], - bidiWorkaround?: boolean, - sleepInterval?: number, - maxSleepInterval?: number, - format?: string, - allFormats?: boolean, - preferFreeFormats?: boolean, - listFormats?: boolean, - youtubeSkipDashManifest?: boolean, - mergeOutputFormat?: string, - writeSub?: boolean, - writeAutoSub?: boolean, - allSubs?: boolean, - listSubs?: boolean, - subFormat?: string, - subLang?: string, - username?: string, - password?: string, - twofactor?: string, - netrc?: boolean, - videoPassword?: string, - apMso?: string, - apUsername?: string, - apPassword?: string, - apListMso?: boolean, - extractAudio?: boolean, - audioFormat?: string, - audioQuality?: number, - recodeVideo?: string, - remuxVideo? : string, - postprocessorArgs?: string, - keepVideo?: boolean, - noPostOverwrites?: boolean, - embedSubs?: boolean, - embedThumbnail?: boolean, - addMetadata?: boolean, - metadataFromTitle?: string, - xattrs?: boolean, - fixup?: string, - preferAvconv?: boolean, - preferFfmpeg?: boolean, - ffmpegLocation?: string, - exec?: string, + help?: boolean + version?: boolean + update?: boolean + ignoreErrors?: boolean + abortOnError?: boolean + dumpUserAgent?: boolean + listExtractors?: boolean + extractorDescriptions?: boolean + forceGenericExtractor?: boolean + defaultSearch?: string + ignoreConfig?: boolean + configLocation?: string + flatPlaylist?: boolean + markWatched?: boolean + noColor?: boolean + proxy?: string + socketTimeout?: number + sourceAddress?: string + forceIpv4?: boolean + forceIpv6?: boolean + geoVerificationProxy?: string + geoBypass?: boolean + geoBypassCountry?: string + geoBypassIpBlock?: string + playlistStart?: number + playlistEnd?: number | 'last' + playlistItems?: string + matchTitle?: string + rejectTitle?: string + maxDownloads?: number + minFilesize?: string + maxFilesize?: string + date?: string + datebefore?: string + dateafter?: string + minViews?: number + maxViews?: number + matchFilter?: string + noPlaylist?: boolean + yesPlaylist?: boolean + ageLimit?: number + downloadArchive?: string + includeAds?: boolean + limitRate?: string + retries?: number | 'infinite' + skipUnavailableFragments?: boolean + abortOnUnavailableFragment?: boolean + keepFragments?: boolean + bufferSize?: string + noResizeBuffer?: boolean + httpChunkSize?: string + playlistReverse?: boolean + playlistRandom?: boolean + xattrSetFilesize?: boolean + hlsPreferNative?: boolean + hlsPreferFfmpeg?: boolean + hlsUseMpegts?: boolean + externalDownloader?: string + externalDownloaderArgs?: string + batchFile?: string + id?: boolean + output?: string + outputNaPlaceholder?: string + autonumberStart?: number + restrictFilenames?: boolean + noOverwrites?: boolean + continue?: boolean + noPart?: boolean + noMtime?: boolean + writeDescription?: boolean + writeInfoJson?: boolean + writeAnnotations?: boolean + loadInfoJson?: string + cookies?: string + cacheDir?: string + noCacheDir?: boolean + rmCacheDir?: boolean + writeThumbnail?: boolean + writeAllThumbnails?: boolean + listThumbnails?: boolean + quiet?: boolean + noWarnings?: boolean + simulate?: boolean + skipDownload?: boolean + getUrl?: boolean + getTitle?: boolean + getId?: boolean + getThumbnail?: boolean + getDuration?: boolean + getFilename?: boolean + getFormat?: boolean + dumpJson?: boolean + dumpSingleJson?: boolean + printJson?: boolean + newline?: boolean + noProgress?: boolean + consoleTitle?: boolean + verbose?: boolean + dumpPages?: boolean + writePages?: boolean + printTraffic?: boolean + callHome?: boolean + encoding?: string + noCheckCertificates?: boolean + preferInsecure?: boolean + userAgent?: string + referer?: string + addHeader?: string[] + bidiWorkaround?: boolean + sleepInterval?: number + maxSleepInterval?: number + format?: string + allFormats?: boolean + preferFreeFormats?: boolean + listFormats?: boolean + youtubeSkipDashManifest?: boolean + mergeOutputFormat?: string + writeSub?: boolean + writeAutoSub?: boolean + allSubs?: boolean + listSubs?: boolean + subFormat?: string + subLang?: string + username?: string + password?: string + twofactor?: string + netrc?: boolean + videoPassword?: string + apMso?: string + apUsername?: string + apPassword?: string + apListMso?: boolean + extractAudio?: boolean + audioFormat?: string + audioQuality?: number + recodeVideo?: string + remuxVideo?: string + postprocessorArgs?: string + keepVideo?: boolean + noPostOverwrites?: boolean + embedSubs?: boolean + embedThumbnail?: boolean + addMetadata?: boolean + metadataFromTitle?: string + xattrs?: boolean + fixup?: string + preferAvconv?: boolean + preferFfmpeg?: boolean + ffmpegLocation?: string + exec?: string convertSubs?: string } - export type YtdlExecFunction = (url: string, flags?: YtFlags, options?: Options) => ExecaChildProcess; + export type YtdlExecFunction = ( + url: string, + flags?: YtFlags, + options?: Options + ) => ExecaChildProcess export type YtdlCreateFuncion = (binaryPath: string) => { - (url: string, flags?: YtFlags, options?: Options): Promise, - exec: YtdlExecFunction, + ( + url: string, + flags?: YtFlags, + options?: Options + ): Promise + exec: YtdlExecFunction } - const youtubeDl: ( - (url: string, flags?: YtFlags, options?: Options) => Promise - ) & { - exec: YtdlExecFunction, - create: YtdlCreateFuncion, + const youtubeDl: (( + url: string, + flags?: YtFlags, + options?: Options + ) => Promise) & { + exec: YtdlExecFunction + create: YtdlCreateFuncion } - export default youtubeDl; - export function exec(...[url, flags, options]: Parameters): ReturnType; - export function create(...[binaryPath]: Parameters): ReturnType; + export default youtubeDl + export function exec ( + ...[url, flags, options]: Parameters + ): ReturnType + export function create ( + ...[binaryPath]: Parameters + ): ReturnType } diff --git a/src/index.js b/src/index.js index 68e7ed6..2c25457 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,7 @@ const args = (url, flags = {}) => const isJSON = (str = '') => str.startsWith('{') const parse = ({ stdout, stderr, ...details }) => { - if (!stderr) return isJSON(stdout) ? JSON.parse(stdout) : stdout + if (stdout !== '' && stdout !== 'null') return isJSON(stdout) ? JSON.parse(stdout) : stdout throw Object.assign(new Error(stderr), details) } diff --git a/test/args.js b/test/args.js index 0afd0bb..cf2fa0f 100644 --- a/test/args.js +++ b/test/args.js @@ -12,7 +12,6 @@ test('no flags', t => { test('parse arguments into flags', t => { const flags = args('https://example.com', { noWarnings: true, - noCallHome: true, noCheckCertificate: true, preferFreeFormats: true, youtubeSkipDashManifest: true, @@ -23,7 +22,6 @@ test('parse arguments into flags', t => { t.deepEqual(flags, [ 'https://example.com', '--no-warnings', - '--no-call-home', '--no-check-certificate', '--prefer-free-formats', '--youtube-skip-dash-manifest', diff --git a/test/error.js b/test/error.js index 78c204c..c586d34 100644 --- a/test/error.js +++ b/test/error.js @@ -4,6 +4,10 @@ const test = require('ava') const youtubedl = require('..') +test('show help', async t => { + await t.throwsAsync(youtubedl(), { instanceOf: Error }) +}) + test('unsupported URLs', async t => { t.plan(4) const url = 'https://www.apple.com/homepod' @@ -24,7 +28,7 @@ test('video unavailable', async t => { t.plan(4) const url = 'https://www.youtube.com/watch?v=x8erEF_1POY' try { - await youtubedl(url, { dumpSingleJson: true }) + await youtubedl(url, { dumpSingleJson: true, noWarnings: true }) } catch (error) { t.is( error.message, diff --git a/test/index.js b/test/index.js index c365067..b4ca17a 100644 --- a/test/index.js +++ b/test/index.js @@ -4,31 +4,27 @@ const test = require('ava') const youtubedl = require('..') -test('throw errors', async t => { - await t.throwsAsync(youtubedl(), { instanceOf: Error }) -}) - test('execute commands', async t => { const output = await youtubedl( 'https://www.youtube.com/watch?v=2Z4m4lnjxkY', { - dumpSingleJson: true + dumpSingleJson: true, + noCheckCertificates: true, + noWarnings: true, + preferFreeFormats: true } ) - t.true(typeof output === 'object') }) -test('conditional JSON parsing', async t => { +test('parse JSON automatically', async t => { const output = await youtubedl( 'https://www.youtube.com/watch?v=tu3Db9onH6k', { - listFormats: true, + noCheckCertificates: true, noWarnings: true, - noCallHome: true, - noCheckCertificate: true + preferFreeFormats: true } ) - t.is(typeof output, 'string') })