From 3eda5d1ee870c81110ffd7f1f1713bc97c00ea55 Mon Sep 17 00:00:00 2001 From: John Shaver Date: Mon, 19 Aug 2024 23:19:59 -0700 Subject: [PATCH] initial commit --- .eslintrc | 40 +++++++++ .gitignore | 175 +++++++++++++++++++++++++++++++++++++++ .prettierrc | 8 ++ README.md | 15 ++++ bun.lockb | Bin 0 -> 52203 bytes index.ts | 78 +++++++++++++++++ lib/fileDownloader.ts | 53 ++++++++++++ lib/m3uWriter.ts | 18 ++++ lib/services/jellyfin.ts | 120 +++++++++++++++++++++++++++ package.json | 21 +++++ tsconfig.json | 27 ++++++ 11 files changed, 555 insertions(+) create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 README.md create mode 100755 bun.lockb create mode 100644 index.ts create mode 100644 lib/fileDownloader.ts create mode 100644 lib/m3uWriter.ts create mode 100644 lib/services/jellyfin.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..5d1185c --- /dev/null +++ b/.eslintrc @@ -0,0 +1,40 @@ +{ + "extends": ["plugin:@typescript-eslint/recommended", "prettier"], + "parser": "@typescript-eslint/parser", + "parserOptions": {}, + "plugins": ["@typescript-eslint"], + "root": true, + "rules": { + "camelcase": "error", + "no-console": ["error", { "allow": ["warn", "error"]}], + "no-empty-function": "error", + "no-empty": "error", + "no-irregular-whitespace": "warn", + "no-unneeded-ternary": "warn", + "no-var": "warn", + "spaced-comment": "warn", + "no-lonely-if": "warn", + "space-before-blocks": ["warn", "always"], + "prefer-destructuring": [ + "warn", + { + "AssignmentExpression": { + "array": false, + "object": false + }, + "VariableDeclarator": { + "array": false, + "object": true + } + }, + { + "enforceForRenamedProperties": false + } + ], + "prefer-template": "warn", + "prefer-spread": "warn", + "prefer-arrow-callback": "warn", + "arrow-spacing": "warn", + "arrow-parens": ["warn", "as-needed"] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9b1ee42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ce39093 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "tabWidth": 2, + "printWidth": 90, + "singleQuote": true, + "bracketSpacing": true, + "semi": true, + "arrowParens": "avoid" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..29c098f --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# jellyfin-to-m3u + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.1.24. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..e80aca4abab31dfc4404d30a19389296eaa94941 GIT binary patch literal 52203 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 { + console.log('Fetching playlist information...'); + if (!JF_SERVER) { + console.error('Missing required env var JF_SERVER. Exiting...'); + process.exit(); + } + if (!JF_USERNAME) { + console.error('Missing required env var JF_USERNAME. Exiting...'); + process.exit(); + } + if (!JF_PASSWORD) { + console.error('Missing required env var JF_PASSWORD. Exiting...'); + process.exit(); + } + if (!DEST_FOLDER) { + console.error('Missing required env var DEST_FOLDER. Exiting...'); + process.exit(); + } + const creds = { + server: JF_SERVER, + user: JF_USERNAME, + password: JF_PASSWORD, + }; + const playlistMetas = await getAllPlaylists(creds); + const answer = await checkbox({ + message: 'Which playlists would you like to download?', + choices: playlistMetas.map((pl: Playlist) => ({ name: pl.Name, value: pl.Id })), + }); + const playlists = await Promise.all( + answer.map(async id => { + const pl = { + ...playlistMetas.find(p => p.Id === id), + }; + pl.songlist = await getSonglistForPlaylist(creds, id); + return pl; + }), + ); + + const allSongs = playlists.reduce( + (songs, pl) => { + console.log('PLAYLIST: ', pl); + pl.songlist?.forEach(song => { + songs[song.Id] = song; + }); + return songs; + }, + {} as Record, + ); + await downloadSongs(creds, Object.values(allSongs), DEST_FOLDER); + + await Promise.all( + playlists.map((pl, i) => + writeM3UFile(DEST_FOLDER, pl.Name || `playlist${i}`, pl.songlist as SongMeta[]), + ), + ); + }); + +await program.parseAsync(process.argv); diff --git a/lib/fileDownloader.ts b/lib/fileDownloader.ts new file mode 100644 index 0000000..a5da27f --- /dev/null +++ b/lib/fileDownloader.ts @@ -0,0 +1,53 @@ +import fs from 'fs'; +import nodePath from 'node:path'; +import { finished } from 'stream/promises'; + +import type { SongMeta, ServerCreds } from './services/jellyfin.ts'; +import { getSongFileBuffer } from './services/jellyfin.ts'; + +const QUEUE: (() => Promise)[] = []; +let downloadCount = 0; +const DOWNLOAD_LIMIT = 10; + +export const downloadSongs = async ( + creds: ServerCreds, + songs: SongMeta[], + path: string, +) => { + const promises = songs.map(s => getSongDownloader(creds, s, path)); + QUEUE.push(...promises); + processQueue(); + await Promise.all(promises); +}; + +const getSongDownloader = + (creds: ServerCreds, song: SongMeta, path: string) => async () => { + const extension = song.Path.split('.').pop(); + const filePath = nodePath.join(path, `${song.Id}.${extension}`); + try { + const write = fs.createWriteStream(filePath, { + flags: 'wx', + }); + console.log('Downloading: ', filePath); + const download = await getSongFileBuffer(creds, song.Id); + await finished(download.pipe(write)); + } catch (err: any) { + if (err?.code !== 'EEXIST') { + throw err; + } + console.log('File already exists, skipping: ', filePath); + } + }; + +const processQueue = () => { + if (downloadCount < DOWNLOAD_LIMIT && QUEUE.length) { + downloadCount++; + const next = QUEUE.shift(); + next && + next().then(() => { + downloadCount--; + processQueue(); + }); + processQueue(); + } +}; diff --git a/lib/m3uWriter.ts b/lib/m3uWriter.ts new file mode 100644 index 0000000..b39fbd4 --- /dev/null +++ b/lib/m3uWriter.ts @@ -0,0 +1,18 @@ +import { writeFile } from 'node:fs/promises'; +import nodePath from 'node:path'; +import m3u from 'm3u'; +import type { SongMeta } from './services/jellyfin.ts'; + +const createM3U = (name: string, songlist: SongMeta[]) => { + const m3uWriter = m3u.writer(); + for (const song of songlist) { + const extension = song.Path.split('.').pop(); + m3uWriter.file(`${song.Id}.${extension}`); + } + return m3uWriter.toString(); +}; + +export const writeM3UFile = async (path: string, name: string, songlist: SongMeta[]) => { + const contents = createM3U(name, songlist); + return writeFile(nodePath.join(path, `${name}.m3u`), contents, 'utf8'); +}; diff --git a/lib/services/jellyfin.ts b/lib/services/jellyfin.ts new file mode 100644 index 0000000..35fdd4c --- /dev/null +++ b/lib/services/jellyfin.ts @@ -0,0 +1,120 @@ +import { hostname, userInfo } from 'node:os'; +import { Readable } from 'stream'; + +interface AuthInfo { + token: string; + userId: string; + authHeaderValue: string; +} + +export interface ServerCreds { + server: string; + user: string; + password: string; +} + +interface View { + Id: string; + CollectionType: string; +} + +export interface Playlist { + Name: string; + Id: string; + MediaType: string; + songlist: SongMeta[]; +} + +export interface SongMeta { + Id: string; + Name: string; + Path: string; +} + +let tokenPromise: Promise | undefined = undefined; + +const getAuthHeaderValue = (token?: string) => { + let val = `MediaBrowser Client="JF-to-M3U",Device="CLI",DeviceId="${ + userInfo().username + }-${hostname()}",Version="0.0.1"`; + + if (token) { + val += `,Token="${token}"`; + } + return val; +}; + +const getAuthInfo = async ({ server, user, password }: ServerCreds) => { + if (!tokenPromise) { + const url = `${server}/Users/authenticatebyname`; + console.log('URL: ', url); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: getAuthHeaderValue(), + }, + body: JSON.stringify({ + Username: user, + Pw: password, + }), + }); + if (!response.ok) { + console.error('Error authenticating to server: ', await response.text()); + console.log('Exiting due unrecoverable error'); + process.exit(); + } + const authInfo = await response.json(); + const token = authInfo.AccessToken; + const userId = authInfo.User.Id; + const authHeaderValue = getAuthHeaderValue(token); + tokenPromise = Promise.resolve({ token, authHeaderValue, userId }); + } + return tokenPromise as Promise; +}; + +export const getAllViews = async (creds: ServerCreds): Promise => { + const auth = await getAuthInfo(creds); + const url = `${creds.server}/UserViews?userId=${auth.userId}`; + + const response = await fetch(url, { headers: { Authorization: auth.authHeaderValue } }); + return (await response.json()).Items as View[]; +}; + +export const getItemsByParent = async (creds: ServerCreds, parentId: string) => { + const auth = await getAuthInfo(creds); + const url = `${creds.server}/Items?userId=${auth.userId}&parentId=${parentId}`; + + const response = await fetch(url, { headers: { Authorization: auth.authHeaderValue } }); + return (await response.json()).Items; +}; + +export const getAllPlaylists = async (creds: ServerCreds): Promise => { + const auth = await getAuthInfo(creds); + const views = await getAllViews(creds); + const musicViews = views.filter(c => c.CollectionType === 'music'); + const playlistViews = views.filter(c => c.CollectionType === 'playlists'); + const playlists: Playlist[] = ( + await Promise.all(playlistViews.map(c => getItemsByParent(creds, c.Id))) + ).flat(Infinity); + return playlists.filter(p => p.MediaType === 'Audio'); +}; + +export const getSonglistForPlaylist = async (creds: ServerCreds, id: string) => { + const auth = await getAuthInfo(creds); + const response = await fetch( + `${creds.server}/Playlists/${id}/Items?userId=${auth.userId}&fields=Path`, + { + headers: { Authorization: auth.authHeaderValue }, + }, + ); + return (await response.json()).Items as SongMeta[]; +}; + +export const getSongFileBuffer = async (creds: ServerCreds, id: string) => { + const auth = await getAuthInfo(creds); + const response = await fetch(`${creds.server}/Items/${id}/File?userId=${auth.userId}`, { + headers: { Authorization: auth.authHeaderValue }, + }); + return Readable.fromWeb(response.body); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..9fb43a4 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "jellyfin-to-m3u", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@inquirer/prompts": "^5.3.8", + "@types/node": "^22.4.1", + "cli-progress": "^3.12.0", + "commander": "^12.1.0", + "dotenv": "^16.4.5", + "eslint": "^9.9.0", + "m3u": "^0.0.2", + "prettier": "^3.3.3" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}