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.
@@ -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}")