- Move phase/plan docs into docs/ - Move legacy node:test files into tests/legacy/ with README - Remove .backup file, test audit artifacts, and unused AI prompt/skill files - Remove broken iOS GitHub workflows (reference missing MovieMapper-iOS/)
401 lines
11 KiB
Markdown
401 lines
11 KiB
Markdown
# MovieMapper iOS Implementation Plan
|
|
|
|
## Architecture Overview
|
|
Since you want **SwiftUI with Swift only** (no Rust backend) and **offline-first**, this will be a complete rewrite in Swift using Apple frameworks.
|
|
|
|
---
|
|
|
|
## Phase 1: Project Setup & Foundation
|
|
|
|
### 1.1 Create Xcode Project
|
|
```bash
|
|
# Create new iOS App project (iPad only)
|
|
xcodebuild -createProject MovieMapper -template iOS-App -destination ./MovieMapper-iOS
|
|
|
|
# Or using Swift Package Manager for more control
|
|
mkdir MovieMapper-iOS && cd MovieMapper-iOS
|
|
swift package init --type executable --name MovieMapper-iOS
|
|
```
|
|
|
|
### 1.2 Directory Structure
|
|
```
|
|
MovieMapper-iOS/
|
|
├── MovieMapper-iOS/ # Main app target
|
|
│ ├── ContentView.swift
|
|
│ ├── MovieMapperApp.swift
|
|
│ └── ...
|
|
├── SharedModels/ # Shared data models
|
|
│ ├── MediaFile.swift
|
|
│ ├── Show.swift
|
|
│ ├── Season.swift
|
|
│ ├── Episode.swift
|
|
│ └── TaggedFile.swift
|
|
├── Services/ # Business logic
|
|
│ ├── FileScanner.swift
|
|
│ ├── MetadataExtractor.swift
|
|
│ ├── TVDBClient.swift
|
|
│ ├── FileMapper.swift
|
|
│ └── AuditLogger.swift
|
|
├── UI/ # SwiftUI views
|
|
│ ├── BrowseView.swift
|
|
│ ├── SearchView.swift
|
|
│ ├── FileListView.swift
|
|
│ ├── SeasonDetailView.swift
|
|
│ └── ModalViews/
|
|
├── Utils/ # Helper functions
|
|
│ ├── Filesystem.swift
|
|
│ ├── FFmpegWrapper.swift
|
|
│ └── DateFormatters.swift
|
|
└── Resources/ # Assets
|
|
├── Assets.xcassets
|
|
└── Localizable.strings
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 2: Core Implementation
|
|
|
|
### 2.1 Data Models (SharedModels/)
|
|
```swift
|
|
struct MediaFile: Identifiable, Codable, Hashable {
|
|
let id: UUID
|
|
let name: String
|
|
let path: String
|
|
let size: Int64
|
|
let modified: Date
|
|
let duration: String // "mm:ss" format
|
|
let quality: String // "1080p", "4K", etc.
|
|
let fps: String // "24fps", "30fps", etc.
|
|
var isFolder: Bool
|
|
var tags: [TagType]
|
|
}
|
|
|
|
enum TagType: String, Codable, CaseIterable {
|
|
case extra = "extra"
|
|
case behindTheScenes = "behind-the-scenes"
|
|
case delete = "delete"
|
|
}
|
|
```
|
|
|
|
### 2.2 File Scanner Service
|
|
- **Framework**: Use `FileManager` with `URLQueryItem` for directory access
|
|
- **Permissions**: Request `NSPhotoLibraryAddUsageDescription` for file access
|
|
- **Non-recursive scanning**: Scan only current directory (match desktop behavior)
|
|
- **Progress updates**: Use Combine/Publishers for real-time UI updates
|
|
|
|
```swift
|
|
class FileScanner {
|
|
func scanDirectory(at path: URL, progress: @escaping (Int, Int, String) -> Void) async throws -> [MediaFile]
|
|
}
|
|
```
|
|
|
|
### 2.3 Metadata Extractor
|
|
- **FFmpeg integration**: Use `ffmpeg-kit` or `SwiftFFmpeg` package
|
|
- **Extract**: duration, resolution, FPS
|
|
- **Error handling**: Gracefully handle corrupted files
|
|
|
|
```swift
|
|
class MetadataExtractor {
|
|
func extractDuration(from url: URL) async throws -> String
|
|
func extractQuality(from url: URL) async throws -> (quality: String, fps: String)
|
|
}
|
|
```
|
|
|
|
### 2.4 TVDB Client
|
|
- **API**: TheTVDB v4 REST API
|
|
- **Authentication**: Bearer token with caching
|
|
- **Features**: Search shows, get details, fetch episodes
|
|
- **Offline caching**: Store recent searches in `UserDefaults` or Core Data
|
|
|
|
```swift
|
|
class TVDBClient {
|
|
func authenticate() async throws
|
|
func search(query: String) async throws -> [Show]
|
|
func getShowDetails(id: Int) async throws -> ShowDetails
|
|
func getSeasonEpisodes(showId: Int, seasonNumber: Int) async throws -> [Episode]
|
|
}
|
|
```
|
|
|
|
### 2.5 File Mapper
|
|
- **Jellyfin naming**: `ShowName S01E01 - quality.ext`
|
|
- **Episode ranges**: Support `S01E01-E03` format
|
|
- **Folder creation**: Create `extras/`, `behind the scenes/`, `commentary/` directories
|
|
|
|
```swift
|
|
class FileMapper {
|
|
func mapFiles(_ files: [MediaFile], to show: Show, season: Season) async throws -> MappingResult
|
|
}
|
|
```
|
|
|
|
### 2.6 Audit Logger
|
|
- **Format**: JSON lines in `.audit` files
|
|
- **Storage**: Write to same directory as files
|
|
- **Content**: Timestamp, action, details
|
|
|
|
```swift
|
|
class AuditLogger {
|
|
func log(action: String, details: [String: Any], in directory: URL) async throws
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 3: UI Implementation
|
|
|
|
### 3.1 Main Views
|
|
|
|
#### BrowseView.swift
|
|
- Directory picker (UIDocumentPickerViewController)
|
|
- Breadcrumb navigation (iPad multi-column)
|
|
- File list with:
|
|
- Folder icons 📁
|
|
- Media file details (duration, quality, FPS)
|
|
- Tag icons (clickable)
|
|
- Play button (moves tagged files)
|
|
|
|
#### SearchView.swift
|
|
- Search bar for TVDB
|
|
- Show results with thumbnails
|
|
- Season selection with episode lists
|
|
- Episode count badges
|
|
|
|
#### FileListView.swift
|
|
- Drag-and-drop reordering (iOS 17+)
|
|
- Episode number editing
|
|
- Tag toggling (extra, behind-the-scenes, delete)
|
|
- Floating action button for moving all tagged files
|
|
|
|
### 3.2 iPad-Specific Features
|
|
- **Multi-column navigation**: Use `NavigationSplitView` for sidebar + content
|
|
- **Split view**: Search on left, files on right
|
|
- **Large screen optimization**: Use more horizontal space
|
|
|
|
### 3.3 Modal Views
|
|
- Video preview modal
|
|
- Confirmation dialogs for destructive actions
|
|
- Loading indicators for async operations
|
|
|
|
---
|
|
|
|
## Phase 4: Testing Strategy
|
|
|
|
### 4.1 Command Line Testing Tools
|
|
|
|
#### 4.1.1 File Scanner Tests
|
|
```bash
|
|
# Create test directory structure
|
|
mkdir -p /tmp/moviemapper_test/{Season\ 01,Season\ 02}
|
|
touch /tmp/moviemapper_test/Season\ 01/episode1.mp4
|
|
touch /tmp/moviemapper_test/Season\ 01/episode2.mkv
|
|
|
|
# Run scanner test
|
|
swift test --filter FileScannerTests/testScanDirectory
|
|
```
|
|
|
|
#### 4.1.2 Metadata Extraction Tests
|
|
```bash
|
|
# Create test video file (using ffmpeg)
|
|
ffmpeg -f lavfi -i testsrc=duration=5:size=1920x1080:rate=30 /tmp/test_video.mp4
|
|
|
|
# Run metadata extraction test
|
|
swift test --filter MetadataExtractorTests/testExtractDuration
|
|
swift test --filter MetadataExtractorTests/testExtractQuality
|
|
```
|
|
|
|
#### 4.1.3 TVDB API Tests
|
|
```bash
|
|
# Set API key (from .env file)
|
|
export TVDB_API_KEY="your-api-key-here"
|
|
|
|
# Run TVDB integration tests
|
|
swift test --filter TVDBClientTests/testAuthenticate
|
|
swift test --filter TVDBClientTests/testSearchShows
|
|
swift test --filter TVDBClientTests/testGetShowDetails
|
|
```
|
|
|
|
#### 4.1.4 File Mapping Tests
|
|
```bash
|
|
# Create test files
|
|
mkdir -p /tmp/moviemapper_test/Season\ 01
|
|
for i in {1..5}; do
|
|
ffmpeg -f lavfi -i testsrc=duration=1:size=1280x720:rate=24 \
|
|
/tmp/moviemapper_test/Season\ 01/video_${i}.mp4
|
|
done
|
|
|
|
# Run mapping test
|
|
swift test --filter FileMapperTests/testMapSingleEpisode
|
|
swift test --filter FileMapperTests/testMapEpisodeRange
|
|
```
|
|
|
|
### 4.2 UI Testing
|
|
```bash
|
|
# Run UI tests
|
|
xcodebuild test -project MovieMapper-iOS.xcodeproj \
|
|
-scheme MovieMapper-iOS \
|
|
-destination 'platform=iOS Simulator,name=iPad Pro (12.9-inch) (17th generation)' \
|
|
-destination-timeout 60
|
|
```
|
|
|
|
### 4.3 Performance Testing
|
|
```bash
|
|
# Test scanning performance with many files
|
|
xcodebuild test -project MovieMapper-iOS.xcodeproj \
|
|
-scheme MovieMapper-iOSPerformance \
|
|
-destination 'platform=iOS Simulator,name=iPad Pro' \
|
|
-enableCodeCoverage YES
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5: Advanced Features
|
|
|
|
### 5.1 Offline-First Architecture
|
|
- **Local caching**: Store show search results in `UserDefaults` or `CoreData`
|
|
- **File scanning**: Work completely offline
|
|
- **TVDB features**: Optional, only when internet available
|
|
|
|
### 5.2 File System Access
|
|
- **Document Picker**: Let user select media library location
|
|
- **Security scoped bookmarks**: Persist access across app launches
|
|
- **iCloud integration**: Optional (for backup)
|
|
|
|
### 5.3 Tagging System
|
|
```swift
|
|
struct TagManager {
|
|
func addTag(_ tag: TagType, to file: MediaFile, in directory: URL) async throws
|
|
func removeTag(_ tag: TagType, from file: MediaFile, in directory: URL) async throws
|
|
func moveTaggedFiles(_ files: [MediaFile], to folder: String) async throws
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 6: Build & Distribution
|
|
|
|
### 6.1 Build Commands
|
|
```bash
|
|
# Build for simulator
|
|
xcodebuild -project MovieMapper-iOS.xcodeproj \
|
|
-scheme MovieMapper-iOS \
|
|
-configuration Debug \
|
|
-sdk iphonesimulator
|
|
|
|
# Build for device
|
|
xcodebuild -project MovieMapper-iOS.xcodeproj \
|
|
-scheme MovieMapper-iOS \
|
|
-configuration Release \
|
|
-sdk iphoneos \
|
|
-archivePath MovieMapper-iOS.xcarchive \
|
|
archive
|
|
|
|
# Export IPA
|
|
xcodebuild -exportArchive \
|
|
-archivePath MovieMapper-iOS.xcarchive \
|
|
-exportOptionsPlist ExportOptions.plist
|
|
```
|
|
|
|
### 6.2 Testing Automation
|
|
```bash
|
|
#!/bin/bash
|
|
# run-tests.sh
|
|
|
|
# Set up test environment
|
|
export TVDB_API_KEY=$(cat .env | grep TVDB_API_KEY | cut -d'=' -f2)
|
|
|
|
# Run unit tests
|
|
swift test
|
|
|
|
# Run UI tests
|
|
xcodebuild test -project MovieMapper-iOS.xcodeproj \
|
|
-scheme MovieMapper-iOS \
|
|
-destination 'platform=iOS Simulator,name=iPad Pro'
|
|
|
|
# Generate test report
|
|
xcodebuild -project MovieMapper-iOS.xcodeproj \
|
|
-scheme MovieMapper-iOS \
|
|
-destination 'platform=iOS Simulator,name=iPad Pro' \
|
|
-quiet \
|
|
-testSummaryReport /tmp/test-results.xml
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 7: Dependencies & Packages
|
|
|
|
### 7.1 Swift Package Manager Dependencies
|
|
```swift
|
|
// swift-tools-version:5.9
|
|
import PackageDescription
|
|
|
|
let package = Package(
|
|
name: "MovieMapper-iOS",
|
|
platforms: [.iOS(.v17)],
|
|
dependencies: [
|
|
.package(url: "https://github.com/ffmpeg-kit/swift.git", from: "6.0.0"),
|
|
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0"),
|
|
.package(url: "https://github.com/rnapier/ReactiveSwift.git", from: "7.0.0")
|
|
],
|
|
targets: [
|
|
.target(
|
|
name: "MovieMapper-iOS",
|
|
dependencies: [
|
|
.product(name: "FFmpegKitSwift", package: "swift-ffmpeg-kit"),
|
|
.product(name: "Alamofire", package: "Alamofire"),
|
|
.product(name: "ReactiveSwift", package: "ReactiveSwift")
|
|
]
|
|
)
|
|
]
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Summary of Key Decisions
|
|
|
|
### Architecture
|
|
- **Pure SwiftUI** with Swift only (no Rust backend as requested)
|
|
- **Combine** for async operations and state management
|
|
- **Structured concurrency** (`async/await`) for all network/file operations
|
|
|
|
### Offline-First Design
|
|
- File scanning works completely offline
|
|
- TVDB features are optional (require internet)
|
|
- Local caching of recent searches and show details
|
|
|
|
### iPad-Specific Optimizations
|
|
- Multi-column navigation with `NavigationSplitView`
|
|
- Large screen layout with sidebar + content
|
|
- Touch-friendly UI elements (minimum 44pt tap targets)
|
|
|
|
### Testing Approach
|
|
- **Command line tests**: Unit tests for all services
|
|
- **UI tests**: Simulator-based automation
|
|
- **Manual testing**: Test on actual iPad devices
|
|
|
|
---
|
|
|
|
## Implementation Timeline
|
|
|
|
| Week | Task |
|
|
|------|------|
|
|
| 1-2 | Project setup, data models, basic file scanner |
|
|
| 3-4 | Metadata extractor, UI scaffolding |
|
|
| 5-6 | TVDB client, search functionality |
|
|
| 7-8 | File mapping, tagging system |
|
|
| 9 | Testing (unit + UI), bug fixes |
|
|
| 10 | iPad optimization, polishing |
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. **Create Xcode project** using the structure above
|
|
2. **Implement core data models** (MediaFile, Show, Season, Episode)
|
|
3. **Build FileScanner service** with progress reporting
|
|
4. **Create SwiftUI views** for directory browsing
|
|
5. **Implement metadata extraction** with FFmpeg
|
|
6. **Add TVDB integration** for show search
|
|
7. **Build tagging and file movement** features
|
|
8. **Write comprehensive tests** for all services
|
|
9. **Optimize for iPad** with split views and large screen layouts
|
|
10. **Test on physical iPad device** before App Store submission |