init
This commit is contained in:
commit
ed8bb86bbd
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual Environment
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Project specific
|
||||
*.mp3
|
||||
*.m4a
|
||||
*.flac
|
||||
music/
|
||||
Music/
|
||||
test_music/
|
||||
22
INSTALL.md
Normal file
22
INSTALL.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Quick Install & Test
|
||||
|
||||
# 1. Install uv
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# 2. Setup project
|
||||
cd music-manager
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
uv pip install -e .
|
||||
uv pip install spotdl
|
||||
|
||||
# 3. Set Spotify credentials (optional)
|
||||
export SPOTIFY_CLIENT_ID="your_id"
|
||||
export SPOTIFY_CLIENT_SECRET="your_secret"
|
||||
|
||||
# 4. Test organize
|
||||
music-organize ~/Downloads/test-music ~/Music/Organized --dry-run
|
||||
|
||||
# 5. Test autopop
|
||||
mkdir -p "~/Music/Test/Daft Punk/Discovery"
|
||||
music-autopop ~/Music/Test --dry-run
|
||||
59
README.md
Normal file
59
README.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Music Manager
|
||||
|
||||
Organize messy music files and auto-populate albums using spotdl (YouTube downloads with Spotify metadata). Works great with Nextcloud Music.
|
||||
|
||||
## Features
|
||||
- Organize files into Artist/Album
|
||||
- Auto-download full albums by folder names
|
||||
- Complete partial albums (optional)
|
||||
- Reads tags or parses "Artist - Title" filenames
|
||||
- Skips existing files, embeds metadata/art
|
||||
|
||||
## Requirements
|
||||
- Python 3.9+
|
||||
- uv (package/venv)
|
||||
- spotdl
|
||||
|
||||
## Install (uv)
|
||||
```bash
|
||||
# In project root
|
||||
uv venv
|
||||
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||
uv pip install -e .
|
||||
uv pip install spotdl
|
||||
```
|
||||
|
||||
Optional (improves matching):
|
||||
```bash
|
||||
export SPOTIFY_CLIENT_ID="your_id"
|
||||
export SPOTIFY_CLIENT_SECRET="your_secret"
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
Organize existing files:
|
||||
```bash
|
||||
music-organize SOURCE DEST # organize
|
||||
music-organize SOURCE DEST --dry-run # preview
|
||||
```
|
||||
|
||||
Auto-populate from folders:
|
||||
```bash
|
||||
# Create folders: Music/Artist/Album (empty or partial)
|
||||
music-autopop LIBRARY # download empty albums
|
||||
music-autopop LIBRARY --dry-run # preview
|
||||
music-autopop LIBRARY --include-partial # complete partial albums
|
||||
```
|
||||
|
||||
## Tips
|
||||
- Use exact artist/album names for best results
|
||||
- Singles (no album) go to Artist/Singles
|
||||
- Always try --dry-run first
|
||||
|
||||
## Troubleshooting
|
||||
- spotdl not found: `uv pip install spotdl`
|
||||
- Recreate venv: `rm -rf .venv && uv venv && source .venv/bin/activate && uv pip install -e .`
|
||||
- Wrong matches: rename folder to exact album name (add year if needed)
|
||||
|
||||
## License
|
||||
MIT
|
||||
19
pyproject.toml
Normal file
19
pyproject.toml
Normal file
@ -0,0 +1,19 @@
|
||||
[project]
|
||||
name = "music-manager"
|
||||
version = "1.0.0"
|
||||
description = "Organize and auto-populate music library for Nextcloud Music"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"mutagen>=1.47.0",
|
||||
"spotipy>=2.24.0",
|
||||
]
|
||||
readme = "README.md"
|
||||
license = { text = "MIT" }
|
||||
|
||||
[project.scripts]
|
||||
music-organize = "music_manager.organizer:main"
|
||||
music-autopop = "music_manager.autopop:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
264
src/music_manager/autopop.py
Normal file
264
src/music_manager/autopop.py
Normal file
@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Auto-populate Music Library
|
||||
Populate empty album folders by downloading from YouTube/Spotify
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def check_spotdl():
|
||||
"""Check if spotdl is installed"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['spotdl', '--version'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
print(f"✓ spotdl found: {result.stdout.strip()}\n")
|
||||
return True
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
print("✗ spotdl not found")
|
||||
print("\nInstall with:")
|
||||
print(" uv pip install spotdl")
|
||||
return False
|
||||
|
||||
|
||||
def scan_library(library_path, include_partial=True):
|
||||
"""Scan library for albums to download"""
|
||||
library = Path(library_path)
|
||||
if not library.exists():
|
||||
print(f"Error: {library_path} does not exist")
|
||||
return []
|
||||
|
||||
albums_to_download = []
|
||||
audio_extensions = {
|
||||
'.mp3', '.m4a', '.flac', '.ogg', '.opus', '.wav', '.aac'
|
||||
}
|
||||
|
||||
print("Scanning library structure...\n")
|
||||
|
||||
for artist_dir in sorted(library.iterdir()):
|
||||
if not artist_dir.is_dir() or artist_dir.name.startswith('.'):
|
||||
continue
|
||||
|
||||
artist_name = artist_dir.name
|
||||
|
||||
for album_dir in sorted(artist_dir.iterdir()):
|
||||
if not album_dir.is_dir() or album_dir.name.startswith('.'):
|
||||
continue
|
||||
|
||||
album_name = album_dir.name
|
||||
|
||||
# Skip Singles
|
||||
if album_name.lower() in ['singles', 'single']:
|
||||
continue
|
||||
|
||||
# Count existing audio files
|
||||
audio_files = [
|
||||
f for f in album_dir.iterdir()
|
||||
if f.is_file() and f.suffix.lower() in audio_extensions
|
||||
]
|
||||
|
||||
if len(audio_files) == 0:
|
||||
print(f"📁 {artist_name} / {album_name} - Empty")
|
||||
albums_to_download.append({
|
||||
'artist': artist_name,
|
||||
'album': album_name,
|
||||
'path': album_dir,
|
||||
'existing_tracks': 0
|
||||
})
|
||||
elif include_partial:
|
||||
print(f"📂 {artist_name} / {album_name} - {len(audio_files)} tracks")
|
||||
albums_to_download.append({
|
||||
'artist': artist_name,
|
||||
'album': album_name,
|
||||
'path': album_dir,
|
||||
'existing_tracks': len(audio_files)
|
||||
})
|
||||
else:
|
||||
print(f"✓ {artist_name} / {album_name} - {len(audio_files)} tracks (skipping)")
|
||||
|
||||
return albums_to_download
|
||||
|
||||
|
||||
def download_album(artist, album, album_path, dry_run=False):
|
||||
"""Download album using spotdl"""
|
||||
|
||||
search_query = f"{artist} - {album}"
|
||||
|
||||
if dry_run:
|
||||
print(f"\n[DRY RUN] Would download: {search_query}")
|
||||
print(f" Destination: {album_path}")
|
||||
return True
|
||||
|
||||
print(f"\n⬇ Downloading: {artist} - {album}")
|
||||
print(f" To: {album_path}")
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
'spotdl',
|
||||
'download',
|
||||
search_query,
|
||||
'--output', str(album_path),
|
||||
'--format', 'mp3',
|
||||
'--bitrate', '320k',
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
output_lines = result.stdout.split('\n')
|
||||
downloaded = sum(
|
||||
1 for line in output_lines
|
||||
if 'Downloaded' in line or 'Downloading' in line
|
||||
)
|
||||
skipped = sum(1 for line in output_lines if 'Skipping' in line)
|
||||
|
||||
if downloaded > 0:
|
||||
print(f" ✓ Downloaded {downloaded} tracks")
|
||||
if skipped > 0:
|
||||
print(f" ⊘ Skipped {skipped} existing tracks")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f" ✗ Error downloading")
|
||||
if result.stderr:
|
||||
error_msg = result.stderr[:200]
|
||||
print(f" {error_msg}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ Error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def create_example_structure(library_path):
|
||||
"""Create example folder structure"""
|
||||
library = Path(library_path)
|
||||
|
||||
examples = [
|
||||
"Daft Punk/Random Access Memories",
|
||||
"Tame Impala/Currents",
|
||||
"Kendrick Lamar/good kid, m.A.A.d city",
|
||||
]
|
||||
|
||||
print(f"Creating example structure in {library_path}...\n")
|
||||
|
||||
for path in examples:
|
||||
full_path = library / path
|
||||
full_path.mkdir(parents=True, exist_ok=True)
|
||||
print(f" Created: {path}")
|
||||
|
||||
print("\nExample structure created!")
|
||||
print("Edit the folders to add your own artists/albums,")
|
||||
print("then run without --create-example")
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Auto-populate music library from folder structure',
|
||||
epilog="""
|
||||
Examples:
|
||||
# Create folder structure
|
||||
mkdir -p "Music/Artist Name/Album Name"
|
||||
mkdir -p "Music/Another Artist/Another Album"
|
||||
|
||||
# Dry run to see what would be downloaded
|
||||
music-autopop ~/Music --dry-run
|
||||
|
||||
# Download all empty albums
|
||||
music-autopop ~/Music
|
||||
|
||||
# Download including partial albums (complete them)
|
||||
music-autopop ~/Music --include-partial
|
||||
""",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'library',
|
||||
help='Path to music library root folder'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Show what would be downloaded without downloading'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--include-partial',
|
||||
action='store_true',
|
||||
help='Also download for folders with existing tracks'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--create-example',
|
||||
action='store_true',
|
||||
help='Create example folder structure'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Create example structure if requested
|
||||
if args.create_example:
|
||||
create_example_structure(args.library)
|
||||
return
|
||||
|
||||
# Check spotdl
|
||||
if not check_spotdl():
|
||||
sys.exit(1)
|
||||
|
||||
# Scan library
|
||||
albums = scan_library(args.library, args.include_partial)
|
||||
|
||||
if not albums:
|
||||
print("\n" + "=" * 70)
|
||||
print("No albums to download!")
|
||||
print("=" * 70)
|
||||
print("\nCreate folders like this:")
|
||||
print(" Artist Name/Album Name/")
|
||||
print("\nOr use --create-example to generate sample structure")
|
||||
return
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(f"Found {len(albums)} albums to download")
|
||||
print("=" * 70)
|
||||
|
||||
if args.dry_run:
|
||||
print("\n[DRY RUN MODE - No files will be downloaded]\n")
|
||||
|
||||
successful = 0
|
||||
failed = 0
|
||||
|
||||
for album_info in albums:
|
||||
if download_album(
|
||||
album_info['artist'],
|
||||
album_info['album'],
|
||||
album_info['path'],
|
||||
args.dry_run
|
||||
):
|
||||
successful += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
if args.dry_run:
|
||||
print(f"Dry run complete! Would download {len(albums)} albums")
|
||||
else:
|
||||
print(f"Done!")
|
||||
print(f" ✓ Successful: {successful}")
|
||||
if failed > 0:
|
||||
print(f" ✗ Failed: {failed}")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
299
src/music_manager/organizer.py
Normal file
299
src/music_manager/organizer.py
Normal file
@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Music Library Organizer
|
||||
Organizes messy music files into Artist/Album structure
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from mutagen import File
|
||||
from mutagen.id3 import ID3NoHeaderError
|
||||
|
||||
try:
|
||||
import spotipy
|
||||
from spotipy.oauth2 import SpotifyClientCredentials
|
||||
SPOTIFY_AVAILABLE = True
|
||||
except ImportError:
|
||||
SPOTIFY_AVAILABLE = False
|
||||
|
||||
|
||||
def sanitize_filename(name):
|
||||
"""Remove invalid characters from filenames"""
|
||||
if not name or name.strip() == "":
|
||||
return "Unknown"
|
||||
name = re.sub(r'[<>:"/\\|?*]', '', name)
|
||||
name = name.strip('. ')
|
||||
return name if name else "Unknown"
|
||||
|
||||
|
||||
def parse_filename(filename):
|
||||
"""Parse 'Artist - Title.mp3' format"""
|
||||
name = Path(filename).stem
|
||||
name = re.sub(r'^[\d/\s]+', '', name).strip()
|
||||
|
||||
if ' - ' in name:
|
||||
parts = name.split(' - ', 1)
|
||||
artist = parts[0].strip()
|
||||
title = parts[1].strip() if len(parts) > 1 else name
|
||||
return artist, title
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def clean_artist_name(artist):
|
||||
"""Remove featuring artists, keep only main artist"""
|
||||
if not artist:
|
||||
return artist
|
||||
|
||||
patterns = [
|
||||
r',.*',
|
||||
r'\s+feat\..*',
|
||||
r'\s+ft\..*',
|
||||
r'\s+featuring.*',
|
||||
r'\s+&.*',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
artist = re.split(pattern, artist, flags=re.IGNORECASE)[0]
|
||||
|
||||
return artist.strip()
|
||||
|
||||
|
||||
def get_metadata(filepath):
|
||||
"""Extract artist, album, and title from audio file or filename"""
|
||||
artist, album, title, track_num = None, None, None, None
|
||||
|
||||
try:
|
||||
audio = File(filepath, easy=True)
|
||||
if audio is not None:
|
||||
artist = audio.get('artist', [None])[0]
|
||||
album = audio.get('album', [None])[0]
|
||||
title = audio.get('title', [None])[0]
|
||||
|
||||
track_data = audio.get('tracknumber', [None])
|
||||
if track_data and track_data[0]:
|
||||
track_str = str(track_data[0]).split('/')[0]
|
||||
try:
|
||||
track_num = int(track_str)
|
||||
except:
|
||||
pass
|
||||
except (ID3NoHeaderError, Exception):
|
||||
pass
|
||||
|
||||
if not artist or not title:
|
||||
filename_artist, filename_title = parse_filename(filepath)
|
||||
if filename_artist:
|
||||
artist = artist or filename_artist
|
||||
title = title or filename_title
|
||||
album = album or "Singles"
|
||||
|
||||
if artist:
|
||||
artist = clean_artist_name(artist)
|
||||
|
||||
artist = artist or "Unknown Artist"
|
||||
album = album or "Unknown Album"
|
||||
title = title or Path(filepath).stem
|
||||
|
||||
return artist, album, title, track_num
|
||||
|
||||
|
||||
def organize_music(source_folder, dest_folder, dry_run=False):
|
||||
"""Organize music files into Artist/Album structure"""
|
||||
source_path = Path(source_folder)
|
||||
dest_path = Path(dest_folder)
|
||||
|
||||
if not source_path.exists():
|
||||
print(f"Error: Source folder {source_folder} does not exist")
|
||||
return {}
|
||||
|
||||
if not dry_run:
|
||||
dest_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
albums = {}
|
||||
audio_extensions = {
|
||||
'.mp3', '.m4a', '.flac', '.ogg', '.opus', '.wav', '.aac'
|
||||
}
|
||||
|
||||
print("Scanning audio files...")
|
||||
file_count = 0
|
||||
|
||||
for file_path in source_path.rglob('*'):
|
||||
if file_path.suffix.lower() in audio_extensions and file_path.is_file():
|
||||
file_count += 1
|
||||
artist, album, title, track_num = get_metadata(file_path)
|
||||
|
||||
key = (artist, album)
|
||||
if key not in albums:
|
||||
albums[key] = []
|
||||
albums[key].append({
|
||||
'path': file_path,
|
||||
'title': title,
|
||||
'track_num': track_num
|
||||
})
|
||||
|
||||
print(f"Found {file_count} files in {len(albums)} albums\n")
|
||||
|
||||
for (artist, album), files in sorted(albums.items()):
|
||||
artist_clean = sanitize_filename(artist)
|
||||
album_clean = sanitize_filename(album)
|
||||
album_path = dest_path / artist_clean / album_clean
|
||||
|
||||
print(f"{'[DRY RUN] ' if dry_run else ''}{artist} / {album}")
|
||||
print(f" └─ {len(files)} tracks")
|
||||
|
||||
if not dry_run:
|
||||
album_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for file_info in sorted(files, key=lambda x: x['track_num'] or 0):
|
||||
source_file = file_info['path']
|
||||
dest_file = album_path / source_file.name
|
||||
|
||||
counter = 1
|
||||
original_dest = dest_file
|
||||
while dest_file.exists() and not dry_run:
|
||||
dest_file = album_path / (
|
||||
f"{original_dest.stem}_{counter}{original_dest.suffix}"
|
||||
)
|
||||
counter += 1
|
||||
|
||||
if dry_run:
|
||||
print(f" • {source_file.name}")
|
||||
else:
|
||||
try:
|
||||
shutil.move(str(source_file), str(dest_file))
|
||||
except Exception as e:
|
||||
print(f" ✗ Error: {source_file.name} - {e}")
|
||||
|
||||
return albums
|
||||
|
||||
|
||||
def find_missing_tracks(
|
||||
albums, dest_folder, spotify_client_id=None, spotify_client_secret=None
|
||||
):
|
||||
"""Use Spotify API to find missing tracks"""
|
||||
if not SPOTIFY_AVAILABLE:
|
||||
print("\n⚠ Skipping missing track detection (spotipy not installed)")
|
||||
return
|
||||
|
||||
if not spotify_client_id or not spotify_client_secret:
|
||||
print("\n⚠ Skipping missing track detection (no Spotify credentials)")
|
||||
return
|
||||
|
||||
try:
|
||||
sp = spotipy.Spotify(
|
||||
auth_manager=SpotifyClientCredentials(
|
||||
client_id=spotify_client_id,
|
||||
client_secret=spotify_client_secret
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"\n✗ Error connecting to Spotify: {e}")
|
||||
return
|
||||
|
||||
dest_path = Path(dest_folder)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Finding missing tracks...")
|
||||
print("=" * 60)
|
||||
|
||||
for (artist, album), files in albums.items():
|
||||
if album == "Singles":
|
||||
continue
|
||||
|
||||
artist_clean = sanitize_filename(artist)
|
||||
album_clean = sanitize_filename(album)
|
||||
album_path = dest_path / artist_clean / album_clean
|
||||
|
||||
try:
|
||||
query = f"artist:{artist} album:{album}"
|
||||
results = sp.search(q=query, type='album', limit=3)
|
||||
|
||||
if not results['albums']['items']:
|
||||
continue
|
||||
|
||||
spotify_album = results['albums']['items'][0]
|
||||
tracks = sp.album_tracks(spotify_album['id'])
|
||||
all_tracks = tracks['items']
|
||||
|
||||
while tracks['next']:
|
||||
tracks = sp.next(tracks)
|
||||
all_tracks.extend(tracks['items'])
|
||||
|
||||
existing_titles = {
|
||||
f['title'].lower().strip() for f in files if f['title']
|
||||
}
|
||||
|
||||
missing_tracks = []
|
||||
for track in all_tracks:
|
||||
if track['name'].lower().strip() not in existing_titles:
|
||||
missing_tracks.append({
|
||||
'number': track['track_number'],
|
||||
'name': track['name'],
|
||||
'duration_ms': track['duration_ms']
|
||||
})
|
||||
|
||||
if missing_tracks:
|
||||
missing_file = album_path / "missing_tracks.txt"
|
||||
with open(missing_file, 'w', encoding='utf-8') as f:
|
||||
f.write(f"Album: {artist} - {album}\n")
|
||||
f.write(f"You have: {len(files)}/{len(all_tracks)} tracks\n")
|
||||
f.write(f"Missing: {len(missing_tracks)}\n\n")
|
||||
|
||||
for track in sorted(missing_tracks, key=lambda x: x['number']):
|
||||
mins = track['duration_ms'] // 60000
|
||||
secs = (track['duration_ms'] % 60000) // 1000
|
||||
f.write(f"{track['number']:2d}. {track['name']} "
|
||||
f"({mins}:{secs:02d})\n")
|
||||
|
||||
f.write(f"\nDownload: spotdl \"{artist} - {album}\"\n")
|
||||
|
||||
print(f"\n{artist} - {album}:")
|
||||
print(f" {len(files)}/{len(all_tracks)} tracks "
|
||||
f"({len(missing_tracks)} missing)")
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Organize music files into Artist/Album structure'
|
||||
)
|
||||
parser.add_argument('source', help='Source folder with music files')
|
||||
parser.add_argument('destination', help='Destination organized folder')
|
||||
parser.add_argument('--dry-run', action='store_true',
|
||||
help='Preview without moving files')
|
||||
parser.add_argument('--skip-missing', action='store_true',
|
||||
help='Skip missing track detection')
|
||||
parser.add_argument('--spotify-client-id', help='Spotify Client ID')
|
||||
parser.add_argument('--spotify-client-secret', help='Spotify Client Secret')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
client_id = args.spotify_client_id or os.getenv('SPOTIFY_CLIENT_ID')
|
||||
client_secret = args.spotify_client_secret or os.getenv('SPOTIFY_CLIENT_SECRET')
|
||||
|
||||
print("=" * 60)
|
||||
print("Music Library Organizer")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
albums = organize_music(args.source, args.destination, args.dry_run)
|
||||
|
||||
if not albums:
|
||||
print("\nNo music files found")
|
||||
return
|
||||
|
||||
if not args.skip_missing and not args.dry_run:
|
||||
find_missing_tracks(albums, args.destination, client_id, client_secret)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Done!")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user