mirror of
https://github.com/darkzoul5/YoutubePlaylistSync.git
synced 2026-07-04 04:53:58 +03:00
Compare commits
5 Commits
v2.2.0
...
7d0c7aa1d5
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d0c7aa1d5 | |||
| 15f2df0cbf | |||
| 22756f35db | |||
| 48bcf2c9df | |||
| 5f6df549ab |
@@ -2,7 +2,17 @@ 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:
|
||||||
@@ -15,4 +25,4 @@ jobs:
|
|||||||
run: pip install ruff
|
run: pip install ruff
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: ruff check .
|
run: ruff check .
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -73,7 +74,7 @@ Queue / retry:
|
|||||||
- Run `ytpl-sync.exe` (GUI).
|
- Run `ytpl-sync.exe` (GUI).
|
||||||
|
|
||||||
## Tray
|
## Tray
|
||||||
|
|
||||||
- The app supports minimizing to tray on close if the OS provides a system tray; use the tray icon menu to quit.
|
- The app supports minimizing to tray on close if the OS provides a system tray; use the tray icon menu to quit.
|
||||||
- Tray behavior settings (Settings page):
|
- Tray behavior settings (Settings page):
|
||||||
- `close_to_tray`: close hides to tray (keeps running).
|
- `close_to_tray`: close hides to tray (keeps running).
|
||||||
|
|||||||
@@ -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
|
||||||
+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.6.9",
|
||||||
"PySide6",
|
"PySide6_Essentials>=6.11.1",
|
||||||
]
|
]
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
test = [
|
test = [
|
||||||
|
|||||||
Reference in New Issue
Block a user