Skip to content

Commit d2de1b4

Browse files
authored
Merge branch 'exislow:master' into show-in-explorer
2 parents 0e10c53 + 28a6fe5 commit d2de1b4

File tree

12 files changed

+988
-154
lines changed

12 files changed

+988
-154
lines changed

.github/copilot-instructions.md

Lines changed: 0 additions & 36 deletions
This file was deleted.

AGENTS.md

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
---
2+
applyTo: "**/*.py"
3+
---
4+
5+
# Project General Coding Standards
6+
7+
## Python Version Support
8+
9+
- Target Python 3.12 and 3.13
10+
- Use modern Python features supported by the minimum version (3.12)
11+
- Avoid deprecated features and use future-proof syntax
12+
13+
## Naming Conventions
14+
15+
- Use snake_case for variable and function names
16+
- Use CamelCase for class names
17+
- Follow PEP 8 style guidelines strictly
18+
- Prefix private class members with underscore (\_)
19+
- Use ALL_CAPS for constants
20+
- Use descriptive names that clearly indicate purpose and content
21+
- Avoid single-letter variable names except for loop counters or mathematical contexts
22+
23+
## Type Annotations
24+
25+
- Use type annotations for ALL function and method parameters, return types, and variables (PEP 484)
26+
- Use modern built-in generics: `list`, `dict`, `set`, `tuple` instead of `List`, `Dict`, `Set`, `Tuple` from `typing`
27+
- Use `None` type for optional parameters: `str | None` instead of `Optional[str]`
28+
- Use union types with `|` operator: `int | str` instead of `Union[int, str]`
29+
- For complex types, import from `collections.abc`: `Callable`, `Iterable`, etc.
30+
- Always specify generic types: use `list[str]` not just `list`
31+
- Use `pathlib.Path` for file paths, not `str`
32+
33+
## Error Handling
34+
35+
- Use try/except blocks for operations that may fail
36+
- Always log errors with contextual information using the project's logger
37+
- Catch specific exceptions, avoid bare `except:` clauses
38+
- Use `finally` blocks for cleanup operations
39+
- For HTTP operations with requests library:
40+
- Use timeout parameter (default: `REQUESTS_TIMEOUT_SEC`)
41+
- Implement retry logic with `requests.adapters.Retry` for network operations
42+
- Always close response objects in `finally` blocks or use context managers
43+
- For file operations:
44+
- Use context managers (`with` statement) for file handling
45+
- Use `pathlib.Path` methods for path operations
46+
- Handle `OSError` and its subclasses appropriately
47+
48+
## Code Style and Formatting
49+
50+
- Line length: maximum 120 characters (as configured in Black and Ruff)
51+
- Use Black formatting style with preview features enabled
52+
- Follow isort configuration for import ordering:
53+
1. FUTURE
54+
2. TYPING
55+
3. STDLIB
56+
4. THIRDPARTY
57+
5. FIRSTPARTY
58+
6. LOCALFOLDER
59+
- Include trailing commas in multi-line constructs
60+
- Use more blank lines to achieve better code organization and readability
61+
- Use 4 spaces for indentation (no tabs)
62+
63+
## Modern Python Features
64+
65+
- Follow PEP 492 – Coroutines with async and await syntax (when applicable)
66+
- Follow PEP 498 – Literal String Interpolation (f-strings)
67+
- Follow PEP 572 – Assignment Expressions (walrus operator `:=` when it improves readability)
68+
- Use structural pattern matching (match/case) for Python 3.10+ when appropriate
69+
- Prefer pathlib.Path over os.path for file operations
70+
- Use Enum and StrEnum for constants with related values
71+
- Use dataclasses or dataclasses-json for structured data
72+
73+
## Concurrency and Threading
74+
75+
- Use `concurrent.futures.ThreadPoolExecutor` for I/O-bound parallel operations
76+
- Always use context managers with executors
77+
- Set appropriate `max_workers` based on operation type (use configuration values)
78+
- Handle futures with `futures.as_completed()` for better responsiveness
79+
- Implement abort/cancellation mechanisms using `threading.Event`
80+
- Cancel pending futures when aborting operations
81+
- Use thread-safe data structures when sharing data between threads
82+
- Avoid blocking operations in GUI threads
83+
84+
## Resource Management
85+
86+
- Always use context managers (`with` statements) for:
87+
- File operations
88+
- Network connections
89+
- Thread pools and executors
90+
- Temporary directories and files
91+
- Use `tempfile.TemporaryDirectory` with `ignore_cleanup_errors=True` for temp operations
92+
- Close network responses explicitly in `finally` blocks or use context managers
93+
- Clean up temporary files after processing
94+
- Use `pathlib.Path.unlink(missing_ok=True)` for safe file deletion
95+
96+
## File and Path Handling
97+
98+
- Use `pathlib.Path` exclusively for path operations
99+
- Sanitize file paths using `pathvalidate.sanitize_filename` and project's `path_file_sanitize`
100+
- Use `.expanduser()` for paths that may contain `~`
101+
- Use `.absolute()` to get absolute paths
102+
- Use `.resolve()` to resolve symlinks
103+
- Check file existence with `Path.exists()`, `Path.is_file()`, `Path.is_dir()`
104+
- Use `os.makedirs(path, exist_ok=True)` or `Path.mkdir(parents=True, exist_ok=True)`
105+
- Handle cross-platform path differences automatically with pathlib
106+
107+
## Code Documentation
108+
109+
- Write docstrings for ALL modules, classes, functions, and methods using Google docstring style
110+
- Include type information in docstrings even when type hints are present
111+
- Document all parameters with their types and descriptions
112+
- Document return values with type and description
113+
- Document raised exceptions
114+
- Use line comments to explain complex logic, algorithms, or non-obvious decisions
115+
- When refactoring code:
116+
- Update or add docstrings to reflect new behavior
117+
- Update existing line comments rather than removing them
118+
- Add TODO comments for known limitations or future improvements
119+
120+
## Logging
121+
122+
- Use the project's logger (via `fn_logger` or similar)
123+
- Log levels:
124+
- `debug`: Detailed diagnostic information
125+
- `info`: General informational messages (e.g., download completion)
126+
- `error`: Error conditions with context
127+
- `exception`: Errors with full traceback
128+
- Include relevant context in log messages (file names, IDs, URLs, etc.)
129+
- Use f-strings for log message formatting
130+
131+
## GUI Development (PySide6/Qt)
132+
133+
- Follow Qt naming conventions for slots and signals
134+
- Use type hints for signal parameters
135+
- Emit signals for cross-thread communication (never call GUI methods directly from worker threads)
136+
- Use Qt's threading mechanisms appropriately
137+
- Handle GUI progress updates via signals
138+
- Implement proper cleanup in close events
139+
- Use `QThread` or `ThreadPoolExecutor` for background operations, never block the GUI thread
140+
141+
## API Integration (TIDAL)
142+
143+
- Always check if media is available before processing (`media.available`)
144+
- Handle `tidalapi.exceptions.TooManyRequests` gracefully
145+
- Implement retry logic for transient failures
146+
- Use sessions appropriately
147+
- Handle stream manifests and encryption properly
148+
- Respect API rate limits and implement delays when configured
149+
150+
## Testing
151+
152+
- Place tests in the `tests/` directory
153+
- Use pytest as the testing framework
154+
- Use descriptive test function names: `test_<functionality_being_tested>`
155+
- Test edge cases: empty lists, None values, invalid inputs
156+
- Mock external dependencies (API calls, file system when appropriate)
157+
- Use fixtures for common test setup
158+
- Aim for meaningful test coverage, not just high percentages
159+
160+
## Security
161+
162+
- Never hardcode credentials (use configuration or environment variables)
163+
- Use base64 encoding only for obfuscation, not security
164+
- Handle sensitive data (tokens, keys) carefully
165+
- Use secure temporary file creation
166+
- Validate and sanitize all user inputs, especially file paths
167+
- Handle decryption keys securely (don't log them)
168+
169+
## Performance
170+
171+
- Use generators for large datasets when possible
172+
- Implement streaming for large file downloads
173+
- Use appropriate chunk sizes for file I/O (use `CHUNK_SIZE` constant)
174+
- Cache expensive computations when safe to do so
175+
- Use batch operations where applicable
176+
- Profile code before optimizing
177+
- Consider memory usage for large collections
178+
179+
## Configuration and Settings
180+
181+
- Use the Settings class for all configuration
182+
- Access settings via `self.settings.data.*`
183+
- Validate configuration values
184+
- Provide sensible defaults
185+
- Use type-safe configuration access
186+
- Document configuration options
187+
188+
## Code Quality Tools
189+
190+
- Run Ruff for linting before committing
191+
- Run Black for formatting before committing
192+
- Run mypy for type checking
193+
- Use pre-commit hooks to automate checks
194+
- Address all linting warnings and errors
195+
- Keep code complexity low (avoid deep nesting, long functions)
196+
197+
## Best Practices Summary
198+
199+
1. **Type Safety**: Always use type hints, enable strict mypy checks
200+
2. **Error Handling**: Catch specific exceptions, log with context, clean up resources
201+
3. **Readability**: Write self-documenting code with clear names and structure
202+
4. **Documentation**: Comprehensive docstrings and comments for complex logic
203+
5. **Testing**: Test edge cases and error conditions
204+
6. **Performance**: Use efficient algorithms and data structures
205+
7. **Maintainability**: Keep functions focused, avoid code duplication
206+
8. **Security**: Validate inputs, handle credentials safely
207+
9. **Compatibility**: Support Python 3.12-3.13, handle cross-platform differences
208+
10. **Standards**: Follow PEP 8, PEP 484, and project-specific configurations

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 🔰 TIDAL Downloader Next Generation! (tidal-dl-ng)
1+
# ![](./tidal_dl_ng/ui/icon32.png) TIDAL Downloader Next Generation! (tidal-dl-ng)
22

33
[![Release](https://img.shields.io/github/v/release/exislow/tidal-dl-ng)](https://img.shields.io/github/v/release/exislow/tidal-dl-ng)
44
[![Build status](https://img.shields.io/github/actions/workflow/status/exislow/tidal-dl-ng/release-or-test-build.yml)](https://github.com/exislow/tidal-dl-ng/actions/workflows/release-or-test-build.yml)
@@ -42,7 +42,7 @@ If you like this projects and want to support it, feel free to buy me a coffee
4242

4343
## 💻 Installation / Upgrade
4444

45-
**Requirements**: Python version 3.12 (other versions might work but are not tested!)
45+
**Requirements**: Python version 3.12 / 3.13 (other versions might work but are not tested!)
4646

4747
```bash
4848
pip install --upgrade tidal-dl-ng

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "tidal-dl-ng"
33
authors = [{ name = "Robert Honz", email = "<[email protected]>" }]
4-
version = "0.30.0"
4+
version = "0.31.4"
55
description = "TIDAL Medial Downloader Next Generation!"
66
readme = "README.md"
77
license = "AGPL-3.0-only"

tidal_dl_ng/config.py

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
import os
33
import shutil
44
from collections.abc import Callable
5-
from contextlib import contextmanager
65
from json import JSONDecodeError
76
from pathlib import Path
8-
from threading import Event
7+
from threading import Event, Lock
98
from typing import Any
109

1110
import tidalapi
@@ -103,6 +102,19 @@ def __init__(self, settings: Settings = None):
103102
self.cls_model = ModelToken
104103
tidal_config: tidalapi.Config = tidalapi.Config(item_limit=10000)
105104
self.session = tidalapi.Session(tidal_config)
105+
self.original_client_id = self.session.config.client_id
106+
self.original_client_secret = self.session.config.client_secret
107+
# Lock to ensure session-switching is thread-safe.
108+
# This lock protects against a race condition where one thread
109+
# changes the session credentials while another is using them.
110+
# It is intentionally held by Download._get_stream_info
111+
# for the *entire* duration of the credential switch AND
112+
# the get_stream() call.
113+
self.stream_lock = Lock()
114+
# State-tracking flag to prevent redundant, expensive
115+
# session re-authentication when the session is already in the
116+
# correct mode (Atmos or Normal).
117+
self.is_atmos_session = False
106118
# self.session.config.client_id = "km8T1xS355y7dd3H"
107119
# self.session.config.client_secret = "vcmeGW1OuZ0fWYMCSZ6vNvSLJlT3XEpW0ambgYt5ZuI="
108120
self.file_path = path_file_token()
@@ -116,7 +128,8 @@ def settings_apply(self, settings: Settings = None) -> bool:
116128
if settings:
117129
self.settings = settings
118130

119-
self.session.audio_quality = tidalapi.Quality(self.settings.data.quality_audio)
131+
if not self.is_atmos_session:
132+
self.session.audio_quality = tidalapi.Quality(self.settings.data.quality_audio)
120133
self.session.video_quality = tidalapi.VideoQuality.high
121134

122135
return True
@@ -162,33 +175,64 @@ def token_persist(self) -> None:
162175
self.set_option("expiry_time", self.session.expiry_time)
163176
self.save()
164177

165-
@contextmanager
166-
def atmos_session_context(self):
178+
def switch_to_atmos_session(self) -> bool:
179+
"""
180+
Switches the shared session to Dolby Atmos credentials.
181+
Only re-authenticates if not already in Atmos mode.
167182
168-
if not self.session.check_login():
169-
print("Not logged in.")
183+
Returns:
184+
bool: True if successful or already in Atmos mode, False otherwise.
185+
"""
186+
# If we are already in Atmos mode, do nothing.
187+
if self.is_atmos_session:
188+
return True
189+
190+
print("Switching session context to Dolby Atmos...")
191+
self.session.config.client_id = ATMOS_CLIENT_ID
192+
self.session.config.client_secret = ATMOS_CLIENT_SECRET
193+
self.session.audio_quality = ATMOS_REQUEST_QUALITY
194+
195+
# Re-login with new credentials
196+
if not self.login_token(do_pkce=self.is_pkce):
197+
print("Warning: Atmos session authentication failed.")
198+
# Try to switch back to normal to be safe
199+
self.restore_normal_session(force=True)
200+
return False
201+
202+
self.is_atmos_session = True # Set the flag
203+
print("Session is now in Atmos mode.")
204+
return True
170205

171-
original_client_id = self.session.config.client_id
172-
original_client_secret = self.session.config.client_secret
173-
original_audio_quality = self.session.audio_quality
206+
def restore_normal_session(self, force: bool = False) -> bool:
207+
"""
208+
Restores the shared session to the original user credentials.
209+
Only re-authenticates if not already in Normal mode.
174210
175-
try:
176-
self.session.config.client_id = ATMOS_CLIENT_ID
177-
self.session.config.client_secret = ATMOS_CLIENT_SECRET
178-
self.session.audio_quality = ATMOS_REQUEST_QUALITY
211+
Args:
212+
force: If True, forces restoration even if already in Normal mode.
213+
214+
Returns:
215+
bool: True if successful or already in Normal mode, False otherwise.
216+
"""
217+
# If we are already in Normal mode (and not forced), do nothing.
218+
if not self.is_atmos_session and not force:
219+
return True
179220

180-
if not self.login_token(do_pkce=self.is_pkce):
181-
print("Warning: Session restore failed.")
221+
print("Restoring session context to Normal...")
222+
self.session.config.client_id = self.original_client_id
223+
self.session.config.client_secret = self.original_client_secret
182224

183-
yield
225+
# Explicitly restore audio quality to user's configured setting
226+
self.session.audio_quality = tidalapi.Quality(self.settings.data.quality_audio)
184227

185-
finally:
186-
self.session.config.client_id = original_client_id
187-
self.session.config.client_secret = original_client_secret
188-
self.session.audio_quality = original_audio_quality
228+
# Re-login with original credentials
229+
if not self.login_token(do_pkce=self.is_pkce):
230+
print("Warning: Restoring the original session context failed. Please restart the application.")
231+
return False
189232

190-
if not self.login_token(do_pkce=self.is_pkce):
191-
print("Warning: Restoring the original session context failed. Please restart the application.")
233+
self.is_atmos_session = False # Set the flag
234+
print("Session is now in Normal mode.")
235+
return True
192236

193237
def login(self, fn_print: Callable) -> bool:
194238
is_token = self.login_token()

0 commit comments

Comments
 (0)