Skip to content

Commit

Permalink
Refactor to full yeild & break up Video/Downloader/Attachments
Browse files Browse the repository at this point in the history
  • Loading branch information
Inrixia committed Apr 7, 2024
1 parent e4bc801 commit b27cb64
Show file tree
Hide file tree
Showing 20 changed files with 642 additions and 712 deletions.
45 changes: 2 additions & 43 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Expand Up @@ -18,7 +18,6 @@
"@ctrl/plex": "^1.5.3",
"@inrixia/db": "2.0.2",
"@inrixia/helpers": "^2.0.10",
"chalk-template": "^1.1.0",
"default-import": "^1.1.5",
"dotenv": "^16.4.5",
"ffbinaries": "^1.1.6",
Expand Down Expand Up @@ -54,4 +53,4 @@
"pkg": "^5.8.1",
"typescript": "^5.4.3"
}
}
}
66 changes: 23 additions & 43 deletions src/float.ts
Expand Up @@ -5,65 +5,54 @@ import { fetchFFMPEG } from "./lib/helpers/fetchFFMPEG.js";
import { defaultSettings } from "./lib/defaults.js";

import { loginFloatplane, User } from "./logins.js";
import { VideoDownloader } from "./lib/Downloader.js";
import chalk from "chalk-template";
import chalk from "chalk";

import type { ContentPost } from "floatplane/content";
import type { Video } from "./lib/Video.js";
import { fetchSubscriptions } from "./subscriptionFetching.js";

import semver from "semver";
const { gt, diff } = semver;

import { promptVideos } from "./lib/prompts/downloader.js";

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Yes, package.json isnt under src, this is fine
import pkg from "../package.json" assert { type: "json" };
import { Self } from "floatplane/user";

