plex-generate-previews 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plex_generate_previews/__init__.py +10 -0
- plex_generate_previews/__main__.py +11 -0
- plex_generate_previews/cli.py +474 -0
- plex_generate_previews/config.py +479 -0
- plex_generate_previews/gpu_detection.py +541 -0
- plex_generate_previews/media_processing.py +439 -0
- plex_generate_previews/plex_client.py +211 -0
- plex_generate_previews/utils.py +135 -0
- plex_generate_previews/version_check.py +178 -0
- plex_generate_previews/worker.py +478 -0
- plex_generate_previews-2.0.0.dist-info/METADATA +728 -0
- plex_generate_previews-2.0.0.dist-info/RECORD +15 -0
- plex_generate_previews-2.0.0.dist-info/WHEEL +5 -0
- plex_generate_previews-2.0.0.dist-info/entry_points.txt +2 -0
- plex_generate_previews-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
"""
|
2
|
+
Utility functions for Plex Video Preview Generator.
|
3
|
+
|
4
|
+
Contains general-purpose utility functions that can be reused across
|
5
|
+
different modules in the application.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import shutil
|
10
|
+
import time
|
11
|
+
import uuid
|
12
|
+
|
13
|
+
|
14
|
+
def calculate_title_width():
|
15
|
+
"""
|
16
|
+
Calculate optimal title width based on terminal size.
|
17
|
+
|
18
|
+
Calculates the maximum number of characters that can be used for
|
19
|
+
displaying media titles in the progress bars, accounting for all
|
20
|
+
other UI elements.
|
21
|
+
|
22
|
+
Returns:
|
23
|
+
int: Maximum characters for title display (20-50 range)
|
24
|
+
"""
|
25
|
+
terminal_width = shutil.get_terminal_size().columns
|
26
|
+
|
27
|
+
worker_prefix = 7 # "GPU 0: " or "CPU 0: "
|
28
|
+
percentage = 6 # " 100% "
|
29
|
+
time_elapsed = 8 # " 00:00:00 "
|
30
|
+
count_display = 12 # " (1/10) "
|
31
|
+
speed_display = 8 # " 2.5x "
|
32
|
+
progress_bar = 20 # Approximate progress bar width
|
33
|
+
|
34
|
+
reserved_space = worker_prefix + percentage + time_elapsed + count_display + speed_display + progress_bar
|
35
|
+
available_width = terminal_width - reserved_space
|
36
|
+
|
37
|
+
# Set reasonable limits: minimum 20 chars, maximum 50 chars
|
38
|
+
return max(min(available_width, 50), 20)
|
39
|
+
|
40
|
+
|
41
|
+
def format_display_title(title: str, media_type: str, title_max_width: int) -> str:
|
42
|
+
"""
|
43
|
+
Format and truncate display title based on media type.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
title: The media title to format
|
47
|
+
media_type: 'episode' or 'movie'
|
48
|
+
title_max_width: Maximum width for the title
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
str: Formatted and truncated title
|
52
|
+
"""
|
53
|
+
if media_type == 'episode':
|
54
|
+
# For episodes, ensure S01E01 format is always visible
|
55
|
+
if len(title) > title_max_width:
|
56
|
+
# Simple truncation: keep last 6 chars (S01E01) + show title
|
57
|
+
season_episode = title[-6:] # Last 6 characters (S01E01)
|
58
|
+
available_space = title_max_width - 6 - 3 # 6 for S01E01, 3 for "..."
|
59
|
+
if available_space > 0:
|
60
|
+
show_title = title[:-6].strip() # Everything except last 6 chars
|
61
|
+
if len(show_title) > available_space:
|
62
|
+
show_title = show_title[:available_space]
|
63
|
+
display_title = f"{show_title}...{season_episode}"
|
64
|
+
else:
|
65
|
+
# Not enough space, just show the season/episode
|
66
|
+
display_title = f"...{season_episode}"
|
67
|
+
else:
|
68
|
+
display_title = title
|
69
|
+
else:
|
70
|
+
# For movies, use the title as-is
|
71
|
+
display_title = title
|
72
|
+
|
73
|
+
# Regular truncation for movies
|
74
|
+
if len(display_title) > title_max_width:
|
75
|
+
display_title = display_title[:title_max_width-3] + "..." # Leave room for "..."
|
76
|
+
|
77
|
+
# Add padding to prevent progress bar jumping (only if not already truncated)
|
78
|
+
if len(display_title) <= title_max_width:
|
79
|
+
padding_needed = title_max_width - len(display_title)
|
80
|
+
display_title = display_title + " " * padding_needed
|
81
|
+
|
82
|
+
return display_title
|
83
|
+
|
84
|
+
|
85
|
+
def is_docker_environment() -> bool:
|
86
|
+
"""Check if running inside a Docker container."""
|
87
|
+
return (
|
88
|
+
os.path.exists('/.dockerenv') or
|
89
|
+
os.environ.get('container') == 'docker' or
|
90
|
+
os.environ.get('DOCKER_CONTAINER') == 'true' or
|
91
|
+
'docker' in os.environ.get('HOSTNAME', '').lower()
|
92
|
+
)
|
93
|
+
|
94
|
+
|
95
|
+
def sanitize_path(path: str) -> str:
|
96
|
+
"""
|
97
|
+
Sanitize file path for cross-platform compatibility.
|
98
|
+
|
99
|
+
Converts forward slashes to backslashes on Windows systems to ensure
|
100
|
+
proper path handling across different operating systems.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
path: The file path to sanitize
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
str: Sanitized file path
|
107
|
+
"""
|
108
|
+
if os.name == 'nt':
|
109
|
+
path = path.replace('/', '\\')
|
110
|
+
return path
|
111
|
+
|
112
|
+
|
113
|
+
def setup_working_directory(tmp_folder: str) -> str:
|
114
|
+
"""
|
115
|
+
Create and set up a unique working temporary directory.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
tmp_folder: Base temporary folder path
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
str: Path to the created working directory
|
122
|
+
|
123
|
+
Raises:
|
124
|
+
OSError: If directory creation fails
|
125
|
+
"""
|
126
|
+
# Create a unique subfolder for this run to avoid conflicts
|
127
|
+
unique_id = f"plex_previews_{int(time.time())}_{str(uuid.uuid4())[:8]}"
|
128
|
+
working_tmp_folder = os.path.join(tmp_folder, unique_id)
|
129
|
+
|
130
|
+
# Create our specific working directory
|
131
|
+
os.makedirs(working_tmp_folder, exist_ok=True)
|
132
|
+
|
133
|
+
return working_tmp_folder
|
134
|
+
|
135
|
+
|
@@ -0,0 +1,178 @@
|
|
1
|
+
"""
|
2
|
+
Version check module for checking if a newer version is available.
|
3
|
+
|
4
|
+
Queries GitHub Releases API to compare current version with latest release.
|
5
|
+
Handles network failures gracefully without interrupting application startup.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import re
|
9
|
+
import requests
|
10
|
+
from typing import Optional, Tuple
|
11
|
+
from loguru import logger
|
12
|
+
from .utils import is_docker_environment
|
13
|
+
|
14
|
+
|
15
|
+
def get_current_version() -> str:
|
16
|
+
"""
|
17
|
+
Get the current version from package metadata.
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
str: Current version string (e.g., "2.0.0")
|
21
|
+
"""
|
22
|
+
try:
|
23
|
+
# Try to get version from package metadata first
|
24
|
+
import importlib.metadata
|
25
|
+
return importlib.metadata.version("plex-generate-previews")
|
26
|
+
except Exception:
|
27
|
+
# Fallback to reading from pyproject.toml using regex
|
28
|
+
try:
|
29
|
+
import os
|
30
|
+
|
31
|
+
# Get the directory containing this file
|
32
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
33
|
+
project_root = os.path.dirname(current_dir)
|
34
|
+
pyproject_path = os.path.join(project_root, "pyproject.toml")
|
35
|
+
|
36
|
+
with open(pyproject_path, 'r') as f:
|
37
|
+
content = f.read()
|
38
|
+
# Simple regex to find version = "X.Y.Z"
|
39
|
+
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
40
|
+
if match:
|
41
|
+
return match.group(1)
|
42
|
+
except Exception:
|
43
|
+
pass
|
44
|
+
|
45
|
+
# Final fallback - return a default version
|
46
|
+
logger.debug("Could not determine current version, using fallback")
|
47
|
+
return "0.0.0"
|
48
|
+
|
49
|
+
|
50
|
+
def parse_version(version_str: str) -> Tuple[int, int, int]:
|
51
|
+
"""
|
52
|
+
Parse a semantic version string into comparable tuple.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
version_str: Version string like "2.0.0" or "1.5.3"
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
Tuple of (major, minor, patch) integers
|
59
|
+
|
60
|
+
Raises:
|
61
|
+
ValueError: If version string format is invalid
|
62
|
+
"""
|
63
|
+
# Remove any 'v' prefix and extract version parts
|
64
|
+
clean_version = version_str.lstrip('v')
|
65
|
+
|
66
|
+
# Match semantic version pattern (major.minor.patch)
|
67
|
+
match = re.match(r'^(\d+)\.(\d+)\.(\d+)(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$', clean_version)
|
68
|
+
|
69
|
+
if not match:
|
70
|
+
raise ValueError(f"Invalid version format: {version_str}")
|
71
|
+
|
72
|
+
major, minor, patch = match.groups()
|
73
|
+
return (int(major), int(minor), int(patch))
|
74
|
+
|
75
|
+
|
76
|
+
def get_latest_github_release() -> Optional[str]:
|
77
|
+
"""
|
78
|
+
Query GitHub API for the latest release version.
|
79
|
+
|
80
|
+
Returns:
|
81
|
+
str: Latest release version string, or None if failed
|
82
|
+
"""
|
83
|
+
try:
|
84
|
+
# GitHub API endpoint for latest release
|
85
|
+
url = "https://api.github.com/repos/stevezau/plex_generate_vid_previews/releases/latest"
|
86
|
+
|
87
|
+
# Set timeout and user agent
|
88
|
+
headers = {
|
89
|
+
'User-Agent': 'plex-generate-previews-version-check'
|
90
|
+
}
|
91
|
+
|
92
|
+
response = requests.get(url, headers=headers, timeout=3)
|
93
|
+
response.raise_for_status()
|
94
|
+
|
95
|
+
data = response.json()
|
96
|
+
latest_version = data.get('tag_name', '')
|
97
|
+
|
98
|
+
if not latest_version:
|
99
|
+
logger.debug("GitHub API returned empty tag_name")
|
100
|
+
return None
|
101
|
+
|
102
|
+
return latest_version
|
103
|
+
|
104
|
+
except requests.exceptions.Timeout:
|
105
|
+
logger.debug("Version check timed out - no internet connection or slow response")
|
106
|
+
return None
|
107
|
+
except requests.exceptions.ConnectionError:
|
108
|
+
logger.debug("Version check failed - no internet connection")
|
109
|
+
return None
|
110
|
+
except requests.exceptions.HTTPError as e:
|
111
|
+
if e.response.status_code == 404:
|
112
|
+
logger.debug("Repository or releases not found on GitHub")
|
113
|
+
elif e.response.status_code == 429:
|
114
|
+
logger.debug("GitHub API rate limit exceeded")
|
115
|
+
else:
|
116
|
+
logger.debug(f"GitHub API error: {e.response.status_code}")
|
117
|
+
return None
|
118
|
+
except requests.exceptions.RequestException as e:
|
119
|
+
logger.debug(f"Version check request failed: {e}")
|
120
|
+
return None
|
121
|
+
except (KeyError, ValueError) as e:
|
122
|
+
logger.debug(f"Invalid response from GitHub API: {e}")
|
123
|
+
return None
|
124
|
+
except Exception as e:
|
125
|
+
logger.debug(f"Unexpected error during version check: {e}")
|
126
|
+
return None
|
127
|
+
|
128
|
+
|
129
|
+
def check_for_updates(skip_check: bool = False) -> None:
|
130
|
+
"""
|
131
|
+
Check for available updates and log appropriate messages.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
skip_check: If True, skip the version check entirely
|
135
|
+
"""
|
136
|
+
if skip_check:
|
137
|
+
logger.debug("Version check skipped by user request")
|
138
|
+
return
|
139
|
+
|
140
|
+
try:
|
141
|
+
# Get current version
|
142
|
+
current_version = get_current_version()
|
143
|
+
logger.debug(f"Current version: {current_version}")
|
144
|
+
|
145
|
+
# Get latest version from GitHub
|
146
|
+
latest_version = get_latest_github_release()
|
147
|
+
if not latest_version:
|
148
|
+
logger.debug("Could not determine latest version")
|
149
|
+
return
|
150
|
+
|
151
|
+
logger.debug(f"Latest version: {latest_version}")
|
152
|
+
|
153
|
+
# Parse versions for comparison
|
154
|
+
try:
|
155
|
+
current_tuple = parse_version(current_version)
|
156
|
+
latest_tuple = parse_version(latest_version)
|
157
|
+
except ValueError as e:
|
158
|
+
logger.debug(f"Version parsing error: {e}")
|
159
|
+
return
|
160
|
+
|
161
|
+
# Compare versions
|
162
|
+
if latest_tuple > current_tuple:
|
163
|
+
# Newer version available - show warning
|
164
|
+
logger.warning(f"⚠️ A newer version is available: {latest_version} (you have: {current_version})")
|
165
|
+
|
166
|
+
# Provide appropriate update instructions based on environment
|
167
|
+
if is_docker_environment():
|
168
|
+
logger.warning("🐳 Update: docker pull stevezzau/plex_generate_vid_previews:latest")
|
169
|
+
else:
|
170
|
+
logger.warning("📦 Update: pip install --upgrade git+https://github.com/stevezau/plex_generate_vid_previews.git")
|
171
|
+
|
172
|
+
logger.warning("🔗 Release notes: https://github.com/stevezau/plex_generate_vid_previews/releases/latest")
|
173
|
+
else:
|
174
|
+
logger.debug("Version is up to date")
|
175
|
+
|
176
|
+
except Exception as e:
|
177
|
+
# Catch any unexpected errors and log at debug level
|
178
|
+
logger.debug(f"Version check failed unexpectedly: {e}")
|