summaryrefslogtreecommitdiff
path: root/docs/FEATURE_DOCUMENTATION.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/FEATURE_DOCUMENTATION.md')
-rw-r--r--docs/FEATURE_DOCUMENTATION.md911
1 files changed, 0 insertions, 911 deletions
diff --git a/docs/FEATURE_DOCUMENTATION.md b/docs/FEATURE_DOCUMENTATION.md
deleted file mode 100644
index 30d08d4..0000000
--- a/docs/FEATURE_DOCUMENTATION.md
+++ /dev/null
@@ -1,911 +0,0 @@
-# Download History Tracking & Duplicate Prevention Feature
-
-## Overview
-
-This feature adds comprehensive download history tracking with duplicate prevention capabilities to tidal-dl-ng. It provides users with visual feedback about previously downloaded tracks and the ability to prevent redundant downloads through a persistent JSON-based history system.
-
-## Table of Contents
-
-1. [Features](#features)
-2. [Architecture](#architecture)
-3. [Implementation Details](#implementation-details)
-4. [User Interface](#user-interface)
-5. [File Structure](#file-structure)
-6. [API Reference](#api-reference)
-7. [Testing](#testing)
-8. [Usage Examples](#usage-examples)
-
----
-
-## Features
-
-### Core Functionality
-
-- **Persistent Download History**: Tracks all downloaded tracks in a JSON file that persists across application restarts
-- **Duplicate Prevention**: Automatically skips tracks that have already been downloaded (when enabled)
-- **Toggle Control**: Users can enable/disable duplicate prevention via GUI menu
-- **Source Tracking**: Records the source of each download (playlist, album, mix, manual)
-- **Thread-Safe Operations**: All history operations are thread-safe for concurrent downloads
-- **Atomic File Operations**: Prevents corruption through atomic write operations
-- **Import/Export**: Allows users to backup and restore their download history
-- **Statistics**: Provides insights into download history (total tracks, by source type, dates)
-- **Visual Feedback**: Console messages are color-coded (green) for better visibility
-
-### User Benefits
-
-- ✅ Avoid wasting bandwidth on duplicate downloads
-- ✅ Keep track of what has been downloaded
-- ✅ Quickly identify already-downloaded content
-- ✅ Portable history through import/export
-- ✅ Source-based organization of download history
-
----
-
-## Architecture
-
-### Design Pattern: Singleton
-
-The `HistoryService` uses the Singleton pattern to ensure only one instance manages the download history throughout the application lifecycle.
-
-```python
-class HistoryService(metaclass=SingletonMeta):
- """Single instance managing all download history operations"""
-```
-
-### Data Structure: Track-Centric
-
-The history uses a track-centric approach with track IDs as keys for O(1) lookup performance:
-
-```json
-{
- "settings": {
- "preventDuplicates": true
- },
- "tracks": {
- "track_id": {
- "sourceType": "playlist",
- "sourceId": "pl-uuid",
- "sourceName": "Playlist Name",
- "downloadDate": "2025-11-29T12:34:56.789Z"
- }
- }
-}
-```
-
-### Thread Safety
-
-All critical operations are protected by a `threading.Lock`:
-
-```python
-with self._lock:
- # Critical section - atomic operations
- self.history_data[track_id] = entry
- self._save_history_internal()
-```
-
----
-
-## Implementation Details
-
-### 1. History Service (`tidal_dl_ng/history.py`)
-
-**File**: `tidal_dl_ng/history.py` (new file, ~550 lines)
-
-**Purpose**: Central service for managing download history with JSON persistence.
-
-#### Key Methods
-
-##### `add_track_to_history()`
-
-```python
-def add_track_to_history(
- self,
- track_id: str,
- source_type: str = "manual",
- source_id: str | None = None,
- source_name: str | None = None
-) -> None:
- """Add a track to download history with source metadata."""
-```
-
-##### `should_skip_download()`
-
-```python
-def should_skip_download(self, track_id: str) -> bool:
- """
- Determine if a track should be skipped based on:
- 1. Track exists in history
- 2. Duplicate prevention is enabled
-
- Returns True if both conditions are met.
- """
-```
-
-##### `update_settings()`
-
-```python
-def update_settings(self, **kwargs: Any) -> None:
- """Update settings and persist immediately to JSON."""
-```
-
-#### Atomic Write Implementation
-
-```python
-def _save_history_internal(self) -> None:
- """
- Atomic write pattern:
- 1. Write to temporary file
- 2. Atomic rename (os.replace on Windows)
- 3. Cleanup on error
- """
- with tempfile.NamedTemporaryFile(...) as tmp_file:
- json.dump(data, tmp_file)
- tmp_path = tmp_file.name
-
- os.replace(tmp_path, self.file_path) # Atomic operation
-```
-
-#### Corruption Recovery
-
-```python
-try:
- data = json.load(f)
-except (json.JSONDecodeError, ValueError):
- # Create backup of corrupted file
- backup_path = self.file_path.with_suffix(".json.bak")
- shutil.copy2(self.file_path, backup_path)
- # Start fresh
- self.history_data = {}
-```
-
----
-
-### 2. Download Integration (`tidal_dl_ng/download.py`)
-
-**Modifications**: Integration points in existing download flow
-
-#### Pre-Download Check
-
-```python
-def item(self, ...) -> tuple[bool, pathlib.Path | str]:
- # Step 2b: Duplicate prevention
- if isinstance(media, Track):
- track_id = str(media.id)
- if self.history_service.should_skip_download(track_id):
- self.fn_logger.info(
- f"Skipped item '{name_builder_item(media)}' (already in history)."
- )
- return False, path_media_dst
-```
-
-#### Post-Download Recording
-
-```python
-# Step 6: Add to history after successful download
-if download_success and isinstance(media, Track):
- try:
- self.history_service.add_track_to_history(
- track_id=str(media.id),
- source_type=source_type,
- source_id=source_id,
- source_name=source_name
- )
- except Exception as e:
- self.fn_logger.warning(f"Failed to add track to history: {e}")
-```
-
-**Note**: Videos are intentionally excluded from history tracking as they use a different workflow.
-
----
-
-### 3. GUI Integration (`tidal_dl_ng/gui.py`)
-
-**Modifications**: Tools menu integration
-
-#### Menu Action Creation
-
-```python
-def _init_menu_actions(self) -> None:
- """Initialize custom menu actions."""
- # Create or find Tools menu
- tools_menu = self._get_or_create_tools_menu()
-
- # Add View History action
- self.a_view_history = QtGui.QAction("View Download History...", self)
- self.a_view_history.triggered.connect(self.on_view_history)
- tools_menu.addAction(self.a_view_history)
-
- # Add separator
- tools_menu.addSeparator()
-
- # Add duplicate prevention toggle
- self.a_toggle_duplicate_prevention = QtGui.QAction(
- "Prevent Duplicate Downloads", self
- )
- self.a_toggle_duplicate_prevention.setCheckable(True)
- is_preventing = self.history_service.get_settings().get("preventDuplicates", True)
- self.a_toggle_duplicate_prevention.setChecked(is_preventing)
- self.a_toggle_duplicate_prevention.triggered.connect(
- self.on_toggle_duplicate_prevention
- )
- tools_menu.addAction(self.a_toggle_duplicate_prevention)
-```
-
-#### Toggle Handler
-
-```python
-def on_toggle_duplicate_prevention(self, enabled: bool) -> None:
- """Toggle duplicate download prevention on or off.
-
- Args:
- enabled: Whether duplicate prevention is enabled.
- """
- self.history_service.update_settings(preventDuplicates=enabled)
- status_msg = "enabled" if enabled else "disabled"
- logger_gui.info(f"Duplicate download prevention {status_msg}")
- self.s_statusbar_message.emit(
- StatusbarMessage(
- message=f"Duplicate prevention {status_msg}.",
- timeout=2500
- )
- )
-```
-
----
-
-### 4. Logger Configuration (`tidal_dl_ng/logger.py`)
-
-**Modifications**: Color configuration for INFO messages
-
-#### Problem
-
-By default, `coloredlogs` doesn't apply any color to INFO level messages (`'info': {}`), making them appear gray and less visible.
-
-#### Solution
-
-```python
-# Configure custom level styles to make INFO messages green
-level_styles = coloredlogs.DEFAULT_LEVEL_STYLES.copy()
-level_styles['info'] = {'color': 'green'}
-
-formatter = coloredlogs.ColoredFormatter(
- fmt=log_fmt,
- level_styles=level_styles
-)
-```
-
-#### Result
-
-All INFO messages (including skip messages) now display in **green** in the application console:
-
-```
-> Downloaded item 'Track Name'. # Green
-> Skipped item 'Track Name' (already in history). # Green
-> Finished list 'Playlist Name'. # Green
-```
-
----
-
-## User Interface
-
-### Tools Menu
-
-**Location**: Menu Bar → Tools
-
-**Structure**:
-
-```
-Tools
-├── View Download History...
-├── ─────────────────────
-└── ☑ Prevent Duplicate Downloads
-```
-
-### Menu Actions
-
-#### 1. View Download History...
-
-Opens the Download History dialog showing:
-
-- All downloaded tracks
-- Grouped by source (playlists, albums, mixes)
-- Download dates and metadata
-- Import/Export buttons
-- Clear history option
-
-#### 2. Prevent Duplicate Downloads (Checkable)
-
-- **Checked (Default)**: Tracks in history will be automatically skipped
-- **Unchecked**: Allows re-downloading tracks even if in history
-- State is persisted to JSON file
-- Immediate effect on download behavior
-- Status message displayed in status bar
-
-### Console Messages
-
-#### Downloaded Item (Success)
-
-```
-> Downloaded item 'Artist - Track Title'.
-```
-
-**Color**: Green
-
-#### Skipped Item (Duplicate)
-
-```
-> Skipped item 'Artist - Track Title' (already in history).
-```
-
-**Color**: Green
-
-#### List Finished
-
-```
-> Finished list 'Playlist Name'.
-```
-
-**Color**: Green
-
----
-
-## File Structure
-
-### JSON File Location
-
-**Path**: `{config_base}/downloaded_history.json`
-
-**Config Base Locations**:
-
-- **Windows**: `%APPDATA%\tidal-dl-ng\`
-- **macOS**: `~/Library/Application Support/tidal-dl-ng/`
-- **Linux**: `~/.config/tidal-dl-ng/`
-
-### JSON Schema
-
-#### Version 1 (Current)
-
-```json
-{
- "_schema_version": 1,
- "_last_updated": "2025-11-29T12:34:56.789Z",
- "settings": {
- "preventDuplicates": true
- },
- "tracks": {
- "123456": {
- "sourceType": "playlist",
- "sourceId": "uuid-123-456",
- "sourceName": "My Awesome Playlist",
- "downloadDate": "2025-11-29T12:30:00.000Z"
- },
- "789012": {
- "sourceType": "album",
- "sourceId": "uuid-789-012",
- "sourceName": "Amazing Album",
- "downloadDate": "2025-11-29T12:35:00.000Z"
- }
- }
-}
-```
-
-#### Field Descriptions
-
-| Field | Type | Description |
-| ---------------------------- | ------------ | ---------------------------------------------------------- |
-| `_schema_version` | integer | Schema version for future migrations |
-| `_last_updated` | ISO 8601 | Last modification timestamp |
-| `settings.preventDuplicates` | boolean | Enable/disable duplicate prevention |
-| `tracks` | object | Map of track_id → track metadata |
-| `tracks[id].sourceType` | string | Source type: "playlist", "album", "mix", "manual", "track" |
-| `tracks[id].sourceId` | string\|null | UUID of source, null for manual |
-| `tracks[id].sourceName` | string\|null | Display name of source |
-| `tracks[id].downloadDate` | ISO 8601 | When the track was downloaded |
-
-### Legacy Format Migration
-
-The service automatically migrates from the legacy format (tracks at root level):
-
-```json
-{
- "_schema_version": 1,
- "123456": {
- "sourceType": "playlist",
- ...
- }
-}
-```
-
-to the new format with separate `settings` and `tracks` sections.
-
----
-
-## API Reference
-
-### HistoryService
-
-#### Initialization
-
-```python
-from tidal_dl_ng.history import HistoryService
-
-history = HistoryService() # Singleton - always returns same instance
-```
-
-#### Core Methods
-
-##### Track Operations
-
-```python
-# Add track to history
-history.add_track_to_history(
- track_id="123456",
- source_type="playlist",
- source_id="pl-uuid",
- source_name="My Playlist"
-)
-
-# Check if track is downloaded
-is_downloaded = history.is_downloaded("123456") # Returns bool
-
-# Get track information
-info = history.get_track_info("123456") # Returns dict or None
-
-# Remove track from history
-removed = history.remove_track_from_history("123456") # Returns bool
-```
-
-##### Duplicate Prevention
-
-```python
-# Check if download should be skipped
-should_skip = history.should_skip_download("123456")
-# Returns True if:
-# 1. Track exists in history AND
-# 2. preventDuplicates setting is enabled
-
-# Update duplicate prevention setting
-history.update_settings(preventDuplicates=True) # or False
-```
-
-##### Settings Management
-
-```python
-# Get current settings
-settings = history.get_settings()
-# Returns: {"preventDuplicates": bool}
-
-# Update settings
-history.update_settings(preventDuplicates=False)
-# Immediately persists to JSON
-```
-
-##### Data Views
-
-```python
-# Get history grouped by source
-by_source = history.get_history_by_source()
-# Returns: {
-# "playlist_uuid-123": [track_info, ...],
-# "album_uuid-456": [track_info, ...],
-# "manual_manual": [track_info, ...]
-# }
-
-# Get statistics
-stats = history.get_statistics()
-# Returns: {
-# "total_tracks": int,
-# "by_source_type": {"playlist": 10, "album": 5, ...},
-# "oldest_download": "ISO 8601",
-# "newest_download": "ISO 8601"
-# }
-```
-
-##### Import/Export
-
-```python
-# Export history
-success, message = history.export_history("/path/to/export.json")
-# Returns: (True, "Successfully exported N tracks") or (False, "Error message")
-
-# Import history (merge mode)
-success, message = history.import_history("/path/to/import.json", merge=True)
-# merge=True: Adds to existing history
-# merge=False: Replaces existing history
-
-# Clear all history
-history.clear_history() # Destructive - removes all tracks
-```
-
-##### Utilities
-
-```python
-# Get history file path
-path = history.get_history_file_path()
-# Returns: "/path/to/downloaded_history.json"
-
-# Save history manually (usually not needed)
-history.save_history() # Most methods auto-save
-```
-
----
-
-## Testing
-
-### Test Coverage
-
-**Total**: 91 tests across 4 test files
-
-#### Test Files
-
-1. **test_history_service.py** (38 tests)
-
- - Service initialization
- - CRUD operations on tracks
- - Duplicate prevention logic
- - Settings management
- - JSON persistence and corruption recovery
- - Import/Export functionality
- - Statistics calculation
- - Thread safety
-
-2. **test_download_duplicate_prevention.py** (10 tests)
-
- - Download skip logic
- - History integration
- - Log message formatting
- - Post-download history updates
- - Settings toggle effects
-
-3. **test_gui_duplicate_prevention.py** (22 tests)
-
- - Menu creation and structure
- - Action state management
- - Handler behavior
- - Settings persistence
- - Status messages
-
-4. **test_logger_configuration.py** (20 tests)
- - Color configuration
- - Formatter setup
- - Message formatting
- - coloredlogs integration
-
-### Running Tests
-
-```bash
-# All tests
-pytest tests/ -v
-
-# Specific test file
-pytest tests/test_history_service.py -v
-
-# With coverage
-pytest tests/ --cov=tidal_dl_ng --cov-report=html
-
-# Specific test class
-pytest tests/test_history_service.py::TestDuplicatePrevention -v
-```
-
-### Test Results
-
-```
-======================== 91 passed in 0.91s ========================
-✅ test_history_service.py: 38 passed
-✅ test_download_duplicate_prevention.py: 10 passed
-✅ test_gui_duplicate_prevention.py: 22 passed
-✅ test_logger_configuration.py: 20 passed
-```
-
----
-
-## Usage Examples
-
-### Example 1: Basic Download with History
-
-```python
-from tidal_dl_ng.download import Download
-from tidal_dl_ng.history import HistoryService
-
-# Initialize download service
-download = Download(...)
-
-# Download a track (automatic history tracking)
-success, path = download.item(
- file_template="{artist_name} - {track_title}",
- media=track,
- source_type="playlist",
- source_id="pl-uuid-123",
- source_name="My Playlist"
-)
-
-# Track is automatically added to history if successful
-
-# Check if track was downloaded
-history = HistoryService()
-if history.is_downloaded(str(track.id)):
- print("Track is in history!")
-```
-
-### Example 2: Preventing Duplicates
-
-```python
-from tidal_dl_ng.history import HistoryService
-
-history = HistoryService()
-
-# Enable duplicate prevention (default)
-history.update_settings(preventDuplicates=True)
-
-# Check before downloading
-track_id = "123456"
-if history.should_skip_download(track_id):
- print(f"Track {track_id} already downloaded - skipping")
-else:
- # Proceed with download
- download.item(...)
-```
-
-### Example 3: Exporting History for Backup
-
-```python
-from tidal_dl_ng.history import HistoryService
-
-history = HistoryService()
-
-# Export current history
-success, message = history.export_history("/backup/my_history.json")
-if success:
- print(f"✅ {message}")
-else:
- print(f"❌ {message}")
-
-# Later, import it back
-success, message = history.import_history(
- "/backup/my_history.json",
- merge=True # Merge with existing
-)
-```
-
-### Example 4: Getting Download Statistics
-
-```python
-from tidal_dl_ng.history import HistoryService
-
-history = HistoryService()
-
-# Get statistics
-stats = history.get_statistics()
-
-print(f"Total tracks downloaded: {stats['total_tracks']}")
-print(f"By source type:")
-for source_type, count in stats['by_source_type'].items():
- print(f" {source_type}: {count}")
-print(f"Oldest download: {stats['oldest_download']}")
-print(f"Newest download: {stats['newest_download']}")
-```
-
-### Example 5: Viewing History by Source
-
-```python
-from tidal_dl_ng.history import HistoryService
-
-history = HistoryService()
-
-# Get history grouped by source
-by_source = history.get_history_by_source()
-
-for source_key, tracks in by_source.items():
- print(f"\n{source_key} ({len(tracks)} tracks):")
- for track in tracks:
- print(f" - Track {track['track_id']} - {track['source_name']}")
- print(f" Downloaded: {track['download_date']}")
-```
-
-### Example 6: GUI Toggle Integration
-
-```python
-# In GUI code
-def on_toggle_duplicate_prevention(self, enabled: bool) -> None:
- """User toggled the menu option."""
- # Update setting
- self.history_service.update_settings(preventDuplicates=enabled)
-
- # Show feedback
- status = "enabled" if enabled else "disabled"
- self.show_status(f"Duplicate prevention {status}")
-
- # Setting is immediately persisted to JSON
-```
-
----
-
-## Migration Guide
-
-### From No History System
-
-1. **No action needed**: The system automatically creates an empty history on first run
-2. Existing downloads won't be in history initially
-3. New downloads will be tracked going forward
-
-### From Legacy Format
-
-The system automatically migrates old format files:
-
-**Old format** (tracks at root):
-
-```json
-{
- "_schema_version": 1,
- "123456": { "sourceType": "...", ... }
-}
-```
-
-**New format** (tracks in section):
-
-```json
-{
- "_schema_version": 1,
- "settings": { "preventDuplicates": true },
- "tracks": {
- "123456": { "sourceType": "...", ... }
- }
-}
-```
-
-Migration happens automatically on load - no user intervention required.
-
----
-
-## Performance Considerations
-
-### Lookup Performance
-
-- **Track existence check**: O(1) - uses Python dict
-- **Should skip check**: O(1) - dict lookup + boolean check
-- **Memory usage**: ~100 bytes per track entry
-- **File size**: ~150-200 bytes per track in JSON
-
-### Benchmarks
-
-| Operation | Time (avg) | Notes |
-| -------------------------- | ---------- | ------------------- |
-| Add track | <1ms | Includes JSON write |
-| Check if downloaded | <0.1ms | Dict lookup only |
-| Should skip check | <0.1ms | Dict lookup + bool |
-| Load history (1000 tracks) | ~50ms | On startup only |
-| Save history (1000 tracks) | ~100ms | Atomic write |
-
-### Concurrency
-
-- **Thread-safe**: All operations use `threading.Lock`
-- **No deadlocks**: Lock held for minimal time
-- **No race conditions**: Atomic file writes prevent corruption
-
----
-
-## Troubleshooting
-
-### Issue: History file corrupted
-
-**Solution**: The system automatically:
-
-1. Creates a backup `.json.bak` file
-2. Starts with empty history
-3. Logs the corruption for investigation
-
-### Issue: Duplicate prevention not working
-
-**Check**:
-
-1. Is the setting enabled? `history.get_settings()["preventDuplicates"]`
-2. Is the track actually in history? `history.is_downloaded(track_id)`
-3. Check logs for skip messages
-
-### Issue: History lost after crash
-
-**Note**: The system uses atomic writes, so:
-
-- Either the old file is intact, OR
-- The new file is complete
-- Never partially written
-
-### Issue: Import fails
-
-**Common causes**:
-
-- Invalid JSON syntax → Fix JSON file
-- Missing required fields → Add `sourceType`, `downloadDate`
-- Wrong file format → Use export from same version
-
----
-
-## Future Enhancements
-
-### Planned Features
-
-- [ ] Track file paths in history (verify file still exists)
-- [ ] Smart re-download detection (file deleted but in history)
-- [ ] Quality-based history (allow re-download if better quality available)
-- [ ] Batch operations (mark multiple as downloaded/not downloaded)
-- [ ] Search and filter in history dialog
-- [ ] Export to CSV/Excel for analysis
-- [ ] Sync history across devices
-
-### API Stability
-
-The current API is considered **stable** and follows semantic versioning:
-
-- Patch versions (1.0.x): Bug fixes only
-- Minor versions (1.x.0): New features, backward compatible
-- Major versions (x.0.0): Breaking changes to API or file format
-
----
-
-## Contributing
-
-### Code Standards
-
-All code follows the project's AGENTS.md guidelines:
-
-- ✅ Type hints for all functions
-- ✅ Docstrings (Google style)
-- ✅ PEP 8 compliance (via Black, Ruff)
-- ✅ Thread safety where needed
-- ✅ Comprehensive tests
-- ✅ Error handling
-
-### Adding New Features
-
-1. Update `HistoryService` class
-2. Add tests to `test_history_service.py`
-3. Update JSON schema if needed
-4. Update this documentation
-5. Run all tests: `pytest tests/`
-6. Run quality checks: `make check`
-
----
-
-## License
-
-This feature is part of tidal-dl-ng and follows the same license as the main project.
-
----
-
-## Changelog
-
-### Version 1.0.0 (2025-11-29)
-
-**Initial Release**
-
-- ✅ Persistent JSON-based download history
-- ✅ Duplicate prevention with toggle
-- ✅ Thread-safe operations
-- ✅ Atomic file writes
-- ✅ Corruption recovery
-- ✅ Import/Export functionality
-- ✅ Statistics and source views
-- ✅ GUI integration (Tools menu)
-- ✅ Green console messages for INFO level
-- ✅ Comprehensive test suite (91 tests)
-- ✅ Complete documentation
-
----
-
-## Support
-
-For issues, questions, or feature requests:
-
-1. Check this documentation
-2. Review test files for usage examples
-3. Open an issue on GitHub with:
- - Description of the problem
- - Steps to reproduce
- - Expected vs actual behavior
- - Log output (if applicable)
-
----
-
-**Last Updated**: 2025-11-29
-**Version**: 1.0.0
-**Status**: Production Ready ✅