diff --git a/CHANGES.md b/CHANGES.md index ae89600..c187003 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,13 @@ # Release notes +## 1.5.0 +Jan 13, 2023 + +- _Refactor module_; all business logic into functions: **make_playlist**, **write_playlist** and **add_extension** +- Add `-S` or `--split` cli argument: see issue #2 +- Fix check image and append mode +- Fix _enabled_encoding_ when is just enabled + ## 1.4.0 Nov 10, 2022 diff --git a/README.md b/README.md index 0d87b62..8119e57 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ``mkpl``: Make playlist +# ``make_playlist``: Make playlist command line tool ``mkpl`` is a _command line tool_ for create playlist file (**M3U format**). @@ -10,7 +10,7 @@ To install ``mkpl``, see here: $ pip install make_playlist # for python enviroment $ dnf copr enable matteoguadrini/mkpl -$ dnf install python-make_playlist -y # for Red Hat, CentOS, Mageia and fedora +$ dnf install python-make_playlist -y # for Red Hat and fedora $ git clone https://github.com/MatteoGuadrini/mkpl.git && cd mkpl $ python setup.py install # for others @@ -40,6 +40,7 @@ $ python setup.py install # for others | -c | --append | Continue playlist instead of override it | | | -w | --windows | Windows style folder separator | | | -v | --verbose | Enable verbosity (debug mode) | | +| -S | --split | Split playlist by directories | | ## Examples @@ -86,7 +87,7 @@ $ python setup.py install # for others mkpl -d "my_files" -r -z 10485760 "multimedia.m3u" ``` -8. Create playlist with only number one and two tracks wit regular expression +8. Create playlist with only number one and two tracks with regular expression ```bash mkpl -d "my_mp3_collection" -r -p "^[12]|[012]{2}" "my music.m3u" @@ -117,11 +118,44 @@ $ python setup.py install # for others mkpl -d "new_collection" -r "my music.m3u" -l http://192.168.1.123/mp3/song1.mp3, http://192.168.1.123/mp3/song2.mp4 ``` -13. Create a playlist and set Windows backslash (\) folder separator (for Windows OS) +13. Create a playlist and set Windows backslash (\\) folder separator (for Windows OS) ```bash mkpl -d "new_collection" -r "my music.m3u" -w ``` + +14. Split playlist into _N_ playlists fon _N_ directories + + ```bash + mkpl -d "folder1" "folder2" "folder3" -r "my_music.m3u" -S + ``` + Result: + ```console + $> ls + my_music.m3u + folder1.m3u + folder2.m3u + folder3.m3u + ... + ``` + +## Use it like Python module + +`mkpl` can also be used as a Python module to customize your scripts. + +```python +from make_playlist import * + +# Prepare playlist list: find multimedia files with name starts between a and f +playlist = make_playlist('/Music/collections', + '^[a-f].*', + ('mp3', 'mp4', 'aac'), + recursive=True, + unique=True) + +# Write playlist to file +write_playlist('/Music/AtoF.m3u', 'wt', playlist) +``` ## Open source _mkpl_ is an open source project. Any contribute, It's welcome. @@ -165,4 +199,4 @@ Thanks to Dane Hillard for writing the _Practices of the Python Pro_ books. Special thanks go to my wife, who understood the hours of absence for this development. Thanks to my children, for the daily inspiration they give me and to make me realize, that life must be simple. -Thanks Python! \ No newline at end of file +Thanks, Python! \ No newline at end of file diff --git a/__info__.py b/__info__.py index f8e279b..48b4e98 100644 --- a/__info__.py +++ b/__info__.py @@ -22,7 +22,7 @@ """Information variable used by modules on this package.""" -__version__ = '1.4.0' +__version__ = '1.5.0' __author__ = 'Matteo Guadrini' __email__ = 'matteo.guadrini@hotmail.it' __homepage__ = 'https://github.com/MatteoGuadrini/mkpl' diff --git a/mkpl.py b/mkpl.py index 164a0c5..46f49cb 100644 --- a/mkpl.py +++ b/mkpl.py @@ -27,16 +27,16 @@ from string import capwords from re import findall, sub from filecmp import cmp -from os.path import join, exists from pathlib import Path from random import shuffle +from os.path import join, exists, isdir, getsize, normpath, basename, dirname # endregion # region globals -FILE_FORMAT = {'mp1', 'mp2', 'mp3', 'mp4', 'aac', 'ogg', 'wav', 'wma', - 'avi', 'xvid', 'divx', 'mpeg', 'mpg', 'mov', 'wmv'} -__version__ = '1.4.0' +FILE_FORMAT = {'mp1', 'mp2', 'mp3', 'mp4', 'aac', 'ogg', 'wav', 'wma', 'm4a', 'aiff', + 'avi', 'xvid', 'divx', 'mpeg', 'mpg', 'mov', 'wmv', 'flac', 'alac'} +__version__ = '1.5.0' # endregion @@ -48,9 +48,9 @@ def get_args(): global FILE_FORMAT parser = argparse.ArgumentParser( - description="Make music playlist", + description="Command line tool to create media playlists in M3U format.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, - epilog='Playlist file is M3U format' + epilog='See latest release from https://github.com/MatteoGuadrini/mkpl' ) parser.add_argument("playlist", help="Playlist file", type=str) @@ -75,6 +75,7 @@ def get_args(): parser.add_argument("-u", "--unique", help="The same files are not placed in the playlist", action='store_true') parser.add_argument("-c", "--append", help="Continue playlist instead of override it", action='store_true') parser.add_argument("-w", "--windows", help="Windows style folder separator", action='store_true') + parser.add_argument("-S", "--split", help="Split playlist by directories", action='store_true') args = parser.parse_args() @@ -85,28 +86,33 @@ def get_args(): else: args.playlist += '.m3u' + # Check if playlist is not a directory + if isdir(args.playlist): + parser.error(f'{args.playlist} is a directory') + # Open playlist file - mode = 'at+' if args.append else 'wt' - args.playlist = open(args.playlist, mode=mode) + args.open_mode = 'at+' if args.append else 'wt' args.enabled_extensions = False args.enabled_title = False args.enabled_encoding = False # Verify extension attribute in append mode if args.append: - args.playlist.seek(0) - first_three_lines = args.playlist.readlines(100) - for line in first_three_lines: - if '#EXTM3U' in line: - args.enabled_extensions = True - if '#PLAYLIST' in line: - args.enabled_title = True - if '#EXTENC' in line: - args.enabled_encoding = True - args.playlist.read() - # Check if extensions are disabled and image is specified - if not args.enabled_extensions and args.image: - args.image = None - print(f'warning: image {args.image} has not been set because the extensions are not present in the file') + with open(args.playlist, mode=args.open_mode) as opened_playlist: + opened_playlist.seek(0) + first_three_lines = opened_playlist.readlines(100) + for line in first_three_lines: + if '#EXTM3U' in line: + args.enabled_extensions = True + if '#PLAYLIST' in line: + args.enabled_title = True + if '#EXTENC' in line: + args.enabled_encoding = True + # Check if extensions are disabled and image is specified + if getsize(args.playlist) > 0: + if not args.enabled_extensions and args.image: + print(f'warning: image {args.image} has not been set because the extension flag' + ' is not present in the playlist') + args.image = None # Check if image file exists if args.image: @@ -144,95 +150,181 @@ def vprint(verbose, *messages): print('debug:', *messages) -def main(): - """Make a playlist""" - - args = get_args() - multimedia_files = list() +def write_playlist(playlist, + open_mode, + files, + enabled_extensions=False, + image=None, + ext_part=None, + max_tracks=None, + verbose=False): + """Write playlist into file""" + with open(playlist, mode=open_mode) as pl: + if image and enabled_extensions: + vprint(verbose, f"set image {image}") + joined_string = f"\n#EXTIMG: {image}\n" + else: + joined_string = '\n' + end_file_string = '\n' + # Write extensions if exists + if ext_part: + pl.write('\n'.join(files[:ext_part]) + joined_string) + # Write all multimedia files + vprint(verbose, f"write playlist {pl.name}") + pl.write(joined_string.join(files[ext_part:max_tracks]) + end_file_string) + + +def make_playlist(directory, + pattern, + file_formats, + recursive=False, + exclude_dirs=None, + unique=False, + absolute=False, + min_size=1, + windows=False, + verbose=False): + """Make playlist list""" + filelist = list() + # Check if directory exists + if not exists(directory): + print(f'warning: {directory} does not exists') + return filelist + # Check if is a directory + if not isdir(directory): + print(f'warning: {directory} is not a directory') + return filelist + # Build a Path object + path = Path(directory) + root = path.parent + vprint(verbose, f"current directory={path}, root={root}") + for fmt in file_formats: + # Check recursive + folder = '**/*' if recursive else '*' + for file in path.glob(folder + f'.{fmt}'): + # Check if in exclude dirs + if any([e_path in str(file) for e_path in exclude_dirs]): + continue + # Check if file is in playlist + if unique: + if file_in_playlist(filelist, + str(file), + root=root if not absolute else None): + continue + # Check absolute file names + size = file.stat().st_size + file = str(file) if absolute else str(file.relative_to(path.parent)) + # Check re pattern + if findall(pattern, file): + # Check file size + if size >= min_size: + vprint(verbose, f"add multimedia file {file}") + filelist.append( + sub('/', r"\\", file) if windows else file + ) + return filelist + + +def add_extension(filelist, cli_args, verbose=False): + """Add extension to playlist list""" + if not isinstance(filelist, list): + raise ValueError(f'{filelist} is not a list object') + + # Check if playlist is an extended M3U + cli_args.ext_part = 0 + if cli_args.title or cli_args.encoding or cli_args.image: + if not cli_args.enabled_extensions: + filelist.insert(0, '#EXTM3U') + vprint(verbose, "enable extension flag") + cli_args.enabled_extensions = True + cli_args.ext_part += 1 + if cli_args.max_tracks: + cli_args.max_tracks += 1 + + # Set title + if cli_args.title: + if not cli_args.enabled_title: + title = capwords(cli_args.title) + filelist.insert(1, f'#PLAYLIST: {title}') + vprint(verbose, f"set title {title}") + cli_args.ext_part += 1 + if cli_args.max_tracks: + cli_args.max_tracks += 1 + else: + print("warning: title is already configured") + + # Set encoding + if cli_args.encoding: + if not cli_args.enabled_encoding: + filelist.insert(1, f'#EXTENC: {cli_args.encoding}') + vprint(verbose, f"set encoding {cli_args.encoding}") + cli_args.ext_part += 1 + if cli_args.max_tracks: + cli_args.max_tracks += 1 + else: + print("warning: encoding is already configured") + + +def _process_playlist(files, cli_args, other_playlist=None): + """Private function cli only for process arguments and make playlist""" # Add link - multimedia_files.extend(args.link) - - vprint(args.verbose, f"formats={FILE_FORMAT}, recursive={args.recursive}, pattern={args.pattern}") - - # Walk to directories - for directory in args.directories: - # Build a Path object - path = Path(directory) - root = path.parent - vprint(args.verbose, f"current directory={path}, root={root}") - for fmt in FILE_FORMAT: - # Check recursive - folder = '**/*' if args.recursive else '*' - for file in path.glob(folder + f'.{fmt}'): - # Check if in exclude dirs - if any([e_path in str(file) for e_path in args.exclude_dirs]): - continue - # Check if file is in playlist - if args.unique: - if file_in_playlist(multimedia_files, - str(file), - root=root if not args.absolute else None): - continue - # Check absolute file names - size = file.stat().st_size - file = str(file) if args.absolute else str(file.relative_to(path.parent)) - # Check re pattern - if findall(args.pattern, file): - # Check file size - if size >= args.size: - vprint(args.verbose, f"add multimedia file {file}") - multimedia_files.append( - sub('/', r"\\", file) if args.windows else file - ) + files.extend(cli_args.link) # Build a playlist - if multimedia_files: - ext_part = 0 + if files: + # Check shuffle - if args.shuffle: - shuffle(multimedia_files) - - # Check if playlist is an extended M3U - if args.title or args.encoding or args.image: - if not args.enabled_extensions: - multimedia_files.insert(0, '#EXTM3U') - args.enabled_extensions = True - ext_part += 1 - if args.max_tracks: - args.max_tracks += 1 - - # Set title - if args.title: - if not args.enabled_title: - multimedia_files.insert(1, f'#PLAYLIST: {capwords(args.title)}') - ext_part += 1 - if args.max_tracks: - args.max_tracks += 1 - else: - print("warning: title is already configured") - - # Set encoding - if args.encoding: - if not args.enabled_extensions: - multimedia_files.insert(1, f'#EXTENC: {args.encoding}') - ext_part += 1 - if args.max_tracks: - args.max_tracks += 1 - else: - print("warning: encoding is already configured") - - with args.playlist as playlist: - vprint(args.verbose, f"write playlist {playlist.name}") - joined_string = f'\n#EXTIMG: {args.image}\n' if args.image and args.enabled_extensions else '\n' - end_file_string = '\n' - # Write extensions if exists - if ext_part: - playlist.write('\n'.join(multimedia_files[:ext_part]) + joined_string) - # Write all multimedia files - playlist.write(joined_string.join(multimedia_files[ext_part:args.max_tracks]) + end_file_string) + if cli_args.shuffle: + shuffle(files) + + # Add extension to playlist + add_extension(files, cli_args, verbose=cli_args.verbose) + + # Write playlist to file + write_playlist(other_playlist if other_playlist else cli_args.playlist, + cli_args.open_mode, + files, + enabled_extensions=cli_args.enabled_extensions, + image=cli_args.image, + ext_part=cli_args.ext_part, + max_tracks=cli_args.max_tracks, + verbose=cli_args.verbose) else: - print(f'warning: no multimedia files are found here: {",".join(args.directories)}') + print(f'warning: no multimedia files are found here: {",".join(cli_args.directories)}') + + +def main(): + """Make a playlist file""" + + args = get_args() + multimedia_files = list() + vprint(args.verbose, f"formats={FILE_FORMAT}, recursive={args.recursive}, " + f"pattern={args.pattern}, split={args.split}") + + # Make multimedia list + for directory in args.directories: + directory_files = make_playlist(directory, + args.pattern, + FILE_FORMAT, + recursive=args.recursive, + exclude_dirs=args.exclude_dirs, + unique=args.unique, + absolute=args.absolute, + min_size=args.size, + windows=args.windows, + verbose=args.verbose) + + multimedia_files.extend(directory_files) + + # Check if you must split into directory playlist + if args.split: + playlist_name = basename(normpath(directory)) + playlist_ext = '.m3u8' if args.encoding == 'UNICODE' else '.m3u' + playlist_path = join(dirname(args.playlist), playlist_name + playlist_ext) + _process_playlist(directory_files, args, playlist_path) + + _process_playlist(multimedia_files, args) # endregion