Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,396 changes: 4,396 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@
"default-import": "^2.0.1",
"dotenv": "^16.4.5",
"ffbinaries": "^1.1.6",
"floatplane": "^4.5.2",
"floatplane": "https://github.com/Inrixia/floatplaneAPI.ts.git#main",
"floatplane-plex-downloader": "file:",
"html-to-text": "^9.0.5",
"json5": "^2.2.3",
"mime-types": "^3.0.0",
"multi-progress-bars": "^5.0.3",
"openid-client": "^6.8.1",
"process.argv": "^0.6.1",
"prom-client": "^15.1.3",
"prompts": "^2.4.2",
Expand Down Expand Up @@ -66,4 +67,4 @@
"got": "^14.4.3",
"typescript": "^5.7.0-beta"
}
}
}
24 changes: 13 additions & 11 deletions src/float.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,19 @@ process.on("SIGTERM", () => process.exit(143));
console.log(chalk`\n{red ///}{grey ===} {cyan Console} {grey ===}{red \\\\\\}`);
}

const latest = await fApi
.got("https://raw.githubusercontent.com/Inrixia/Floatplane-Downloader/master/package.json")
.json<{ version: string }>()
.catch(() => ({ version: DownloaderVersion }));

if (gt(latest.version, DownloaderVersion))
console.log(
chalk`There is a ${diff(latest.version, DownloaderVersion)} update available! ${DownloaderVersion} > ${
latest.version
}.\nHead to {cyanBright https://github.com/Inrixia/Floatplane-Downloader/releases} to update!\n`,
);
if (args.updateCheck) {
const latest = await fApi
.got("https://raw.githubusercontent.com/Inrixia/Floatplane-Downloader/master/package.json")
.json<{ version: string }>()
.catch(() => ({ version: DownloaderVersion }));

if (gt(latest.version, DownloaderVersion))
console.log(
chalk`There is a ${diff(latest.version, DownloaderVersion)} update available! ${DownloaderVersion} > ${
latest.version
}.\nHead to {cyanBright https://github.com/Inrixia/Floatplane-Downloader/releases} to update!\n`,
);
}