async function fetchSubscriptionVideos(): Promise<Video[]> {
// Function that pops items out of seek and destroy until the array is empty
const posts: Promise<ContentPost>[] = [];
async function* seekAndDestroy(): AsyncGenerator<ContentPost, void, unknown> {
while (settings.floatplane.seekAndDestroy.length > 0) {
const guid = settings.floatplane.seekAndDestroy.pop();
if (guid === undefined) continue;
console.log(chalk`Seek and Destroy: {red ${guid}}`);
posts.push(fApi.content.post(guid));
yield fApi.content.post(guid);
}

const newVideos: Video[] = [];
for await (const subscription of fetchSubscriptions()) {
await subscription.deleteOldVideos();
for await (const video of subscription.fetchNewVideos()) newVideos.push(video);
for await (const video of subscription.seekAndDestroy(await Promise.all(posts))) newVideos.push(video);
}

// If we havent found any new videos, then reset polling size to 5 to avoid excessive api requests.
if (newVideos.length === 0) settings.floatplane.videosToSearch = defaultSettings.floatplane.videosToSearch;

return newVideos;
}

const queueVideo = VideoDownloader.queueVideo.bind(VideoDownloader);

/**
* Main function that triggeres everything else in the script
*/
const downloadNewVideos = async () => {
let subVideos = await fetchSubscriptionVideos();
if (settings.extras.promptVideos) {
if (args.headless) {
console.log("Cannot prompt for videos in headless mode! Disabling promptVideos...");
settings.extras.promptVideos = false;
} else {
subVideos = await promptVideos(subVideos);
const userSubs = fetchSubscriptions();

for await (const contentPost of seekAndDestroy()) {
for await (const subscription of userSubs) {
if (contentPost.creator.id === subscription.creatorId) {
for await (const video of subscription.seekAndDestroy(contentPost)) video.download();
}
}
}
return Promise.all(subVideos.map(queueVideo)).then(() => {
// Enforce search limits after searching once.
settings.floatplane.videosToSearch = defaultSettings.floatplane.videosToSearch;
});

for await (const subscription of userSubs) {
await subscription.deleteOldVideos();
for await (const video of subscription.fetchNewVideos()) video.download();
}

// Enforce search limits after searching once.
settings.floatplane.videosToSearch = defaultSettings.floatplane.videosToSearch;

if (settings.floatplane.waitForNewVideos === true) {
console.log(`Checking for new videos in 5 minutes...`);
setTimeout(downloadNewVideos, 5 * 60 * 1000);
}
};

// Fix for docker
Expand Down Expand Up @@ -115,13 +104,4 @@ process.on("SIGTERM", process.exit);
console.log(chalk`Initalized! Running version {cyan ${DownloaderVersion}} instance {magenta ${user!.id}}`);

await downloadNewVideos();

if (settings.floatplane.waitForNewVideos === true) {
const waitLoop = async () => {
await downloadNewVideos();
setTimeout(waitLoop, 5 * 60 * 1000);
console.log(`Checking for new videos in 5 minutes...`);
};
waitLoop();
}
})();
124 changes: 124 additions & 0 deletions src/lib/Attachment.ts
@@ -0,0 +1,124 @@
import db from "@inrixia/db";
import { nPad } from "@inrixia/helpers/math";
import { ValueOfA } from "@inrixia/helpers/ts";
import { settings } from "./helpers/index.js";
import sanitize from "sanitize-filename";

import { dirname, basename, extname } from "path";

import { rename, readdir } from "fs/promises";

type AttachmentInfo = {
partialBytes?: number;
muxedBytes?: number;
filePath: string;
releaseDate: number;
videoTitle: string;
};

type AttachmentAttributes = {
attachmentId: string;
videoTitle: string;
channelTitle: string;
releaseDate: Date;
};

enum Extensions {
Muxed = ".mp4",
Partial = ".partial",
NFO = ".nfo",
Thumbnail = ".png",
}

export class Attachment implements AttachmentAttributes {
private static readonly AttachmentsDB: Record<string, AttachmentInfo> = db<Record<string, AttachmentInfo>>(`./db/attachments.json`);
public static readonly Extensions = Extensions;

public readonly attachmentId: string;
public readonly channelTitle: string;
public readonly videoTitle: string;
public readonly releaseDate: Date;

public readonly filePath: string;
public readonly folderPath: string;

public readonly artworkPath: string;
public readonly nfoPath: string;
public readonly partialPath: string;
public readonly muxedPath: string;

constructor({ attachmentId, channelTitle, videoTitle, releaseDate }: AttachmentAttributes) {
this.attachmentId = attachmentId;
this.channelTitle = channelTitle;
this.releaseDate = releaseDate;
this.videoTitle = videoTitle;

this.filePath = this.formatFilePath(settings.filePathFormatting)
.split("/")
.map((pathPart) => (pathPart.startsWith(".") ? pathPart : sanitize(pathPart)))
.join("/");

// Ensure filePath is not exceeding maximum length
if (this.filePath.length > 250) this.filePath = this.filePath.substring(0, 250);

this.folderPath = this.filePath.substring(0, this.filePath.lastIndexOf("/"));

this.artworkPath = `${this.filePath}${settings.artworkSuffix}`;
this.nfoPath = `${this.filePath}${Extensions.NFO}`;
this.partialPath = `${this.filePath}${Extensions.Partial}`;
this.muxedPath = `${this.filePath}${Extensions.Muxed}`;

const attachmentInfo = (Attachment.AttachmentsDB[this.attachmentId] ??= {
releaseDate: this.releaseDate.getTime(),
filePath: this.filePath,
videoTitle: this.videoTitle,
});
// If the attachment existed on another path then move it.
if (attachmentInfo.filePath !== this.filePath) {
rename(this.artworkPath.replace(this.filePath, attachmentInfo.filePath), this.artworkPath).catch(() => null);
rename(this.partialPath.replace(this.filePath, attachmentInfo.filePath), this.partialPath).catch(() => null);
rename(this.muxedPath.replace(this.filePath, attachmentInfo.filePath), this.muxedPath).catch(() => null);
rename(this.nfoPath.replace(this.filePath, attachmentInfo.filePath), this.nfoPath).catch(() => null);
attachmentInfo.filePath = this.filePath;
}
if (attachmentInfo.videoTitle !== this.videoTitle) attachmentInfo.videoTitle = this.videoTitle;
}

public static find(filter: (video: AttachmentInfo) => boolean) {
return Object.values(this.AttachmentsDB).filter(filter);
}
public attachmentInfo(): AttachmentInfo {
return Attachment.AttachmentsDB[this.attachmentId];
}

public static FilePathOptions = ["%channelTitle%", "%year%", "%month%", "%day%", "%hour%", "%minute%", "%second%", "%videoTitle%"] as const;
protected formatFilePath(string: string): string {
const formatLookup: Record<ValueOfA<typeof Attachment.FilePathOptions>, string> = {
"%channelTitle%": this.channelTitle,
"%year%": this.releaseDate.getFullYear().toString(),
"%month%": nPad(this.releaseDate.getMonth() + 1),
"%day%": nPad(this.releaseDate.getDate()),
"%hour%": nPad(this.releaseDate.getHours()),
"%minute%": nPad(this.releaseDate.getMinutes()),
"%second%": nPad(this.releaseDate.getSeconds()),
"%videoTitle%": this.videoTitle.replace(/ - /g, " ").replace(/\//g, " ").replace(/\\/g, " "),
};

for (const [match, value] of Object.entries(formatLookup)) {
string = string.replace(new RegExp(match, "g"), value);
}
return string;
}

public async artworkFileExtension() {
const fileDir = dirname(this.artworkPath);
const fileName = basename(this.artworkPath);

const filesInDir = await readdir(fileDir);
const matchingFile = filesInDir.find(
(file) => file.startsWith(fileName) && !file.endsWith(Extensions.NFO) && !file.endsWith(Extensions.Partial) && !file.endsWith(Extensions.Muxed),
);
if (matchingFile) return extname(matchingFile);
return undefined;
}
}

0 comments on commit b27cb64

Please sign in to comment.