Compare commits
13
Commits
9cd7fb204e
..
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a45407384e | ||
|
|
708c7dc4cd | ||
|
|
21383a391a | ||
|
|
97e40731b2 | ||
|
|
65777071f4 | ||
|
|
404b61aa00 | ||
|
|
cf4b7fa106 | ||
|
|
0c71d3c58b | ||
|
|
5ec816c163 | ||
|
|
c7a381c4b9 | ||
|
|
a30791a036 | ||
|
|
55cd3e7ec0 | ||
|
|
5d5371c991 |
@@ -0,0 +1,29 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [1.0.0] - 2025-04-17
|
||||
|
||||
### Added
|
||||
- Initial release of **videnc-reeffed** CLI for encoding videos.
|
||||
- Support for:
|
||||
- Specifying input video file and IMDb ID validation.
|
||||
- Removing subtitles with `--nosubtitles`.
|
||||
- Encoding presets (`--preset`) and CRF settings (`--crf`).
|
||||
- Selecting original media type (`--original-media-type`).
|
||||
- Adjusting film grain amount (`--film-grain`).
|
||||
- Implemented core functions:
|
||||
- `run_ffprobe` for extracting stream metadata.
|
||||
- `parse_streams` to model video, audio, and subtitle streams.
|
||||
- `apply_stream_settings` to apply encoding parameters.
|
||||
- `confirm_and_write_script` to generate and confirm overwrite of `encode.sh` script.
|
||||
- Integrated modules and helpers:
|
||||
- `models/streams` data models: `VideoFile`, `VideoStream`, `AudioStream`, `SubtitleStream`, `OriginalMediaType`.
|
||||
- `movie_details.get_movie_details` for enriching metadata.
|
||||
- `lib/fs.write_shell_script` for file system operations.
|
||||
|
||||
### Changed
|
||||
- Adopted `src` directory layout according to standard Python packaging.
|
||||
|
||||
### Fixed
|
||||
- None (initial release).
|
||||
@@ -1,39 +1,102 @@
|
||||
# VidEnc
|
||||
# VidEnc: Video Encoding with OMDB Metadata
|
||||
|
||||
This is a command-line application that analyzes video files using `ffprobe` to extract stream information (video, audio, subtitles). It then uses the OMDb API to fetch additional movie details such as title and release date, given an IMDB ID.
|
||||
VidEnc streamlines complex video transcoding workflows into a single CLI command by generating a portable FFmpeg-based shell script that:
|
||||
- Converts video streams to AV1 for maximum compression efficiency
|
||||
- Encodes audio tracks to Opus for reduced file size without sacrificing quality
|
||||
- Adjusts anamorphic sources to a correct 1:1 sample aspect ratio automatically
|
||||
- Retrieves and embeds movie title and release date from OMDB using your IMDb ID
|
||||
- Strips all other metadata to produce a clean, optimized output
|
||||
|
||||
## Workflow
|
||||
Designed for media archivists, home theater enthusiasts, and content creators, VidEnc offers customizable presets (CRF, film grain strength, speed), original media type tagging, optional subtitle handling, and shell completion support for a seamless CLI experience.
|
||||
|
||||
1. **Input:** The application takes a video file path as input, along with an optional IMDB ID and a flag to disable subtitle processing.
|
||||
---
|
||||
|
||||
2. **FFprobe Analysis:** It runs `ffprobe` on the input video file to extract stream information in JSON format.
|
||||
## 🚀 Features
|
||||
|
||||
3. **Stream Parsing:** The JSON output is parsed to identify video, audio, and subtitle streams.
|
||||
- Encode video streams to AV1 and audio streams to Opus
|
||||
- Convert anamorphic video to 1:1 pixel aspect ratio
|
||||
- Fetch and inject movie title and release date from OMDB
|
||||
- Strip all other metadata to produce a clean output
|
||||
- Configurable presets, CRF, film grain, media type, and subtitle handling
|
||||
- Automatic generation of a reusable `encode.sh` script
|
||||
- Shell completion support
|
||||
|
||||
4. **OMDb API Lookup (Optional):** If an IMDB ID is provided, the application queries the OMDb API to retrieve movie title and release date.
|
||||
## 🔮 Future Features
|
||||
|
||||
5. **Output:** Finally, it prints a command string based on the extracted information. This command string is likely intended for further video processing or encoding.
|
||||
- Every audio stream that is side loaded, must be converted to 5.1 channel layout with `channelmap=channel_layout=5.1`
|
||||
- Streamlit UI for interactive configuration and metadata preview.
|
||||
|
||||
## Usage
|
||||
## 🔧 Prerequisites
|
||||
|
||||
- **Python** >= 3.12
|
||||
- **FFmpeg** (including `ffprobe`) installed and available in your `PATH`
|
||||
- **OMDB API key**: Sign up at https://www.omdbapi.com/ and set `API_KEY` as an environment variable or in a `.env` file
|
||||
|
||||
## ⚙️ Installation
|
||||
|
||||
1. Clone the repository (or navigate to your project folder):
|
||||
```bash
|
||||
python src/app.py <video_file> [-i <imdb_id>] [-ns]
|
||||
git clone https://github.com/Kessinen/VidEnc.git
|
||||
cd VidEnc
|
||||
```
|
||||
2. Install Python dependencies using **uv**:
|
||||
```bash
|
||||
uv install
|
||||
```
|
||||
3. Create a `.env` file with your OMDB API key:
|
||||
```bash
|
||||
echo "API_KEY=your_omdb_api_key" > .env
|
||||
```
|
||||
|
||||
* `<video_file>`: Path to the video file.
|
||||
* `-i <imdb_id>`: (Optional) IMDB ID of the movie.
|
||||
* `-ns`: (Optional) Flag to disable subtitle processing.
|
||||
## 📝 Usage
|
||||
|
||||
|
||||
## Example
|
||||
### Generate the encoding script
|
||||
|
||||
```bash
|
||||
python src/app.py my_movie.mp4 -i tt1234567
|
||||
# via uv:
|
||||
uv run src/app.py path/to/video.mp4 --imdb tt1234567
|
||||
|
||||
# or directly:
|
||||
python src/app.py path/to/video.mp4 -i tt1234567
|
||||
```
|
||||
|
||||
This would analyze `my_movie.mp4`, fetch details from OMDb using IMDB ID `tt1234567`, and print a command string.
|
||||
This will probe the input, fetch metadata, and write an executable `encode.sh` next to your video.
|
||||
|
||||
## Unit Tests
|
||||
### Execute the encoding script
|
||||
|
||||
There are no unit tests included in this project.
|
||||
```bash
|
||||
bash encode.sh
|
||||
```
|
||||
|
||||
## ⚙️ CLI Options
|
||||
|
||||
| Option | Description | Default |
|
||||
|-------------------------------|----------------------------------------------------------------------|-------------|
|
||||
| -ns, --nosubtitles | Disable subtitle processing | false |
|
||||
| -p, --preset INT | Encoding preset (1–10 scale; higher = faster, lower quality) | 2 |
|
||||
| -c, --crf INT | Constant Rate Factor for video quality | 30 |
|
||||
| -o, --original-media-type | Source media type (DVD, BluRay, TVRip, WebRip, Other) | DVD |
|
||||
| -fg, --film-grain INT | Film grain strength | 20 |
|
||||
| --install-completion | Install shell completion for your current shell | — |
|
||||
| --show-completion | Display shell completion script for customization | — |
|
||||
|
||||
```bash
|
||||
# Install bash completion (example):
|
||||
python src/app.py --install-completion
|
||||
```
|
||||
|
||||
## 🔍 How It Works
|
||||
|
||||
1. **Probe** the input file with `ffprobe` to collect stream data.
|
||||
2. **Parse** video, audio, and subtitle streams into a structured model.
|
||||
3. **Fetch** title and release date from OMDB using the provided IMDb ID.
|
||||
4. **Configure** each video stream with presets, CRF, film grain, and media type.
|
||||
5. **Generate** a single FFmpeg command to re-encode streams, convert SAR, embed metadata, and strip all other metadata.
|
||||
6. **Write** the command into `encode.sh`, prompting before overwrite if it exists.
|
||||
|
||||
## 💡 Contributing
|
||||
|
||||
Contributions, issues, and feature requests are welcome! Feel free to open a PR or issue in the [GitHub repository](https://github.com/Kessinen/VidEnc).
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "videnc-reeffed"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
@@ -10,4 +10,5 @@ dependencies = [
|
||||
"python-dotenv>=1.0.1",
|
||||
"requests>=2.32.3",
|
||||
"rich>=13.9.4",
|
||||
"typer>=0.15.2",
|
||||
]
|
||||
|
||||
+73
-31
@@ -1,13 +1,15 @@
|
||||
import json
|
||||
import subprocess
|
||||
import click
|
||||
import typer
|
||||
from pathlib import Path
|
||||
# from rich import print
|
||||
import re
|
||||
import click
|
||||
|
||||
from models.streams import VideoFile, VideoStream, AudioStream, SubtitleStream
|
||||
from models.streams import VideoFile, VideoStream, AudioStream, SubtitleStream, OriginalMediaType
|
||||
from movie_details import get_movie_details
|
||||
from lib.fs import write_shell_script
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
def parse_streams(stream_data: dict, VideoFile: VideoFile) -> VideoFile:
|
||||
for stream in stream_data["streams"]:
|
||||
@@ -23,43 +25,83 @@ def parse_streams(stream_data: dict, VideoFile: VideoFile) -> VideoFile:
|
||||
return VideoFile
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("filename", type=click.Path(exists=True))
|
||||
@click.option("-i", "--imdb", type=str, help="IMDB ID")
|
||||
@click.option("-ns", "--nosubtitles", is_flag=True, help="No subtitles")
|
||||
@click.option("-p", "--preset", type=int, default=2, help="Encoding preset (default: 2)")
|
||||
@click.option("-c", "--crf", type=int, default=30, help="CRF value (default: 30)")
|
||||
@click.option('-o', '--original-media-type', default='DVD', help='Set the ORIGINAL_MEDIA_TYPE metadata')
|
||||
def main(filename: Path, imdb: str, nosubtitles: bool, preset: int, crf: int, original_media_type: str):
|
||||
"""Run ffprobe on a file and print the output in JSON format."""
|
||||
video_details = VideoFile(
|
||||
imdb=imdb, video_path=filename, allow_subtitle=not nosubtitles
|
||||
)
|
||||
video_details = get_movie_details(video_details)
|
||||
def validate_imdb(ctx: click.Context, param: click.Parameter, value: str) -> str:
|
||||
"""Validate that an IMDb ID matches 'tt' followed by 7-8 digits."""
|
||||
if value is None:
|
||||
return value
|
||||
if not re.match(r"^tt\d{7,8}$", value):
|
||||
raise click.BadParameter("IMDb ID must be 'tt' followed by 7-8 digits (e.g. tt1234567)")
|
||||
return value
|
||||
|
||||
|
||||
# Helper functions for SOLID refactoring
|
||||
def run_ffprobe(filename: Path) -> dict:
|
||||
"""Execute ffprobe and return parsed JSON data."""
|
||||
cmd = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"quiet",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_format",
|
||||
"-show_streams",
|
||||
"ffprobe", "-v", "quiet",
|
||||
"-print_format", "json",
|
||||
"-show_format", "-show_streams",
|
||||
filename,
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, check=True)
|
||||
data = parse_streams(json.loads(result.stdout), video_details)
|
||||
for stream in data.video_streams:
|
||||
except FileNotFoundError:
|
||||
typer.secho("Error: ffprobe not found. Please install FFmpeg.", fg="red", err=True)
|
||||
raise typer.Exit(code=1)
|
||||
except subprocess.CalledProcessError as e:
|
||||
typer.secho("Error: ffprobe failed to probe the file.", fg="red", err=True)
|
||||
error_output = e.stderr.decode() if e.stderr else str(e)
|
||||
typer.secho(error_output, fg="red", err=True)
|
||||
raise typer.Exit(code=1)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
def prepare_video_file(filename: Path, imdb: str, nosubtitles: bool) -> VideoFile:
|
||||
"""Initialize VideoFile with metadata."""
|
||||
return VideoFile(imdb=imdb, video_path=filename, allow_subtitle=not nosubtitles)
|
||||
|
||||
def apply_stream_settings(video_details: VideoFile, preset: int, crf: int, original_media_type: OriginalMediaType, film_grain: int) -> VideoFile:
|
||||
"""Apply encoding settings to video streams."""
|
||||
video_details.original_media_type = original_media_type
|
||||
for stream in video_details.video_streams:
|
||||
stream.preset = preset
|
||||
stream.crf = crf
|
||||
stream.original_media_type = original_media_type
|
||||
export_data = " ".join(data.cmd())
|
||||
export_path = Path(filename).parent / "encode.sh"
|
||||
stream.film_grain = film_grain
|
||||
return video_details
|
||||
|
||||
def confirm_and_write_script(export_data: str, export_path: Path):
|
||||
"""Confirm overwrite and write shell script."""
|
||||
if export_path.exists():
|
||||
if not typer.confirm(f"'{export_path}' already exists. Overwrite? "):
|
||||
typer.echo("Aborting.")
|
||||
raise typer.Exit()
|
||||
export_path.unlink()
|
||||
write_shell_script(export_data, export_path)
|
||||
print(Path(filename).parent)
|
||||
print(" ".join(data.cmd()))
|
||||
# json.dump(data, sys.stdout, indent=4)
|
||||
|
||||
@app.command()
|
||||
def main(
|
||||
filename: Path = typer.Argument(..., exists=True, help="Input video file"),
|
||||
imdb: str = typer.Option(None, "-i", "--imdb", callback=validate_imdb, help="IMDB ID"),
|
||||
nosubtitles: bool = typer.Option(False, "-ns", "--nosubtitles", help="Remove subtitles"),
|
||||
preset: int = typer.Option(2, "-p", "--preset", help="Encoding preset"),
|
||||
crf: int = typer.Option(30, "-c", "--crf", help="CRF value"),
|
||||
original_media_type: OriginalMediaType = typer.Option(OriginalMediaType.DVD, "-o", "--original-media-type", case_sensitive=False, help="Source of the original media."),
|
||||
film_grain: int = typer.Option(20, "-fg", "--film-grain", help="Film grain amount (default: 20)")
|
||||
):
|
||||
video_details = prepare_video_file(filename, imdb, nosubtitles)
|
||||
video_details = get_movie_details(video_details)
|
||||
|
||||
stream_data = run_ffprobe(filename)
|
||||
video_details = parse_streams(stream_data, video_details)
|
||||
video_details = apply_stream_settings(video_details, preset, crf, original_media_type, film_grain)
|
||||
|
||||
export_data = " ".join(video_details.cmd())
|
||||
export_path = filename.parent / "encode.sh"
|
||||
confirm_and_write_script(export_data, export_path)
|
||||
|
||||
typer.echo(str(filename.parent))
|
||||
typer.echo(export_data)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
app()
|
||||
|
||||
+64
-44
@@ -1,15 +1,22 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from functools import cached_property
|
||||
|
||||
from lib.fs import sanitize_filename
|
||||
|
||||
class OriginalMediaType(str, Enum):
|
||||
DVD = "DVD"
|
||||
BluRay = "BluRay"
|
||||
TVRip = "TVRip"
|
||||
WebRip = "WebRip"
|
||||
Other = "Other"
|
||||
|
||||
|
||||
class Tags(BaseModel):
|
||||
title: Optional[str] = Field(None)
|
||||
language: Optional[str] = Field(None)
|
||||
title: str | None = Field(None)
|
||||
language: str | None = Field(None)
|
||||
|
||||
|
||||
class Disposition(BaseModel):
|
||||
@@ -18,22 +25,31 @@ class Disposition(BaseModel):
|
||||
|
||||
|
||||
class VideoStream(BaseModel):
|
||||
index: int = Field(None)
|
||||
width: int = Field(None)
|
||||
height: int = Field(None)
|
||||
avg_frame_rate: str = Field(None, alias="avg_frame_rate")
|
||||
codec_name: str = Field(None)
|
||||
index: int | None = Field(None)
|
||||
width: int | None = Field(None)
|
||||
height: int | None = Field(None)
|
||||
avg_frame_rate: str | None = Field(None, alias="avg_frame_rate")
|
||||
codec_name: str | None = Field(None)
|
||||
disposition: Disposition
|
||||
tags: Tags | None = Field(None)
|
||||
preset: int = Field(2)
|
||||
crf: int = Field(30)
|
||||
original_media_type: str | None = Field(None)
|
||||
film_grain: int = Field(20)
|
||||
|
||||
@cached_property
|
||||
def fps(self) -> float:
|
||||
frame_numbers = [int(x) for x in self.avg_frame_rate.split("/")]
|
||||
return frame_numbers[0] / frame_numbers[1]
|
||||
"""Calculate frames per second from avg_frame_rate string."""
|
||||
try:
|
||||
if not self.avg_frame_rate:
|
||||
return 0.0
|
||||
num, denom = (int(x) for x in self.avg_frame_rate.split("/"))
|
||||
return num / denom if denom else 0.0
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
def cmd_flags(self) -> list[str]:
|
||||
"""Generate ffmpeg command flags for this video stream."""
|
||||
return [
|
||||
"-map",
|
||||
f"0:{self.index}",
|
||||
@@ -44,11 +60,11 @@ class VideoStream(BaseModel):
|
||||
"-preset",
|
||||
str(self.preset),
|
||||
"-svtav1-params",
|
||||
"tune=0:film-grain=20:scd=1",
|
||||
f"tune=0:film-grain={self.film_grain}:scd=1",
|
||||
"-vf",
|
||||
"'scale=iw*sar:ih,setsar=1,scale=-2:ih:lanczos'",
|
||||
"-g",
|
||||
f"{round(self.fps) * 5}",
|
||||
str(round(self.fps) * 5),
|
||||
]
|
||||
|
||||
|
||||
@@ -61,20 +77,24 @@ class AudioStream(BaseModel):
|
||||
tags: Tags | None = Field(None)
|
||||
|
||||
def cmd_flags(self, index: int) -> list[str]:
|
||||
"""Generate ffmpeg command flags for this audio stream."""
|
||||
side_loaded = []
|
||||
if "side" in self.channel_layout:
|
||||
side_loaded = [f"-filter:a:{index}", "'channelmap=channel_layout=5.1'"]
|
||||
default_disposition = (
|
||||
[f"-disposition:a:{index}", "default"] if self.disposition.default else []
|
||||
)
|
||||
if "side" in self.channel_layout:
|
||||
side_loaded = [f"-filter:a:{index}", "'channelmap=channel_layout=5.1'"]
|
||||
language = getattr(self.tags, 'language', None) if self.tags else None
|
||||
title = getattr(self.tags, 'title', None) if self.tags else None
|
||||
meta_flags = []
|
||||
if language:
|
||||
meta_flags += [f"-metadata:s:a:{index}", f"language={language}"]
|
||||
if title:
|
||||
meta_flags += [f"-metadata:s:a:{index}", f"title='{title}'"]
|
||||
return (
|
||||
["-map", f"0:{self.index}", f"-c:a:{index}", "libopus"]
|
||||
+ side_loaded
|
||||
+ [f"-metadata:s:a:{index}", f"language={self.tags.language}"]
|
||||
+ [
|
||||
f"-metadata:s:a:{index}",
|
||||
f"title='{self.tags.title}'",
|
||||
]
|
||||
+ meta_flags
|
||||
+ default_disposition
|
||||
)
|
||||
|
||||
@@ -86,23 +106,27 @@ class SubtitleStream(BaseModel):
|
||||
tags: Tags | None = Field(None)
|
||||
|
||||
def cmd_flags(self, index: int) -> list[str]:
|
||||
return ["-map", f"0:s:{index}", f"-c:s:{index}", "copy"] + [
|
||||
f"-metadata:s:s:{index}",
|
||||
f"language={self.tags.language}",
|
||||
]
|
||||
"""Generate ffmpeg command flags for this subtitle stream."""
|
||||
language = getattr(self.tags, 'language', None) if self.tags else None
|
||||
meta_flags = []
|
||||
if language:
|
||||
meta_flags += [f"-metadata:s:s:{index}", f"language={language}"]
|
||||
return ["-map", f"0:s:{index}", f"-c:s:{index}", "copy"] + meta_flags
|
||||
|
||||
|
||||
class VideoFile(BaseModel):
|
||||
video_path: Path
|
||||
title: str = Field(None)
|
||||
date_released: Optional[datetime] = Field(None)
|
||||
imdb: Optional[str] = Field(None)
|
||||
video_streams: list[VideoStream] = Field([])
|
||||
audio_streams: list[AudioStream] = Field([])
|
||||
subtitle_streams: list[SubtitleStream] = Field([])
|
||||
title: str | None = Field(None)
|
||||
date_released: datetime | None = Field(None)
|
||||
imdb: str | None = Field(None)
|
||||
video_streams: list[VideoStream] = Field(default_factory=list)
|
||||
audio_streams: list[AudioStream] = Field(default_factory=list)
|
||||
subtitle_streams: list[SubtitleStream] = Field(default_factory=list)
|
||||
allow_subtitle: bool = Field(True)
|
||||
original_media_type: OriginalMediaType = Field(OriginalMediaType.DVD)
|
||||
|
||||
def cmd(self) -> list[str]:
|
||||
"""Generate the full ffmpeg command for this video file."""
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-hide_banner",
|
||||
@@ -112,17 +136,15 @@ class VideoFile(BaseModel):
|
||||
f"'{str(self.video_path.absolute())}'",
|
||||
"-map_metadata",
|
||||
"-1",
|
||||
"-metadata",
|
||||
f"title='{self.title}'",
|
||||
"-metadata",
|
||||
f"date_released={self.date_released.strftime('%Y-%m-%d')}",
|
||||
"-metadata",
|
||||
f"imdb={self.imdb}",
|
||||
"-metadata",
|
||||
"original_media_type=DVD",
|
||||
"-pix_fmt",
|
||||
"yuv420p10le",
|
||||
]
|
||||
if self.title:
|
||||
cmd += ["-metadata", f"title='{self.title}'"]
|
||||
if self.date_released:
|
||||
cmd += ["-metadata", f"date_released={self.date_released.strftime('%Y-%m-%d')}"]
|
||||
if self.imdb:
|
||||
cmd += ["-metadata", f"imdb={self.imdb}"]
|
||||
cmd += ["-metadata", f"original_media_type={self.original_media_type.value}"]
|
||||
cmd += ["-pix_fmt", "yuv420p10le"]
|
||||
for stream in self.video_streams:
|
||||
cmd.extend(stream.cmd_flags())
|
||||
for i, stream in enumerate(self.audio_streams):
|
||||
@@ -130,12 +152,10 @@ class VideoFile(BaseModel):
|
||||
if self.allow_subtitle:
|
||||
for i, stream in enumerate(self.subtitle_streams):
|
||||
cmd.extend(stream.cmd_flags(i))
|
||||
|
||||
year = self.date_released.year if self.date_released else "unknown"
|
||||
output_file = (
|
||||
self.video_path.absolute().parent
|
||||
/ f"{sanitize_filename(self.title)}.{self.date_released.year}.mkv"
|
||||
/ f"{sanitize_filename(self.title or 'untitled')}.{year}.mkv"
|
||||
)
|
||||
|
||||
cmd.extend([f"'{str(output_file)}'"])
|
||||
|
||||
cmd.append(f"'{str(output_file)}'")
|
||||
return cmd
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.12"
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.13'",
|
||||
@@ -67,7 +68,7 @@ name = "click"
|
||||
version = "8.1.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
|
||||
wheels = [
|
||||
@@ -208,6 +209,30 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
@@ -236,6 +261,7 @@ dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "requests" },
|
||||
{ name = "rich" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -245,4 +271,5 @@ requires-dist = [
|
||||
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
||||
{ name = "requests", specifier = ">=2.32.3" },
|
||||
{ name = "rich", specifier = ">=13.9.4" },
|
||||
{ name = "typer", specifier = ">=0.15.2" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user