mirror of
https://github.com/darkzoul5/YoutubePlaylistSync.git
synced 2026-09-19 21:12:20 +03:00
Compare commits
10
Commits
v2.2.0
...
51bc994bc9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51bc994bc9 | ||
|
|
e97c419b47 | ||
|
|
a7f1564581 | ||
|
|
3b1cdda19d | ||
|
|
2dc119a2f1 | ||
|
|
7d0c7aa1d5 | ||
|
|
15f2df0cbf | ||
|
|
22756f35db | ||
|
|
48bcf2c9df | ||
|
|
5f6df549ab |
@@ -29,7 +29,7 @@ jobs:
|
|||||||
needs: test
|
needs: test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
name: Unit tests
|
name: Unit tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-python@v6
|
- uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
ffmpeg: [bundled, none]
|
ffmpeg: [bundled, none]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Derive version
|
- name: Derive version
|
||||||
id: version
|
id: version
|
||||||
@@ -204,7 +204,30 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
mkdir -p package/bin ffmpeg_tmp
|
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
|
tar -xf ffmpeg.tar.xz -C ffmpeg_tmp --strip-components=1
|
||||||
mv ffmpeg_tmp/ffmpeg package/bin/ffmpeg
|
mv ffmpeg_tmp/ffmpeg package/bin/ffmpeg
|
||||||
chmod +x package/bin/ffmpeg
|
chmod +x package/bin/ffmpeg
|
||||||
@@ -250,7 +273,7 @@ jobs:
|
|||||||
needs: build
|
needs: build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
|
|||||||
@@ -2,14 +2,24 @@ name: Lint Python code
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- "assets/**"
|
||||||
|
- "README.md"
|
||||||
pull_request:
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- "assets/**"
|
||||||
|
- "README.md"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Install Ruff
|
- name: Install Ruff
|
||||||
run: pip install ruff
|
run: pip install ruff
|
||||||
|
|||||||
@@ -5,21 +5,15 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths-ignore:
|
||||||
- "src/**"
|
- "assets/**"
|
||||||
- "tests/**"
|
- "README.md"
|
||||||
- "pyproject.toml"
|
|
||||||
- "pytest.ini"
|
|
||||||
- "ytpl-sync-entry.py"
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths-ignore:
|
||||||
- "src/**"
|
- "assets/**"
|
||||||
- "tests/**"
|
- "README.md"
|
||||||
- "pyproject.toml"
|
|
||||||
- "pytest.ini"
|
|
||||||
- "ytpl-sync-entry.py"
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -34,7 +28,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -78,6 +78,9 @@ jobs:
|
|||||||
if: steps.detect.outputs.needs_update == 'true'
|
if: steps.detect.outputs.needs_update == 'true'
|
||||||
uses: peter-evans/create-pull-request@v8
|
uses: peter-evans/create-pull-request@v8
|
||||||
with:
|
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
|
branch: chore/refresh-yt-dlp
|
||||||
commit-message: "chore: bump yt-dlp to ${{ steps.detect.outputs.latest_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 }}"
|
title: "chore: bump yt-dlp to ${{ steps.detect.outputs.latest_yt_dlp }}"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Local-first YouTube playlist synchronization client.
|
|||||||
|
|
||||||
## What's Included
|
## What's Included
|
||||||
|
|
||||||
- GUI (PySide6) playlist manager + sync runner
|
- GUI (PySide6 Essentials) playlist manager + sync runner
|
||||||
- Scanner (yt-dlp extract-only), diff engine, filesystem scan
|
- Scanner (yt-dlp extract-only), diff engine, filesystem scan
|
||||||
- Safe reordering via two-pass rename, recycle deletions
|
- Safe reordering via two-pass rename, recycle deletions
|
||||||
- Async download queue with simple retry (yt-dlp Python API)
|
- Async download queue with simple retry (yt-dlp Python API)
|
||||||
@@ -30,6 +30,7 @@ Download the latest release from this repo's Releases page and pick one:
|
|||||||
- `ytpl-sync-windows-{version}.zip` / `ytpl-sync-linux-{version}.tar.gz` (no ffmpeg bundled)
|
- `ytpl-sync-windows-{version}.zip` / `ytpl-sync-linux-{version}.tar.gz` (no ffmpeg bundled)
|
||||||
|
|
||||||
## Configure
|
## Configure
|
||||||
|
|
||||||
Application uses a json config that canbe edited from UI or manually
|
Application uses a json config that canbe edited from UI or manually
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# MP3 Metadata Plan
|
||||||
|
|
||||||
|
## Subject Area
|
||||||
|
|
||||||
|
- Add MP3 tag writing for downloaded YouTube playlist items.
|
||||||
|
- Scope is limited to `.mp3` outputs produced by `audio` mode and the MP3 side of `both` mode.
|
||||||
|
- Metadata is sourced from YouTube/yt-dlp and embedded after audio extraction.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
- 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 per-playlist setting to enable or disable MP3 metadata embedding.
|
||||||
|
|
||||||
|
## Required Metadata
|
||||||
|
|
||||||
|
- `title` ← video title
|
||||||
|
- `artist` ← uploader, fallback to channel
|
||||||
|
- `album` ← album name if present
|
||||||
|
- `tracknumber` ← playlist index
|
||||||
|
- `date` / `year` ← upload date
|
||||||
|
- `comment` ← source URL
|
||||||
|
- `genre` ← if available
|
||||||
|
- `album_art` ← thumbnail
|
||||||
|
|
||||||
|
## Configuration Requirement
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## Current Constraints
|
||||||
|
|
||||||
|
- The current playlist scan keeps only a minimal item shape: title, video id, and playlist index.
|
||||||
|
- The scanner uses flat extraction, which is sufficient for diffing but not for full tag data.
|
||||||
|
- MP3 extraction currently transcodes audio but does not write ID3 metadata.
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
- Keep playlist diffing fast by retaining the current flat scan for remote playlist structure.
|
||||||
|
- Fetch full metadata only for items that are actually going to be downloaded or repaired.
|
||||||
|
- Write metadata only after MP3 extraction completes successfully.
|
||||||
|
- Treat metadata embedding as a post-processing step that can fail softly without discarding the MP3.
|
||||||
|
|
||||||
|
## Work Breakdown
|
||||||
|
|
||||||
|
### 1. Extend the metadata model
|
||||||
|
|
||||||
|
- Add optional fields to `PlaylistItem` for:
|
||||||
|
- uploader
|
||||||
|
- channel
|
||||||
|
- album
|
||||||
|
- upload_date
|
||||||
|
- genre
|
||||||
|
- thumbnail_url
|
||||||
|
- webpage_url
|
||||||
|
- Keep `artist` as a derived value instead of storing a separate field.
|
||||||
|
|
||||||
|
### 2. Fetch full per-video metadata
|
||||||
|
|
||||||
|
- Introduce a metadata fetch step for each item selected for download.
|
||||||
|
- Use yt-dlp per-video extraction to retrieve richer fields than the flat playlist entry provides.
|
||||||
|
- Prefer canonical values from the video page payload for upload date, uploader/channel, album, genre, thumbnail, and source URL.
|
||||||
|
|
||||||
|
### 3. Carry metadata through the download pipeline
|
||||||
|
|
||||||
|
- 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 per-playlist MP3 metadata enabled/disabled setting into the post-processing step.
|
||||||
|
|
||||||
|
### 4. Add an MP3 tag writer
|
||||||
|
|
||||||
|
- Add `mutagen` as the ID3 writing dependency.
|
||||||
|
- Implement a focused tagging component that maps `PlaylistItem` metadata into ID3 frames.
|
||||||
|
- Omit fields when the source value is missing instead of writing placeholders.
|
||||||
|
|
||||||
|
### 5. Map fields into ID3 tags
|
||||||
|
|
||||||
|
- `title` → video title
|
||||||
|
- `artist` → uploader, fallback to channel
|
||||||
|
- `album` → album if present
|
||||||
|
- `tracknumber` → playlist index
|
||||||
|
- `date/year` → parsed upload date
|
||||||
|
- `comment` → canonical source URL
|
||||||
|
- `genre` → genre if present
|
||||||
|
|
||||||
|
### 6. Embed album art
|
||||||
|
|
||||||
|
- Download the selected thumbnail for the video after the media download succeeds.
|
||||||
|
- Attach thumbnail data as embedded cover art when the image type is supported.
|
||||||
|
- Fail soft if thumbnail retrieval or embedding fails, and keep the MP3 intact.
|
||||||
|
|
||||||
|
### 7. Integrate into modes
|
||||||
|
|
||||||
|
- `audio` mode:
|
||||||
|
- download source media
|
||||||
|
- extract MP3
|
||||||
|
- write MP3 tags only when the setting is enabled
|
||||||
|
- delete temporary/source MP4 if configured
|
||||||
|
- `both` mode:
|
||||||
|
- download source media
|
||||||
|
- extract MP3
|
||||||
|
- write MP3 tags only when the setting is enabled
|
||||||
|
- keep MP4 unchanged
|
||||||
|
- `video` mode:
|
||||||
|
- no MP3 tagging path
|
||||||
|
|
||||||
|
### 8. Add configuration surface
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- If download fails, no tagging runs.
|
||||||
|
- If extraction fails, no tagging runs.
|
||||||
|
- If metadata embedding is disabled, skip the tagging step entirely.
|
||||||
|
- If tagging fails, mark the tag step as failed in logs/events but keep the MP3 file.
|
||||||
|
- If thumbnail embedding fails, continue with text metadata only.
|
||||||
|
- Missing `album` or `genre` is normal and should not be treated as an error.
|
||||||
|
|
||||||
|
## Testing Plan
|
||||||
|
|
||||||
|
- Unit test metadata mapping from yt-dlp info to the internal metadata model.
|
||||||
|
- Unit test ID3 writing against a temporary MP3 fixture.
|
||||||
|
- Unit test fallback behavior:
|
||||||
|
- uploader missing, channel present
|
||||||
|
- album missing
|
||||||
|
- genre missing
|
||||||
|
- thumbnail missing
|
||||||
|
- Integration test the audio post-processing path with tagging mocked.
|
||||||
|
- Integration test the both-mode MP3 path with tagging mocked.
|
||||||
|
|
||||||
|
## Documentation Updates
|
||||||
|
|
||||||
|
- Document that MP3 tags are written only for `.mp3` outputs.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## Dependency Decision
|
||||||
|
|
||||||
|
- Recommended library: `mutagen`
|
||||||
|
- Reason:
|
||||||
|
- direct ID3 support
|
||||||
|
- reliable field-level control
|
||||||
|
- suitable for embedding cover art
|
||||||
|
- avoids depending on ffmpeg metadata flags for all tag logic
|
||||||
|
|
||||||
|
## Delivery Order
|
||||||
|
|
||||||
|
- First: add config setting and defaults
|
||||||
|
- Second: extend metadata model and add full metadata fetch
|
||||||
|
- Third: add MP3 tag writer and field mapping
|
||||||
|
- Fourth: add thumbnail embedding
|
||||||
|
- Fifth: wire tagging into `audio` and `both`
|
||||||
|
- Sixth: add tests and docs
|
||||||
@@ -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
|
|
||||||
+2
-2
@@ -12,8 +12,8 @@ license = { file = "LICENSE" }
|
|||||||
keywords = ["youtube", "yt-dlp", "playlist", "sync"]
|
keywords = ["youtube", "yt-dlp", "playlist", "sync"]
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"yt-dlp>=2026.3.17",
|
"yt-dlp>=2026.7.4",
|
||||||
"PySide6",
|
"PySide6_Essentials>=6.11.1",
|
||||||
]
|
]
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
test = [
|
test = [
|
||||||
|
|||||||
Reference in New Issue
Block a user