if (args.sanityCheck) {
console.log("Sanity check passed!");
Expand Down
9 changes: 9 additions & 0 deletions src/lib/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const defaultArgs: Args = {
password: "",
token: "",
headless: false,
updateCheck: true,
plexUsername: "",
plexPassword: "",
sanityCheck: false,
Expand All @@ -44,6 +45,14 @@ export const defaultSettings: Settings = {
videoResolution: "1080",
waitForNewVideos: true,
seekAndDestroy: [],
api: {
tokenSet: null,
client: {
server: "https://auth.floatplane.com/realms/floatplane",
clientId: "floatplane-downloader",
},
baseUrl: "https://www.floatplane.com",
},
},
maxDownloadSpeed: -1,
plex: {
Expand Down
81 changes: 45 additions & 36 deletions src/lib/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,55 @@ export const DownloaderVersion = isSea() ? getAsset("./version", "utf-8") : JSON

import type { PartialArgs, Settings } from "../types.js";

import { FileCookieStore } from "tough-cookie-file-store";
import { CookieJar } from "tough-cookie";
export const cookieJar = new CookieJar(new FileCookieStore("./db/cookies.json"));
export const settings = db<Settings>("./db/settings.json", { template: defaultSettings, pretty: true, forceCreate: true, updateOnExternalChanges: true });
recursiveUpdate(settings, defaultSettings);

const argv = ARGV(process.argv.slice(2))<PartialArgs>({});
rebuildTypes(argv, { ...defaultSettings, ...defaultArgs });
recursiveUpdate(settings, argv, { setUndefined: false, setDefined: true });

const env = getEnv();
rebuildTypes(env, { ...defaultSettings, ...defaultArgs });

if (env.__FPDSettings !== undefined) {
if (typeof env.__FPDSettings !== "string") throw new Error("The __FPDSettings environment variable cannot be parsed!");
recursiveUpdate(settings, parse(env.__FPDSettings.replaceAll('\\"', '"')), { setUndefined: false, setDefined: true });
}

recursiveUpdate(settings, env, { setUndefined: false, setDefined: true });

export const args = { ...argv, ...env };

// eslint-disable-next-line no-control-regex
const headlessStdoutRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
// Override stdout if headless to not include formatting tags
if (args.headless === true) {
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
type StdoutArgs = Parameters<typeof process.stdout.write>;

process.stdout.write = ((...params: StdoutArgs) => {
if (typeof params[0] === "string") params[0] = `[${new Date().toLocaleString()}] ${params[0].replace(headlessStdoutRegex, "")}`;
return originalStdoutWrite(...params);
}) as typeof process.stdout.write;
}

import { Floatplane } from "floatplane";
export const fApi = new Floatplane(
cookieJar,
{
tokenSet: settings.floatplane.api.tokenSet,
tokenSetHook: (tokenSet) => {
// Makes it compatible with JSON storage (db package)
tokenSet.expires_at = tokenSet.expires_at instanceof Date ? tokenSet.expires_at.toISOString() : tokenSet.expires_at;

settings.floatplane.api.tokenSet = tokenSet;
},
clientSettings: {
server: settings.floatplane.api.client.server,
clientId: settings.floatplane.api.client.clientId,
},
},
`Floatplane-Downloader/${DownloaderVersion} (Inrix, +https://github.com/Inrixia/Floatplane-Downloader), CFNetwork`,
settings.floatplane.api.baseUrl,
);

// Add floatplane api request metrics
Expand Down Expand Up @@ -57,35 +98,3 @@ fApi.extend({
],
},
});

export const settings = db<Settings>("./db/settings.json", { template: defaultSettings, pretty: true, forceCreate: true, updateOnExternalChanges: true });
recursiveUpdate(settings, defaultSettings);

const argv = ARGV(process.argv.slice(2))<PartialArgs>({});
rebuildTypes(argv, { ...defaultSettings, ...defaultArgs });
recursiveUpdate(settings, argv, { setUndefined: false, setDefined: true });

const env = getEnv();
rebuildTypes(env, { ...defaultSettings, ...defaultArgs });

if (env.__FPDSettings !== undefined) {
if (typeof env.__FPDSettings !== "string") throw new Error("The __FPDSettings environment variable cannot be parsed!");
recursiveUpdate(settings, parse(env.__FPDSettings.replaceAll('\\"', '"')), { setUndefined: false, setDefined: true });
}

recursiveUpdate(settings, env, { setUndefined: false, setDefined: true });

export const args = { ...argv, ...env };

// eslint-disable-next-line no-control-regex
const headlessStdoutRegex = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
// Override stdout if headless to not include formatting tags
if (args.headless === true) {
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
type StdoutArgs = Parameters<typeof process.stdout.write>;

process.stdout.write = ((...params: StdoutArgs) => {
if (typeof params[0] === "string") params[0] = `[${new Date().toLocaleString()}] ${params[0].replace(headlessStdoutRegex, "")}`;
return originalStdoutWrite(...params);
}) as typeof process.stdout.write;
}
10 changes: 10 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export type Resolutions = ["360", "720", "1080", "2160"];

import type { ValueOfA } from "@inrixia/helpers/ts";
import { TokenEndpointResponse } from "openid-client";

export type ChannelOptions = {
title: string;
Expand Down Expand Up @@ -30,6 +31,7 @@ export type Args = {
password: string;
token: string;
headless: boolean;
updateCheck: boolean;
plexUsername: string;
plexPassword: string;
sanityCheck: boolean;
Expand All @@ -54,6 +56,14 @@ export type Settings = {
videosToSearch: number;
waitForNewVideos: boolean;
seekAndDestroy: string[];
api: {
tokenSet: (TokenEndpointResponse & { expires_at?: Date | string }) | null;
client: {
server: string;
clientId: string;
};
baseUrl: string;
};
};
maxDownloadSpeed: number;
filePathFormatting: string;
Expand Down
47 changes: 20 additions & 27 deletions src/logins.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,33 @@
import { loopError } from "@inrixia/helpers/object";
import { floatplane, plex } from "./lib/prompts/index.js";
import { plex } from "./lib/prompts/index.js";
import { fApi, args } from "./lib/helpers/index.js";
import { MyPlexAccount } from "@ctrl/plex";

import type { LoginSuccess } from "floatplane/auth";
export type User = LoginSuccess["user"];

export const loginFloatplane = async (): Promise<User> => {
let loginResponse;
const { headless, username, password, token } = args;
if (headless === true) {
if (username === undefined || password === undefined) {
throw new Error('Need floatplane username/password to login. Please pass them as --username="" --password="" or enviroment variables!');
}
loginResponse = await fApi.auth.login(username, password);

if (loginResponse.needs2FA) {
if (token === undefined) throw new Error('Need floatplane 2Factor token to login. Please pass it as --token="" or an enviroment variable!');
loginResponse = await fApi.auth.factor(token);
}
} else {
loginResponse = await loopError(
async () => fApi.auth.login(await floatplane.username(), await floatplane.password()),
async (err) => console.error(`\nLooks like those login details didnt work, Please try again... ${err}`),
if ((await fApi.isAuthenticated()) !== true) {
let loopCounter = 0;
await loopError(
async () => {
const loginResponse = await fApi.deviceLogin();
return loginResponse;
},
async (err) => {
loopCounter++;
console.error(`\nLooks like those login details didnt work, Please try again... ${err}`);
if (loopCounter >= 5) {
console.error("Failed to login to floatplane after 5 attempts!");
throw err;
}
},
);

if (loginResponse.needs2FA) {
console.log("Looks like you have 2Factor authentication enabled. Nice!\n");
loginResponse = await loopError(
async () => fApi.auth.factor(await floatplane.token()),
async (err) => console.error(`\nLooks like that 2Factor token didnt work, Please try again... ${err}`),
);
}
}
if (loginResponse.user !== undefined) console.log(`\nSigned in as \u001b[36m${loginResponse.user.username}\u001b[0m!\n`);
return loginResponse.user;

const self = await fApi.user.self();
console.log(`\nLogged in as \u001b[36m${self.username}\u001b[0m!\n`);
return self;
};

export const loginPlex = async (): Promise<string> => {
Expand Down
2 changes: 1 addition & 1 deletion src/quickStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const quickStart = async (): Promise<void> => {
if ((await fApi.isAuthenticated()) !== true) {
console.log("Please login to floatplane...");
await loginFloatplane();
} else console.log("Already logged in!");
} else console.log("Logged in!");

console.log("\n== \u001b[38;5;208mGeneral\u001b[0m ==\n");
settings.floatplane.videosToSearch = await prompts.floatplane.videosToSearch(settings.floatplane.videosToSearch);
Expand Down
4 changes: 2 additions & 2 deletions wiki/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,12 @@ Saves video thubnails alongside each video. These are required for nice thumbnai

<br>

**extras.safeNfo**:
**extras.saveNfo**:
Saves video metadata to nfo files alongside each video.

```json
"extras": {
"safeNfo": true
"saveNfo": true
}
```

Expand Down