#!/usr/bin/python3 import os, sys, argparse, re, subprocess, fnmatch parser = argparse.ArgumentParser(description='Sync music library and playlists to sd card') parser.add_argument('-l', '--library', default=os.path.expanduser('~/Music/Library'), help='Path to music library') parser.add_argument('-s', '--sd-path', default='/mnt/sd', help='Directory to sync to. Parent directory of library and playlists directories') parser.add_argument('-p', '--playlist-dir', default=os.path.expanduser('~/.quodlibet/playlists'), help='Path to playlist directory') parser.add_argument('--library-only', action='store_true', help='Only sync the library and not the playlists') parser.add_argument('--playlists-only', action='store_true', help='Only sync the playlists and not the library') parser.add_argument('--dry-run', action='store_true', help='Test run without making any changes') parser.add_argument('--confirm', action='store_true', help='Don\'t ask for confirmation interactively') parser.add_argument('--no-progress', action='store_true', help='Don\'t show progress while syncing library') parser.add_argument('--ignore-nosync', action='store_true', help='Don\'t use .nosync file if it exists') args = parser.parse_args() library = os.path.join(os.path.abspath(args.library), '') sd_path = os.path.abspath(args.sd_path) src_playlist_dir = os.path.abspath(args.playlist_dir) def get_confirm(message): response = input(message) return re.match('y|yes', response, re.IGNORECASE) excludes = [] nosync = os.path.join(library, '.nosync') if os.path.exists(nosync) and not args.ignore_nosync: with open(nosync) as nosync_lines: for line in nosync_lines: line = line[:-1] if line.startswith(library): line = '/' + line[len(library):] excludes.append(line) exclude_re = [] for ex in excludes: exclude_re.append(re.compile(fnmatch.translate(os.path.join(library, ex))[:-7])) def is_excluded(song): return any(exre.match(song) for exre in exclude_re) def sync_library(): print('Beginning library sync') rsync_args = ['rsync', '--recursive', '--times', '--verbose', '--delete', '--compress', '--modify-window=2'] if args.dry_run: rsync_args.append('--dry-run') if not args.no_progress: rsync_args.extend(['--info=progress2']) for ex in excludes: rsync_args.extend(['--exclude', ex]) rsync_args.extend(['--', library, os.path.join(sd_path, 'library')]) if not (args.confirm or get_confirm("Command: {2}\nSync library from {0} to {1}? [y/n] ".format(library, sd_path, ' '.join(rsync_args)))): print('Library sync cancelled') return try: returncode = subprocess.run(rsync_args).returncode except OSError as e: print('Unable to start rsync process: {0}'.format(e)) if returncode != 0: print('Error: rsync exited with code {0}'.format(returncode)) return print('Successfully synced library') def sync_playlists(): print('Beginning playlist sync') dst_playlist_dir = os.path.join(sd_path, 'playlists') if not (args.confirm or get_confirm("Sync playlists from {0} to {1}? [y/n] ".format(src_playlist_dir, dst_playlist_dir))): print('Playlist sync cancelled') return if not os.path.isdir(dst_playlist_dir) and not args.dry_run: try: os.mkdir(dst_playlist_dir) except OSError as e: print('Unable to create directory {0}: {1}. Skipping playlist sync'.format(dst_playlist_dir, e)) return try: with open(os.path.join(sd_path, 'playlists.txt'), 'r') as playlists: for playlist_name in playlists: playlist_name = playlist_name.strip() print('Attempting to sync playlist: {0}'.format(playlist_name)) src_playlist_path = os.path.join(src_playlist_dir, playlist_name) if not os.path.isfile(src_playlist_path): print('Playlist not found, skipping') continue dst_playlist_path = os.path.join(dst_playlist_dir, playlist_name) + '.m3u' added_songs = set() ignored_songs = 0 if not args.dry_run: with open(src_playlist_path, 'r') as src_playlist: with open(dst_playlist_path, 'w') as dst_playlist: for line in src_playlist: if line.startswith(library) and line not in added_songs and not is_excluded(line): dst_playlist.write(line.replace(library, '../library/')) added_songs.add(line) else: ignored_songs += 1 print('Successfuly synced playlist. {0} songs ignored as duplicates or not in library'.format(ignored_songs)) except OSError: print('Playlists file not found, skipping playlist sync') return print('Finished syncing playlists') if args.library_only and args.playlists_only: sys.exit('--only-library and --only-playlist must not both be provided') if not args.playlists_only: sync_library() if not args.library_only: sync_playlists()