Skip to content
Merged
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
38 changes: 11 additions & 27 deletions ffmpeg4discord/__main__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,4 @@
"""
This module provides the main entry point for the ffmpeg4discord application.

It supports both command-line and web-based interfaces for performing two-pass
video encoding using FFmpeg. The script allows users to specify various encoding
parameters and target file sizes, and it will iteratively adjust the encoding
settings to meet the desired output file size.

Functions:
- twopass_loop: Executes the two-pass encoding process in a loop until the target file size is achieved.
- open_browser: Opens the default web browser to the specified port.
- format_commands: Lists the ffmpeg commands for an encode with a short explanation.
- success_message: The message shown when an encode finishes.
- source_facts: Facts about the input video that the web page checks the form against.
- form_values: The values the web UI's form shows.
- parse_encode_form: Checks the web UI's form and turns it into EncodeSettings.
- main: The main function that parses command-line arguments and starts the encoding process or web server.
"""
"""The ff4d command: runs an encode from the command line, or starts the web UI."""

import atexit
import logging
Expand All @@ -34,7 +17,7 @@
from flask import Flask, redirect, render_template, request, send_file, url_for

from ffmpeg4discord import arguments
from ffmpeg4discord.twopass import EncodeResult, EncodeSettings, TwoPass, available_codecs
from ffmpeg4discord.twopass import EncodeResult, EncodeSettings, TwoPass, available_codecs, red
from ffmpeg4discord.versioning import check_for_update

# smallest fraction the target size is cut by on each retry
Expand Down Expand Up @@ -73,14 +56,12 @@ def twopass_loop(
break

print(
"\033[31m"
+ dedent(
red(
f"""
The output file size ({round(result.output_filesize, 2)}MB) is still above the target of {target_filesize}MB.
Restarting...
"""
).strip()
+ "\033[0m"
)
)
Path(result.output_filename).unlink()

Expand Down Expand Up @@ -412,7 +393,7 @@ def parse_encode_form(form, twopass: TwoPass, codecs: list[str]) -> tuple[Encode

@contextmanager
def exit_on_setting_error():
"""TwoPass raises ValueError for settings that can't work. Log its message, which says what to change, and exit."""
"""TwoPass and get_args raise ValueError for bad settings. Log the message, which says what to change, and exit."""
try:
yield
except ValueError as exc:
Expand All @@ -424,8 +405,10 @@ def main() -> None:
"""
The main function that parses command-line arguments and starts the encoding process or web server.
"""
# Best-effort version check (never blocks startup)
version_info = check_for_update(timeout_s=1.5)
logging.getLogger().setLevel(logging.INFO)

# If PyPI is slow or unreachable, this gives up after a short timeout and skips the update notice
version_info = check_for_update()

# CLI: show version + update notice early (for both CLI mode and web-launch mode)
if version_info.current_version:
Expand All @@ -446,7 +429,8 @@ def main() -> None:
)

# get args from the command line
args = arguments.get_args()
with exit_on_setting_error():
args = arguments.get_args()
web = args.pop("web")
approx = args.pop("approx")
dry_run = args.pop("dry_run")
Expand Down
88 changes: 18 additions & 70 deletions ffmpeg4discord/arguments.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
"""
This module provides functions for parsing command-line arguments and
loading configuration settings for the ffmpeg4discord application.

Functions:
- is_port_in_use: Checks if a given port is currently in use.
- load_config: Loads configuration settings from a JSON file.
- get_args: Parses command-line arguments and returns them as a dictionary.
"""
"""Reads ff4d's command-line flags and config files."""

import json
import logging
Expand All @@ -15,11 +7,10 @@
from argparse import ArgumentParser, BooleanOptionalAction
from pathlib import Path
from random import randint
from textwrap import dedent

import platformdirs

from ffmpeg4discord.twopass import available_codecs
from ffmpeg4discord.twopass import FALLBACK_AUDIO_BR, available_codecs


def is_port_in_use(port: int) -> bool:
Expand Down Expand Up @@ -99,7 +90,9 @@ def build_parser() -> ArgumentParser:
type=float,
help="The output file size in MB.",
)
parser.add_argument("-a", "--audio-br", type=float, default=96, help="Audio bitrate in kbps.")
parser.add_argument(
"-a", "--audio-br", type=float, default=FALLBACK_AUDIO_BR // 1000, help="Audio bitrate in kbps."
)
parser.add_argument(
"-c",
"--codec",
Expand Down Expand Up @@ -167,15 +160,6 @@ def build_parser() -> ArgumentParser:
return parser


def _normalize_amix_args(args: dict) -> dict:
"""Make --amix-normalize imply --amix, unless amix was explicitly set false."""
# If user/config turned on normalize but never explicitly disabled amix,
# treat that as "we are mixing, with normalization".
if args.get("amix_normalize") and not args.get("amix"):
args["amix"] = True
return args


def _search_for_default_config(args: dict) -> dict:
"""
Search for config files in platform-specific default locations and include them
Expand Down Expand Up @@ -249,58 +233,20 @@ def _extract_times(args: dict) -> dict:


def _parse_astreams(args: dict) -> dict:
"""Parse --astreams from comma-separated string (or list) into a list[int] or None."""

"""Turn --astreams, e.g. "0,2", into a list of track numbers. A config file can also give a list, e.g. [0, 2]."""
raw = args.get("astreams")
if raw is None:
if raw is None or raw == "":
args["astreams"] = None # use every audio track
return args

# Allow config/json to provide a list already.
if isinstance(raw, list):
try:
parsed = [int(x) for x in raw]
except (TypeError, ValueError):
logging.error("Invalid astreams list. Expected list of ints.")
args["astreams"] = None
return args
else:
raw_str = str(raw).strip()
if raw_str == "":
args["astreams"] = None
return args

parts = [p.strip() for p in raw_str.split(",") if p.strip() != ""]
try:
parsed = [int(p) for p in parts]
except ValueError:
logging.error(
"\033[31m"
+ dedent(
"""
Invalid --astreams format.
parts = [str(part).strip() for part in (raw if isinstance(raw, list) else str(raw).split(","))]
# isdecimal() is False for negative numbers, decimals, and blanks
if not all(part.isdecimal() for part in parts):
raise ValueError(
f"Invalid --astreams value {raw!r}. Use audio track numbers starting at 0, separated by commas, e.g. 0,2."
)

Provide a comma-separated list of integers, e.g. --astreams "0,1,2".
Ignoring --astreams.
"""
).strip()
+ "\033[0m"
)
args["astreams"] = None
return args

# Normalize: remove duplicates, preserve order.
seen: set[int] = set()
normalized: list[int] = []
for idx in parsed:
if idx < 0:
logging.warning(f"Ignoring negative audio stream index in --astreams: {idx}")
continue
if idx in seen:
continue
seen.add(idx)
normalized.append(idx)

args["astreams"] = normalized
args["astreams"] = list(dict.fromkeys(int(part) for part in parts)) # removes duplicates, keeps the order
return args


Expand All @@ -318,5 +264,7 @@ def get_args() -> dict:
args = _assign_port(args)
args = _extract_times(args)
args = _parse_astreams(args)
args = _normalize_amix_args(args)
# Normalizing only applies to mixed audio, so --amix-normalize turns on --amix
if args.get("amix_normalize"):
args["amix"] = True
return args
Loading
Loading