feat: base functionality
This commit is contained in:
@@ -1,8 +1,13 @@
|
|||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --watch main.ts"
|
"dev": "deno run --watch main.ts",
|
||||||
|
"run": "deno run --allow-read src/main.ts",
|
||||||
|
"build": "deno compile --allow-read --allow-write --allow-run --allow-env --allow-net --output ./videnc ./src/main.ts"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@std/assert": "jsr:@std/assert@1"
|
"@std/assert": "jsr:@std/assert@1",
|
||||||
|
"@std/cli": "jsr:@std/cli@^1.0.21",
|
||||||
|
"@std/fs": "jsr:@std/fs@^1.0.19",
|
||||||
|
"@std/path": "jsr:@std/path@^1.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"specifiers": {
|
||||||
|
"jsr:@std/assert@1": "1.0.14",
|
||||||
|
"jsr:@std/cli@^1.0.21": "1.0.21",
|
||||||
|
"jsr:@std/fs@^1.0.19": "1.0.19",
|
||||||
|
"jsr:@std/internal@^1.0.10": "1.0.10",
|
||||||
|
"jsr:@std/internal@^1.0.9": "1.0.10",
|
||||||
|
"jsr:@std/path@^1.1.1": "1.1.2",
|
||||||
|
"jsr:@std/path@^1.1.2": "1.1.2"
|
||||||
|
},
|
||||||
|
"jsr": {
|
||||||
|
"@std/[email protected]": {
|
||||||
|
"integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/internal@^1.0.10"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@std/[email protected]": {
|
||||||
|
"integrity": "cd25b050bdf6282e321854e3822bee624f07aca7636a3a76d95f77a3a919ca2a"
|
||||||
|
},
|
||||||
|
"@std/[email protected]": {
|
||||||
|
"integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/internal@^1.0.9",
|
||||||
|
"jsr:@std/path@^1.1.1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@std/[email protected]": {
|
||||||
|
"integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7"
|
||||||
|
},
|
||||||
|
"@std/[email protected]": {
|
||||||
|
"integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/internal@^1.0.10"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/assert@1",
|
||||||
|
"jsr:@std/cli@^1.0.21",
|
||||||
|
"jsr:@std/fs@^1.0.19",
|
||||||
|
"jsr:@std/path@^1.1.2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { parsedVideoInfo } from "./types.ts";
|
||||||
|
import { basename, dirname } from "@std/path";
|
||||||
|
|
||||||
|
function escapeFilename(filename: string): string {
|
||||||
|
return filename.replace(/[^a-zA-Z0-9]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encodingCommandBuilder(videoInfo: parsedVideoInfo): string {
|
||||||
|
let videoName: string | null = null;
|
||||||
|
const videoPath = dirname(videoInfo.filename);
|
||||||
|
if (videoInfo.metadata) {
|
||||||
|
videoName = videoInfo.metadata.title;
|
||||||
|
}
|
||||||
|
const args = [
|
||||||
|
"ffmpeg \\\n",
|
||||||
|
"-v",
|
||||||
|
"quiet \\\n",
|
||||||
|
"-hide_banner \\\n",
|
||||||
|
"-i",
|
||||||
|
`"${videoInfo.filename}" \\\n`,
|
||||||
|
"-map_metadata",
|
||||||
|
"-1 \\\n",
|
||||||
|
"-map",
|
||||||
|
"0:v \\\n",
|
||||||
|
"-c:v",
|
||||||
|
"libsvtav1 \\\n",
|
||||||
|
"-pix_fmt",
|
||||||
|
"yuv420p10le \\\n",
|
||||||
|
];
|
||||||
|
if (videoInfo.MPix < 1) {
|
||||||
|
args.push("-crf", "29 \\\n");
|
||||||
|
} else {
|
||||||
|
args.push("-crf", "25 \\\n");
|
||||||
|
}
|
||||||
|
args.push(
|
||||||
|
"-svtav1-params",
|
||||||
|
"tune=0:film-grain=5:scd=1:film-grain-denoise=1 \\\n",
|
||||||
|
);
|
||||||
|
args.push("-preset", "2 \\\n");
|
||||||
|
|
||||||
|
if (videoInfo.isAnamorphic || videoInfo.isInterlaced) {
|
||||||
|
const filter_string: string[] = [];
|
||||||
|
if (videoInfo.isInterlaced) {
|
||||||
|
filter_string.push("bwdif=mode=0");
|
||||||
|
}
|
||||||
|
if (videoInfo.isAnamorphic) {
|
||||||
|
filter_string.push(
|
||||||
|
"scale=iw*sar:ih:lanczos,setsar=1,scale=-2:ih:lanczos",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
args.push(
|
||||||
|
"-vf",
|
||||||
|
`${filter_string.join(",")} \\\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push("-g", `${Math.round(videoInfo.fps * 5)} \\\n`);
|
||||||
|
|
||||||
|
if (videoInfo.metadata) {
|
||||||
|
args.push("-metadata", `title="${videoInfo.metadata.title}" \\\n`);
|
||||||
|
args.push(
|
||||||
|
"-metadata",
|
||||||
|
`date_released="${videoInfo.metadata.date_released}" \\\n`,
|
||||||
|
);
|
||||||
|
args.push("-metadata", `imdb_id="${videoInfo.metadata.imdb_id}" \\\n`);
|
||||||
|
args.push(
|
||||||
|
"-metadata",
|
||||||
|
`original_media_type="${videoInfo.metadata.original_media_type}" \\\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, audioStream] of videoInfo.audioStreams?.entries() ?? []) {
|
||||||
|
args.push(
|
||||||
|
"-map",
|
||||||
|
`0:a:${index}`,
|
||||||
|
);
|
||||||
|
args.push(
|
||||||
|
`-c:a:${index}`,
|
||||||
|
"libopus",
|
||||||
|
);
|
||||||
|
if (audioStream.isSideloaded) {
|
||||||
|
args.push(
|
||||||
|
`-channel_layout:a:${index}`,
|
||||||
|
`${audioStream.channel_layout}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
args.push(
|
||||||
|
`-metadata:s:a:${index}`,
|
||||||
|
`language=${audioStream.language} \\\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
const [index, subtitleStream] of videoInfo.subtitleStreams?.entries() ?? []
|
||||||
|
) {
|
||||||
|
args.push(
|
||||||
|
"-map",
|
||||||
|
`0:s:${index}`,
|
||||||
|
);
|
||||||
|
args.push(
|
||||||
|
`-c:s:${index}`,
|
||||||
|
"copy",
|
||||||
|
);
|
||||||
|
args.push(
|
||||||
|
`-metadata:s:s:${index}`,
|
||||||
|
`language=${subtitleStream.language}`,
|
||||||
|
);
|
||||||
|
if (subtitleStream.language === "fin") {
|
||||||
|
args.push(
|
||||||
|
`-disposition:s:${index}`,
|
||||||
|
`default \\\n`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
args.push(
|
||||||
|
`-disposition:s:${index}`,
|
||||||
|
`-1 \\\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoName) {
|
||||||
|
args.push(
|
||||||
|
`"${videoPath}/${
|
||||||
|
escapeFilename(videoName)
|
||||||
|
}.${videoInfo.metadata?.imdb_id}.mkv"`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
args.push(
|
||||||
|
`"${videoPath}/${
|
||||||
|
basename(videoInfo.filename.replace(".mkv", ""))
|
||||||
|
}.av1.mkv"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedArray = args.map((item) => {
|
||||||
|
// Only trim if it doesn't end with newline
|
||||||
|
if (item.endsWith("\\\n")) {
|
||||||
|
return item; // Keep as is
|
||||||
|
}
|
||||||
|
return item.trim();
|
||||||
|
});
|
||||||
|
|
||||||
|
return trimmedArray.join(" ");
|
||||||
|
}
|
||||||
+107
-4
@@ -1,8 +1,111 @@
|
|||||||
export function add(a: number, b: number): number {
|
import { parseArgs } from "@std/cli";
|
||||||
return a + b;
|
import { exists } from "@std/fs";
|
||||||
|
import { resolve } from "@std/path";
|
||||||
|
import {
|
||||||
|
OMDBMovieInfo,
|
||||||
|
OMDBSeriesInfo,
|
||||||
|
parsedVideoInfo,
|
||||||
|
VideoInfo,
|
||||||
|
} from "./types.ts";
|
||||||
|
import { VideoParser } from "./videoParser.ts";
|
||||||
|
import { encodingCommandBuilder } from "./commandBuilder.ts";
|
||||||
|
|
||||||
|
function analyzeVideo(videoFile: string): VideoInfo {
|
||||||
|
const cmd = "ffprobe";
|
||||||
|
const args = [
|
||||||
|
"-v",
|
||||||
|
"quiet",
|
||||||
|
"-print_format",
|
||||||
|
"json",
|
||||||
|
"-show_format",
|
||||||
|
"-show_streams",
|
||||||
|
videoFile,
|
||||||
|
];
|
||||||
|
const process = new Deno.Command(cmd, { args });
|
||||||
|
const { stdout, stderr } = process.outputSync();
|
||||||
|
const videoInfo = JSON.parse(new TextDecoder().decode(stdout));
|
||||||
|
return videoInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts
|
function buildVideoInfo(
|
||||||
|
videoInfo: VideoInfo,
|
||||||
|
movieInfo?: OMDBMovieInfo | OMDBSeriesInfo,
|
||||||
|
): parsedVideoInfo {
|
||||||
|
const parser = new VideoParser(videoInfo);
|
||||||
|
if (movieInfo) {
|
||||||
|
parser.metadata = {
|
||||||
|
title: movieInfo.Title,
|
||||||
|
date_released: movieInfo.Released,
|
||||||
|
imdb_id: movieInfo.imdbID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return parser.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMovieInfo(
|
||||||
|
imdbID: string,
|
||||||
|
): Promise<OMDBMovieInfo | OMDBSeriesInfo> {
|
||||||
|
const omdbAPIKey = Deno.env.get("OMDB_API_KEY");
|
||||||
|
const response = await fetch(
|
||||||
|
`https://www.omdbapi.com/?i=${imdbID}&apikey=${omdbAPIKey}`,
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const movieInfo = await response.json();
|
||||||
|
if (movieInfo.Response === "False") {
|
||||||
|
throw new Error(movieInfo.Error);
|
||||||
|
}
|
||||||
|
return movieInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeShellScript(command: string) {
|
||||||
|
if (await exists("./encode.sh")) {
|
||||||
|
let counter = 1;
|
||||||
|
while (true) {
|
||||||
|
if (await exists(`./encode_${counter}.sh`)) {
|
||||||
|
counter++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Deno.writeTextFileSync(`./encode_${counter}.sh`, command);
|
||||||
|
console.log(`Created ./encode_${counter}.sh`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const script = `#!/bin/bash\n${command}`;
|
||||||
|
Deno.writeTextFileSync("./encode.sh", script);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// Parse arguments
|
||||||
|
const args = parseArgs(Deno.args);
|
||||||
|
if (!args.i) {
|
||||||
|
console.log("Usage: deno run main.ts -i <video_file>");
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
// Check if file exists
|
||||||
|
const videoFile: string = String(args.i);
|
||||||
|
if (!await exists(videoFile)) {
|
||||||
|
console.log("File does not exist");
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
// Analyze video
|
||||||
|
const videoInfo = analyzeVideo(videoFile);
|
||||||
|
videoInfo.format.filename = resolve(videoFile);
|
||||||
|
//const imdbID = "tt0108333";
|
||||||
|
const imdbID = prompt("Enter IMDb ID. Leave empty to skip");
|
||||||
|
let jsonVideoInfo;
|
||||||
|
if (imdbID) {
|
||||||
|
const movieInfo = await fetchMovieInfo(imdbID);
|
||||||
|
jsonVideoInfo = buildVideoInfo(videoInfo, movieInfo);
|
||||||
|
} else {
|
||||||
|
jsonVideoInfo = buildVideoInfo(videoInfo);
|
||||||
|
}
|
||||||
|
const command = encodingCommandBuilder(jsonVideoInfo);
|
||||||
|
await writeShellScript(command);
|
||||||
|
}
|
||||||
|
|
||||||
if (import.meta.main) {
|
if (import.meta.main) {
|
||||||
console.log("Add 2 + 3 =", add(2, 3));
|
main();
|
||||||
}
|
}
|
||||||
|
|||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
export interface Disposition {
|
||||||
|
default: boolean;
|
||||||
|
dub: boolean;
|
||||||
|
original: boolean;
|
||||||
|
comment: boolean;
|
||||||
|
lyrics: boolean;
|
||||||
|
karaoke: boolean;
|
||||||
|
forced: boolean;
|
||||||
|
hearing_impaired: boolean;
|
||||||
|
visual_impaired: boolean;
|
||||||
|
clean_effects: boolean;
|
||||||
|
attached_pic: boolean;
|
||||||
|
timed_thumbnails: boolean;
|
||||||
|
non_diegetic: boolean;
|
||||||
|
captions: boolean;
|
||||||
|
descriptions: boolean;
|
||||||
|
metadata: boolean;
|
||||||
|
dependent: boolean;
|
||||||
|
still_image: boolean;
|
||||||
|
multilayer: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoTrack {
|
||||||
|
index: number;
|
||||||
|
codec_name: string;
|
||||||
|
codec_long_name: string;
|
||||||
|
profile: string;
|
||||||
|
codec_type: string;
|
||||||
|
codec_tag_string: string;
|
||||||
|
codec_tag: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
coded_width: number;
|
||||||
|
coded_height: number;
|
||||||
|
closed_captions: number;
|
||||||
|
film_grain: number;
|
||||||
|
has_b_frames: number;
|
||||||
|
sample_aspect_ratio: string;
|
||||||
|
display_aspect_ratio: string;
|
||||||
|
pix_fmt: string;
|
||||||
|
level: number;
|
||||||
|
color_range: string;
|
||||||
|
chroma_location: string;
|
||||||
|
field_order: string;
|
||||||
|
refs: number;
|
||||||
|
r_frame_rate: string;
|
||||||
|
avg_frame_rate: string;
|
||||||
|
time_base: string;
|
||||||
|
start_pts: number;
|
||||||
|
start_time: string;
|
||||||
|
extradata_size: number;
|
||||||
|
disposition: Disposition;
|
||||||
|
tags: {
|
||||||
|
language: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioTrack {
|
||||||
|
index: number;
|
||||||
|
codec_name: string;
|
||||||
|
codec_long_name: string;
|
||||||
|
profile: string;
|
||||||
|
codec_type: string;
|
||||||
|
codec_tag_string: string;
|
||||||
|
codec_tag: string;
|
||||||
|
channels: number;
|
||||||
|
channel_layout: string;
|
||||||
|
sample_rate: number;
|
||||||
|
bits_per_sample: number;
|
||||||
|
bits_per_raw_sample: number;
|
||||||
|
block_align: number;
|
||||||
|
frame_size: number;
|
||||||
|
initial_padding: number;
|
||||||
|
final_padding: number;
|
||||||
|
bit_rate: number;
|
||||||
|
max_bit_rate: number;
|
||||||
|
bits_per_frame: number;
|
||||||
|
id: number;
|
||||||
|
disposition: Disposition;
|
||||||
|
tags: {
|
||||||
|
language: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubtitlesTrack {
|
||||||
|
index: number;
|
||||||
|
codec_name: string;
|
||||||
|
codec_long_name: string;
|
||||||
|
profile: string;
|
||||||
|
codec_type: string;
|
||||||
|
codec_tag_string: string;
|
||||||
|
codec_tag: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
coded_width: number;
|
||||||
|
coded_height: number;
|
||||||
|
closed_captions: number;
|
||||||
|
film_grain: number;
|
||||||
|
has_b_frames: number;
|
||||||
|
sample_aspect_ratio: string;
|
||||||
|
display_aspect_ratio: string;
|
||||||
|
pix_fmt: string;
|
||||||
|
level: number;
|
||||||
|
color_range: string;
|
||||||
|
chroma_location: string;
|
||||||
|
field_order: string;
|
||||||
|
refs: number;
|
||||||
|
r_frame_rate: string;
|
||||||
|
avg_frame_rate: string;
|
||||||
|
time_base: string;
|
||||||
|
start_pts: number;
|
||||||
|
start_time: string;
|
||||||
|
extradata_size: number;
|
||||||
|
disposition: Disposition;
|
||||||
|
tags: {
|
||||||
|
language: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoInfo {
|
||||||
|
format: {
|
||||||
|
filename: string;
|
||||||
|
nb_streams: number;
|
||||||
|
nb_programs: number;
|
||||||
|
nb_stream_groups: number;
|
||||||
|
format_name: string;
|
||||||
|
format_long_name: string;
|
||||||
|
start_time: string;
|
||||||
|
duration: string;
|
||||||
|
size: string;
|
||||||
|
bit_rate: string;
|
||||||
|
probe_score: number;
|
||||||
|
tags: {
|
||||||
|
encoder: string;
|
||||||
|
creation_time: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
streams: (VideoTrack | AudioTrack | SubtitlesTrack)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OMDBMovieInfo {
|
||||||
|
Title: string;
|
||||||
|
Year: string;
|
||||||
|
Rated: string;
|
||||||
|
Released: string;
|
||||||
|
Runtime: string;
|
||||||
|
Genre: string;
|
||||||
|
Director: string;
|
||||||
|
Writer: string;
|
||||||
|
Actors: string;
|
||||||
|
Plot: string;
|
||||||
|
Language: string;
|
||||||
|
Country: string;
|
||||||
|
Awards: string;
|
||||||
|
Poster: string;
|
||||||
|
Ratings: { Source: string; Value: string }[];
|
||||||
|
Metascore: string;
|
||||||
|
imdbRating: string;
|
||||||
|
imdbVotes: string;
|
||||||
|
imdbID: string;
|
||||||
|
Type: string;
|
||||||
|
DVD: string;
|
||||||
|
BoxOffice: string;
|
||||||
|
Production: string;
|
||||||
|
Website: string;
|
||||||
|
Response: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OMDBSeriesInfo {
|
||||||
|
Title: string;
|
||||||
|
Year: string;
|
||||||
|
Rated: string;
|
||||||
|
Released: string;
|
||||||
|
Runtime: string;
|
||||||
|
Genre: string;
|
||||||
|
Director: string;
|
||||||
|
Writer: string;
|
||||||
|
Actors: string;
|
||||||
|
Plot: string;
|
||||||
|
Language: string;
|
||||||
|
Country: string;
|
||||||
|
Awards: string;
|
||||||
|
Poster: string;
|
||||||
|
Ratings: { Source: string; Value: string }[];
|
||||||
|
Metascore: string;
|
||||||
|
imdbRating: string;
|
||||||
|
imdbVotes: string;
|
||||||
|
imdbID: string;
|
||||||
|
Type: string;
|
||||||
|
totalSeasons: string;
|
||||||
|
Response: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface parsedAudioStream {
|
||||||
|
index: number;
|
||||||
|
language: string;
|
||||||
|
isSideloaded: boolean;
|
||||||
|
channels: number;
|
||||||
|
channel_layout: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface parsedSubtitleStream {
|
||||||
|
index: number;
|
||||||
|
language: string;
|
||||||
|
codec: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface parsedMetadata {
|
||||||
|
title: string;
|
||||||
|
date_released: string;
|
||||||
|
imdb_id: string;
|
||||||
|
original_media_type?: string;
|
||||||
|
season_number?: number;
|
||||||
|
episode_number?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface parsedVideoInfo {
|
||||||
|
filename: string;
|
||||||
|
metadata?: parsedMetadata;
|
||||||
|
MPix: number;
|
||||||
|
fps: number;
|
||||||
|
isAnamorphic: boolean;
|
||||||
|
isInterlaced: boolean;
|
||||||
|
audioStreams?: parsedAudioStream[];
|
||||||
|
subtitleStreams?: parsedSubtitleStream[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import {
|
||||||
|
AudioTrack,
|
||||||
|
parsedAudioStream,
|
||||||
|
parsedMetadata,
|
||||||
|
parsedSubtitleStream,
|
||||||
|
parsedVideoInfo,
|
||||||
|
SubtitlesTrack,
|
||||||
|
VideoInfo,
|
||||||
|
VideoTrack,
|
||||||
|
} from "./types.ts";
|
||||||
|
|
||||||
|
const months: Record<string, string> = {
|
||||||
|
"Jan": "01",
|
||||||
|
"Feb": "02",
|
||||||
|
"Mar": "03",
|
||||||
|
"Apr": "04",
|
||||||
|
"May": "05",
|
||||||
|
"Jun": "06",
|
||||||
|
"Jul": "07",
|
||||||
|
"Aug": "08",
|
||||||
|
"Sep": "09",
|
||||||
|
"Oct": "10",
|
||||||
|
"Nov": "11",
|
||||||
|
"Dec": "12",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class VideoParser {
|
||||||
|
public metadata?: parsedMetadata;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private videoInfo: VideoInfo,
|
||||||
|
metadata?: parsedMetadata,
|
||||||
|
) {
|
||||||
|
if (metadata) {
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseFps(fps: string): number {
|
||||||
|
const [numerator, denominator] = fps.split("/");
|
||||||
|
const result = Number(numerator) / Number(denominator);
|
||||||
|
return Math.round(result * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDate(date: string): string {
|
||||||
|
const [day, month, year] = date.split(" ");
|
||||||
|
const convertedDate = `${year}-${months[month]}-${day}`;
|
||||||
|
return convertedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseMPix(): number {
|
||||||
|
const videoStream = this.videoInfo.streams[0] as VideoTrack;
|
||||||
|
return videoStream.width * videoStream.height / 1000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
getFps() {
|
||||||
|
for (const stream of this.videoInfo.streams) {
|
||||||
|
if (stream.codec_type === "video") {
|
||||||
|
const videoStream = stream as VideoTrack;
|
||||||
|
return this.parseFps(videoStream.avg_frame_rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAudioSideloaded(index: number): boolean {
|
||||||
|
const audioStream = this.videoInfo.streams[index] as AudioTrack;
|
||||||
|
if (audioStream.channel_layout.endsWith("(side)")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAnamorphic(index: number = 0): boolean {
|
||||||
|
const videoStream = this.videoInfo.streams[index] as VideoTrack;
|
||||||
|
if (videoStream.sample_aspect_ratio !== "1:1") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInterlaced(index: number = 0): boolean {
|
||||||
|
const videoStream = this.videoInfo.streams[index] as VideoTrack;
|
||||||
|
if (videoStream.field_order !== "progressive") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(): parsedVideoInfo {
|
||||||
|
const fps = this.getFps();
|
||||||
|
const MPix = this.parseMPix();
|
||||||
|
const isAnamorphic = this.isAnamorphic();
|
||||||
|
const isInterlaced = this.isInterlaced();
|
||||||
|
const audioStreams: parsedAudioStream[] = this.videoInfo.streams.filter((
|
||||||
|
s,
|
||||||
|
) => s.codec_type === "audio").map((s) => {
|
||||||
|
const audioStream = s as AudioTrack;
|
||||||
|
return {
|
||||||
|
language: audioStream.tags.language,
|
||||||
|
isSideloaded: this.isAudioSideloaded(this.videoInfo.streams.indexOf(s)),
|
||||||
|
channels: audioStream.channels,
|
||||||
|
channel_layout: audioStream.channel_layout.replace("(side)", ""),
|
||||||
|
index: this.videoInfo.streams.indexOf(s),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const subtitleStreams: parsedSubtitleStream[] = this.videoInfo.streams
|
||||||
|
.filter((s) => s.codec_type === "subtitle").map((s) => {
|
||||||
|
const subtitleStream = s as SubtitlesTrack;
|
||||||
|
return {
|
||||||
|
language: subtitleStream.tags.language,
|
||||||
|
index: this.videoInfo.streams.indexOf(s),
|
||||||
|
codec: subtitleStream.codec_name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (this.metadata) {
|
||||||
|
this.metadata.date_released = this.parseDate(
|
||||||
|
this.metadata.date_released,
|
||||||
|
);
|
||||||
|
if (MPix < 1) {
|
||||||
|
this.metadata.original_media_type = "DVD";
|
||||||
|
} else {
|
||||||
|
this.metadata.original_media_type = "BluRay";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
filename: this.videoInfo.format.filename,
|
||||||
|
metadata: this.metadata,
|
||||||
|
fps,
|
||||||
|
MPix,
|
||||||
|
isAnamorphic,
|
||||||
|
isInterlaced,
|
||||||
|
audioStreams,
|
||||||
|
subtitleStreams,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user