1
0
mirror of https://github.com/darkzoul5/YoutubePlaylistSync.git synced 2026-07-03 04:23:59 +03:00

7 Commits

Author SHA1 Message Date
dependabot[bot] e97c419b47 chore(deps): bump actions/checkout from 6 to 7 (#17)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-20 23:23:48 +03:00
dark_zoul a7f1564581 remove outdated plans 2026-06-18 11:31:06 +03:00
dark_zoul 3b1cdda19d ci(release): verify linux ffmpeg download 2026-06-11 15:57:47 +03:00
darkzoul5 2dc119a2f1 Merge pull request #16 from darkzoul5/chore/refresh-yt-dlp
chore: bump yt-dlp to 2026.6.9
2026-06-11 15:40:45 +03:00
darkzoul5 7d0c7aa1d5 chore: bump yt-dlp to 2026.6.9 2026-06-11 12:38:15 +00:00
dark_zoul 15f2df0cbf ci: use dedicated token for yt-dlp update PRs 2026-06-11 15:37:52 +03:00
dark_zoul 22756f35db ci: switch workflow path filters to paths-ignore 2026-06-11 13:05:51 +03:00
9 changed files with 59 additions and 831 deletions
+28 -5
View File
@@ -29,7 +29,7 @@ jobs:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -71,7 +71,7 @@ jobs:
name: Unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
@@ -97,7 +97,7 @@ jobs:
ffmpeg: [bundled, none]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Derive version
id: version
@@ -204,7 +204,30 @@ jobs:
run: |
set -euo pipefail
mkdir -p package/bin ffmpeg_tmp
curl -L "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz" -o ffmpeg.tar.xz
primary_url="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"
curl_common=(
--fail
--location
--retry 5
--retry-all-errors
--retry-delay 2
--user-agent "Mozilla/5.0 (GitHub Actions; ytpl-sync release workflow)"
)
curl "${curl_common[@]}" "$primary_url" -o ffmpeg.tar.xz
curl "${curl_common[@]}" "${primary_url}.md5" -o ffmpeg.tar.xz.md5
expected_md5="$(awk '{print $1}' ffmpeg.tar.xz.md5)"
printf '%s *ffmpeg.tar.xz\n' "$expected_md5" | md5sum -c -
if ! tar -tf ffmpeg.tar.xz >/dev/null 2>&1; then
echo "Downloaded FFmpeg payload is not a valid tar archive" >&2
ls -l ffmpeg.tar.xz >&2 || true
head -c 256 ffmpeg.tar.xz >&2 || true
exit 1
fi
tar -xf ffmpeg.tar.xz -C ffmpeg_tmp --strip-components=1
mv ffmpeg_tmp/ffmpeg package/bin/ffmpeg
chmod +x package/bin/ffmpeg
@@ -250,7 +273,7 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
fetch-depth: 0
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
+12 -2
View File
@@ -2,17 +2,27 @@ name: Lint Python code
on:
push:
branches:
- main
paths-ignore:
- "assets/**"
- "README.md"
pull_request:
branches:
- main
paths-ignore:
- "assets/**"
- "README.md"
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Install Ruff
run: pip install ruff
- name: Run linter
run: ruff check .
run: ruff check .
+7 -13
View File
@@ -5,21 +5,15 @@ on:
push:
branches:
- main
paths:
- "src/**"
- "tests/**"
- "pyproject.toml"
- "pytest.ini"
- "ytpl-sync-entry.py"
paths-ignore:
- "assets/**"
- "README.md"
pull_request:
branches:
- main
paths:
- "src/**"
- "tests/**"
- "pyproject.toml"
- "pytest.ini"
- "ytpl-sync-entry.py"
paths-ignore:
- "assets/**"
- "README.md"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -34,7 +28,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
+4 -1
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -78,6 +78,9 @@ jobs:
if: steps.detect.outputs.needs_update == 'true'
uses: peter-evans/create-pull-request@v8
with:
# Use a non-GITHUB_TOKEN credential so the resulting PR triggers CI workflows.
# Configure secrets.PR_WORKFLOW_TOKEN with contents:write and pull-requests:write.
token: ${{ secrets.PR_WORKFLOW_TOKEN || github.token }}
branch: chore/refresh-yt-dlp
commit-message: "chore: bump yt-dlp to ${{ steps.detect.outputs.latest_yt_dlp }}"
title: "chore: bump yt-dlp to ${{ steps.detect.outputs.latest_yt_dlp }}"
-24
View File
@@ -1,24 +0,0 @@
# GUI Plan
## Python-first Desktop Architecture
- **Primary GUI framework**: `PySide6` (Qt for Python).
## Core Features to Implement
1. **Dashboard Overview**: List all tracked playlists, their status (Last Sync), and total size.
2. **Interactive Configuration**: Wizard-style setup for new playlists (URL detection, folder picker).
3. **Queue Manager**: Visual progress bars for active downloads, showing speed, ETA, and current video title.
4. **Log Viewer**: Real-time streaming of yt-dlp logs for troubleshooting.
5. **Settings Panel**: Global settings for binary paths (ffmpeg), max parallel jobs, and Docker detection toggle.
## Phase 1 Roadmap: "The Bridge"
- [ ] **PySide6 Skeleton**: Basic window with `QWebEngine` (if hybrid) or native `QWidget` dashboard.
- [ ] **Packaging**: `pyinstaller` configuration to bundle both backend and frontend into a single `.exe`.
## Packaging & Distribution (brief)
- Bundle the backend and GUI into one distributable.
- Windows: use `pyinstaller` or `briefcase` to create an executable/installer. Consider creating an MSI or Inno Setup installer for a polished UX.
- Linux: provide AppImage, Snap, or distribution-specific packages (deb/rpm) — AppImage is a good starting point for single-file distribution.
+6 -6
View File
@@ -11,7 +11,7 @@
- Write useful MP3 metadata for downloaded playlist items without affecting video-only downloads.
- Keep the implementation reliable when optional fields are missing.
- Preserve successful downloads even when metadata embedding partially fails.
- Provide a setting to enable or disable MP3 metadata embedding.
- Provide a per-playlist setting to enable or disable MP3 metadata embedding.
## Required Metadata
@@ -26,7 +26,7 @@
## Configuration Requirement
- Add a setting to turn MP3 metadata embedding on or off.
- Add a per-playlist setting to turn MP3 metadata embedding on or off.
- Default should be explicitly defined during implementation; recommended default is `enabled` for new configs.
- The setting should only affect `.mp3` metadata writing and should not change download selection, extraction, or `.mp4` handling.
@@ -68,7 +68,7 @@
- Ensure the enriched `PlaylistItem` reaches the download job and post-processing stage.
- Keep this propagation in-memory unless restart-safe metadata persistence becomes necessary later.
- Avoid changing unrelated sync behavior for video-only items.
- Carry the MP3 metadata enabled/disabled setting into the post-processing step.
- Carry the per-playlist MP3 metadata enabled/disabled setting into the post-processing step.
### 4. Add an MP3 tag writer
@@ -109,8 +109,8 @@
### 8. Add configuration surface
- Add the new setting to the config model and default config output.
- Expose the setting in the GUI/settings surface if MP3 behavior is already user-configurable there.
- Add the new per-playlist setting to the playlist config model and default config output.
- Expose the setting in the playlist configuration UI, not as a global app setting.
- Keep the naming explicit, for example `write_mp3_metadata` or `embed_mp3_metadata`.
## Error Handling Rules
@@ -137,7 +137,7 @@
## Documentation Updates
- Document that MP3 tags are written only for `.mp3` outputs.
- Document the new setting that enables or disables MP3 metadata embedding.
- Document the new per-playlist setting that enables or disables MP3 metadata embedding.
- Document the field fallback rules, especially artist and album behavior.
- Document that album art comes from the video thumbnail, not playlist artwork.
- Document that some YouTube items will not expose album or genre information.
@@ -1,778 +0,0 @@
# YouTube Playlist Sync — Project Conversion Plan
---
# Project Direction
Convert the project from:
```text
Single-purpose YouTube playlist downloader
```
into:
```text
Persistent YouTube playlist synchronization client
```
The application becomes state-driven.
---
# Core Product Goals
## Main Features
- Sync playlists locally
- Download missing items
- Remove deleted playlist items
- Keep exact playlist ordering
- Support audio/video modes
- Multiple playlists
- Background auto-sync
- GUI configuration
- Queue management
- Logs/history
---
# Explicit Non-Goals (Current Scope)
Not planned right now:
- Built-in media playback
- Advanced naming templates
- Drag-and-drop manual ordering
- Private playlist sync
- Channel subscriptions
- Metadata editing
Design the architecture so these can be added later.
---
# Recommended Stack
## Core Language
- Python 3.12+
Reason:
- Native yt-dlp ecosystem
- Easier async/background work
- Better packaging than many expect
- Simpler iteration speed
---
# GUI
## Recommended
- PySide6
Why:
- Modern Qt ecosystem
- Better long-term support
- Cleaner than Tkinter
- Easier dynamic playlist UI
- Good threading support
- Better styling
Avoid:
- Tkinter for this scale
- Electron/Tauri unless you want a web stack
---
# Downloader Backend
## Recommended
- yt-dlp Python API
Install:
```bash
pip install yt-dlp
```
Do NOT:
- scrape YouTube manually
- parse HTML yourself
- depend on external unofficial APIs
Use yt-dlp for:
- metadata extraction
- playlist scanning
- downloading
- postprocessing
Use your own app for:
- sync logic
- ordering
- deletion
- scheduling
- state tracking
---
# Media Processing
## Recommended
- ffmpeg
Needed for:
- audio extraction
- remuxing
- conversions
- thumbnail embedding later
Recommended approach:
- auto-detect ffmpeg
- optionally bundle with packaged app
---
# Database
## Recommended
- SQLite
Reason:
- Zero setup
- Local-first architecture
- Perfect for sync metadata
- Easy migrations
- Reliable
SQLite is extremely important for this project.
Do NOT rely only on:
- filenames
- folders
- JSON
---
# Background Scheduling
## Recommended
- APScheduler
Use for:
- interval syncs
- delayed jobs
- retry jobs
- startup sync
---
# Async/Concurrency
## Recommended
- asyncio
Use for:
- concurrent playlist syncs
- GUI-safe task execution
- download queue
- cancellation
- progress updates
---
# Logging
## Recommended
- loguru
or:
- standard logging module
Need:
- rotating logs
- GUI log panel
- error history
- debug support
---
# Packaging
## Recommended
### During development
```text
venv + pip
```
### Release builds
Choose one:
| Tool | Notes |
| ----------- | ------------------------------- |
| Nuitka | Best performance and protection |
| PyInstaller | Easier and common |
Nuitka is probably best long-term.
---
# Major Architectural Changes
---
# 1. Move to State-Based Sync Architecture
Current downloader logic is likely:
```text
Playlist URL
Download everything
```
Replace with:
```text
Remote Playlist State
Stored Local State
Filesystem State
Diff Engine
Sync Actions
```
This is the single most important change.
---
# 2. Introduce Playlist Metadata Database
Create persistent tracking.
Suggested tables:
## playlists
```sql
CREATE TABLE playlists (
id TEXT PRIMARY KEY,
name TEXT,
url TEXT,
path TEXT,
mode TEXT,
auto_sync INTEGER,
sync_interval_minutes INTEGER,
last_sync TEXT
);
```
## playlist\_items
```sql
CREATE TABLE playlist_items (
playlist_id TEXT,
video_id TEXT,
title TEXT,
playlist_index INTEGER,
local_filename TEXT,
downloaded INTEGER,
last_seen TEXT,
PRIMARY KEY (playlist_id, video_id)
);
```
---
# 3. Implement Playlist Scanner Layer
Create dedicated metadata extraction.
Suggested structure:
```text
core/
├── scanner/
│ └── playlist_scanner.py
```
Responsibilities:
- fetch playlist entries
- extract video IDs
- detect unavailable videos
- return normalized playlist state
Use:
```python
extract_info(download=False)
```
This allows playlist scanning without downloading.
---
# 4. Create Diff Engine
The app should compare:
```text
Remote playlist
vs
Database state
vs
Filesystem state
```
Output actions:
```text
DOWNLOAD
DELETE
RENAME
REORDER
SKIP
REPAIR
```
Suggested file:
```text
core/sync/diff_engine.py
```
This becomes the heart of the application.
---
# 5. Add Download Queue System
Multi-playlist sync requires a queue.
Suggested states:
```text
Queued
Downloading
Converting
Completed
Failed
Skipped
Cancelled
```
Suggested structure:
```text
core/download/
├── queue_manager.py
├── downloader.py
└── workers.py
```
---
# 6. Implement Stable File Naming
Recommended naming:
```text
0001 - Title.ext
```
Benefits:
- native filesystem sorting
- easy reorder support
- easy repairs
- user friendly
Use:
```python
%(playlist_index)04d - %(title)s.%(ext)s
```
through yt-dlp.
---
# 7. Implement Safe Reordering
Playlist ordering changes frequently.
Never rename directly.
Use:
```text
Temporary rename pass
Final rename pass
```
Example:
```text
0001.mp3 → temp_a
0002.mp3 → temp_b
temp_a → 0002.mp3
```
Avoid collisions.
---
# 8. Implement Deletion Strategy
Recommended:
Instead of immediate delete:
```text
playlist/.recycle/
```
Move removed files there.
Benefits:
- safer
- recoverable
- easier debugging
Optional:
- auto-clean after X days
---
# 9. Redesign GUI Around Playlists
Current downloader GUIs are usually task-oriented.
You should move to:
```text
Playlist-oriented UI
```
Recommended sections:
```text
Sidebar
├── Playlists
├── Queue
├── History
├── Logs
└── Settings
```
---
# 10. Support Infinite Playlist Entries
Use dynamic UI generation.
Example:
```python
class PlaylistConfig:
url: str
path: str
mode: str
auto_sync: bool
```
GUI should render from:
```python
list[PlaylistConfig]
```
Do NOT hardcode playlist pages.
---
# 11. Add Background Sync
Start simple.
## Phase 1
- Timer-based sync
- Tray icon
- Run minimized
## Phase 2
- Background daemon/service
- Headless mode
- Autostart support
---
# 12. Add Progress/Event System
Needed for GUI responsiveness.
Recommended:
```text
event_bus.py
```
Events:
```text
DownloadStarted
DownloadProgress
SyncStarted
SyncFinished
FileDeleted
PlaylistUpdated
```
This decouples GUI from backend.
---
# 13. Introduce Config Management
Recommended:
```text
config.json
```
Only for:
- app settings
- UI preferences
- non-relational settings
Do NOT store sync state in JSON.
---
# Suggested Folder Structure
```text
app/
├── core/
│ ├── scanner/
│ ├── sync/
│ ├── download/
│ ├── database/
│ ├── scheduler/
│ └── events/
├── gui/
│ ├── pages/
│ ├── widgets/
│ ├── dialogs/
│ └── models/
├── config/
├── logs/
├── data/
└── main.py
```
---
# Suggested Sync Flow
```text
Load playlists
Scheduler triggers sync
Scanner fetches remote playlist
Database state loaded
Filesystem scanned
Diff engine computes actions
Queue downloads
Reorder files
Move removed files
Update database
Emit GUI events
```
---
# Recommended MVP Conversion Order
## Phase 1 — Backend Foundation
Implement:
- SQLite
- playlist scanner
- diff engine
- download wrapper
- basic sync logic
No GUI redesign yet.
---
# Phase 2 — Stable Syncing
Implement:
- deletion handling
- reorder handling
- queue system
- retry system
- logs
---
# Phase 3 — GUI Rewrite
Implement:
- playlist manager UI
- queue page
- logs page
- settings page
- dynamic playlists
---
# Phase 4 — Automation
Implement:
- background sync
- tray mode
- startup sync
- periodic sync
---
# Important Recommendations
## Recommendation 1
Treat:
```text
video_id
```
as the canonical identity.
Never titles.
---
# Recommendation 2
Do NOT rely on yt-dlp archive files alone.
Your own DB should be the source of truth.
---
# Recommendation 3
Keep download logic isolated.
yt-dlp should be replaceable internally.
---
# Recommendation 4
Do not overcomplicate the GUI early.
Focus on sync correctness first.
Sync reliability matters more than appearance.
---
# Recommendation 5
Design everything around interruption recovery.
The app should survive:
- crashes
- partial downloads
- force closes
- network failures
- playlist changes mid-sync
---
# Recommendation 6
Keep the application local-first.
No account system. No cloud backend. No telemetry.
That becomes a strong project identity.
---
# Final Recommended Identity
Instead of:
```text
YouTube Downloader GUI
```
Position the project as:
```text
Local-first YouTube playlist synchronization client.
```
That identity is:
- clearer
- more unique
- technically stronger
- easier to expand later
+1 -1
View File
@@ -12,7 +12,7 @@ license = { file = "LICENSE" }
keywords = ["youtube", "yt-dlp", "playlist", "sync"]
requires-python = ">=3.10"
dependencies = [
"yt-dlp>=2026.3.17",
"yt-dlp>=2026.6.9",
"PySide6_Essentials>=6.11.1",
]
[project.optional-dependencies]